-
Notifications
You must be signed in to change notification settings - Fork 9
296 custom type object dynamic fields filter #365
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fc6ca62
23fec19
7de2e99
4d3396f
5caa6c8
103fce7
acf6172
d0dad5b
25deee4
71067f2
5da2558
76f8358
3d04899
e6c4123
52b958e
0fb1310
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,9 @@ | ||
| import django_filters | ||
| from dataclasses import dataclass | ||
| from typing import Any, Dict, Optional, Type | ||
|
|
||
| from django.contrib.postgres.fields import ArrayField | ||
| from django.db.models import JSONField, Q | ||
| from django.db.models import JSONField, QuerySet, Q | ||
|
|
||
| from extras.choices import CustomFieldTypeChoices | ||
| from netbox.filtersets import NetBoxModelFilterSet | ||
|
|
@@ -13,6 +16,67 @@ | |
| ) | ||
|
|
||
|
|
||
| @dataclass | ||
| class FilterSpec: | ||
| """ | ||
| Declarative specification describing how a custom field type | ||
| should be translated into a django-filter Filter instance. | ||
| """ | ||
| filter_class: Type[django_filters.Filter] | ||
| lookup_expr: Optional[str] = None | ||
| extra_kwargs: Optional[Dict[str, Any]] = None | ||
|
|
||
| def build( | ||
| self, field_name: str, label: str, queryset: Optional[QuerySet] = None, **kwargs | ||
| ) -> django_filters.Filter: | ||
| """ | ||
| Instantiate and return a django-filter Filter. | ||
| Allows overriding defaults via **kwargs. | ||
| """ | ||
| filter_kwargs = { | ||
| "field_name": field_name, | ||
| "label": label, | ||
| } | ||
|
|
||
| if self.lookup_expr: | ||
| filter_kwargs["lookup_expr"] = self.lookup_expr | ||
|
|
||
| if queryset is not None: | ||
| filter_kwargs["queryset"] = queryset | ||
|
|
||
| # Apply defaults from the spec | ||
| if self.extra_kwargs: | ||
| filter_kwargs.update(self.extra_kwargs) | ||
|
ifoughal marked this conversation as resolved.
|
||
|
|
||
| # Apply dynamic overrides (e.g. resolved choices) | ||
| filter_kwargs.update(kwargs) | ||
|
|
||
| return self.filter_class(**filter_kwargs) | ||
|
|
||
|
|
||
| FIELD_TYPE_FILTERS = { | ||
| CustomFieldTypeChoices.TYPE_TEXT: FilterSpec(django_filters.CharFilter, lookup_expr="icontains"), | ||
| CustomFieldTypeChoices.TYPE_LONGTEXT: FilterSpec(django_filters.CharFilter, lookup_expr="icontains"), | ||
| CustomFieldTypeChoices.TYPE_INTEGER: FilterSpec(django_filters.NumberFilter, lookup_expr="exact"), | ||
| CustomFieldTypeChoices.TYPE_DECIMAL: FilterSpec(django_filters.NumberFilter, lookup_expr="exact"), | ||
| CustomFieldTypeChoices.TYPE_BOOLEAN: FilterSpec(django_filters.BooleanFilter), | ||
| CustomFieldTypeChoices.TYPE_DATE: FilterSpec(django_filters.DateFilter, lookup_expr="exact"), | ||
| CustomFieldTypeChoices.TYPE_DATETIME: FilterSpec(django_filters.DateTimeFilter, lookup_expr="exact"), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure on here - normally we do something like:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a draft for this logic, but it would require multiple changes, also to note on models.py 1272) # TODO: Move all this logic to field_types.py get_filterform_field methodsmaybe a refactor would be needed prior to implementing this? Then, this logic should be added: if self.type in (
CustomFieldTypeChoices.TYPE_DATE,
CustomFieldTypeChoices.TYPE_DATETIME,
):
before_filter = self.to_filter(lookup_expr="lte")
if before_filter is not None:
filters[f"{self.name}__before"] = before_filter
after_filter = self.to_filter(lookup_expr="gte")
if after_filter is not None:
filters[f"{self.name}__after"] = after_filter |
||
| CustomFieldTypeChoices.TYPE_URL: FilterSpec(django_filters.CharFilter, lookup_expr="icontains"), | ||
| CustomFieldTypeChoices.TYPE_JSON: FilterSpec(django_filters.CharFilter, lookup_expr="icontains"), | ||
| CustomFieldTypeChoices.TYPE_SELECT: FilterSpec( | ||
| django_filters.ChoiceFilter, | ||
| extra_kwargs={"choices": lambda f: f.choices} | ||
| ), | ||
| CustomFieldTypeChoices.TYPE_MULTISELECT: FilterSpec( | ||
| django_filters.MultipleChoiceFilter, | ||
| extra_kwargs={"choices": lambda f: f.choices} | ||
| ), | ||
| CustomFieldTypeChoices.TYPE_OBJECT: FilterSpec(django_filters.ModelChoiceFilter), | ||
| CustomFieldTypeChoices.TYPE_MULTIOBJECT: FilterSpec(django_filters.ModelMultipleChoiceFilter), | ||
| } | ||
|
|
||
|
|
||
| class CustomObjectTypeFilterSet(NetBoxModelFilterSet): | ||
| class Meta: | ||
| model = CustomObjectType | ||
|
|
@@ -23,10 +87,40 @@ class Meta: | |
| ) | ||
|
|
||
|
|
||
| def build_filter_for_field(field) -> Optional[django_filters.Filter]: | ||
| if not (spec := FIELD_TYPE_FILTERS.get(field.type)): | ||
| return None | ||
|
|
||
| queryset = None | ||
| if field.type in ( | ||
| CustomFieldTypeChoices.TYPE_OBJECT, | ||
| CustomFieldTypeChoices.TYPE_MULTIOBJECT, | ||
| ): | ||
| related_object_type = getattr(field, "related_object_type", None) | ||
| if not related_object_type: | ||
| # Defensive guard: if data integrity is compromised and the related object type | ||
| # is missing, skip building a filter for this field rather than raising. | ||
| return None | ||
| queryset = related_object_type.model_class().objects.all() | ||
|
|
||
| extra_kwargs = {} | ||
| if spec.extra_kwargs: | ||
| for key, value in spec.extra_kwargs.items(): | ||
| extra_kwargs[key] = value(field) if callable(value) else value | ||
|
|
||
| return spec.build( | ||
| field_name=field.name, | ||
| label=field.label, | ||
| queryset=queryset, | ||
| **extra_kwargs, | ||
| ) | ||
|
arthanson marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def get_filterset_class(model): | ||
| """ | ||
| Create and return a filterset class for the given custom object model. | ||
| """ | ||
| # Get standard fields from the model | ||
| fields = [field.name for field in model._meta.fields] | ||
|
|
||
| meta = type( | ||
|
|
@@ -35,8 +129,6 @@ def get_filterset_class(model): | |
| { | ||
| "model": model, | ||
| "fields": fields, | ||
| # TODO: overrides should come from FieldType | ||
| # These are placeholders; should use different logic | ||
| "filter_overrides": { | ||
| JSONField: { | ||
| "filter_class": django_filters.CharFilter, | ||
|
|
@@ -70,10 +162,16 @@ def search(self, queryset, name, value): | |
|
|
||
| attrs = { | ||
| "Meta": meta, | ||
| "__module__": "database.filtersets", | ||
| "__module__": "netbox_custom_objects.filtersets", | ||
| "search": search, | ||
| } | ||
|
|
||
| # For each custom field, add a corresponding filter | ||
| for field in model.custom_object_type.fields.all(): | ||
| filter_instance = build_filter_for_field(field) | ||
| if filter_instance: | ||
| attrs[field.name] = filter_instance | ||
|
|
||
| return type( | ||
| f"{model._meta.object_name}FilterSet", | ||
| (NetBoxModelFilterSet,), | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.