mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-05 11:46:50 -06:00
Merge 9491356c7e into 20c260b126
This commit is contained in:
@@ -20,6 +20,10 @@ A dictionary mapping data backend types to their respective classes. These are u
|
|||||||
|
|
||||||
Stores registration made using `netbox.denormalized.register()`. For each model, a list of related models and their field mappings is maintained to facilitate automatic updates.
|
Stores registration made using `netbox.denormalized.register()`. For each model, a list of related models and their field mappings is maintained to facilitate automatic updates.
|
||||||
|
|
||||||
|
### `filtersets`
|
||||||
|
|
||||||
|
A dictionary mapping each model (identified by its app and label) to its filterset class, if one has been registered for it. Filtersets are registered using the `@register_filterset` decorator.
|
||||||
|
|
||||||
### `model_features`
|
### `model_features`
|
||||||
|
|
||||||
A dictionary of model features (e.g. custom fields, tags, etc.) mapped to the functions used to qualify a model as supporting each feature. Model features are registered using the `register_model_feature()` function in `netbox.utils`.
|
A dictionary of model features (e.g. custom fields, tags, etc.) mapped to the functions used to qualify a model as supporting each feature. Model features are registered using the `register_model_feature()` function in `netbox.utils`.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from dcim.filtersets import CabledObjectFilterSet
|
|||||||
from dcim.models import Interface, Location, Region, Site, SiteGroup
|
from dcim.models import Interface, Location, Region, Site, SiteGroup
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||||
|
from netbox.plugins.registration import register_filterset
|
||||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||||
from utilities.filters import (
|
from utilities.filters import (
|
||||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
||||||
@@ -29,6 +30,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@@ -93,6 +95,7 @@ class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
@@ -120,6 +123,7 @@ class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
||||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
@@ -147,6 +151,7 @@ class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -154,6 +159,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
@@ -265,6 +271,7 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -360,6 +367,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class CircuitGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
class CircuitGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -367,6 +375,7 @@ class CircuitGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'description')
|
fields = ('id', 'name', 'slug', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -466,6 +475,7 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
|
class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -473,6 +483,7 @@ class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='provider_network__provider',
|
field_name='provider_network__provider',
|
||||||
@@ -529,6 +540,7 @@ class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
|
class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from django.db.models import Q
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, PrimaryModelFilterSet
|
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, PrimaryModelFilterSet
|
||||||
|
from netbox.plugins.registration import register_filterset
|
||||||
from netbox.utils import get_data_backend_choices
|
from netbox.utils import get_data_backend_choices
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from utilities.filters import ContentTypeFilter
|
from utilities.filters import ContentTypeFilter
|
||||||
@@ -20,6 +21,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class DataSourceFilterSet(PrimaryModelFilterSet):
|
class DataSourceFilterSet(PrimaryModelFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=get_data_backend_choices,
|
choices=get_data_backend_choices,
|
||||||
@@ -48,6 +50,7 @@ class DataSourceFilterSet(PrimaryModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class DataFileFilterSet(ChangeLoggedModelFilterSet):
|
class DataFileFilterSet(ChangeLoggedModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search'
|
method='search'
|
||||||
@@ -75,6 +78,7 @@ class DataFileFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class JobFilterSet(BaseFilterSet):
|
class JobFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -139,6 +143,7 @@ class JobFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ObjectTypeFilterSet(BaseFilterSet):
|
class ObjectTypeFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -164,6 +169,7 @@ class ObjectTypeFilterSet(BaseFilterSet):
|
|||||||
return queryset.filter(features__icontains=value)
|
return queryset.filter(features__icontains=value)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ObjectChangeFilterSet(BaseFilterSet):
|
class ObjectChangeFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -203,6 +209,7 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ConfigRevisionFilterSet(BaseFilterSet):
|
class ConfigRevisionFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from netbox.filtersets import (
|
|||||||
AttributeFiltersMixin, BaseFilterSet, ChangeLoggedModelFilterSet, NestedGroupModelFilterSet,
|
AttributeFiltersMixin, BaseFilterSet, ChangeLoggedModelFilterSet, NestedGroupModelFilterSet,
|
||||||
OrganizationalModelFilterSet, PrimaryModelFilterSet, NetBoxModelFilterSet,
|
OrganizationalModelFilterSet, PrimaryModelFilterSet, NetBoxModelFilterSet,
|
||||||
)
|
)
|
||||||
|
from netbox.plugins.registration import register_filterset
|
||||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||||
from tenancy.models import *
|
from tenancy.models import *
|
||||||
from users.filterset_mixins import OwnerFilterMixin
|
from users.filterset_mixins import OwnerFilterMixin
|
||||||
@@ -84,6 +85,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class RegionFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
|
class RegionFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@@ -114,6 +116,7 @@ class RegionFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'description')
|
fields = ('id', 'name', 'slug', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class SiteGroupFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
|
class SiteGroupFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
@@ -144,6 +147,7 @@ class SiteGroupFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'description')
|
fields = ('id', 'name', 'slug', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=SiteStatusChoices,
|
choices=SiteStatusChoices,
|
||||||
@@ -208,6 +212,7 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
|
|||||||
return queryset.filter(qs_filter).distinct()
|
return queryset.filter(qs_filter).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, NestedGroupModelFilterSet):
|
class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, NestedGroupModelFilterSet):
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@@ -287,6 +292,7 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, NestedGroupMode
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class RackRoleFilterSet(OrganizationalModelFilterSet):
|
class RackRoleFilterSet(OrganizationalModelFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -294,6 +300,7 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class RackTypeFilterSet(PrimaryModelFilterSet):
|
class RackTypeFilterSet(PrimaryModelFilterSet):
|
||||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
@@ -332,6 +339,7 @@ class RackTypeFilterSet(PrimaryModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@@ -448,6 +456,7 @@ class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
@@ -537,6 +546,7 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -544,6 +554,7 @@ class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet)
|
|||||||
fields = ('id', 'name', 'slug', 'description')
|
fields = ('id', 'name', 'slug', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
||||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
@@ -687,6 +698,7 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
|||||||
return queryset.exclude(inventoryitemtemplates__isnull=value)
|
return queryset.exclude(inventoryitemtemplates__isnull=value)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ModuleTypeProfileFilterSet(PrimaryModelFilterSet):
|
class ModuleTypeProfileFilterSet(PrimaryModelFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -703,6 +715,7 @@ class ModuleTypeProfileFilterSet(PrimaryModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ModuleTypeFilterSet(AttributeFiltersMixin, PrimaryModelFilterSet):
|
class ModuleTypeFilterSet(AttributeFiltersMixin, PrimaryModelFilterSet):
|
||||||
profile_id = django_filters.ModelMultipleChoiceFilter(
|
profile_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=ModuleTypeProfile.objects.all(),
|
queryset=ModuleTypeProfile.objects.all(),
|
||||||
@@ -819,6 +832,7 @@ class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -826,6 +840,7 @@ class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
|
|||||||
fields = ('id', 'name', 'label', 'type', 'description')
|
fields = ('id', 'name', 'label', 'type', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -833,6 +848,7 @@ class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDevi
|
|||||||
fields = ('id', 'name', 'label', 'type', 'description')
|
fields = ('id', 'name', 'label', 'type', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -840,6 +856,7 @@ class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
|||||||
fields = ('id', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
fields = ('id', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||||
feed_leg = django_filters.MultipleChoiceFilter(
|
feed_leg = django_filters.MultipleChoiceFilter(
|
||||||
choices=PowerOutletFeedLegChoices,
|
choices=PowerOutletFeedLegChoices,
|
||||||
@@ -855,6 +872,7 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
|
|||||||
fields = ('id', 'name', 'label', 'type', 'color', 'feed_leg', 'description')
|
fields = ('id', 'name', 'label', 'type', 'color', 'feed_leg', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=InterfaceTypeChoices,
|
choices=InterfaceTypeChoices,
|
||||||
@@ -879,6 +897,7 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
|||||||
fields = ('id', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description')
|
fields = ('id', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
@@ -893,6 +912,7 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
|||||||
fields = ('id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description')
|
fields = ('id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
@@ -904,6 +924,7 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
|
|||||||
fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
|
fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -911,6 +932,7 @@ class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
|||||||
fields = ('id', 'name', 'label', 'position', 'description')
|
fields = ('id', 'name', 'label', 'position', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -918,6 +940,7 @@ class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
|
|||||||
fields = ('id', 'name', 'label', 'description')
|
fields = ('id', 'name', 'label', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=InventoryItemTemplate.objects.all(),
|
queryset=InventoryItemTemplate.objects.all(),
|
||||||
@@ -961,6 +984,7 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo
|
|||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class DeviceRoleFilterSet(NestedGroupModelFilterSet):
|
class DeviceRoleFilterSet(NestedGroupModelFilterSet):
|
||||||
config_template_id = django_filters.ModelMultipleChoiceFilter(
|
config_template_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=ConfigTemplate.objects.all(),
|
queryset=ConfigTemplate.objects.all(),
|
||||||
@@ -995,6 +1019,7 @@ class DeviceRoleFilterSet(NestedGroupModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'color', 'vm_role', 'description')
|
fields = ('id', 'name', 'slug', 'color', 'vm_role', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class PlatformFilterSet(NestedGroupModelFilterSet):
|
class PlatformFilterSet(NestedGroupModelFilterSet):
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
@@ -1052,6 +1077,7 @@ class PlatformFilterSet(NestedGroupModelFilterSet):
|
|||||||
return queryset.filter(Q(manufacturer=None) | Q(manufacturer__device_types=value))
|
return queryset.filter(Q(manufacturer=None) | Q(manufacturer__device_types=value))
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class DeviceFilterSet(
|
class DeviceFilterSet(
|
||||||
PrimaryModelFilterSet,
|
PrimaryModelFilterSet,
|
||||||
TenancyFilterSet,
|
TenancyFilterSet,
|
||||||
@@ -1354,6 +1380,7 @@ class DeviceFilterSet(
|
|||||||
return queryset.exclude(params)
|
return queryset.exclude(params)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class VirtualDeviceContextFilterSet(PrimaryModelFilterSet, TenancyFilterSet, PrimaryIPFilterSet):
|
class VirtualDeviceContextFilterSet(PrimaryModelFilterSet, TenancyFilterSet, PrimaryIPFilterSet):
|
||||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='device',
|
field_name='device',
|
||||||
@@ -1403,6 +1430,7 @@ class VirtualDeviceContextFilterSet(PrimaryModelFilterSet, TenancyFilterSet, Pri
|
|||||||
return queryset.exclude(params)
|
return queryset.exclude(params)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ModuleFilterSet(PrimaryModelFilterSet):
|
class ModuleFilterSet(PrimaryModelFilterSet):
|
||||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='module_type__manufacturer',
|
field_name='module_type__manufacturer',
|
||||||
@@ -1691,6 +1719,7 @@ class PathEndpointFilterSet(django_filters.FilterSet):
|
|||||||
return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False))
|
return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False))
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ConsolePortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
class ConsolePortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
@@ -1702,6 +1731,7 @@ class ConsolePortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
|
|||||||
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_position')
|
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_position')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ConsoleServerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
class ConsoleServerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
@@ -1713,6 +1743,7 @@ class ConsoleServerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFi
|
|||||||
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_position')
|
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_position')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class PowerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
class PowerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=PowerPortTypeChoices,
|
choices=PowerPortTypeChoices,
|
||||||
@@ -1727,6 +1758,7 @@ class PowerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=PowerOutletTypeChoices,
|
choices=PowerOutletTypeChoices,
|
||||||
@@ -1753,6 +1785,7 @@ class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class MACAddressFilterSet(PrimaryModelFilterSet):
|
class MACAddressFilterSet(PrimaryModelFilterSet):
|
||||||
mac_address = MultiValueMACAddressFilter()
|
mac_address = MultiValueMACAddressFilter()
|
||||||
assigned_object_type = ContentTypeFilter()
|
assigned_object_type = ContentTypeFilter()
|
||||||
@@ -1934,6 +1967,7 @@ class CommonInterfaceFilterSet(django_filters.FilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class InterfaceFilterSet(
|
class InterfaceFilterSet(
|
||||||
ModularDeviceComponentFilterSet,
|
ModularDeviceComponentFilterSet,
|
||||||
CabledObjectFilterSet,
|
CabledObjectFilterSet,
|
||||||
@@ -2096,6 +2130,7 @@ class InterfaceFilterSet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
|
class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
@@ -2113,6 +2148,7 @@ class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet)
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
|
class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
@@ -2127,6 +2163,7 @@ class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ModuleBayFilterSet(ModularDeviceComponentFilterSet):
|
class ModuleBayFilterSet(ModularDeviceComponentFilterSet):
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=ModuleBay.objects.all(),
|
queryset=ModuleBay.objects.all(),
|
||||||
@@ -2143,6 +2180,7 @@ class ModuleBayFilterSet(ModularDeviceComponentFilterSet):
|
|||||||
fields = ('id', 'name', 'label', 'position', 'description')
|
fields = ('id', 'name', 'label', 'position', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class DeviceBayFilterSet(DeviceComponentFilterSet):
|
class DeviceBayFilterSet(DeviceComponentFilterSet):
|
||||||
installed_device_id = django_filters.ModelMultipleChoiceFilter(
|
installed_device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@@ -2160,6 +2198,7 @@ class DeviceBayFilterSet(DeviceComponentFilterSet):
|
|||||||
fields = ('id', 'name', 'label', 'description')
|
fields = ('id', 'name', 'label', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class InventoryItemFilterSet(DeviceComponentFilterSet):
|
class InventoryItemFilterSet(DeviceComponentFilterSet):
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=InventoryItem.objects.all(),
|
queryset=InventoryItem.objects.all(),
|
||||||
@@ -2212,6 +2251,7 @@ class InventoryItemFilterSet(DeviceComponentFilterSet):
|
|||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
|
class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -2219,6 +2259,7 @@ class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
||||||
master_id = django_filters.ModelMultipleChoiceFilter(
|
master_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@@ -2295,6 +2336,7 @@ class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
|||||||
return queryset.filter(qs_filter).distinct()
|
return queryset.filter(qs_filter).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
||||||
termination_a_type = ContentTypeFilter(
|
termination_a_type = ContentTypeFilter(
|
||||||
field_name='terminations__termination_type'
|
field_name='terminations__termination_type'
|
||||||
@@ -2467,6 +2509,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
|||||||
return self.filter_by_termination_object(queryset, CircuitTermination, value)
|
return self.filter_by_termination_object(queryset, CircuitTermination, value)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
|
class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
|
||||||
termination_type = ContentTypeFilter()
|
termination_type = ContentTypeFilter()
|
||||||
|
|
||||||
@@ -2475,6 +2518,7 @@ class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
fields = ('id', 'cable', 'cable_end', 'position', 'termination_type', 'termination_id')
|
fields = ('id', 'cable', 'cable_end', 'position', 'termination_type', 'termination_id')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@@ -2533,6 +2577,7 @@ class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
|||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class PowerFeedFilterSet(PrimaryModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet, TenancyFilterSet):
|
class PowerFeedFilterSet(PrimaryModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet, TenancyFilterSet):
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
|
|||||||
@@ -638,6 +638,7 @@ class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm):
|
|||||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q')
|
selector_fields = ('filter_id', 'q')
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from django.utils.translation import gettext as _
|
|||||||
from core.models import DataSource, ObjectType
|
from core.models import DataSource, ObjectType
|
||||||
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet, PrimaryModelFilterSet
|
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet, PrimaryModelFilterSet
|
||||||
|
from netbox.plugins.registration import register_filterset
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from users.filterset_mixins import OwnerFilterMixin
|
from users.filterset_mixins import OwnerFilterMixin
|
||||||
from users.models import Group, User
|
from users.models import Group, User
|
||||||
@@ -40,6 +41,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ScriptFilterSet(BaseFilterSet):
|
class ScriptFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -62,6 +64,7 @@ class ScriptFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class WebhookFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
class WebhookFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -91,6 +94,7 @@ class WebhookFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class EventRuleFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
class EventRuleFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -131,6 +135,7 @@ class EventRuleFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
|||||||
return queryset.filter(event_types__overlap=value)
|
return queryset.filter(event_types__overlap=value)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class CustomFieldFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
class CustomFieldFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -180,6 +185,7 @@ class CustomFieldFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class CustomFieldChoiceSetFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
class CustomFieldChoiceSetFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -208,6 +214,7 @@ class CustomFieldChoiceSetFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet
|
|||||||
return queryset.filter(extra_choices__overlap=value)
|
return queryset.filter(extra_choices__overlap=value)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class CustomLinkFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
class CustomLinkFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -238,6 +245,7 @@ class CustomLinkFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ExportTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
class ExportTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -276,6 +284,7 @@ class ExportTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class SavedFilterFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
class SavedFilterFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -328,6 +337,7 @@ class SavedFilterFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
|||||||
return queryset.filter(Q(enabled=False) | Q(Q(shared=False) & ~Q(user=user)))
|
return queryset.filter(Q(enabled=False) | Q(Q(shared=False) & ~Q(user=user)))
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class TableConfigFilterSet(ChangeLoggedModelFilterSet):
|
class TableConfigFilterSet(ChangeLoggedModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -381,6 +391,7 @@ class TableConfigFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
return queryset.filter(Q(enabled=False) | Q(Q(shared=False) & ~Q(user=user)))
|
return queryset.filter(Q(enabled=False) | Q(Q(shared=False) & ~Q(user=user)))
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class BookmarkFilterSet(BaseFilterSet):
|
class BookmarkFilterSet(BaseFilterSet):
|
||||||
created = django_filters.DateTimeFilter()
|
created = django_filters.DateTimeFilter()
|
||||||
object_type_id = MultiValueNumberFilter()
|
object_type_id = MultiValueNumberFilter()
|
||||||
@@ -401,6 +412,7 @@ class BookmarkFilterSet(BaseFilterSet):
|
|||||||
fields = ('id', 'object_id')
|
fields = ('id', 'object_id')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class NotificationGroupFilterSet(ChangeLoggedModelFilterSet):
|
class NotificationGroupFilterSet(ChangeLoggedModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -444,6 +456,7 @@ class NotificationGroupFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet):
|
class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -465,6 +478,7 @@ class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class JournalEntryFilterSet(NetBoxModelFilterSet):
|
class JournalEntryFilterSet(NetBoxModelFilterSet):
|
||||||
created = django_filters.DateTimeFromToRangeFilter()
|
created = django_filters.DateTimeFromToRangeFilter()
|
||||||
assigned_object_type = ContentTypeFilter()
|
assigned_object_type = ContentTypeFilter()
|
||||||
@@ -495,6 +509,7 @@ class JournalEntryFilterSet(NetBoxModelFilterSet):
|
|||||||
return queryset.filter(comments__icontains=value)
|
return queryset.filter(comments__icontains=value)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class TagFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
class TagFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -555,6 +570,7 @@ class TagFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class TaggedItemFilterSet(BaseFilterSet):
|
class TaggedItemFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -590,6 +606,7 @@ class TaggedItemFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ConfigContextProfileFilterSet(PrimaryModelFilterSet):
|
class ConfigContextProfileFilterSet(PrimaryModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -620,6 +637,7 @@ class ConfigContextProfileFilterSet(PrimaryModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ConfigContextFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
class ConfigContextFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -789,6 +807,7 @@ class ConfigContextFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ConfigTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
class ConfigTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
|||||||
@@ -287,6 +287,7 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
|
|
||||||
|
|
||||||
class TableConfigFilterForm(SavedFiltersMixin, FilterForm):
|
class TableConfigFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
|
model = TableConfig
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id'),
|
FieldSet('q', 'filter_id'),
|
||||||
FieldSet('object_type_id', 'enabled', 'shared', 'weight', name=_('Attributes')),
|
FieldSet('object_type_id', 'enabled', 'shared', 'weight', name=_('Attributes')),
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ from dcim.models import Device, Interface, Region, Site, SiteGroup
|
|||||||
from netbox.filtersets import (
|
from netbox.filtersets import (
|
||||||
ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet, PrimaryModelFilterSet,
|
ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet, PrimaryModelFilterSet,
|
||||||
)
|
)
|
||||||
|
from netbox.plugins.registration import register_filterset
|
||||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||||
|
|
||||||
from utilities.filters import (
|
from utilities.filters import (
|
||||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||||
)
|
)
|
||||||
@@ -47,6 +47,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||||
import_target_id = django_filters.ModelMultipleChoiceFilter(
|
import_target_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='import_targets',
|
field_name='import_targets',
|
||||||
@@ -85,6 +86,7 @@ class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
fields = ('id', 'name', 'rd', 'enforce_unique', 'description')
|
fields = ('id', 'name', 'rd', 'enforce_unique', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||||
importing_vrf_id = django_filters.ModelMultipleChoiceFilter(
|
importing_vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='importing_vrfs',
|
field_name='importing_vrfs',
|
||||||
@@ -144,6 +146,7 @@ class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
fields = ('id', 'name', 'description')
|
fields = ('id', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class RIRFilterSet(OrganizationalModelFilterSet):
|
class RIRFilterSet(OrganizationalModelFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -151,6 +154,7 @@ class RIRFilterSet(OrganizationalModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'is_private', 'description')
|
fields = ('id', 'name', 'slug', 'is_private', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||||
family = django_filters.NumberFilter(
|
family = django_filters.NumberFilter(
|
||||||
field_name='prefix',
|
field_name='prefix',
|
||||||
@@ -198,6 +202,7 @@ class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
|
|||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ASNRangeFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
class ASNRangeFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||||
rir_id = django_filters.ModelMultipleChoiceFilter(
|
rir_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
@@ -223,6 +228,7 @@ class ASNRangeFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ASNFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
class ASNFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||||
rir_id = django_filters.ModelMultipleChoiceFilter(
|
rir_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
@@ -285,6 +291,7 @@ class ASNFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class RoleFilterSet(OrganizationalModelFilterSet):
|
class RoleFilterSet(OrganizationalModelFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -292,6 +299,7 @@ class RoleFilterSet(OrganizationalModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'description', 'weight')
|
fields = ('id', 'name', 'slug', 'description', 'weight')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||||
family = django_filters.NumberFilter(
|
family = django_filters.NumberFilter(
|
||||||
field_name='prefix',
|
field_name='prefix',
|
||||||
@@ -458,6 +466,7 @@ class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet,
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class IPRangeFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
class IPRangeFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||||
family = django_filters.NumberFilter(
|
family = django_filters.NumberFilter(
|
||||||
field_name='start_address',
|
field_name='start_address',
|
||||||
@@ -550,6 +559,7 @@ class IPRangeFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
|||||||
return queryset.filter(q)
|
return queryset.filter(q)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||||
family = django_filters.NumberFilter(
|
family = django_filters.NumberFilter(
|
||||||
field_name='address',
|
field_name='address',
|
||||||
@@ -786,6 +796,7 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class FHRPGroupFilterSet(PrimaryModelFilterSet):
|
class FHRPGroupFilterSet(PrimaryModelFilterSet):
|
||||||
protocol = django_filters.MultipleChoiceFilter(
|
protocol = django_filters.MultipleChoiceFilter(
|
||||||
choices=FHRPGroupProtocolChoices
|
choices=FHRPGroupProtocolChoices
|
||||||
@@ -833,6 +844,7 @@ class FHRPGroupFilterSet(PrimaryModelFilterSet):
|
|||||||
return queryset.filter(ip_filter)
|
return queryset.filter(ip_filter)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
|
class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
|
||||||
interface_type = ContentTypeFilter()
|
interface_type = ContentTypeFilter()
|
||||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
@@ -887,6 +899,7 @@ class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class VLANGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
class VLANGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||||
scope_type = ContentTypeFilter()
|
scope_type = ContentTypeFilter()
|
||||||
region = django_filters.NumberFilter(
|
region = django_filters.NumberFilter(
|
||||||
@@ -936,6 +949,7 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@@ -1087,6 +1101,7 @@ class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class VLANTranslationPolicyFilterSet(PrimaryModelFilterSet):
|
class VLANTranslationPolicyFilterSet(PrimaryModelFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -1103,6 +1118,7 @@ class VLANTranslationPolicyFilterSet(PrimaryModelFilterSet):
|
|||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class VLANTranslationRuleFilterSet(NetBoxModelFilterSet):
|
class VLANTranslationRuleFilterSet(NetBoxModelFilterSet):
|
||||||
policy_id = django_filters.ModelMultipleChoiceFilter(
|
policy_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=VLANTranslationPolicy.objects.all(),
|
queryset=VLANTranslationPolicy.objects.all(),
|
||||||
@@ -1134,6 +1150,7 @@ class VLANTranslationRuleFilterSet(NetBoxModelFilterSet):
|
|||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ServiceTemplateFilterSet(PrimaryModelFilterSet):
|
class ServiceTemplateFilterSet(PrimaryModelFilterSet):
|
||||||
port = NumericArrayFilter(
|
port = NumericArrayFilter(
|
||||||
field_name='ports',
|
field_name='ports',
|
||||||
@@ -1154,6 +1171,7 @@ class ServiceTemplateFilterSet(PrimaryModelFilterSet):
|
|||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ServiceFilterSet(ContactModelFilterSet, PrimaryModelFilterSet):
|
class ServiceFilterSet(ContactModelFilterSet, PrimaryModelFilterSet):
|
||||||
parent_object_type = ContentTypeFilter()
|
parent_object_type = ContentTypeFilter()
|
||||||
device = MultiValueCharFilter(
|
device = MultiValueCharFilter(
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
|
|
||||||
elif isinstance(existing_filter, (
|
elif isinstance(existing_filter, (
|
||||||
django_filters.filters.CharFilter,
|
django_filters.filters.CharFilter,
|
||||||
|
django_filters.ChoiceFilter,
|
||||||
django_filters.MultipleChoiceFilter,
|
django_filters.MultipleChoiceFilter,
|
||||||
filters.MultiValueCharFilter,
|
filters.MultiValueCharFilter,
|
||||||
filters.MultiValueMACAddressFilter
|
filters.MultiValueMACAddressFilter
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
from utilities.forms.fields import DynamicModelChoiceField
|
from utilities.forms.fields import DynamicModelChoiceField, QueryField
|
||||||
|
from utilities.forms.mixins import FilterModifierMixin
|
||||||
from .mixins import CustomFieldsMixin, SavedFiltersMixin
|
from .mixins import CustomFieldsMixin, SavedFiltersMixin
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@@ -15,7 +16,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form):
|
class NetBoxModelFilterSetForm(FilterModifierMixin, CustomFieldsMixin, SavedFiltersMixin, forms.Form):
|
||||||
"""
|
"""
|
||||||
Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the
|
Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the
|
||||||
corresponding FilterSet *must* provide a `q` filter.
|
corresponding FilterSet *must* provide a `q` filter.
|
||||||
@@ -27,7 +28,7 @@ class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form)
|
|||||||
selector_fields: An iterable of names of fields to display by default when rendering the form as
|
selector_fields: An iterable of names of fields to display by default when rendering the form as
|
||||||
a selector widget
|
a selector widget
|
||||||
"""
|
"""
|
||||||
q = forms.CharField(
|
q = QueryField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Search')
|
label=_('Search')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from .navigation import PluginMenu, PluginMenuButton, PluginMenuItem
|
|||||||
from .templates import PluginTemplateExtension
|
from .templates import PluginTemplateExtension
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'register_filterset',
|
||||||
'register_graphql_schema',
|
'register_graphql_schema',
|
||||||
'register_menu',
|
'register_menu',
|
||||||
'register_menu_items',
|
'register_menu_items',
|
||||||
@@ -44,6 +45,18 @@ def register_template_extensions(class_list):
|
|||||||
registry['plugins']['template_extensions'][model].append(template_extension)
|
registry['plugins']['template_extensions'][model].append(template_extension)
|
||||||
|
|
||||||
|
|
||||||
|
def register_filterset(filterset_class):
|
||||||
|
"""
|
||||||
|
Decorator for registering a FilterSet with the application registry.
|
||||||
|
|
||||||
|
Uses model identifier as key to match search index pattern.
|
||||||
|
"""
|
||||||
|
model = filterset_class._meta.model
|
||||||
|
label = f'{model._meta.app_label}.{model._meta.model_name}'
|
||||||
|
registry['filtersets'][label] = filterset_class
|
||||||
|
return filterset_class
|
||||||
|
|
||||||
|
|
||||||
def register_menu(menu):
|
def register_menu(menu):
|
||||||
if not isinstance(menu, PluginMenu):
|
if not isinstance(menu, PluginMenu):
|
||||||
raise TypeError(_("{item} must be an instance of netbox.plugins.PluginMenuItem").format(item=menu))
|
raise TypeError(_("{item} must be an instance of netbox.plugins.PluginMenuItem").format(item=menu))
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ registry = Registry({
|
|||||||
'data_backends': dict(),
|
'data_backends': dict(),
|
||||||
'denormalized_fields': collections.defaultdict(list),
|
'denormalized_fields': collections.defaultdict(list),
|
||||||
'event_types': dict(),
|
'event_types': dict(),
|
||||||
|
'filtersets': dict(),
|
||||||
'model_features': dict(),
|
'model_features': dict(),
|
||||||
'models': collections.defaultdict(set),
|
'models': collections.defaultdict(set),
|
||||||
'plugins': dict(),
|
'plugins': dict(),
|
||||||
|
|||||||
3013
netbox/project-static/dist/graphiql/graphiql.min.css
vendored
3013
netbox/project-static/dist/graphiql/graphiql.min.css
vendored
File diff suppressed because it is too large
Load Diff
96214
netbox/project-static/dist/graphiql/graphiql.min.js
vendored
96214
netbox/project-static/dist/graphiql/graphiql.min.js
vendored
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox.css
vendored
2
netbox/project-static/dist/netbox.css
vendored
File diff suppressed because one or more lines are too long
8
netbox/project-static/dist/netbox.js
vendored
8
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
8
netbox/project-static/dist/netbox.js.map
vendored
8
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
179
netbox/project-static/src/forms/filterModifiers.ts
Normal file
179
netbox/project-static/src/forms/filterModifiers.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { getElements } from '../util';
|
||||||
|
|
||||||
|
// Modifier codes for empty/null checking
|
||||||
|
// These map to Django's 'empty' lookup: field__empty=true/false
|
||||||
|
const MODIFIER_EMPTY_TRUE = 'empty_true';
|
||||||
|
const MODIFIER_EMPTY_FALSE = 'empty_false';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize filter modifier functionality.
|
||||||
|
*
|
||||||
|
* Handles transformation of field names based on modifier selection
|
||||||
|
* at form submission time using the FormData API.
|
||||||
|
*/
|
||||||
|
export function initFilterModifiers(): void {
|
||||||
|
for (const form of getElements<HTMLFormElement>('form')) {
|
||||||
|
const modifierSelects = form.querySelectorAll<HTMLSelectElement>('.modifier-select');
|
||||||
|
if (modifierSelects.length === 0) continue;
|
||||||
|
|
||||||
|
initializeFromURL(form);
|
||||||
|
|
||||||
|
modifierSelects.forEach(select => {
|
||||||
|
select.addEventListener('change', () => handleModifierChange(select));
|
||||||
|
handleModifierChange(select);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Must use submit event for GET forms
|
||||||
|
form.addEventListener('submit', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
handleFormDataTransform(form, formData);
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
if (value && String(value).trim()) {
|
||||||
|
params.append(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use getAttribute to avoid collision with form fields named 'action'
|
||||||
|
const actionUrl = form.getAttribute('action') || form.action;
|
||||||
|
window.location.href = `${actionUrl}?${params.toString()}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle modifier dropdown changes - disable/enable value input for empty lookups.
|
||||||
|
*/
|
||||||
|
function handleModifierChange(modifierSelect: HTMLSelectElement): void {
|
||||||
|
const group = modifierSelect.closest('.filter-modifier-group');
|
||||||
|
if (!group) return;
|
||||||
|
|
||||||
|
const wrapper = group.querySelector('.filter-value-container');
|
||||||
|
if (!wrapper) return;
|
||||||
|
|
||||||
|
const valueInput = wrapper.querySelector<
|
||||||
|
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||||
|
>('input, select, textarea');
|
||||||
|
|
||||||
|
if (!valueInput) return;
|
||||||
|
|
||||||
|
const modifier = modifierSelect.value;
|
||||||
|
|
||||||
|
if (modifier === MODIFIER_EMPTY_TRUE || modifier === MODIFIER_EMPTY_FALSE) {
|
||||||
|
valueInput.disabled = true;
|
||||||
|
valueInput.value = '';
|
||||||
|
const placeholder = modifierSelect.dataset.emptyPlaceholder || '(automatically set)';
|
||||||
|
valueInput.setAttribute('placeholder', placeholder);
|
||||||
|
} else {
|
||||||
|
valueInput.disabled = false;
|
||||||
|
valueInput.removeAttribute('placeholder');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform field names in FormData based on modifier selection.
|
||||||
|
*/
|
||||||
|
function handleFormDataTransform(form: HTMLFormElement, formData: FormData): void {
|
||||||
|
const modifierGroups = form.querySelectorAll('.filter-modifier-group');
|
||||||
|
|
||||||
|
for (const group of modifierGroups) {
|
||||||
|
const modifierSelect = group.querySelector<HTMLSelectElement>('.modifier-select');
|
||||||
|
const wrapper = group.querySelector('.filter-value-container');
|
||||||
|
if (!wrapper) continue;
|
||||||
|
|
||||||
|
const valueInput = wrapper.querySelector<
|
||||||
|
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||||
|
>('input, select, textarea');
|
||||||
|
|
||||||
|
if (!modifierSelect || !valueInput) continue;
|
||||||
|
|
||||||
|
const currentName = valueInput.name;
|
||||||
|
const modifier = modifierSelect.value;
|
||||||
|
|
||||||
|
if (modifier === MODIFIER_EMPTY_TRUE || modifier === MODIFIER_EMPTY_FALSE) {
|
||||||
|
formData.delete(currentName);
|
||||||
|
const boolValue = modifier === MODIFIER_EMPTY_TRUE ? 'true' : 'false';
|
||||||
|
formData.set(`${currentName}__empty`, boolValue);
|
||||||
|
} else {
|
||||||
|
const values = formData.getAll(currentName);
|
||||||
|
|
||||||
|
if (values.length > 0 && values.some(v => String(v).trim())) {
|
||||||
|
formData.delete(currentName);
|
||||||
|
const newName = modifier === 'exact' ? currentName : `${currentName}__${modifier}`;
|
||||||
|
|
||||||
|
for (const value of values) {
|
||||||
|
if (String(value).trim()) {
|
||||||
|
formData.append(newName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
formData.delete(currentName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize form state from URL parameters.
|
||||||
|
* Restores modifier selection and values from query string.
|
||||||
|
*
|
||||||
|
* Process:
|
||||||
|
* 1. Parse URL parameters
|
||||||
|
* 2. For each modifier group, check which lookup variant exists in URL
|
||||||
|
* 3. Set modifier dropdown to match
|
||||||
|
* 4. Populate value field with parameter value
|
||||||
|
*/
|
||||||
|
function initializeFromURL(form: HTMLFormElement): void {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
const modifierGroups = form.querySelectorAll('.filter-modifier-group');
|
||||||
|
|
||||||
|
for (const group of modifierGroups) {
|
||||||
|
const modifierSelect = group.querySelector<HTMLSelectElement>('.modifier-select');
|
||||||
|
const wrapper = group.querySelector('.filter-value-container');
|
||||||
|
if (!wrapper) continue;
|
||||||
|
|
||||||
|
const valueInput = wrapper.querySelector<
|
||||||
|
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||||
|
>('input, select, textarea');
|
||||||
|
|
||||||
|
if (!modifierSelect || !valueInput) continue;
|
||||||
|
|
||||||
|
const baseFieldName = valueInput.name;
|
||||||
|
|
||||||
|
// Special handling for empty - check if field__empty exists in URL
|
||||||
|
const emptyParam = `${baseFieldName}__empty`;
|
||||||
|
if (urlParams.has(emptyParam)) {
|
||||||
|
const emptyValue = urlParams.get(emptyParam);
|
||||||
|
const modifier = emptyValue === 'true' ? MODIFIER_EMPTY_TRUE : MODIFIER_EMPTY_FALSE;
|
||||||
|
modifierSelect.value = modifier;
|
||||||
|
continue; // Don't set value input for empty
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const option of modifierSelect.options) {
|
||||||
|
const lookup = option.value;
|
||||||
|
|
||||||
|
// Skip empty_true/false as they're handled above
|
||||||
|
if (lookup === MODIFIER_EMPTY_TRUE || lookup === MODIFIER_EMPTY_FALSE) continue;
|
||||||
|
|
||||||
|
const paramName = lookup === 'exact' ? baseFieldName : `${baseFieldName}__${lookup}`;
|
||||||
|
|
||||||
|
if (urlParams.has(paramName)) {
|
||||||
|
modifierSelect.value = lookup;
|
||||||
|
|
||||||
|
if (valueInput instanceof HTMLSelectElement && valueInput.multiple) {
|
||||||
|
const values = urlParams.getAll(paramName);
|
||||||
|
for (const option of valueInput.options) {
|
||||||
|
option.selected = values.includes(option.value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
valueInput.value = urlParams.get(paramName) || '';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { initFormElements } from './elements';
|
import { initFormElements } from './elements';
|
||||||
|
import { initFilterModifiers } from './filterModifiers';
|
||||||
import { initSpeedSelector } from './speedSelector';
|
import { initSpeedSelector } from './speedSelector';
|
||||||
|
|
||||||
export function initForms(): void {
|
export function initForms(): void {
|
||||||
for (const func of [initFormElements, initSpeedSelector]) {
|
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers]) {
|
||||||
func();
|
func();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,3 +32,11 @@ form.object-edit {
|
|||||||
border: 1px solid $red;
|
border: 1px solid $red;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter modifier dropdown sizing
|
||||||
|
.modifier-select {
|
||||||
|
min-width: 10rem;
|
||||||
|
max-width: 15rem;
|
||||||
|
width: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from django.utils.translation import gettext as _
|
|||||||
from netbox.filtersets import (
|
from netbox.filtersets import (
|
||||||
NestedGroupModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
|
NestedGroupModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
|
||||||
)
|
)
|
||||||
|
from netbox.plugins.registration import register_filterset
|
||||||
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
|
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ __all__ = (
|
|||||||
# Contacts
|
# Contacts
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ContactGroupFilterSet(NestedGroupModelFilterSet):
|
class ContactGroupFilterSet(NestedGroupModelFilterSet):
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
@@ -59,6 +61,7 @@ class ContactGroupFilterSet(NestedGroupModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'description')
|
fields = ('id', 'name', 'slug', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ContactRoleFilterSet(OrganizationalModelFilterSet):
|
class ContactRoleFilterSet(OrganizationalModelFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -66,6 +69,7 @@ class ContactRoleFilterSet(OrganizationalModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'description')
|
fields = ('id', 'name', 'slug', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ContactFilterSet(PrimaryModelFilterSet):
|
class ContactFilterSet(PrimaryModelFilterSet):
|
||||||
group_id = TreeNodeMultipleChoiceFilter(
|
group_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
@@ -100,6 +104,7 @@ class ContactFilterSet(PrimaryModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ContactAssignmentFilterSet(NetBoxModelFilterSet):
|
class ContactAssignmentFilterSet(NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -170,6 +175,7 @@ class ContactModelFilterSet(django_filters.FilterSet):
|
|||||||
# Tenancy
|
# Tenancy
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class TenantGroupFilterSet(NestedGroupModelFilterSet):
|
class TenantGroupFilterSet(NestedGroupModelFilterSet):
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
@@ -200,6 +206,7 @@ class TenantGroupFilterSet(NestedGroupModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'description')
|
fields = ('id', 'name', 'slug', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class TenantFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
class TenantFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||||
group_id = TreeNodeMultipleChoiceFilter(
|
group_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from django.utils.translation import gettext as _
|
|||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from extras.models import NotificationGroup
|
from extras.models import NotificationGroup
|
||||||
from netbox.filtersets import BaseFilterSet
|
from netbox.filtersets import BaseFilterSet
|
||||||
|
from netbox.plugins.registration import register_filterset
|
||||||
from users.models import Group, ObjectPermission, Owner, OwnerGroup, Token, User
|
from users.models import Group, ObjectPermission, Owner, OwnerGroup, Token, User
|
||||||
from utilities.filters import ContentTypeFilter
|
from utilities.filters import ContentTypeFilter
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class GroupFilterSet(BaseFilterSet):
|
class GroupFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -64,6 +66,7 @@ class GroupFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class UserFilterSet(BaseFilterSet):
|
class UserFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -120,6 +123,7 @@ class UserFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class TokenFilterSet(BaseFilterSet):
|
class TokenFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -181,6 +185,7 @@ class TokenFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ObjectPermissionFilterSet(BaseFilterSet):
|
class ObjectPermissionFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -248,6 +253,7 @@ class ObjectPermissionFilterSet(BaseFilterSet):
|
|||||||
return queryset.exclude(actions__contains=[action])
|
return queryset.exclude(actions__contains=[action])
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class OwnerGroupFilterSet(BaseFilterSet):
|
class OwnerGroupFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -267,6 +273,7 @@ class OwnerGroupFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class OwnerFilterSet(BaseFilterSet):
|
class OwnerFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
|||||||
0
netbox/utilities/filtersets.py
Normal file
0
netbox/utilities/filtersets.py
Normal file
@@ -17,11 +17,20 @@ __all__ = (
|
|||||||
'JSONField',
|
'JSONField',
|
||||||
'LaxURLField',
|
'LaxURLField',
|
||||||
'MACAddressField',
|
'MACAddressField',
|
||||||
|
'QueryField',
|
||||||
'SlugField',
|
'SlugField',
|
||||||
'TagFilterField',
|
'TagFilterField',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class QueryField(forms.CharField):
|
||||||
|
"""
|
||||||
|
A CharField subclass used for global search/query fields in filter forms.
|
||||||
|
This field type signals to FilterModifierMixin to skip enhancement with lookup modifiers.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommentField(forms.CharField):
|
class CommentField(forms.CharField):
|
||||||
"""
|
"""
|
||||||
A textarea with support for Markdown rendering. Exists mostly just to add a standard `help_text`.
|
A textarea with support for Markdown rendering. Exists mostly just to add a standard `help_text`.
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ from django import forms
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from netbox.models.features import ChangeLoggingMixin
|
from netbox.models.features import ChangeLoggingMixin
|
||||||
from utilities.forms.mixins import BackgroundJobMixin
|
from utilities.forms.fields import QueryField
|
||||||
|
from utilities.forms.mixins import BackgroundJobMixin, FilterModifierMixin
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'BulkDeleteForm',
|
'BulkDeleteForm',
|
||||||
@@ -140,11 +141,11 @@ class CSVModelForm(forms.ModelForm):
|
|||||||
return super().clean()
|
return super().clean()
|
||||||
|
|
||||||
|
|
||||||
class FilterForm(forms.Form):
|
class FilterForm(FilterModifierMixin, forms.Form):
|
||||||
"""
|
"""
|
||||||
Base Form class for FilterSet forms.
|
Base Form class for FilterSet forms.
|
||||||
"""
|
"""
|
||||||
q = forms.CharField(
|
q = QueryField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Search')
|
label=_('Search')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,13 +5,100 @@ from django import forms
|
|||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from netbox.registry import registry
|
||||||
|
from utilities.forms.fields import ColorField, QueryField, TagFilterField
|
||||||
|
from utilities.forms.widgets import FilterModifierWidget
|
||||||
|
from utilities.forms.widgets.modifiers import MODIFIER_EMPTY_FALSE, MODIFIER_EMPTY_TRUE
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'BackgroundJobMixin',
|
'BackgroundJobMixin',
|
||||||
'CheckLastUpdatedMixin',
|
'CheckLastUpdatedMixin',
|
||||||
'DistanceValidationMixin',
|
'DistanceValidationMixin',
|
||||||
|
'FilterModifierMixin',
|
||||||
|
'FORM_FIELD_LOOKUPS',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Mapping of form field types to their supported lookups
|
||||||
|
FORM_FIELD_LOOKUPS = {
|
||||||
|
QueryField: [],
|
||||||
|
forms.BooleanField: [],
|
||||||
|
forms.NullBooleanField: [],
|
||||||
|
forms.CharField: [
|
||||||
|
('exact', _('is')),
|
||||||
|
('n', _('is not')),
|
||||||
|
('ic', _('contains')),
|
||||||
|
('isw', _('starts with')),
|
||||||
|
('iew', _('ends with')),
|
||||||
|
('ie', _('equals (case-insensitive)')),
|
||||||
|
('regex', _('matches pattern')),
|
||||||
|
('iregex', _('matches pattern (case-insensitive)')),
|
||||||
|
(MODIFIER_EMPTY_TRUE, _('is empty')),
|
||||||
|
(MODIFIER_EMPTY_FALSE, _('is not empty')),
|
||||||
|
],
|
||||||
|
forms.IntegerField: [
|
||||||
|
('exact', _('is')),
|
||||||
|
('n', _('is not')),
|
||||||
|
('gt', _('greater than')),
|
||||||
|
('gte', _('at least')),
|
||||||
|
('lt', _('less than')),
|
||||||
|
('lte', _('at most')),
|
||||||
|
(MODIFIER_EMPTY_TRUE, _('is empty')),
|
||||||
|
(MODIFIER_EMPTY_FALSE, _('is not empty')),
|
||||||
|
],
|
||||||
|
forms.DecimalField: [
|
||||||
|
('exact', _('is')),
|
||||||
|
('n', _('is not')),
|
||||||
|
('gt', _('greater than')),
|
||||||
|
('gte', _('at least')),
|
||||||
|
('lt', _('less than')),
|
||||||
|
('lte', _('at most')),
|
||||||
|
(MODIFIER_EMPTY_TRUE, _('is empty')),
|
||||||
|
(MODIFIER_EMPTY_FALSE, _('is not empty')),
|
||||||
|
],
|
||||||
|
forms.DateField: [
|
||||||
|
('exact', _('is')),
|
||||||
|
('n', _('is not')),
|
||||||
|
('gt', _('after')),
|
||||||
|
('gte', _('on or after')),
|
||||||
|
('lt', _('before')),
|
||||||
|
('lte', _('on or before')),
|
||||||
|
(MODIFIER_EMPTY_TRUE, _('is empty')),
|
||||||
|
(MODIFIER_EMPTY_FALSE, _('is not empty')),
|
||||||
|
],
|
||||||
|
forms.ModelChoiceField: [
|
||||||
|
('exact', _('is')),
|
||||||
|
('n', _('is not')),
|
||||||
|
(MODIFIER_EMPTY_TRUE, _('is empty')),
|
||||||
|
(MODIFIER_EMPTY_FALSE, _('is not empty')),
|
||||||
|
],
|
||||||
|
ColorField: [
|
||||||
|
('exact', _('is')),
|
||||||
|
('n', _('is not')),
|
||||||
|
(MODIFIER_EMPTY_TRUE, _('is empty')),
|
||||||
|
(MODIFIER_EMPTY_FALSE, _('is not empty')),
|
||||||
|
],
|
||||||
|
TagFilterField: [
|
||||||
|
('exact', _('has these tags')),
|
||||||
|
('n', _('does not have these tags')),
|
||||||
|
(MODIFIER_EMPTY_TRUE, _('is empty')),
|
||||||
|
(MODIFIER_EMPTY_FALSE, _('is not empty')),
|
||||||
|
],
|
||||||
|
forms.ChoiceField: [
|
||||||
|
('exact', _('is')),
|
||||||
|
('n', _('is not')),
|
||||||
|
(MODIFIER_EMPTY_TRUE, _('is empty')),
|
||||||
|
(MODIFIER_EMPTY_FALSE, _('is not empty')),
|
||||||
|
],
|
||||||
|
forms.MultipleChoiceField: [
|
||||||
|
('exact', _('is')),
|
||||||
|
('n', _('is not')),
|
||||||
|
(MODIFIER_EMPTY_TRUE, _('is empty')),
|
||||||
|
(MODIFIER_EMPTY_FALSE, _('is not empty')),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BackgroundJobMixin(forms.Form):
|
class BackgroundJobMixin(forms.Form):
|
||||||
background_job = forms.BooleanField(
|
background_job = forms.BooleanField(
|
||||||
label=_('Background job'),
|
label=_('Background job'),
|
||||||
@@ -75,3 +162,68 @@ class DistanceValidationMixin(forms.Form):
|
|||||||
MaxValueValidator(Decimal(100000)),
|
MaxValueValidator(Decimal(100000)),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterModifierMixin:
|
||||||
|
"""
|
||||||
|
Mixin that enhances filter form fields with lookup modifier dropdowns.
|
||||||
|
|
||||||
|
Automatically detects fields that could benefit from multiple lookup options
|
||||||
|
and wraps their widgets with FilterModifierWidget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._enhance_fields_with_modifiers()
|
||||||
|
|
||||||
|
def _enhance_fields_with_modifiers(self):
|
||||||
|
"""Wrap compatible field widgets with FilterModifierWidget."""
|
||||||
|
|
||||||
|
model = getattr(self, 'model', None)
|
||||||
|
if model is None and hasattr(self, '_meta'):
|
||||||
|
model = getattr(self._meta, 'model', None)
|
||||||
|
|
||||||
|
filterset_class = None
|
||||||
|
if model:
|
||||||
|
key = f'{model._meta.app_label}.{model._meta.model_name}'
|
||||||
|
filterset_class = registry['filtersets'].get(key)
|
||||||
|
|
||||||
|
filterset = filterset_class() if filterset_class else None
|
||||||
|
|
||||||
|
for field_name, field in self.fields.items():
|
||||||
|
lookups = self._get_lookup_choices(field)
|
||||||
|
|
||||||
|
if filterset:
|
||||||
|
lookups = self._verify_lookups_with_filterset(field_name, lookups, filterset)
|
||||||
|
|
||||||
|
if len(lookups) > 1:
|
||||||
|
field.widget = FilterModifierWidget(
|
||||||
|
widget=field.widget,
|
||||||
|
lookups=lookups
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_lookup_choices(self, field):
|
||||||
|
"""Determine the available lookup choices for a given field.
|
||||||
|
|
||||||
|
Returns an empty list for fields that should not be enhanced.
|
||||||
|
"""
|
||||||
|
for field_class in field.__class__.__mro__:
|
||||||
|
if field_lookups := FORM_FIELD_LOOKUPS.get(field_class):
|
||||||
|
return field_lookups
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _verify_lookups_with_filterset(self, field_name, lookups, filterset):
|
||||||
|
"""Verify which lookups are actually supported by the FilterSet."""
|
||||||
|
verified_lookups = []
|
||||||
|
|
||||||
|
for lookup_code, lookup_label in lookups:
|
||||||
|
if lookup_code in (MODIFIER_EMPTY_TRUE, MODIFIER_EMPTY_FALSE):
|
||||||
|
filter_key = f'{field_name}__empty'
|
||||||
|
else:
|
||||||
|
filter_key = f'{field_name}__{lookup_code}' if lookup_code != 'exact' else field_name
|
||||||
|
|
||||||
|
if filter_key in filterset.filters:
|
||||||
|
verified_lookups.append((lookup_code, lookup_label))
|
||||||
|
|
||||||
|
return verified_lookups
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from .apiselect import *
|
from .apiselect import *
|
||||||
from .datetime import *
|
from .datetime import *
|
||||||
from .misc import *
|
from .misc import *
|
||||||
|
from .modifiers import *
|
||||||
from .select import *
|
from .select import *
|
||||||
|
|||||||
113
netbox/utilities/forms/widgets/modifiers.py
Normal file
113
netbox/utilities/forms/widgets/modifiers.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'FilterModifierWidget',
|
||||||
|
'MODIFIER_EMPTY_FALSE',
|
||||||
|
'MODIFIER_EMPTY_TRUE',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Modifier codes for empty/null checking
|
||||||
|
# These map to Django's 'empty' lookup: field__empty=true/false
|
||||||
|
MODIFIER_EMPTY_TRUE = 'empty_true'
|
||||||
|
MODIFIER_EMPTY_FALSE = 'empty_false'
|
||||||
|
|
||||||
|
|
||||||
|
class FilterModifierWidget(forms.Widget):
|
||||||
|
"""
|
||||||
|
Wraps an existing widget to add a modifier dropdown for filter lookups.
|
||||||
|
|
||||||
|
The original widget's semantics (name, id, attributes) are preserved.
|
||||||
|
The modifier dropdown controls which lookup type is used (exact, contains, etc.).
|
||||||
|
"""
|
||||||
|
template_name = 'widgets/filter_modifier.html'
|
||||||
|
|
||||||
|
def __init__(self, widget, lookups, attrs=None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
widget: The widget being wrapped (e.g., TextInput, NumberInput)
|
||||||
|
lookups: List of (lookup_code, label) tuples (e.g., [('exact', 'Is'), ('ic', 'Contains')])
|
||||||
|
attrs: Additional widget attributes
|
||||||
|
"""
|
||||||
|
self.original_widget = widget
|
||||||
|
self.lookups = lookups
|
||||||
|
super().__init__(attrs or getattr(widget, 'attrs', {}))
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
"""
|
||||||
|
Extract value from data, checking all possible lookup variants.
|
||||||
|
|
||||||
|
When form redisplays after validation error, the data may contain
|
||||||
|
serial__ic=test but the field is named serial. This method searches
|
||||||
|
all lookup variants to find the value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Just the value string for form validation. The modifier is reconstructed
|
||||||
|
during rendering from the query parameter names.
|
||||||
|
"""
|
||||||
|
# Special handling for empty - check if field__empty exists
|
||||||
|
empty_param = f"{name}__empty"
|
||||||
|
if empty_param in data:
|
||||||
|
# Return the boolean value for empty lookup
|
||||||
|
return data.get(empty_param)
|
||||||
|
|
||||||
|
# Try exact field name first
|
||||||
|
value = self.original_widget.value_from_datadict(data, files, name)
|
||||||
|
|
||||||
|
# If not found, check all modifier variants
|
||||||
|
# Note: SelectMultiple returns [] (empty list) when not found, not None
|
||||||
|
if value is None or (isinstance(value, list) and len(value) == 0):
|
||||||
|
for lookup, _ in self.lookups:
|
||||||
|
if lookup == 'exact':
|
||||||
|
continue # Already checked above
|
||||||
|
# Skip empty_true/false variants - they're handled above
|
||||||
|
if lookup in (MODIFIER_EMPTY_TRUE, MODIFIER_EMPTY_FALSE):
|
||||||
|
continue
|
||||||
|
lookup_name = f"{name}__{lookup}"
|
||||||
|
test_value = self.original_widget.value_from_datadict(data, files, lookup_name)
|
||||||
|
if test_value is not None:
|
||||||
|
value = test_value
|
||||||
|
break
|
||||||
|
|
||||||
|
# Return None if no value found (prevents field appearing in changed_data)
|
||||||
|
# Handle all widget empty value representations
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, str) and not value.strip():
|
||||||
|
return None
|
||||||
|
if isinstance(value, (list, tuple)) and len(value) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Return just the value for form validation
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_context(self, name, value, attrs):
|
||||||
|
"""
|
||||||
|
Build context for template rendering.
|
||||||
|
|
||||||
|
Includes both the original widget's context and our modifier-specific data.
|
||||||
|
Note: value is now just a simple value (string/int/etc), not a dict.
|
||||||
|
The JavaScript initializeFromURL() will set the correct modifier dropdown
|
||||||
|
value based on URL parameters.
|
||||||
|
"""
|
||||||
|
# Propagate any attrs set on the wrapper (like data-url from get_bound_field)
|
||||||
|
# to the original widget before rendering
|
||||||
|
self.original_widget.attrs.update(self.attrs)
|
||||||
|
|
||||||
|
# Get context from the original widget
|
||||||
|
original_context = self.original_widget.get_context(name, value, attrs)
|
||||||
|
|
||||||
|
# Build our wrapper context
|
||||||
|
context = super().get_context(name, value, attrs)
|
||||||
|
context['widget']['original_widget'] = original_context['widget']
|
||||||
|
context['widget']['lookups'] = self.lookups
|
||||||
|
context['widget']['field_name'] = name
|
||||||
|
|
||||||
|
# Default to 'exact' - JavaScript will update based on URL params
|
||||||
|
context['widget']['current_modifier'] = 'exact'
|
||||||
|
context['widget']['current_value'] = value or ''
|
||||||
|
|
||||||
|
# Translatable placeholder for empty lookups
|
||||||
|
context['widget']['empty_placeholder'] = _('(automatically set)')
|
||||||
|
|
||||||
|
return context
|
||||||
18
netbox/utilities/templates/widgets/filter_modifier.html
Normal file
18
netbox/utilities/templates/widgets/filter_modifier.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<div class="d-flex filter-modifier-group">
|
||||||
|
{% if widget.lookups %}
|
||||||
|
{# Modifier dropdown - NO name attribute, just a UI control #}
|
||||||
|
<select class="form-select modifier-select"
|
||||||
|
data-field="{{ widget.field_name }}"
|
||||||
|
data-empty-placeholder="{{ widget.empty_placeholder }}"
|
||||||
|
aria-label="Modifier">
|
||||||
|
{% for lookup, label in widget.lookups %}
|
||||||
|
<option value="{{ lookup }}"{% if widget.current_modifier == lookup %} selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Original widget - rendered exactly as it would be without our wrapper #}
|
||||||
|
<div class="ms-2 flex-grow-1 filter-value-container">
|
||||||
|
{% include widget.original_widget.template_name with widget=widget.original_widget %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -5,9 +5,11 @@ from urllib.parse import quote
|
|||||||
from django import template
|
from django import template
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
from django.utils.html import conditional_escape
|
from django.utils.html import conditional_escape
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from utilities.forms import get_selected_values, TableConfigForm
|
from utilities.forms import get_selected_values, TableConfigForm
|
||||||
|
from utilities.forms.mixins import FORM_FIELD_LOOKUPS
|
||||||
from utilities.views import get_viewname, get_action_url
|
from utilities.views import get_viewname, get_action_url
|
||||||
from netbox.settings import DISK_BASE_UNIT, RAM_BASE_UNIT
|
from netbox.settings import DISK_BASE_UNIT, RAM_BASE_UNIT
|
||||||
|
|
||||||
@@ -418,7 +420,20 @@ def applied_filters(context, model, form, query_params):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
querydict = query_params.copy()
|
querydict = query_params.copy()
|
||||||
if filter_name not in querydict:
|
|
||||||
|
# Check if this is a modifier-enhanced field
|
||||||
|
# Field may be in querydict as field__lookup instead of field
|
||||||
|
param_name = None
|
||||||
|
if filter_name in querydict:
|
||||||
|
param_name = filter_name
|
||||||
|
else:
|
||||||
|
# Check for modifier variants (field__ic, field__isw, etc.)
|
||||||
|
for key in querydict.keys():
|
||||||
|
if key.startswith(f'{filter_name}__'):
|
||||||
|
param_name = key
|
||||||
|
break
|
||||||
|
|
||||||
|
if param_name is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip saved filters, as they're displayed alongside the quick search widget
|
# Skip saved filters, as they're displayed alongside the quick search widget
|
||||||
@@ -426,14 +441,46 @@ def applied_filters(context, model, form, query_params):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
bound_field = form.fields[filter_name].get_bound_field(form, filter_name)
|
bound_field = form.fields[filter_name].get_bound_field(form, filter_name)
|
||||||
querydict.pop(filter_name)
|
querydict.pop(param_name)
|
||||||
|
|
||||||
|
# Extract modifier from parameter name (e.g., "serial__ic" → "ic")
|
||||||
|
if '__' in param_name:
|
||||||
|
modifier = param_name.split('__', 1)[1]
|
||||||
|
else:
|
||||||
|
modifier = 'exact'
|
||||||
|
|
||||||
|
# Get display value
|
||||||
display_value = ', '.join([str(v) for v in get_selected_values(form, filter_name)])
|
display_value = ', '.join([str(v) for v in get_selected_values(form, filter_name)])
|
||||||
|
|
||||||
|
# Get the correct lookup label for this field's type
|
||||||
|
lookup_label = None
|
||||||
|
if modifier != 'exact':
|
||||||
|
field = form.fields[filter_name]
|
||||||
|
for field_class in field.__class__.__mro__:
|
||||||
|
if field_lookups := FORM_FIELD_LOOKUPS.get(field_class):
|
||||||
|
for lookup_code, label in field_lookups:
|
||||||
|
if lookup_code == modifier:
|
||||||
|
lookup_label = label
|
||||||
|
break
|
||||||
|
if lookup_label:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Special handling for empty lookup (boolean value)
|
||||||
|
if modifier == 'empty':
|
||||||
|
if display_value.lower() in ('true', '1'):
|
||||||
|
link_text = f'{bound_field.label} {_("is empty")}'
|
||||||
|
else:
|
||||||
|
link_text = f'{bound_field.label} {_("is not empty")}'
|
||||||
|
elif lookup_label:
|
||||||
|
link_text = f'{bound_field.label} {lookup_label}: {display_value}'
|
||||||
|
else:
|
||||||
|
link_text = f'{bound_field.label}: {display_value}'
|
||||||
|
|
||||||
applied_filters.append({
|
applied_filters.append({
|
||||||
'name': filter_name,
|
'name': param_name, # Use actual param name for removal link
|
||||||
'value': form.cleaned_data[filter_name],
|
'value': form.cleaned_data.get(filter_name),
|
||||||
'link_url': f'?{querydict.urlencode()}',
|
'link_url': f'?{querydict.urlencode()}',
|
||||||
'link_text': f'{bound_field.label}: {display_value}',
|
'link_text': link_text,
|
||||||
})
|
})
|
||||||
|
|
||||||
save_link = None
|
save_link = None
|
||||||
|
|||||||
293
netbox/utilities/tests/test_filter_modifiers.py
Normal file
293
netbox/utilities/tests/test_filter_modifiers.py
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.db import models
|
||||||
|
from django.http import QueryDict
|
||||||
|
from django.template import Context
|
||||||
|
from django.test import RequestFactory, TestCase
|
||||||
|
|
||||||
|
import dcim.filtersets # noqa: F401 - Import to register Device filterset
|
||||||
|
from dcim.forms.filtersets import DeviceFilterForm
|
||||||
|
from dcim.models import Device
|
||||||
|
from netbox.filtersets import BaseFilterSet
|
||||||
|
from netbox.plugins.registration import register_filterset
|
||||||
|
from users.models import User
|
||||||
|
from utilities.forms.fields import TagFilterField
|
||||||
|
from utilities.forms.mixins import FilterModifierMixin
|
||||||
|
from utilities.forms.widgets import FilterModifierWidget
|
||||||
|
from utilities.templatetags.helpers import applied_filters
|
||||||
|
|
||||||
|
|
||||||
|
# Test model for FilterModifierMixin tests
|
||||||
|
class TestModel(models.Model):
|
||||||
|
"""Dummy model for testing filter modifiers."""
|
||||||
|
char_field = models.CharField(max_length=100, blank=True)
|
||||||
|
integer_field = models.IntegerField(null=True, blank=True)
|
||||||
|
decimal_field = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
|
||||||
|
date_field = models.DateField(null=True, blank=True)
|
||||||
|
boolean_field = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = 'utilities'
|
||||||
|
managed = False # Don't create actual database table
|
||||||
|
|
||||||
|
|
||||||
|
# Test filterset using BaseFilterSet to automatically generate lookups
|
||||||
|
@register_filterset
|
||||||
|
class TestFilterSet(BaseFilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = TestModel
|
||||||
|
fields = ['char_field', 'integer_field', 'decimal_field', 'date_field', 'boolean_field']
|
||||||
|
|
||||||
|
|
||||||
|
class FilterModifierWidgetTest(TestCase):
|
||||||
|
"""Tests for FilterModifierWidget value extraction and rendering."""
|
||||||
|
|
||||||
|
def test_value_from_datadict_finds_value_in_lookup_variant(self):
|
||||||
|
"""
|
||||||
|
Widget should find value from serial__ic when field is named serial.
|
||||||
|
This is critical for form redisplay after validation errors.
|
||||||
|
"""
|
||||||
|
widget = FilterModifierWidget(
|
||||||
|
widget=forms.TextInput(),
|
||||||
|
lookups=[('exact', 'Is'), ('ic', 'Contains'), ('isw', 'Starts With')]
|
||||||
|
)
|
||||||
|
data = QueryDict('serial__ic=test123')
|
||||||
|
|
||||||
|
result = widget.value_from_datadict(data, {}, 'serial')
|
||||||
|
|
||||||
|
self.assertEqual(result, 'test123')
|
||||||
|
|
||||||
|
def test_value_from_datadict_handles_exact_match(self):
|
||||||
|
"""Widget should detect exact match when field name has no modifier."""
|
||||||
|
widget = FilterModifierWidget(
|
||||||
|
widget=forms.TextInput(),
|
||||||
|
lookups=[('exact', 'Is'), ('ic', 'Contains')]
|
||||||
|
)
|
||||||
|
data = QueryDict('serial=test456')
|
||||||
|
|
||||||
|
result = widget.value_from_datadict(data, {}, 'serial')
|
||||||
|
|
||||||
|
self.assertEqual(result, 'test456')
|
||||||
|
|
||||||
|
def test_value_from_datadict_returns_none_when_no_value(self):
|
||||||
|
"""Widget should return None when no data present to avoid appearing in changed_data."""
|
||||||
|
widget = FilterModifierWidget(
|
||||||
|
widget=forms.TextInput(),
|
||||||
|
lookups=[('exact', 'Is'), ('ic', 'Contains')]
|
||||||
|
)
|
||||||
|
data = QueryDict('')
|
||||||
|
|
||||||
|
result = widget.value_from_datadict(data, {}, 'serial')
|
||||||
|
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_get_context_includes_original_widget_and_lookups(self):
|
||||||
|
"""Widget context should include original widget context and lookup choices."""
|
||||||
|
widget = FilterModifierWidget(
|
||||||
|
widget=forms.TextInput(),
|
||||||
|
lookups=[('exact', 'Is'), ('ic', 'Contains'), ('isw', 'Starts With')]
|
||||||
|
)
|
||||||
|
value = 'test'
|
||||||
|
|
||||||
|
context = widget.get_context('serial', value, {})
|
||||||
|
|
||||||
|
self.assertIn('original_widget', context['widget'])
|
||||||
|
self.assertEqual(
|
||||||
|
context['widget']['lookups'],
|
||||||
|
[('exact', 'Is'), ('ic', 'Contains'), ('isw', 'Starts With')]
|
||||||
|
)
|
||||||
|
self.assertEqual(context['widget']['field_name'], 'serial')
|
||||||
|
self.assertEqual(context['widget']['current_modifier'], 'exact') # Defaults to exact, JS updates from URL
|
||||||
|
self.assertEqual(context['widget']['current_value'], 'test')
|
||||||
|
|
||||||
|
def test_widget_renders_modifier_dropdown_and_input(self):
|
||||||
|
"""Widget should render modifier dropdown alongside original input."""
|
||||||
|
widget = FilterModifierWidget(
|
||||||
|
widget=forms.TextInput(),
|
||||||
|
lookups=[('exact', 'Is'), ('ic', 'Contains')]
|
||||||
|
)
|
||||||
|
|
||||||
|
html = widget.render('serial', 'test', {})
|
||||||
|
|
||||||
|
# Should contain modifier dropdown
|
||||||
|
self.assertIn('class="form-select modifier-select"', html)
|
||||||
|
self.assertIn('data-field="serial"', html)
|
||||||
|
self.assertIn('<option value="exact" selected>Is</option>', html)
|
||||||
|
self.assertIn('<option value="ic">Contains</option>', html)
|
||||||
|
|
||||||
|
# Should contain original input
|
||||||
|
self.assertIn('type="text"', html)
|
||||||
|
self.assertIn('name="serial"', html)
|
||||||
|
self.assertIn('value="test"', html)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterModifierMixinTest(TestCase):
|
||||||
|
"""Tests for FilterModifierMixin form field enhancement."""
|
||||||
|
|
||||||
|
def test_mixin_enhances_char_field_with_modifiers(self):
|
||||||
|
"""CharField should be enhanced with contains/starts/ends modifiers."""
|
||||||
|
class TestForm(FilterModifierMixin, forms.Form):
|
||||||
|
char_field = forms.CharField(required=False)
|
||||||
|
model = TestModel
|
||||||
|
|
||||||
|
form = TestForm()
|
||||||
|
|
||||||
|
self.assertIsInstance(form.fields['char_field'].widget, FilterModifierWidget)
|
||||||
|
lookup_codes = [lookup[0] for lookup in form.fields['char_field'].widget.lookups]
|
||||||
|
expected_lookups = ['exact', 'n', 'ic', 'isw', 'iew', 'ie', 'regex', 'iregex', 'empty_true', 'empty_false']
|
||||||
|
self.assertEqual(lookup_codes, expected_lookups)
|
||||||
|
|
||||||
|
def test_mixin_skips_boolean_fields(self):
|
||||||
|
"""Boolean fields should not be enhanced."""
|
||||||
|
class TestForm(FilterModifierMixin, forms.Form):
|
||||||
|
boolean_field = forms.BooleanField(required=False)
|
||||||
|
model = TestModel
|
||||||
|
|
||||||
|
form = TestForm()
|
||||||
|
|
||||||
|
self.assertNotIsInstance(form.fields['boolean_field'].widget, FilterModifierWidget)
|
||||||
|
|
||||||
|
def test_mixin_enhances_tag_filter_field(self):
|
||||||
|
"""TagFilterField should be enhanced even though it's a MultipleChoiceField."""
|
||||||
|
class TestForm(FilterModifierMixin, forms.Form):
|
||||||
|
tag = TagFilterField(Device)
|
||||||
|
model = Device
|
||||||
|
|
||||||
|
form = TestForm()
|
||||||
|
|
||||||
|
self.assertIsInstance(form.fields['tag'].widget, FilterModifierWidget)
|
||||||
|
tag_lookups = [lookup[0] for lookup in form.fields['tag'].widget.lookups]
|
||||||
|
# Device filterset has tag and tag__n but not tag__empty
|
||||||
|
expected_lookups = ['exact', 'n']
|
||||||
|
self.assertEqual(tag_lookups, expected_lookups)
|
||||||
|
|
||||||
|
def test_mixin_enhances_integer_field(self):
|
||||||
|
"""IntegerField should be enhanced with comparison modifiers."""
|
||||||
|
class TestForm(FilterModifierMixin, forms.Form):
|
||||||
|
integer_field = forms.IntegerField(required=False)
|
||||||
|
model = TestModel
|
||||||
|
|
||||||
|
form = TestForm()
|
||||||
|
|
||||||
|
self.assertIsInstance(form.fields['integer_field'].widget, FilterModifierWidget)
|
||||||
|
lookup_codes = [lookup[0] for lookup in form.fields['integer_field'].widget.lookups]
|
||||||
|
expected_lookups = ['exact', 'n', 'gt', 'gte', 'lt', 'lte', 'empty_true', 'empty_false']
|
||||||
|
self.assertEqual(lookup_codes, expected_lookups)
|
||||||
|
|
||||||
|
def test_mixin_enhances_decimal_field(self):
|
||||||
|
"""DecimalField should be enhanced with comparison modifiers."""
|
||||||
|
class TestForm(FilterModifierMixin, forms.Form):
|
||||||
|
decimal_field = forms.DecimalField(required=False)
|
||||||
|
model = TestModel
|
||||||
|
|
||||||
|
form = TestForm()
|
||||||
|
|
||||||
|
self.assertIsInstance(form.fields['decimal_field'].widget, FilterModifierWidget)
|
||||||
|
lookup_codes = [lookup[0] for lookup in form.fields['decimal_field'].widget.lookups]
|
||||||
|
expected_lookups = ['exact', 'n', 'gt', 'gte', 'lt', 'lte', 'empty_true', 'empty_false']
|
||||||
|
self.assertEqual(lookup_codes, expected_lookups)
|
||||||
|
|
||||||
|
def test_mixin_enhances_date_field(self):
|
||||||
|
"""DateField should be enhanced with date-appropriate modifiers."""
|
||||||
|
class TestForm(FilterModifierMixin, forms.Form):
|
||||||
|
date_field = forms.DateField(required=False)
|
||||||
|
model = TestModel
|
||||||
|
|
||||||
|
form = TestForm()
|
||||||
|
|
||||||
|
self.assertIsInstance(form.fields['date_field'].widget, FilterModifierWidget)
|
||||||
|
lookup_codes = [lookup[0] for lookup in form.fields['date_field'].widget.lookups]
|
||||||
|
expected_lookups = ['exact', 'n', 'gt', 'gte', 'lt', 'lte', 'empty_true', 'empty_false']
|
||||||
|
self.assertEqual(lookup_codes, expected_lookups)
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendedLookupFilterPillsTest(TestCase):
|
||||||
|
"""Tests for filter pill rendering of extended lookups."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.user = User.objects.create(username='test_user')
|
||||||
|
|
||||||
|
def test_negation_lookup_filter_pill(self):
|
||||||
|
"""Filter pill should show 'is not' for negation lookup."""
|
||||||
|
query_params = QueryDict('serial__n=ABC123')
|
||||||
|
form = DeviceFilterForm(query_params)
|
||||||
|
|
||||||
|
request = RequestFactory().get('/', query_params)
|
||||||
|
request.user = self.user
|
||||||
|
context = Context({'request': request})
|
||||||
|
result = applied_filters(context, Device, form, query_params)
|
||||||
|
|
||||||
|
self.assertGreater(len(result['applied_filters']), 0)
|
||||||
|
filter_pill = result['applied_filters'][0]
|
||||||
|
self.assertIn('is not', filter_pill['link_text'].lower())
|
||||||
|
self.assertIn('ABC123', filter_pill['link_text'])
|
||||||
|
|
||||||
|
def test_regex_lookup_filter_pill(self):
|
||||||
|
"""Filter pill should show 'matches pattern' for regex lookup."""
|
||||||
|
query_params = QueryDict('serial__regex=^ABC.*')
|
||||||
|
form = DeviceFilterForm(query_params)
|
||||||
|
|
||||||
|
request = RequestFactory().get('/', query_params)
|
||||||
|
request.user = self.user
|
||||||
|
context = Context({'request': request})
|
||||||
|
result = applied_filters(context, Device, form, query_params)
|
||||||
|
|
||||||
|
self.assertGreater(len(result['applied_filters']), 0)
|
||||||
|
filter_pill = result['applied_filters'][0]
|
||||||
|
self.assertIn('matches pattern', filter_pill['link_text'].lower())
|
||||||
|
|
||||||
|
def test_exact_lookup_filter_pill(self):
|
||||||
|
"""Filter pill should show field label and value without lookup modifier for exact match."""
|
||||||
|
query_params = QueryDict('serial=ABC123')
|
||||||
|
form = DeviceFilterForm(query_params)
|
||||||
|
|
||||||
|
request = RequestFactory().get('/', query_params)
|
||||||
|
request.user = self.user
|
||||||
|
context = Context({'request': request})
|
||||||
|
result = applied_filters(context, Device, form, query_params)
|
||||||
|
|
||||||
|
self.assertGreater(len(result['applied_filters']), 0)
|
||||||
|
filter_pill = result['applied_filters'][0]
|
||||||
|
# Should not contain lookup modifier text
|
||||||
|
self.assertNotIn('is not', filter_pill['link_text'].lower())
|
||||||
|
self.assertNotIn('matches pattern', filter_pill['link_text'].lower())
|
||||||
|
self.assertNotIn('contains', filter_pill['link_text'].lower())
|
||||||
|
# Should contain field label and value
|
||||||
|
self.assertIn('Serial', filter_pill['link_text'])
|
||||||
|
self.assertIn('ABC123', filter_pill['link_text'])
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyLookupTest(TestCase):
|
||||||
|
"""Tests for empty (is empty/not empty) lookup support."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.user = User.objects.create(username='test_user')
|
||||||
|
|
||||||
|
def test_empty_true_appears_in_filter_pills(self):
|
||||||
|
"""Filter pill should show 'Is Empty' for empty=true."""
|
||||||
|
query_params = QueryDict('serial__empty=true')
|
||||||
|
form = DeviceFilterForm(query_params)
|
||||||
|
|
||||||
|
request = RequestFactory().get('/', query_params)
|
||||||
|
request.user = self.user
|
||||||
|
context = Context({'request': request})
|
||||||
|
result = applied_filters(context, Device, form, query_params)
|
||||||
|
|
||||||
|
self.assertGreater(len(result['applied_filters']), 0)
|
||||||
|
filter_pill = result['applied_filters'][0]
|
||||||
|
self.assertIn('empty', filter_pill['link_text'].lower())
|
||||||
|
|
||||||
|
def test_empty_false_appears_in_filter_pills(self):
|
||||||
|
"""Filter pill should show 'Is Not Empty' for empty=false."""
|
||||||
|
query_params = QueryDict('serial__empty=false')
|
||||||
|
form = DeviceFilterForm(query_params)
|
||||||
|
|
||||||
|
request = RequestFactory().get('/', query_params)
|
||||||
|
request.user = self.user
|
||||||
|
context = Context({'request': request})
|
||||||
|
result = applied_filters(context, Device, form, query_params)
|
||||||
|
|
||||||
|
self.assertGreater(len(result['applied_filters']), 0)
|
||||||
|
filter_pill = result['applied_filters'][0]
|
||||||
|
self.assertIn('not empty', filter_pill['link_text'].lower())
|
||||||
@@ -10,8 +10,8 @@ from extras.filtersets import LocalConfigContextFilterSet
|
|||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
from ipam.filtersets import PrimaryIPFilterSet
|
from ipam.filtersets import PrimaryIPFilterSet
|
||||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||||
|
from netbox.plugins.registration import register_filterset
|
||||||
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
||||||
|
|
||||||
from users.filterset_mixins import OwnerFilterMixin
|
from users.filterset_mixins import OwnerFilterMixin
|
||||||
from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
|
from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
|
||||||
from .choices import *
|
from .choices import *
|
||||||
@@ -27,6 +27,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ClusterTypeFilterSet(OrganizationalModelFilterSet):
|
class ClusterTypeFilterSet(OrganizationalModelFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -34,6 +35,7 @@ class ClusterTypeFilterSet(OrganizationalModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'description')
|
fields = ('id', 'name', 'slug', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ClusterGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
class ClusterGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -41,6 +43,7 @@ class ClusterGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet)
|
|||||||
fields = ('id', 'name', 'slug', 'description')
|
fields = ('id', 'name', 'slug', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ScopedFilterSet, ContactModelFilterSet):
|
class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ScopedFilterSet, ContactModelFilterSet):
|
||||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
@@ -81,6 +84,7 @@ class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ScopedFilterSet,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class VirtualMachineFilterSet(
|
class VirtualMachineFilterSet(
|
||||||
PrimaryModelFilterSet,
|
PrimaryModelFilterSet,
|
||||||
TenancyFilterSet,
|
TenancyFilterSet,
|
||||||
@@ -241,6 +245,7 @@ class VirtualMachineFilterSet(
|
|||||||
return queryset.exclude(params)
|
return queryset.exclude(params)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class VMInterfaceFilterSet(CommonInterfaceFilterSet, OwnerFilterMixin, NetBoxModelFilterSet):
|
class VMInterfaceFilterSet(CommonInterfaceFilterSet, OwnerFilterMixin, NetBoxModelFilterSet):
|
||||||
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='virtual_machine__cluster',
|
field_name='virtual_machine__cluster',
|
||||||
@@ -303,6 +308,7 @@ class VMInterfaceFilterSet(CommonInterfaceFilterSet, OwnerFilterMixin, NetBoxMod
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class VirtualDiskFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
class VirtualDiskFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
||||||
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='virtual_machine',
|
field_name='virtual_machine',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from core.models import ObjectType
|
|||||||
from dcim.models import Device, Interface
|
from dcim.models import Device, Interface
|
||||||
from ipam.models import IPAddress, RouteTarget, VLAN
|
from ipam.models import IPAddress, RouteTarget, VLAN
|
||||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||||
|
from netbox.plugins.registration import register_filterset
|
||||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||||
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
|
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
|
||||||
from virtualization.models import VirtualMachine, VMInterface
|
from virtualization.models import VirtualMachine, VMInterface
|
||||||
@@ -26,6 +27,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class TunnelGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
class TunnelGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -33,6 +35,7 @@ class TunnelGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'description')
|
fields = ('id', 'name', 'slug', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class TunnelFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
class TunnelFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=TunnelStatusChoices
|
choices=TunnelStatusChoices
|
||||||
@@ -75,6 +78,7 @@ class TunnelFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilte
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class TunnelTerminationFilterSet(NetBoxModelFilterSet):
|
class TunnelTerminationFilterSet(NetBoxModelFilterSet):
|
||||||
tunnel_id = django_filters.ModelMultipleChoiceFilter(
|
tunnel_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='tunnel',
|
field_name='tunnel',
|
||||||
@@ -124,6 +128,7 @@ class TunnelTerminationFilterSet(NetBoxModelFilterSet):
|
|||||||
fields = ('id', 'termination_id')
|
fields = ('id', 'termination_id')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class IKEProposalFilterSet(PrimaryModelFilterSet):
|
class IKEProposalFilterSet(PrimaryModelFilterSet):
|
||||||
ike_policy_id = django_filters.ModelMultipleChoiceFilter(
|
ike_policy_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='ike_policies',
|
field_name='ike_policies',
|
||||||
@@ -163,6 +168,7 @@ class IKEProposalFilterSet(PrimaryModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class IKEPolicyFilterSet(PrimaryModelFilterSet):
|
class IKEPolicyFilterSet(PrimaryModelFilterSet):
|
||||||
version = django_filters.MultipleChoiceFilter(
|
version = django_filters.MultipleChoiceFilter(
|
||||||
choices=IKEVersionChoices
|
choices=IKEVersionChoices
|
||||||
@@ -194,6 +200,7 @@ class IKEPolicyFilterSet(PrimaryModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class IPSecProposalFilterSet(PrimaryModelFilterSet):
|
class IPSecProposalFilterSet(PrimaryModelFilterSet):
|
||||||
ipsec_policy_id = django_filters.ModelMultipleChoiceFilter(
|
ipsec_policy_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='ipsec_policies',
|
field_name='ipsec_policies',
|
||||||
@@ -227,6 +234,7 @@ class IPSecProposalFilterSet(PrimaryModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class IPSecPolicyFilterSet(PrimaryModelFilterSet):
|
class IPSecPolicyFilterSet(PrimaryModelFilterSet):
|
||||||
pfs_group = django_filters.MultipleChoiceFilter(
|
pfs_group = django_filters.MultipleChoiceFilter(
|
||||||
choices=DHGroupChoices
|
choices=DHGroupChoices
|
||||||
@@ -255,6 +263,7 @@ class IPSecPolicyFilterSet(PrimaryModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class IPSecProfileFilterSet(PrimaryModelFilterSet):
|
class IPSecProfileFilterSet(PrimaryModelFilterSet):
|
||||||
mode = django_filters.MultipleChoiceFilter(
|
mode = django_filters.MultipleChoiceFilter(
|
||||||
choices=IPSecModeChoices
|
choices=IPSecModeChoices
|
||||||
@@ -294,6 +303,7 @@ class IPSecProfileFilterSet(PrimaryModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class L2VPNFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
class L2VPNFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=L2VPNTypeChoices,
|
choices=L2VPNTypeChoices,
|
||||||
@@ -340,6 +350,7 @@ class L2VPNFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilter
|
|||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
|
class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
|
||||||
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
|
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=L2VPN.objects.all(),
|
queryset=L2VPN.objects.all(),
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ class L2VPNFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFil
|
|||||||
class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
|
class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = L2VPNTermination
|
model = L2VPNTermination
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('filter_id', 'l2vpn_id'),
|
FieldSet('filter_id', 'tag', 'l2vpn_id'),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
|
'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
|
||||||
name=_('Assigned Object')
|
name=_('Assigned Object')
|
||||||
@@ -303,3 +303,4 @@ class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
|
|||||||
},
|
},
|
||||||
label=_('Virtual Machine')
|
label=_('Virtual Machine')
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from dcim.base_filtersets import ScopedFilterSet
|
|||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.filtersets import NestedGroupModelFilterSet, PrimaryModelFilterSet
|
from netbox.filtersets import NestedGroupModelFilterSet, PrimaryModelFilterSet
|
||||||
|
from netbox.plugins.registration import register_filterset
|
||||||
from tenancy.filtersets import TenancyFilterSet
|
from tenancy.filtersets import TenancyFilterSet
|
||||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||||
from .choices import *
|
from .choices import *
|
||||||
@@ -18,6 +19,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class WirelessLANGroupFilterSet(NestedGroupModelFilterSet):
|
class WirelessLANGroupFilterSet(NestedGroupModelFilterSet):
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=WirelessLANGroup.objects.all()
|
queryset=WirelessLANGroup.objects.all()
|
||||||
@@ -44,6 +46,7 @@ class WirelessLANGroupFilterSet(NestedGroupModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'description')
|
fields = ('id', 'name', 'slug', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class WirelessLANFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet):
|
class WirelessLANFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet):
|
||||||
group_id = TreeNodeMultipleChoiceFilter(
|
group_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=WirelessLANGroup.objects.all(),
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
@@ -87,6 +90,7 @@ class WirelessLANFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilter
|
|||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
|
@register_filterset
|
||||||
class WirelessLinkFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
class WirelessLinkFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||||
interface_a_id = django_filters.ModelMultipleChoiceFilter(
|
interface_a_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Interface.objects.all()
|
queryset=Interface.objects.all()
|
||||||
|
|||||||
Reference in New Issue
Block a user