mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-27 07:37:45 -06:00
Merge branch 'feature' into 20564-port-mappings
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.
|
||||
|
||||
### `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`
|
||||
|
||||
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`.
|
||||
|
||||
@@ -6,12 +6,17 @@ Filter sets define the mechanisms available for filtering or searching through a
|
||||
|
||||
To support additional functionality standard to NetBox models, such as tag assignment and custom field support, the `NetBoxModelFilterSet` class is available for use by plugins. This should be used as the base filter set class for plugin models which inherit from `NetBoxModel`. Within this class, individual filters can be declared as directed by the `django-filters` documentation. An example is provided below.
|
||||
|
||||
!!! info "New in NetBox v4.5: FilterSet Registration"
|
||||
NetBox v4.5 introduced the `register_filterset()` utility function. This enables plugins to register their filtersets to receive advanced functionality, such as the automatic attachment of field-specific lookup modifiers on the filter form. Registration is optional: Unregistered filtersets will continue to work as before, but will not receive the enhanced functionality.
|
||||
|
||||
```python
|
||||
# filtersets.py
|
||||
import django_filters
|
||||
from netbox.filtersets import NetBoxModelFilterSet
|
||||
from utilities.filtersets import register_filterset
|
||||
from .models import MyModel
|
||||
|
||||
@register_filterset
|
||||
class MyFilterSet(NetBoxModelFilterSet):
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=(
|
||||
@@ -42,7 +47,7 @@ class MyModelListView(ObjectListView):
|
||||
filterset = MyModelFilterSet
|
||||
```
|
||||
|
||||
To enable a filter set on a REST API endpoint, set the `filterset_class` attribute on the API view:
|
||||
To enable a filter set on a REST API endpoint, set the `filterset_class` attribute on the API view:
|
||||
|
||||
```python
|
||||
# api/views.py
|
||||
@@ -62,7 +67,9 @@ The `ObjectListView` has a field called Quick Search. For Quick Search to work t
|
||||
```python
|
||||
from django.db.models import Q
|
||||
from netbox.filtersets import NetBoxModelFilterSet
|
||||
from utilities.filtersets import register_filterset
|
||||
|
||||
@register_filterset
|
||||
class MyFilterSet(NetBoxModelFilterSet):
|
||||
...
|
||||
def search(self, queryset, name, value):
|
||||
@@ -90,7 +97,9 @@ This class filters `tags` using the `slug` field. For example:
|
||||
```python
|
||||
from django_filters import FilterSet
|
||||
from extras.filters import TagFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
|
||||
@register_filterset
|
||||
class MyModelFilterSet(FilterSet):
|
||||
tag = TagFilter()
|
||||
```
|
||||
@@ -106,7 +115,9 @@ This class filters `tags` using the `id` field. For example:
|
||||
```python
|
||||
from django_filters import FilterSet
|
||||
from extras.filters import TagIDFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
|
||||
@register_filterset
|
||||
class MyModelFilterSet(FilterSet):
|
||||
tag_id = TagIDFilter()
|
||||
```
|
||||
|
||||
@@ -325,14 +325,14 @@ class CircuitTypeType(OrganizationalObjectType):
|
||||
|
||||
### Change filters.py
|
||||
|
||||
Strawberry currently doesn't directly support django-filter, so an explicit filters.py file will need to be created. NetBox includes a new `autotype_decorator` used to automatically wrap FilterSets to reduce the required code to a minimum.
|
||||
Filter classes should inherit from `netbox.graphql.filters.BaseModelFilter`.
|
||||
|
||||
```python title="New"
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from circuits import filtersets, models
|
||||
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
from netbox.graphql.filters import BaseModelFilter
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilter',
|
||||
@@ -340,8 +340,7 @@ __all__ = (
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Circuit, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitFilterSet)
|
||||
class CircuitFilter(BaseFilterMixin):
|
||||
class CircuitFilter(BaseModelFilter):
|
||||
pass
|
||||
|
||||
```
|
||||
|
||||
@@ -25,10 +25,12 @@ from extras.models import Bookmark
|
||||
from extras.tables import BookmarkTable, NotificationTable, SubscriptionTable
|
||||
from netbox.authentication import get_auth_backend_display, get_saml_idps
|
||||
from netbox.config import get_config
|
||||
from netbox.ui import layout
|
||||
from netbox.views import generic
|
||||
from users import forms
|
||||
from users.models import UserConfig
|
||||
from users.tables import TokenTable
|
||||
from users.ui.panels import TokenExamplePanel, TokenPanel
|
||||
from utilities.request import safe_for_redirect
|
||||
from utilities.string import remove_linebreaks
|
||||
from utilities.views import register_model_view
|
||||
@@ -342,12 +344,21 @@ class UserTokenListView(LoginRequiredMixin, View):
|
||||
|
||||
@register_model_view(UserToken)
|
||||
class UserTokenView(LoginRequiredMixin, View):
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
TokenPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
TokenExamplePanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get(self, request, pk):
|
||||
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
|
||||
|
||||
return render(request, 'account/token.html', {
|
||||
'object': token,
|
||||
'layout': self.layout,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from utilities.filtersets import register_filterset
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@@ -29,6 +30,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -93,6 +95,7 @@ class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
@@ -120,6 +123,7 @@ class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
).distinct()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
@@ -147,6 +151,7 @@ class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
||||
).distinct()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -154,6 +159,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
@@ -265,6 +271,7 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
||||
).distinct()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -360,6 +367,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
).distinct()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CircuitGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -367,6 +375,7 @@ class CircuitGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -466,6 +475,7 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -473,6 +483,7 @@ class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_network__provider',
|
||||
@@ -529,6 +540,7 @@ class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
).distinct()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
||||
@@ -3,17 +3,18 @@ from typing import Annotated, TYPE_CHECKING
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from netbox.graphql.filter_mixins import OrganizationalModelFilterMixin
|
||||
from strawberry_django import BaseFilterLookup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from netbox.graphql.enums import ColorEnum
|
||||
|
||||
__all__ = (
|
||||
'BaseCircuitTypeFilterMixin',
|
||||
'CircuitTypeFilterMixin',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseCircuitTypeFilterMixin(OrganizationalModelFilterMixin):
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
class CircuitTypeFilterMixin:
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
@@ -7,17 +7,12 @@ from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup, DateFilterLookup
|
||||
|
||||
from circuits import models
|
||||
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
||||
from circuits.graphql.filter_mixins import CircuitTypeFilterMixin
|
||||
from dcim.graphql.filter_mixins import CabledObjectModelFilterMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
|
||||
from netbox.graphql.filter_mixins import (
|
||||
DistanceFilterMixin,
|
||||
ImageAttachmentFilterMixin,
|
||||
OrganizationalModelFilterMixin,
|
||||
PrimaryModelFilterMixin,
|
||||
)
|
||||
from netbox.graphql.filter_mixins import DistanceFilterMixin, ImageAttachmentFilterMixin
|
||||
from netbox.graphql.filters import ChangeLoggedModelFilter, OrganizationalModelFilter, PrimaryModelFilter
|
||||
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
|
||||
from .filter_mixins import BaseCircuitTypeFilterMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.graphql.filters import ContentTypeFilter
|
||||
@@ -43,10 +38,9 @@ __all__ = (
|
||||
|
||||
@strawberry_django.filter_type(models.CircuitTermination, lookups=True)
|
||||
class CircuitTerminationFilter(
|
||||
BaseObjectTypeFilterMixin,
|
||||
CustomFieldsFilterMixin,
|
||||
TagsFilterMixin,
|
||||
ChangeLogFilterMixin,
|
||||
ChangeLoggedModelFilter,
|
||||
CabledObjectModelFilterMixin,
|
||||
):
|
||||
circuit: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
@@ -95,7 +89,7 @@ class CircuitFilter(
|
||||
ImageAttachmentFilterMixin,
|
||||
DistanceFilterMixin,
|
||||
TenancyFilterMixin,
|
||||
PrimaryModelFilterMixin
|
||||
PrimaryModelFilter
|
||||
):
|
||||
cid: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
@@ -124,19 +118,17 @@ class CircuitFilter(
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.CircuitType, lookups=True)
|
||||
class CircuitTypeFilter(BaseCircuitTypeFilterMixin):
|
||||
class CircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.CircuitGroup, lookups=True)
|
||||
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
|
||||
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.CircuitGroupAssignment, lookups=True)
|
||||
class CircuitGroupAssignmentFilter(
|
||||
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
|
||||
):
|
||||
class CircuitGroupAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
member_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -151,7 +143,7 @@ class CircuitGroupAssignmentFilter(
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Provider, lookups=True)
|
||||
class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
@@ -161,7 +153,7 @@ class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ProviderAccount, lookups=True)
|
||||
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -171,7 +163,7 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
|
||||
class ProviderNetworkFilter(PrimaryModelFilterMixin):
|
||||
class ProviderNetworkFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -181,12 +173,12 @@ class ProviderNetworkFilter(PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
|
||||
class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin):
|
||||
class VirtualCircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
|
||||
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
cid: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -209,9 +201,7 @@ class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualCircuitTermination, lookups=True)
|
||||
class VirtualCircuitTerminationFilter(
|
||||
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
|
||||
):
|
||||
class VirtualCircuitTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
virtual_circuit: Annotated['VirtualCircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, Primary
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from users.models import User
|
||||
from utilities.filters import ContentTypeFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@@ -20,6 +21,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class DataSourceFilterSet(PrimaryModelFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=get_data_backend_choices,
|
||||
@@ -48,6 +50,7 @@ class DataSourceFilterSet(PrimaryModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class DataFileFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search'
|
||||
@@ -75,6 +78,7 @@ class DataFileFilterSet(ChangeLoggedModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class JobFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -139,6 +143,7 @@ class JobFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ObjectTypeFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -164,6 +169,7 @@ class ObjectTypeFilterSet(BaseFilterSet):
|
||||
return queryset.filter(features__icontains=value)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ObjectChangeFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -203,6 +209,7 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ConfigRevisionFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
||||
@@ -4,31 +4,18 @@ from typing import Annotated, TYPE_CHECKING
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry import ID
|
||||
from strawberry_django import FilterLookup, DatetimeFilterLookup
|
||||
from strawberry_django import DatetimeFilterLookup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .filters import *
|
||||
|
||||
__all__ = (
|
||||
'BaseFilterMixin',
|
||||
'BaseObjectTypeFilterMixin',
|
||||
'ChangeLogFilterMixin',
|
||||
'ChangeLoggingMixin',
|
||||
)
|
||||
|
||||
|
||||
# @strawberry.input
|
||||
class BaseFilterMixin: ...
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseObjectTypeFilterMixin(BaseFilterMixin):
|
||||
id: FilterLookup[ID] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChangeLogFilterMixin(BaseFilterMixin):
|
||||
id: FilterLookup[ID] | None = strawberry_django.filter_field()
|
||||
class ChangeLoggingMixin:
|
||||
# TODO: "changelog" is not a valid field name; needs to be updated for ObjectChange
|
||||
changelog: Annotated['ObjectChangeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
||||
@@ -8,8 +8,7 @@ from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup
|
||||
|
||||
from core import models
|
||||
from core.graphql.filter_mixins import BaseFilterMixin
|
||||
from netbox.graphql.filter_mixins import PrimaryModelFilterMixin
|
||||
from netbox.graphql.filters import BaseModelFilter, PrimaryModelFilter
|
||||
from .enums import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -25,8 +24,7 @@ __all__ = (
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.DataFile, lookups=True)
|
||||
class DataFileFilter(BaseFilterMixin):
|
||||
id: FilterLookup[ID] | None = strawberry_django.filter_field()
|
||||
class DataFileFilter(BaseModelFilter):
|
||||
created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
last_updated: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
source: Annotated['DataSourceFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
@@ -41,7 +39,7 @@ class DataFileFilter(BaseFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.DataSource, lookups=True)
|
||||
class DataSourceFilter(PrimaryModelFilterMixin):
|
||||
class DataSourceFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
type: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
source_url: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -60,8 +58,7 @@ class DataSourceFilter(PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ObjectChange, lookups=True)
|
||||
class ObjectChangeFilter(BaseFilterMixin):
|
||||
id: FilterLookup[ID] | None = strawberry_django.filter_field()
|
||||
class ObjectChangeFilter(BaseModelFilter):
|
||||
time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
user_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -88,7 +85,6 @@ class ObjectChangeFilter(BaseFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(DjangoContentType, lookups=True)
|
||||
class ContentTypeFilter(BaseFilterMixin):
|
||||
id: FilterLookup[ID] | None = strawberry_django.filter_field()
|
||||
class ContentTypeFilter(BaseModelFilter):
|
||||
app_label: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
model: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -22,6 +22,7 @@ from utilities.filters import (
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
|
||||
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from utilities.filtersets import register_filterset
|
||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
|
||||
from vpn.models import L2VPN
|
||||
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
||||
@@ -84,6 +85,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class RegionFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -114,6 +116,7 @@ class RegionFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class SiteGroupFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
@@ -144,6 +147,7 @@ class SiteGroupFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=SiteStatusChoices,
|
||||
@@ -208,6 +212,7 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
|
||||
return queryset.filter(qs_filter).distinct()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, NestedGroupModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -287,6 +292,7 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, NestedGroupMode
|
||||
return queryset
|
||||
|
||||
|
||||
@register_filterset
|
||||
class RackRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -294,6 +300,7 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class RackTypeFilterSet(PrimaryModelFilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@@ -332,6 +339,7 @@ class RackTypeFilterSet(PrimaryModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -448,6 +456,7 @@ class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Rack.objects.all(),
|
||||
@@ -537,6 +546,7 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -544,6 +554,7 @@ class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet)
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@@ -687,6 +698,7 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
||||
return queryset.exclude(inventoryitemtemplates__isnull=value)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ModuleTypeProfileFilterSet(PrimaryModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -703,6 +715,7 @@ class ModuleTypeProfileFilterSet(PrimaryModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ModuleTypeFilterSet(AttributeFiltersMixin, PrimaryModelFilterSet):
|
||||
profile_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ModuleTypeProfile.objects.all(),
|
||||
@@ -819,6 +832,7 @@ class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -826,6 +840,7 @@ class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
|
||||
fields = ('id', 'name', 'label', 'type', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -833,6 +848,7 @@ class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDevi
|
||||
fields = ('id', 'name', 'label', 'type', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -840,6 +856,7 @@ class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
||||
fields = ('id', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
feed_leg = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
@@ -855,6 +872,7 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
|
||||
fields = ('id', 'name', 'label', 'type', 'color', 'feed_leg', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=InterfaceTypeChoices,
|
||||
@@ -879,6 +897,7 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
||||
fields = ('id', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PortTypeChoices,
|
||||
@@ -896,6 +915,7 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
||||
fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PortTypeChoices,
|
||||
@@ -913,6 +933,7 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
|
||||
fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -920,6 +941,7 @@ class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
||||
fields = ('id', 'name', 'label', 'position', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -927,6 +949,7 @@ class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
|
||||
fields = ('id', 'name', 'label', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=InventoryItemTemplate.objects.all(),
|
||||
@@ -970,6 +993,7 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class DeviceRoleFilterSet(NestedGroupModelFilterSet):
|
||||
config_template_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
@@ -1004,6 +1028,7 @@ class DeviceRoleFilterSet(NestedGroupModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'color', 'vm_role', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class PlatformFilterSet(NestedGroupModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Platform.objects.all(),
|
||||
@@ -1061,6 +1086,7 @@ class PlatformFilterSet(NestedGroupModelFilterSet):
|
||||
return queryset.filter(Q(manufacturer=None) | Q(manufacturer__device_types=value))
|
||||
|
||||
|
||||
@register_filterset
|
||||
class DeviceFilterSet(
|
||||
PrimaryModelFilterSet,
|
||||
TenancyFilterSet,
|
||||
@@ -1363,6 +1389,7 @@ class DeviceFilterSet(
|
||||
return queryset.exclude(params)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VirtualDeviceContextFilterSet(PrimaryModelFilterSet, TenancyFilterSet, PrimaryIPFilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device',
|
||||
@@ -1412,6 +1439,7 @@ class VirtualDeviceContextFilterSet(PrimaryModelFilterSet, TenancyFilterSet, Pri
|
||||
return queryset.exclude(params)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ModuleFilterSet(PrimaryModelFilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='module_type__manufacturer',
|
||||
@@ -1700,6 +1728,7 @@ class PathEndpointFilterSet(django_filters.FilterSet):
|
||||
return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False))
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ConsolePortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=ConsolePortTypeChoices,
|
||||
@@ -1711,6 +1740,7 @@ class ConsolePortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
|
||||
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_position')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ConsoleServerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=ConsolePortTypeChoices,
|
||||
@@ -1722,6 +1752,7 @@ class ConsoleServerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFi
|
||||
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_position')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class PowerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerPortTypeChoices,
|
||||
@@ -1736,6 +1767,7 @@ class PowerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet,
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerOutletTypeChoices,
|
||||
@@ -1762,6 +1794,7 @@ class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class MACAddressFilterSet(PrimaryModelFilterSet):
|
||||
mac_address = MultiValueMACAddressFilter()
|
||||
assigned_object_type = ContentTypeFilter()
|
||||
@@ -1943,6 +1976,7 @@ class CommonInterfaceFilterSet(django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class InterfaceFilterSet(
|
||||
ModularDeviceComponentFilterSet,
|
||||
CabledObjectFilterSet,
|
||||
@@ -2105,6 +2139,7 @@ class InterfaceFilterSet(
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PortTypeChoices,
|
||||
@@ -2125,6 +2160,7 @@ class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet)
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PortTypeChoices,
|
||||
@@ -2145,6 +2181,7 @@ class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ModuleBayFilterSet(ModularDeviceComponentFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ModuleBay.objects.all(),
|
||||
@@ -2161,6 +2198,7 @@ class ModuleBayFilterSet(ModularDeviceComponentFilterSet):
|
||||
fields = ('id', 'name', 'label', 'position', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class DeviceBayFilterSet(DeviceComponentFilterSet):
|
||||
installed_device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
@@ -2178,6 +2216,7 @@ class DeviceBayFilterSet(DeviceComponentFilterSet):
|
||||
fields = ('id', 'name', 'label', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class InventoryItemFilterSet(DeviceComponentFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
@@ -2230,6 +2269,7 @@ class InventoryItemFilterSet(DeviceComponentFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -2237,6 +2277,7 @@ class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
||||
master_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
@@ -2313,6 +2354,7 @@ class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
||||
return queryset.filter(qs_filter).distinct()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
||||
termination_a_type = ContentTypeFilter(
|
||||
field_name='terminations__termination_type'
|
||||
@@ -2485,6 +2527,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
||||
return self.filter_by_termination_object(queryset, CircuitTermination, value)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
|
||||
termination_type = ContentTypeFilter()
|
||||
|
||||
@@ -2493,6 +2536,7 @@ class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
|
||||
fields = ('id', 'cable', 'cable_end', 'position', 'termination_type', 'termination_id')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -2551,6 +2595,7 @@ class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class PowerFeedFilterSet(PrimaryModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet, TenancyFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
|
||||
@@ -638,6 +638,7 @@ class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm):
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q')
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
||||
|
||||
@@ -6,9 +6,7 @@ import strawberry_django
|
||||
from strawberry import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseFilterMixin, ChangeLogFilterMixin
|
||||
from core.graphql.filters import ContentTypeFilter
|
||||
from netbox.graphql.filter_mixins import NetBoxModelFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin
|
||||
from .enums import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -22,16 +20,16 @@ __all__ = (
|
||||
'ComponentModelFilterMixin',
|
||||
'ComponentTemplateFilterMixin',
|
||||
'InterfaceBaseFilterMixin',
|
||||
'ModularComponentModelFilterMixin',
|
||||
'ModularComponentFilterMixin',
|
||||
'ModularComponentTemplateFilterMixin',
|
||||
'RackBaseFilterMixin',
|
||||
'RackFilterMixin',
|
||||
'RenderConfigFilterMixin',
|
||||
'ScopedFilterMixin',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScopedFilterMixin(BaseFilterMixin):
|
||||
class ScopedFilterMixin:
|
||||
scope_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -39,7 +37,7 @@ class ScopedFilterMixin(BaseFilterMixin):
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComponentModelFilterMixin(NetBoxModelFilterMixin):
|
||||
class ComponentModelFilterMixin:
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
device_id: ID | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -48,7 +46,7 @@ class ComponentModelFilterMixin(NetBoxModelFilterMixin):
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModularComponentModelFilterMixin(ComponentModelFilterMixin):
|
||||
class ModularComponentFilterMixin(ComponentModelFilterMixin):
|
||||
module: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
module_id: ID | None = strawberry_django.filter_field()
|
||||
inventory_items: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
@@ -57,7 +55,7 @@ class ModularComponentModelFilterMixin(ComponentModelFilterMixin):
|
||||
|
||||
|
||||
@dataclass
|
||||
class CabledObjectModelFilterMixin(BaseFilterMixin):
|
||||
class CabledObjectModelFilterMixin:
|
||||
cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
cable_id: ID | None = strawberry_django.filter_field()
|
||||
cable_end: (
|
||||
@@ -67,7 +65,7 @@ class CabledObjectModelFilterMixin(BaseFilterMixin):
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComponentTemplateFilterMixin(ChangeLogFilterMixin):
|
||||
class ComponentTemplateFilterMixin:
|
||||
device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -85,7 +83,7 @@ class ModularComponentTemplateFilterMixin(ComponentTemplateFilterMixin):
|
||||
|
||||
|
||||
@dataclass
|
||||
class RenderConfigFilterMixin(BaseFilterMixin):
|
||||
class RenderConfigFilterMixin:
|
||||
config_template: Annotated['ConfigTemplateFilter', strawberry.lazy('extras.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -93,7 +91,7 @@ class RenderConfigFilterMixin(BaseFilterMixin):
|
||||
|
||||
|
||||
@dataclass
|
||||
class InterfaceBaseFilterMixin(BaseFilterMixin):
|
||||
class InterfaceBaseFilterMixin:
|
||||
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
mtu: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -124,7 +122,7 @@ class InterfaceBaseFilterMixin(BaseFilterMixin):
|
||||
|
||||
|
||||
@dataclass
|
||||
class RackBaseFilterMixin(WeightFilterMixin, PrimaryModelFilterMixin):
|
||||
class RackFilterMixin:
|
||||
width: BaseFilterLookup[Annotated['RackWidthEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
@@ -6,29 +6,25 @@ import strawberry_django
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
||||
from dcim import models
|
||||
from dcim.constants import *
|
||||
from dcim.graphql.enums import InterfaceKindEnum
|
||||
from dcim.graphql.filter_mixins import (
|
||||
ComponentModelFilterMixin, ComponentTemplateFilterMixin, ModularComponentFilterMixin,
|
||||
ModularComponentTemplateFilterMixin, RackFilterMixin,
|
||||
)
|
||||
from extras.graphql.filter_mixins import ConfigContextFilterMixin
|
||||
from netbox.graphql.filter_mixins import (
|
||||
PrimaryModelFilterMixin,
|
||||
OrganizationalModelFilterMixin,
|
||||
NestedGroupModelFilterMixin,
|
||||
ImageAttachmentFilterMixin,
|
||||
WeightFilterMixin,
|
||||
from netbox.graphql.filter_mixins import ImageAttachmentFilterMixin, WeightFilterMixin
|
||||
from netbox.graphql.filters import (
|
||||
BaseModelFilter, ChangeLoggedModelFilter, NestedGroupModelFilter, OrganizationalModelFilter, PrimaryModelFilter,
|
||||
NetBoxModelFilter,
|
||||
)
|
||||
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
|
||||
from virtualization.models import VMInterface
|
||||
|
||||
from .filter_mixins import (
|
||||
CabledObjectModelFilterMixin,
|
||||
ComponentModelFilterMixin,
|
||||
ComponentTemplateFilterMixin,
|
||||
InterfaceBaseFilterMixin,
|
||||
ModularComponentModelFilterMixin,
|
||||
ModularComponentTemplateFilterMixin,
|
||||
RackBaseFilterMixin,
|
||||
RenderConfigFilterMixin,
|
||||
)
|
||||
|
||||
@@ -98,7 +94,7 @@ __all__ = (
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Cable, lookups=True)
|
||||
class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
|
||||
class CableFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
type: BaseFilterLookup[Annotated['CableTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -121,7 +117,7 @@ class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.CableTermination, lookups=True)
|
||||
class CableTerminationFilter(ChangeLogFilterMixin):
|
||||
class CableTerminationFilter(ChangeLoggedModelFilter):
|
||||
cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
cable_id: ID | None = strawberry_django.filter_field()
|
||||
cable_end: BaseFilterLookup[Annotated['CableEndEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
@@ -134,7 +130,7 @@ class CableTerminationFilter(ChangeLogFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ConsolePort, lookups=True)
|
||||
class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
class ConsolePortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
|
||||
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -144,14 +140,14 @@ class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ConsolePortTemplate, lookups=True)
|
||||
class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ConsoleServerPort, lookups=True)
|
||||
class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
class ConsoleServerPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
|
||||
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -161,7 +157,7 @@ class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectMode
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ConsoleServerPortTemplate, lookups=True)
|
||||
class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -174,7 +170,7 @@ class DeviceFilter(
|
||||
ImageAttachmentFilterMixin,
|
||||
RenderConfigFilterMixin,
|
||||
ConfigContextFilterMixin,
|
||||
PrimaryModelFilterMixin,
|
||||
PrimaryModelFilter,
|
||||
):
|
||||
device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -287,7 +283,7 @@ class DeviceFilter(
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.DeviceBay, lookups=True)
|
||||
class DeviceBayFilter(ComponentModelFilterMixin):
|
||||
class DeviceBayFilter(ComponentModelFilterMixin, NetBoxModelFilter):
|
||||
installed_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -295,12 +291,12 @@ class DeviceBayFilter(ComponentModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.DeviceBayTemplate, lookups=True)
|
||||
class DeviceBayTemplateFilter(ComponentTemplateFilterMixin):
|
||||
class DeviceBayTemplateFilter(ComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.InventoryItemTemplate, lookups=True)
|
||||
class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
|
||||
class InventoryItemTemplateFilter(ComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
parent: Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -320,7 +316,7 @@ class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.DeviceRole, lookups=True)
|
||||
class DeviceRoleFilter(OrganizationalModelFilterMixin, RenderConfigFilterMixin):
|
||||
class DeviceRoleFilter(RenderConfigFilterMixin, OrganizationalModelFilter):
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -328,7 +324,7 @@ class DeviceRoleFilter(OrganizationalModelFilterMixin, RenderConfigFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.DeviceType, lookups=True)
|
||||
class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
|
||||
class DeviceTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryModelFilter):
|
||||
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -404,7 +400,7 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.FrontPort, lookups=True)
|
||||
class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
class FrontPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -414,7 +410,7 @@ class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.FrontPortTemplate, lookups=True)
|
||||
class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -424,7 +420,7 @@ class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.PortMapping, lookups=True)
|
||||
class PortMappingFilter(BaseObjectTypeFilterMixin):
|
||||
class PortMappingFilter(BaseModelFilter):
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
front_port: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -437,7 +433,7 @@ class PortMappingFilter(BaseObjectTypeFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.PortTemplateMapping, lookups=True)
|
||||
class PortTemplateMappingFilter(BaseObjectTypeFilterMixin):
|
||||
class PortTemplateMappingFilter(BaseModelFilter):
|
||||
device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -455,7 +451,7 @@ class PortTemplateMappingFilter(BaseObjectTypeFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.MACAddress, lookups=True)
|
||||
class MACAddressFilter(PrimaryModelFilterMixin):
|
||||
class MACAddressFilter(PrimaryModelFilter):
|
||||
mac_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -482,7 +478,12 @@ class MACAddressFilter(PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Interface, lookups=True)
|
||||
class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin, CabledObjectModelFilterMixin):
|
||||
class InterfaceFilter(
|
||||
ModularComponentFilterMixin,
|
||||
InterfaceBaseFilterMixin,
|
||||
CabledObjectModelFilterMixin,
|
||||
NetBoxModelFilter
|
||||
):
|
||||
vcdcs: Annotated['VirtualDeviceContextFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -572,7 +573,7 @@ class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.InterfaceTemplate, lookups=True)
|
||||
class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -594,7 +595,7 @@ class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.InventoryItem, lookups=True)
|
||||
class InventoryItemFilter(ComponentModelFilterMixin):
|
||||
class InventoryItemFilter(ComponentModelFilterMixin, NetBoxModelFilter):
|
||||
parent: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -621,14 +622,14 @@ class InventoryItemFilter(ComponentModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.InventoryItemRole, lookups=True)
|
||||
class InventoryItemRoleFilter(OrganizationalModelFilterMixin):
|
||||
class InventoryItemRoleFilter(OrganizationalModelFilter):
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Location, lookups=True)
|
||||
class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilterMixin):
|
||||
class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilter):
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
status: BaseFilterLookup[Annotated['LocationStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
@@ -644,12 +645,12 @@ class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilt
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Manufacturer, lookups=True)
|
||||
class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilterMixin):
|
||||
class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Module, lookups=True)
|
||||
class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
|
||||
class ModuleFilter(ConfigContextFilterMixin, PrimaryModelFilter):
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
device_id: ID | None = strawberry_django.filter_field()
|
||||
module_bay: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
@@ -698,7 +699,7 @@ class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ModuleBay, lookups=True)
|
||||
class ModuleBayFilter(ModularComponentModelFilterMixin):
|
||||
class ModuleBayFilter(ModularComponentFilterMixin, NetBoxModelFilter):
|
||||
parent: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -707,17 +708,17 @@ class ModuleBayFilter(ModularComponentModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ModuleBayTemplate, lookups=True)
|
||||
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
position: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ModuleTypeProfile, lookups=True)
|
||||
class ModuleTypeProfileFilter(PrimaryModelFilterMixin):
|
||||
class ModuleTypeProfileFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ModuleType, lookups=True)
|
||||
class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
|
||||
class ModuleTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryModelFilter):
|
||||
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -768,7 +769,7 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Platform, lookups=True)
|
||||
class PlatformFilter(OrganizationalModelFilterMixin):
|
||||
class PlatformFilter(OrganizationalModelFilter):
|
||||
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -780,7 +781,7 @@ class PlatformFilter(OrganizationalModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.PowerFeed, lookups=True)
|
||||
class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
power_panel: Annotated['PowerPanelFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -815,7 +816,7 @@ class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryM
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.PowerOutlet, lookups=True)
|
||||
class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
class PowerOutletFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -835,7 +836,7 @@ class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.PowerOutletTemplate, lookups=True)
|
||||
class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
|
||||
class PowerOutletTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -849,7 +850,7 @@ class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.PowerPanel, lookups=True)
|
||||
class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilterMixin):
|
||||
class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilter):
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
@@ -862,7 +863,7 @@ class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryMo
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.PowerPort, lookups=True)
|
||||
class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
class PowerPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -875,7 +876,7 @@ class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.PowerPortTemplate, lookups=True)
|
||||
class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -888,7 +889,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.RackType, lookups=True)
|
||||
class RackTypeFilter(RackBaseFilterMixin):
|
||||
class RackTypeFilter(RackFilterMixin, WeightFilterMixin, PrimaryModelFilter):
|
||||
form_factor: BaseFilterLookup[Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -903,7 +904,14 @@ class RackTypeFilter(RackBaseFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Rack, lookups=True)
|
||||
class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, RackBaseFilterMixin):
|
||||
class RackFilter(
|
||||
ContactFilterMixin,
|
||||
ImageAttachmentFilterMixin,
|
||||
TenancyFilterMixin,
|
||||
WeightFilterMixin,
|
||||
RackFilterMixin,
|
||||
PrimaryModelFilter
|
||||
):
|
||||
form_factor: BaseFilterLookup[Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -937,7 +945,7 @@ class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.RackReservation, lookups=True)
|
||||
class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rack_id: ID | None = strawberry_django.filter_field()
|
||||
units: Annotated['IntegerArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
@@ -952,14 +960,14 @@ class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.RackRole, lookups=True)
|
||||
class RackRoleFilter(OrganizationalModelFilterMixin):
|
||||
class RackRoleFilter(OrganizationalModelFilter):
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.RearPort, lookups=True)
|
||||
class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
class RearPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -972,7 +980,7 @@ class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMi
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.RearPortTemplate, lookups=True)
|
||||
class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
class RearPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -985,7 +993,7 @@ class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Region, lookups=True)
|
||||
class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
||||
class RegionFilter(ContactFilterMixin, NestedGroupModelFilter):
|
||||
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -995,7 +1003,7 @@ class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Site, lookups=True)
|
||||
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: BaseFilterLookup[Annotated['SiteStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
@@ -1031,7 +1039,7 @@ class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.SiteGroup, lookups=True)
|
||||
class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
||||
class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilter):
|
||||
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -1041,7 +1049,7 @@ class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualChassis, lookups=True)
|
||||
class VirtualChassisFilter(PrimaryModelFilterMixin):
|
||||
class VirtualChassisFilter(PrimaryModelFilter):
|
||||
master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
master_id: ID | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -1053,7 +1061,7 @@ class VirtualChassisFilter(PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualDeviceContext, lookups=True)
|
||||
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
device_id: ID | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -12,6 +12,7 @@ from users.models import Group, User
|
||||
from utilities.filters import (
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
|
||||
)
|
||||
from utilities.filtersets import register_filterset
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
from .choices import *
|
||||
from .filters import TagFilter, TagIDFilter
|
||||
@@ -40,6 +41,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ScriptFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -62,6 +64,7 @@ class ScriptFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class WebhookFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -91,6 +94,7 @@ class WebhookFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class EventRuleFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -131,6 +135,7 @@ class EventRuleFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
||||
return queryset.filter(event_types__overlap=value)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CustomFieldFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -180,6 +185,7 @@ class CustomFieldFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CustomFieldChoiceSetFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -208,6 +214,7 @@ class CustomFieldChoiceSetFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet
|
||||
return queryset.filter(extra_choices__overlap=value)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CustomLinkFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -238,6 +245,7 @@ class CustomLinkFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ExportTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -276,6 +284,7 @@ class ExportTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class SavedFilterFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -328,6 +337,7 @@ class SavedFilterFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
return queryset.filter(Q(enabled=False) | Q(Q(shared=False) & ~Q(user=user)))
|
||||
|
||||
|
||||
@register_filterset
|
||||
class TableConfigFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -381,6 +391,7 @@ class TableConfigFilterSet(ChangeLoggedModelFilterSet):
|
||||
return queryset.filter(Q(enabled=False) | Q(Q(shared=False) & ~Q(user=user)))
|
||||
|
||||
|
||||
@register_filterset
|
||||
class BookmarkFilterSet(BaseFilterSet):
|
||||
created = django_filters.DateTimeFilter()
|
||||
object_type_id = MultiValueNumberFilter()
|
||||
@@ -401,6 +412,7 @@ class BookmarkFilterSet(BaseFilterSet):
|
||||
fields = ('id', 'object_id')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class NotificationGroupFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -444,6 +456,7 @@ class NotificationGroupFilterSet(ChangeLoggedModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -465,6 +478,7 @@ class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class JournalEntryFilterSet(NetBoxModelFilterSet):
|
||||
created = django_filters.DateTimeFromToRangeFilter()
|
||||
assigned_object_type = ContentTypeFilter()
|
||||
@@ -495,6 +509,7 @@ class JournalEntryFilterSet(NetBoxModelFilterSet):
|
||||
return queryset.filter(comments__icontains=value)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class TagFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -555,6 +570,7 @@ class TagFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class TaggedItemFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -590,6 +606,7 @@ class TaggedItemFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ConfigContextProfileFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -620,6 +637,7 @@ class ConfigContextProfileFilterSet(PrimaryModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ConfigContextFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -789,6 +807,7 @@ class ConfigContextFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ConfigTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
||||
@@ -287,6 +287,7 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
|
||||
|
||||
|
||||
class TableConfigFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = TableConfig
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('object_type_id', 'enabled', 'shared', 'weight', name=_('Attributes')),
|
||||
|
||||
@@ -3,9 +3,6 @@ from typing import Annotated, TYPE_CHECKING
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry_django import FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseFilterMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from netbox.graphql.filter_lookups import JSONFilter
|
||||
@@ -16,37 +13,30 @@ __all__ = (
|
||||
'JournalEntriesFilterMixin',
|
||||
'TagsFilterMixin',
|
||||
'ConfigContextFilterMixin',
|
||||
'TagBaseFilterMixin',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CustomFieldsFilterMixin(BaseFilterMixin):
|
||||
class CustomFieldsFilterMixin:
|
||||
custom_field_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class JournalEntriesFilterMixin(BaseFilterMixin):
|
||||
class JournalEntriesFilterMixin:
|
||||
journal_entries: Annotated['JournalEntryFilter', strawberry.lazy('extras.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TagsFilterMixin(BaseFilterMixin):
|
||||
class TagsFilterMixin:
|
||||
tags: Annotated['TagFilter', strawberry.lazy('extras.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigContextFilterMixin(BaseFilterMixin):
|
||||
class ConfigContextFilterMixin:
|
||||
local_context_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TagBaseFilterMixin(BaseFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -5,10 +5,10 @@ import strawberry_django
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
||||
from extras import models
|
||||
from extras.graphql.filter_mixins import TagBaseFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin
|
||||
from netbox.graphql.filter_mixins import PrimaryModelFilterMixin, SyncedDataFilterMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
|
||||
from netbox.graphql.filter_mixins import SyncedDataFilterMixin
|
||||
from netbox.graphql.filters import ChangeLoggedModelFilter, PrimaryModelFilter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.graphql.filters import ContentTypeFilter
|
||||
@@ -42,7 +42,7 @@ __all__ = (
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ConfigContext, lookups=True)
|
||||
class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
|
||||
class ConfigContextFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -99,14 +99,14 @@ class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Chan
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ConfigContextProfile, lookups=True)
|
||||
class ConfigContextProfileFilter(SyncedDataFilterMixin, PrimaryModelFilterMixin):
|
||||
class ConfigContextProfileFilter(SyncedDataFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] = strawberry_django.filter_field()
|
||||
tags: Annotated['TagFilter', strawberry.lazy('extras.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ConfigTemplate, lookups=True)
|
||||
class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
|
||||
class ConfigTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
template_code: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -120,7 +120,7 @@ class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.CustomField, lookups=True)
|
||||
class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class CustomFieldFilter(ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['CustomFieldTypeEnum', strawberry.lazy('extras.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -179,7 +179,7 @@ class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.CustomFieldChoiceSet, lookups=True)
|
||||
class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class CustomFieldChoiceSetFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
base_choices: (
|
||||
@@ -194,7 +194,7 @@ class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.CustomLink, lookups=True)
|
||||
class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class CustomLinkFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
link_text: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -212,7 +212,7 @@ class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ExportTemplate, lookups=True)
|
||||
class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
|
||||
class ExportTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
template_code: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -226,7 +226,7 @@ class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ImageAttachment, lookups=True)
|
||||
class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class ImageAttachmentFilter(ChangeLoggedModelFilter):
|
||||
object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -241,7 +241,7 @@ class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.JournalEntry, lookups=True)
|
||||
class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
class JournalEntryFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -257,7 +257,7 @@ class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, Tag
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.NotificationGroup, lookups=True)
|
||||
class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class NotificationGroupFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
groups: Annotated['GroupFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
@@ -265,7 +265,7 @@ class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.SavedFilter, lookups=True)
|
||||
class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class SavedFilterFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -282,7 +282,7 @@ class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.TableConfig, lookups=True)
|
||||
class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class TableConfigFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
@@ -295,7 +295,9 @@ class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Tag, lookups=True)
|
||||
class TagFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin, TagBaseFilterMixin):
|
||||
class TagFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -303,7 +305,7 @@ class TagFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin, TagBaseFilterMi
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Webhook, lookups=True)
|
||||
class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
class WebhookFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
payload_url: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -324,7 +326,7 @@ class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilt
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.EventRule, lookups=True)
|
||||
class EventRuleFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
class EventRuleFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
event_types: Annotated['StringArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
|
||||
@@ -46,6 +46,10 @@ class ConfigContextQuerySet(RestrictedQuerySet):
|
||||
# Match against the directly assigned role as well as any parent roles.
|
||||
device_roles = obj.role.get_ancestors(include_self=True) if obj.role else []
|
||||
|
||||
# Match against the directly assigned platform as well as any parent platforms.
|
||||
platform = getattr(obj, 'platform', None)
|
||||
platforms = platform.get_ancestors(include_self=True) if platform else []
|
||||
|
||||
queryset = self.filter(
|
||||
Q(regions__in=regions) | Q(regions=None),
|
||||
Q(site_groups__in=sitegroups) | Q(site_groups=None),
|
||||
@@ -53,7 +57,7 @@ class ConfigContextQuerySet(RestrictedQuerySet):
|
||||
Q(locations__in=locations) | Q(locations=None),
|
||||
Q(device_types=device_type) | Q(device_types=None),
|
||||
Q(roles__in=device_roles) | Q(roles=None),
|
||||
Q(platforms=obj.platform) | Q(platforms=None),
|
||||
Q(platforms__in=platforms) | Q(platforms=None),
|
||||
Q(cluster_types=cluster_type) | Q(cluster_types=None),
|
||||
Q(cluster_groups=cluster_group) | Q(cluster_groups=None),
|
||||
Q(clusters=cluster) | Q(clusters=None),
|
||||
@@ -103,7 +107,6 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
|
||||
"content_type__model": self.model._meta.model_name
|
||||
}
|
||||
base_query = Q(
|
||||
Q(platforms=OuterRef('platform')) | Q(platforms=None),
|
||||
Q(cluster_types=OuterRef('cluster__type')) | Q(cluster_types=None),
|
||||
Q(cluster_groups=OuterRef('cluster__group')) | Q(cluster_groups=None),
|
||||
Q(clusters=OuterRef('cluster')) | Q(clusters=None),
|
||||
@@ -167,6 +170,15 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
|
||||
) | Q(roles=None)),
|
||||
Q.AND
|
||||
)
|
||||
base_query.add(
|
||||
(Q(
|
||||
platforms__tree_id=OuterRef('platform__tree_id'),
|
||||
platforms__level__lte=OuterRef('platform__level'),
|
||||
platforms__lft__lte=OuterRef('platform__lft'),
|
||||
platforms__rght__gte=OuterRef('platform__rght'),
|
||||
) | Q(platforms=None)),
|
||||
Q.AND
|
||||
)
|
||||
|
||||
return base_query
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import django_filters
|
||||
import netaddr
|
||||
from dcim.base_filtersets import ScopedFilterSet
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
@@ -10,15 +9,16 @@ from drf_spectacular.utils import extend_schema_field
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from circuits.models import Provider
|
||||
from dcim.base_filtersets import ScopedFilterSet
|
||||
from dcim.models import Device, Interface, Region, Site, SiteGroup
|
||||
from netbox.filtersets import (
|
||||
ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet, PrimaryModelFilterSet,
|
||||
)
|
||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
|
||||
from utilities.filters import (
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from utilities.filtersets import register_filterset
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from vpn.models import L2VPN
|
||||
from .choices import *
|
||||
@@ -47,6 +47,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
import_target_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='import_targets',
|
||||
@@ -85,6 +86,7 @@ class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
fields = ('id', 'name', 'rd', 'enforce_unique', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
importing_vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='importing_vrfs',
|
||||
@@ -144,6 +146,7 @@ class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
fields = ('id', 'name', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class RIRFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -151,6 +154,7 @@ class RIRFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'is_private', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
family = django_filters.NumberFilter(
|
||||
field_name='prefix',
|
||||
@@ -198,6 +202,7 @@ class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
|
||||
return queryset.none()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ASNRangeFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
rir_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RIR.objects.all(),
|
||||
@@ -223,6 +228,7 @@ class ASNRangeFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ASNFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
rir_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RIR.objects.all(),
|
||||
@@ -285,6 +291,7 @@ class ASNFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class RoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -292,6 +299,7 @@ class RoleFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'description', 'weight')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
family = django_filters.NumberFilter(
|
||||
field_name='prefix',
|
||||
@@ -458,6 +466,7 @@ class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet,
|
||||
).distinct()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class IPRangeFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
family = django_filters.NumberFilter(
|
||||
field_name='start_address',
|
||||
@@ -550,6 +559,7 @@ class IPRangeFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
||||
return queryset.filter(q)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
family = django_filters.NumberFilter(
|
||||
field_name='address',
|
||||
@@ -786,6 +796,7 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class FHRPGroupFilterSet(PrimaryModelFilterSet):
|
||||
protocol = django_filters.MultipleChoiceFilter(
|
||||
choices=FHRPGroupProtocolChoices
|
||||
@@ -833,6 +844,7 @@ class FHRPGroupFilterSet(PrimaryModelFilterSet):
|
||||
return queryset.filter(ip_filter)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
|
||||
interface_type = ContentTypeFilter()
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@@ -887,6 +899,7 @@ class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VLANGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
scope_type = ContentTypeFilter()
|
||||
region = django_filters.NumberFilter(
|
||||
@@ -936,6 +949,7 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -1087,6 +1101,7 @@ class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
).distinct()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VLANTranslationPolicyFilterSet(PrimaryModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -1103,6 +1118,7 @@ class VLANTranslationPolicyFilterSet(PrimaryModelFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VLANTranslationRuleFilterSet(NetBoxModelFilterSet):
|
||||
policy_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VLANTranslationPolicy.objects.all(),
|
||||
@@ -1134,6 +1150,7 @@ class VLANTranslationRuleFilterSet(NetBoxModelFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ServiceTemplateFilterSet(PrimaryModelFilterSet):
|
||||
port = NumericArrayFilter(
|
||||
field_name='ports',
|
||||
@@ -1154,6 +1171,7 @@ class ServiceTemplateFilterSet(PrimaryModelFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ServiceFilterSet(ContactModelFilterSet, PrimaryModelFilterSet):
|
||||
parent_object_type = ContentTypeFilter()
|
||||
device = MultiValueCharFilter(
|
||||
|
||||
@@ -3,21 +3,20 @@ from typing import Annotated, TYPE_CHECKING
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from core.graphql.filter_mixins import BaseFilterMixin
|
||||
from strawberry_django import BaseFilterLookup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from netbox.graphql.filter_lookups import IntegerLookup
|
||||
from .enums import *
|
||||
|
||||
__all__ = (
|
||||
'ServiceBaseFilterMixin',
|
||||
'ServiceFilterMixin',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceBaseFilterMixin(BaseFilterMixin):
|
||||
protocol: Annotated['ServiceProtocolEnum', strawberry.lazy('ipam.graphql.enums')] | None = (
|
||||
class ServiceFilterMixin:
|
||||
protocol: BaseFilterLookup[Annotated['ServiceProtocolEnum', strawberry.lazy('ipam.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
ports: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
|
||||
@@ -9,12 +9,13 @@ from netaddr.core import AddrFormatError
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup, DateFilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
||||
from dcim.graphql.filter_mixins import ScopedFilterMixin
|
||||
from dcim.models import Device
|
||||
from ipam import models
|
||||
from ipam.graphql.filter_mixins import ServiceBaseFilterMixin
|
||||
from netbox.graphql.filter_mixins import NetBoxModelFilterMixin, OrganizationalModelFilterMixin, PrimaryModelFilterMixin
|
||||
from ipam.graphql.filter_mixins import ServiceFilterMixin
|
||||
from netbox.graphql.filters import (
|
||||
ChangeLoggedModelFilter, NetBoxModelFilter, OrganizationalModelFilter, PrimaryModelFilter,
|
||||
)
|
||||
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
|
||||
from virtualization.models import VMInterface
|
||||
|
||||
@@ -49,7 +50,7 @@ __all__ = (
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ASN, lookups=True)
|
||||
class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rir_id: ID | None = strawberry_django.filter_field()
|
||||
asn: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
@@ -64,7 +65,7 @@ class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ASNRange, lookups=True)
|
||||
class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
|
||||
class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
@@ -78,7 +79,7 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Aggregate, lookups=True)
|
||||
class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
prefix: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rir_id: ID | None = strawberry_django.filter_field()
|
||||
@@ -111,7 +112,7 @@ class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.FHRPGroup, lookups=True)
|
||||
class FHRPGroupFilter(PrimaryModelFilterMixin):
|
||||
class FHRPGroupFilter(PrimaryModelFilter):
|
||||
group_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -129,7 +130,7 @@ class FHRPGroupFilter(PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.FHRPGroupAssignment, lookups=True)
|
||||
class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
|
||||
interface_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -168,7 +169,7 @@ class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.IPAddress, lookups=True)
|
||||
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
vrf_id: ID | None = strawberry_django.filter_field()
|
||||
@@ -219,7 +220,7 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.IPRange, lookups=True)
|
||||
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
start_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
end_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
@@ -273,7 +274,7 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMi
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Prefix, lookups=True)
|
||||
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
prefix: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
vrf_id: ID | None = strawberry_django.filter_field()
|
||||
@@ -310,19 +311,19 @@ class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, Pr
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.RIR, lookups=True)
|
||||
class RIRFilter(OrganizationalModelFilterMixin):
|
||||
class RIRFilter(OrganizationalModelFilter):
|
||||
is_private: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Role, lookups=True)
|
||||
class RoleFilter(OrganizationalModelFilterMixin):
|
||||
class RoleFilter(OrganizationalModelFilter):
|
||||
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.RouteTarget, lookups=True)
|
||||
class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
importing_vrfs: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -339,7 +340,7 @@ class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Service, lookups=True)
|
||||
class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilterMixin):
|
||||
class ServiceFilter(ContactFilterMixin, ServiceFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -351,12 +352,12 @@ class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilt
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ServiceTemplate, lookups=True)
|
||||
class ServiceTemplateFilter(ServiceBaseFilterMixin, PrimaryModelFilterMixin):
|
||||
class ServiceTemplateFilter(ServiceFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VLAN, lookups=True)
|
||||
class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class VLANFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
group: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
@@ -388,19 +389,19 @@ class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VLANGroup, lookups=True)
|
||||
class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilterMixin):
|
||||
class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilter):
|
||||
vid_ranges: Annotated['IntegerRangeArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VLANTranslationPolicy, lookups=True)
|
||||
class VLANTranslationPolicyFilter(PrimaryModelFilterMixin):
|
||||
class VLANTranslationPolicyFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VLANTranslationRule, lookups=True)
|
||||
class VLANTranslationRuleFilter(NetBoxModelFilterMixin):
|
||||
class VLANTranslationRuleFilter(NetBoxModelFilter):
|
||||
policy: Annotated['VLANTranslationPolicyFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -415,7 +416,7 @@ class VLANTranslationRuleFilter(NetBoxModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VRF, lookups=True)
|
||||
class VRFFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class VRFFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rd: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
enforce_unique: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -38,7 +38,7 @@ class TokenAuthentication(BaseAuthentication):
|
||||
try:
|
||||
auth_value = auth[1].decode()
|
||||
except UnicodeError:
|
||||
raise exceptions.AuthenticationFailed("Invalid authorization header: Token contains invalid characters")
|
||||
raise exceptions.AuthenticationFailed('Invalid authorization header: Token contains invalid characters')
|
||||
|
||||
# Infer token version from presence or absence of prefix
|
||||
version = 2 if auth_value.startswith(TOKEN_PREFIX) else 1
|
||||
@@ -75,17 +75,21 @@ class TokenAuthentication(BaseAuthentication):
|
||||
client_ip = get_client_ip(request)
|
||||
if client_ip is None:
|
||||
raise exceptions.AuthenticationFailed(
|
||||
"Client IP address could not be determined for validation. Check that the HTTP server is "
|
||||
"correctly configured to pass the required header(s)."
|
||||
'Client IP address could not be determined for validation. Check that the HTTP server is '
|
||||
'correctly configured to pass the required header(s).'
|
||||
)
|
||||
if not token.validate_client_ip(client_ip):
|
||||
raise exceptions.AuthenticationFailed(
|
||||
f"Source IP {client_ip} is not permitted to authenticate using this token."
|
||||
)
|
||||
|
||||
# Enforce the Token is enabled
|
||||
if not token.enabled:
|
||||
raise exceptions.AuthenticationFailed('Token disabled')
|
||||
|
||||
# Enforce the Token's expiration time, if one has been set.
|
||||
if token.is_expired:
|
||||
raise exceptions.AuthenticationFailed("Token expired")
|
||||
raise exceptions.AuthenticationFailed('Token expired')
|
||||
|
||||
# Update last used, but only once per minute at most. This reduces write load on the database
|
||||
if not token.last_used or (timezone.now() - token.last_used).total_seconds() > 60:
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.conf import settings
|
||||
from django_rq.queues import get_connection
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.views import APIView
|
||||
@@ -12,6 +13,7 @@ from rq.worker import Worker
|
||||
|
||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||
from netbox.plugins.utils import get_installed_plugins
|
||||
from users.api.serializers import UserSerializer
|
||||
from utilities.apps import get_installed_apps
|
||||
|
||||
|
||||
@@ -62,3 +64,15 @@ class StatusView(APIView):
|
||||
'python-version': platform.python_version(),
|
||||
'rq-workers-running': Worker.count(get_connection('default')),
|
||||
})
|
||||
|
||||
|
||||
class AuthenticationCheckView(APIView):
|
||||
"""
|
||||
Return the user making the request, if authenticated successfully.
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
||||
def get(self, request):
|
||||
serializer = UserSerializer(request.user, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
|
||||
@@ -152,6 +152,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
||||
|
||||
elif isinstance(existing_filter, (
|
||||
django_filters.filters.CharFilter,
|
||||
django_filters.ChoiceFilter,
|
||||
django_filters.MultipleChoiceFilter,
|
||||
filters.MultiValueCharFilter,
|
||||
filters.MultiValueMACAddressFilter
|
||||
|
||||
@@ -4,7 +4,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from extras.choices import *
|
||||
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
|
||||
|
||||
__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
|
||||
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
|
||||
a selector widget
|
||||
"""
|
||||
q = forms.CharField(
|
||||
q = QueryField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
|
||||
@@ -4,19 +4,11 @@ from typing import TypeVar, TYPE_CHECKING, Annotated
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup, DatetimeFilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseFilterMixin, BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, JournalEntriesFilterMixin, TagsFilterMixin
|
||||
|
||||
__all__ = (
|
||||
'DistanceFilterMixin',
|
||||
'ImageAttachmentFilterMixin',
|
||||
'NestedGroupModelFilterMixin',
|
||||
'NetBoxModelFilterMixin',
|
||||
'OrganizationalModelFilterMixin',
|
||||
'PrimaryModelFilterMixin',
|
||||
'SyncedDataFilterMixin',
|
||||
'WeightFilterMixin',
|
||||
)
|
||||
@@ -30,51 +22,15 @@ if TYPE_CHECKING:
|
||||
from extras.graphql.filters import *
|
||||
|
||||
|
||||
class NetBoxModelFilterMixin(
|
||||
ChangeLogFilterMixin,
|
||||
CustomFieldsFilterMixin,
|
||||
JournalEntriesFilterMixin,
|
||||
TagsFilterMixin,
|
||||
BaseObjectTypeFilterMixin,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class NestedGroupModelFilterMixin(NetBoxModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
parent_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrganizationalModelFilterMixin(
|
||||
ChangeLogFilterMixin,
|
||||
CustomFieldsFilterMixin,
|
||||
TagsFilterMixin,
|
||||
BaseObjectTypeFilterMixin,
|
||||
):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrimaryModelFilterMixin(NetBoxModelFilterMixin):
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageAttachmentFilterMixin(BaseFilterMixin):
|
||||
class ImageAttachmentFilterMixin:
|
||||
images: Annotated['ImageAttachmentFilter', strawberry.lazy('extras.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeightFilterMixin(BaseFilterMixin):
|
||||
class WeightFilterMixin:
|
||||
weight: FilterLookup[float] | None = strawberry_django.filter_field()
|
||||
weight_unit: BaseFilterLookup[Annotated['WeightUnitEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -82,7 +38,7 @@ class WeightFilterMixin(BaseFilterMixin):
|
||||
|
||||
|
||||
@dataclass
|
||||
class SyncedDataFilterMixin(BaseFilterMixin):
|
||||
class SyncedDataFilterMixin:
|
||||
data_source: Annotated['DataSourceFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -97,7 +53,7 @@ class SyncedDataFilterMixin(BaseFilterMixin):
|
||||
|
||||
|
||||
@dataclass
|
||||
class DistanceFilterMixin(BaseFilterMixin):
|
||||
class DistanceFilterMixin:
|
||||
distance: FilterLookup[float] | None = strawberry_django.filter_field()
|
||||
distance_unit: BaseFilterLookup[Annotated['DistanceUnitEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
||||
61
netbox/netbox/graphql/filters.py
Normal file
61
netbox/netbox/graphql/filters.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import strawberry_django
|
||||
from strawberry import ID
|
||||
from strawberry_django import FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import ChangeLoggingMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, JournalEntriesFilterMixin, TagsFilterMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .filters import *
|
||||
|
||||
__all__ = (
|
||||
'BaseModelFilter',
|
||||
'ChangeLoggedModelFilter',
|
||||
'NestedGroupModelFilter',
|
||||
'NetBoxModelFilter',
|
||||
'OrganizationalModelFilter',
|
||||
'PrimaryModelFilter',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseModelFilter:
|
||||
id: FilterLookup[ID] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
class ChangeLoggedModelFilter(ChangeLoggingMixin, BaseModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
class NetBoxModelFilter(
|
||||
CustomFieldsFilterMixin,
|
||||
JournalEntriesFilterMixin,
|
||||
TagsFilterMixin,
|
||||
ChangeLoggingMixin,
|
||||
BaseModelFilter
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class NestedGroupModelFilter(NetBoxModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
parent_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrganizationalModelFilter(NetBoxModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrimaryModelFilter(NetBoxModelFilter):
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -26,6 +26,7 @@ registry = Registry({
|
||||
'data_backends': dict(),
|
||||
'denormalized_fields': collections.defaultdict(list),
|
||||
'event_types': dict(),
|
||||
'filtersets': dict(),
|
||||
'model_features': dict(),
|
||||
'models': collections.defaultdict(set),
|
||||
'plugins': dict(),
|
||||
|
||||
@@ -32,6 +32,18 @@ class AppTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_authentication_check(self):
|
||||
url = reverse('api-authentication-check')
|
||||
|
||||
# Test an unauthenticated request
|
||||
response = self.client.get(f'{url}')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# Test an authenticated request
|
||||
response = self.client.get(f'{url}', **self.header)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['id'], self.user.pk)
|
||||
|
||||
|
||||
class OptionalLimitOffsetPaginationTest(TestCase):
|
||||
|
||||
|
||||
@@ -66,6 +66,32 @@ class TokenAuthenticationTestCase(APITestCase):
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.data['detail'], "Invalid v2 token")
|
||||
|
||||
@override_settings(LOGIN_REQUIRED=True, EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_token_enabled(self):
|
||||
url = reverse('dcim-api:site-list')
|
||||
|
||||
# Create v1 & v2 tokens
|
||||
token1 = Token.objects.create(version=1, user=self.user, enabled=True)
|
||||
token2 = Token.objects.create(version=2, user=self.user, enabled=True)
|
||||
|
||||
# Request with an enabled token should succeed
|
||||
response = self.client.get(url, HTTP_AUTHORIZATION=f'Token {token1.token}')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(url, HTTP_AUTHORIZATION=f'Bearer {TOKEN_PREFIX}{token2.key}.{token2.token}')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Request with a disabled token should fail
|
||||
token1.enabled = False
|
||||
token1.save()
|
||||
token2.enabled = False
|
||||
token2.save()
|
||||
response = self.client.get(url, HTTP_AUTHORIZATION=f'Token {token1.token}')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.data['detail'], 'Token disabled')
|
||||
response = self.client.get(url, HTTP_AUTHORIZATION=f'Bearer {TOKEN_PREFIX}{token2.key}.{token2.token}')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.data['detail'], 'Token disabled')
|
||||
|
||||
@override_settings(LOGIN_REQUIRED=True, EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_token_expiration(self):
|
||||
url = reverse('dcim-api:site-list')
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.views.decorators.cache import cache_page
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||
|
||||
from account.views import LoginView, LogoutView
|
||||
from netbox.api.views import APIRootView, StatusView
|
||||
from netbox.api.views import APIRootView, AuthenticationCheckView, StatusView
|
||||
from netbox.graphql.schema import schema
|
||||
from netbox.graphql.views import NetBoxGraphQLView
|
||||
from netbox.plugins.urls import plugin_patterns, plugin_api_patterns
|
||||
@@ -53,6 +53,7 @@ _patterns = [
|
||||
path('api/vpn/', include('vpn.api.urls')),
|
||||
path('api/wireless/', include('wireless.api.urls')),
|
||||
path('api/status/', StatusView.as_view(), name='api-status'),
|
||||
path('api/authentication-check/', AuthenticationCheckView.as_view(), name='api-authentication-check'),
|
||||
|
||||
# REST API schema
|
||||
path(
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
dist
|
||||
node_modules
|
||||
.cache
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:import/typescript",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"arrowFunctions": true
|
||||
}
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"settings": {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"]
|
||||
},
|
||||
"import/resolver": {
|
||||
"typescript": {}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"no-unused-vars": "off",
|
||||
"no-inner-declarations": "off",
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"global-require": "off",
|
||||
"import/no-dynamic-require": "off",
|
||||
"import/prefer-default-export": "off",
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-namespace": "off",
|
||||
"@typescript-eslint/no-empty-interface": [
|
||||
"error",
|
||||
{
|
||||
"allowSingleExtends": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
3025
netbox/project-static/dist/graphiql/graphiql.min.css
vendored
3025
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
86
netbox/project-static/eslint.config.js
Normal file
86
netbox/project-static/eslint.config.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import { fixupConfigRules, fixupPluginRules } from "@eslint/compat";
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import prettier from "eslint-plugin-prettier";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['**/dist', '**/node_modules', '**/.cache']),
|
||||
{
|
||||
extends: fixupConfigRules(
|
||||
compat.extends(
|
||||
'eslint:recommended',
|
||||
'plugin:import/typescript',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'prettier',
|
||||
),
|
||||
),
|
||||
|
||||
plugins: {
|
||||
'@typescript-eslint': fixupPluginRules(typescriptEslint),
|
||||
prettier: fixupPluginRules(prettier),
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
arrowFunctions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {},
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'no-inner-declarations': 'off',
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
'global-require': 'off',
|
||||
'import/no-dynamic-require': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'@typescript-eslint/no-inferrable-types': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
'error',
|
||||
{
|
||||
allowSingleExtends: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "netbox-graphiql",
|
||||
"version": "4.3.0",
|
||||
"version": "4.5.0",
|
||||
"description": "NetBox GraphiQL Custom Front End",
|
||||
"main": "dist/graphiql.js",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@graphiql/plugin-explorer": "3.2.5",
|
||||
"graphiql": "3.8.3",
|
||||
"graphql": "16.10.0",
|
||||
"@graphiql/plugin-explorer": "3.2.6",
|
||||
"graphiql": "4.1.2",
|
||||
"graphql": "16.12.0",
|
||||
"js-cookie": "3.0.5",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "netbox",
|
||||
"version": "4.4.0",
|
||||
"type": "module",
|
||||
"version": "4.5.0",
|
||||
"main": "dist/netbox.js",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
@@ -8,14 +9,14 @@
|
||||
"netbox-graphiql"
|
||||
],
|
||||
"scripts": {
|
||||
"bundle": "node bundle.js",
|
||||
"bundle:styles": "node bundle.js --styles",
|
||||
"bundle:scripts": "node bundle.js --scripts",
|
||||
"bundle": "node bundle.cjs",
|
||||
"bundle:styles": "node bundle.cjs --styles",
|
||||
"bundle:scripts": "node bundle.cjs --scripts",
|
||||
"format": "yarn format:scripts && yarn format:styles",
|
||||
"format:scripts": "prettier -w src/**/*.ts",
|
||||
"format:styles": "prettier -w styles/**/*.scss",
|
||||
"validate": "yarn validate:types && yarn validate:lint",
|
||||
"validate:lint": "eslint -c .eslintrc ./src/**/*.ts",
|
||||
"validate:lint": "eslint ./src/**/*.ts",
|
||||
"validate:types": "tsc --noEmit",
|
||||
"validate:formatting": "yarn validate:formatting:scripts && yarn validate:formatting:styles",
|
||||
"validate:formatting:styles": "prettier -c styles/**/*.scss",
|
||||
@@ -36,20 +37,24 @@
|
||||
"typeface-roboto-mono": "1.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.0.0",
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/node": "^22.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"esbuild": "^0.25.11",
|
||||
"@types/cookie": "^1.0.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
||||
"@typescript-eslint/parser": "^8.48.1",
|
||||
"esbuild": "^0.27.0",
|
||||
"esbuild-sass-plugin": "^3.3.1",
|
||||
"eslint": "<9.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "<5.5"
|
||||
"globals": "^16.5.0",
|
||||
"prettier": "^3.7.3",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/bootstrap/**/@popperjs/core": "^2.11.6"
|
||||
|
||||
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 { initFilterModifiers } from './filterModifiers';
|
||||
import { initSpeedSelector } from './speedSelector';
|
||||
|
||||
export function initForms(): void {
|
||||
for (const func of [initFormElements, initSpeedSelector]) {
|
||||
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers]) {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,3 +32,11 @@ form.object-edit {
|
||||
border: 1px solid $red;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter modifier dropdown sizing
|
||||
.modifier-select {
|
||||
min-width: 10rem;
|
||||
max-width: 15rem;
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
9
netbox/templates/users/panels/token_example.html
Normal file
9
netbox/templates/users/panels/token_example.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends 'ui/panels/_base.html' %}
|
||||
|
||||
{% block panel_content %}
|
||||
<div id="token-example" class="card-body font-monospace">curl -X GET \<br />
|
||||
-H "Authorization: {{ object.get_auth_header_prefix }}<mark><TOKEN></mark>" \<br />
|
||||
-H "Content-Type: application/json" \<br />
|
||||
-H "Accept: application/json; indent=4" \<br />
|
||||
{{ request.scheme }}://{{ request.get_host }}{% url "api-status" %}</div>
|
||||
{% endblock panel_content %}
|
||||
@@ -1,69 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block title %}{% trans "Token" %} {{ object }}{% endblock %}
|
||||
|
||||
{% block subtitle %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Token" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Version" %}</th>
|
||||
<td>{{ object.version }}</td>
|
||||
</tr>
|
||||
{% if object.version == 1 %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Token" %}</th>
|
||||
<td>{{ object.partial }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Key" %}</th>
|
||||
<td>{{ object }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Pepper ID" %}</th>
|
||||
<td>{{ object.pepper_id }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "User" %}</th>
|
||||
<td>
|
||||
<a href="{% url 'users:user' pk=object.user.pk %}">{{ object.user }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Write enabled" %}</th>
|
||||
<td>{% checkmark object.write_enabled %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Created" %}</th>
|
||||
<td>{{ object.created|isodatetime }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Expires" %}</th>
|
||||
<td>{{ object.expires|isodatetime|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Last used" %}</th>
|
||||
<td>{{ object.last_used|isodatetime|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Allowed IPs" %}</th>
|
||||
<td>{{ object.allowed_ips|join:", "|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,6 +6,7 @@ from netbox.filtersets import (
|
||||
NestedGroupModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
|
||||
)
|
||||
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
from .models import *
|
||||
|
||||
__all__ = (
|
||||
@@ -24,6 +25,7 @@ __all__ = (
|
||||
# Contacts
|
||||
#
|
||||
|
||||
@register_filterset
|
||||
class ContactGroupFilterSet(NestedGroupModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
@@ -59,6 +61,7 @@ class ContactGroupFilterSet(NestedGroupModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ContactRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -66,6 +69,7 @@ class ContactRoleFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ContactFilterSet(PrimaryModelFilterSet):
|
||||
group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
@@ -100,6 +104,7 @@ class ContactFilterSet(PrimaryModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ContactAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -170,6 +175,7 @@ class ContactModelFilterSet(django_filters.FilterSet):
|
||||
# Tenancy
|
||||
#
|
||||
|
||||
@register_filterset
|
||||
class TenantGroupFilterSet(NestedGroupModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
@@ -200,6 +206,7 @@ class TenantGroupFilterSet(NestedGroupModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class TenantFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
|
||||
@@ -5,8 +5,6 @@ import strawberry
|
||||
import strawberry_django
|
||||
from strawberry import ID
|
||||
|
||||
from core.graphql.filter_mixins import BaseFilterMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from netbox.graphql.filter_lookups import TreeNodeFilter
|
||||
from .filters import ContactAssignmentFilter, TenantFilter, TenantGroupFilter
|
||||
@@ -18,14 +16,14 @@ __all__ = (
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContactFilterMixin(BaseFilterMixin):
|
||||
class ContactFilterMixin:
|
||||
contacts: Annotated['ContactAssignmentFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TenancyFilterMixin(BaseFilterMixin):
|
||||
class TenancyFilterMixin:
|
||||
tenant: Annotated['TenantFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
@@ -5,12 +5,9 @@ import strawberry_django
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import ChangeLogFilterMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
|
||||
from netbox.graphql.filter_mixins import (
|
||||
NestedGroupModelFilterMixin,
|
||||
OrganizationalModelFilterMixin,
|
||||
PrimaryModelFilterMixin,
|
||||
from netbox.graphql.filters import (
|
||||
ChangeLoggedModelFilter, NestedGroupModelFilter, OrganizationalModelFilter, PrimaryModelFilter,
|
||||
)
|
||||
from tenancy import models
|
||||
from .filter_mixins import ContactFilterMixin
|
||||
@@ -57,7 +54,7 @@ __all__ = (
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Tenant, lookups=True)
|
||||
class TenantFilter(PrimaryModelFilterMixin, ContactFilterMixin):
|
||||
class TenantFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
@@ -136,7 +133,7 @@ class TenantFilter(PrimaryModelFilterMixin, ContactFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.TenantGroup, lookups=True)
|
||||
class TenantGroupFilter(OrganizationalModelFilterMixin):
|
||||
class TenantGroupFilter(OrganizationalModelFilter):
|
||||
parent: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -150,7 +147,7 @@ class TenantGroupFilter(OrganizationalModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Contact, lookups=True)
|
||||
class ContactFilter(PrimaryModelFilterMixin):
|
||||
class ContactFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
title: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
phone: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -166,19 +163,19 @@ class ContactFilter(PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ContactRole, lookups=True)
|
||||
class ContactRoleFilter(OrganizationalModelFilterMixin):
|
||||
class ContactRoleFilter(OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ContactGroup, lookups=True)
|
||||
class ContactGroupFilter(NestedGroupModelFilterMixin):
|
||||
class ContactGroupFilter(NestedGroupModelFilter):
|
||||
parent: Annotated['ContactGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ContactAssignment, lookups=True)
|
||||
class ContactAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
class ContactAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
@@ -32,10 +32,10 @@ class TokenSerializer(ValidatedModelSerializer):
|
||||
model = Token
|
||||
fields = (
|
||||
'id', 'url', 'display_url', 'display', 'version', 'key', 'user', 'description', 'created', 'expires',
|
||||
'last_used', 'write_enabled', 'pepper_id', 'allowed_ips', 'token',
|
||||
'last_used', 'enabled', 'write_enabled', 'pepper_id', 'allowed_ips', 'token',
|
||||
)
|
||||
read_only_fields = ('key',)
|
||||
brief_fields = ('id', 'url', 'display', 'version', 'key', 'write_enabled', 'description')
|
||||
brief_fields = ('id', 'url', 'display', 'version', 'key', 'enabled', 'write_enabled', 'description')
|
||||
|
||||
def get_fields(self):
|
||||
fields = super().get_fields()
|
||||
@@ -79,7 +79,7 @@ class TokenProvisionSerializer(TokenSerializer):
|
||||
model = Token
|
||||
fields = (
|
||||
'id', 'url', 'display_url', 'display', 'version', 'user', 'key', 'created', 'expires', 'last_used', 'key',
|
||||
'write_enabled', 'description', 'allowed_ips', 'username', 'password', 'token',
|
||||
'enabled', 'write_enabled', 'description', 'allowed_ips', 'username', 'password', 'token',
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import django_filters
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
@@ -8,6 +7,7 @@ from extras.models import NotificationGroup
|
||||
from netbox.filtersets import BaseFilterSet
|
||||
from users.models import Group, ObjectPermission, Owner, OwnerGroup, Token, User
|
||||
from utilities.filters import ContentTypeFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
|
||||
__all__ = (
|
||||
'GroupFilterSet',
|
||||
@@ -19,6 +19,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class GroupFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -64,6 +65,7 @@ class GroupFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class UserFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -120,6 +122,7 @@ class UserFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class TokenFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -167,7 +170,8 @@ class TokenFilterSet(BaseFilterSet):
|
||||
class Meta:
|
||||
model = Token
|
||||
fields = (
|
||||
'id', 'version', 'key', 'pepper_id', 'write_enabled', 'description', 'created', 'expires', 'last_used',
|
||||
'id', 'version', 'key', 'pepper_id', 'enabled', 'write_enabled',
|
||||
'description', 'created', 'expires', 'last_used',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -180,6 +184,7 @@ class TokenFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ObjectPermissionFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -247,6 +252,7 @@ class ObjectPermissionFilterSet(BaseFilterSet):
|
||||
return queryset.exclude(actions__contains=[action])
|
||||
|
||||
|
||||
@register_filterset
|
||||
class OwnerGroupFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -266,6 +272,7 @@ class OwnerGroupFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class OwnerFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
||||
@@ -99,6 +99,11 @@ class TokenBulkEditForm(BulkEditForm):
|
||||
queryset=Token.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect,
|
||||
label=_('Enabled')
|
||||
)
|
||||
write_enabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect,
|
||||
@@ -122,7 +127,7 @@ class TokenBulkEditForm(BulkEditForm):
|
||||
|
||||
model = Token
|
||||
fieldsets = (
|
||||
FieldSet('write_enabled', 'description', 'expires', 'allowed_ips'),
|
||||
FieldSet('enabled', 'write_enabled', 'description', 'expires', 'allowed_ips'),
|
||||
)
|
||||
nullable_fields = (
|
||||
'expires', 'description', 'allowed_ips',
|
||||
|
||||
@@ -52,7 +52,7 @@ class TokenImportForm(CSVModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Token
|
||||
fields = ('user', 'version', 'token', 'write_enabled', 'expires', 'description',)
|
||||
fields = ('user', 'version', 'token', 'enabled', 'write_enabled', 'expires', 'description',)
|
||||
|
||||
|
||||
class OwnerGroupImportForm(CSVModelForm):
|
||||
|
||||
@@ -114,7 +114,7 @@ class TokenFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = Token
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id',),
|
||||
FieldSet('version', 'user_id', 'write_enabled', 'expires', 'last_used', name=_('Token')),
|
||||
FieldSet('version', 'user_id', 'enabled', 'write_enabled', 'expires', 'last_used', name=_('Token')),
|
||||
)
|
||||
version = forms.ChoiceField(
|
||||
choices=add_blank_choice(TokenVersionChoices),
|
||||
@@ -125,6 +125,13 @@ class TokenFilterForm(SavedFiltersMixin, FilterForm):
|
||||
required=False,
|
||||
label=_('User')
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
),
|
||||
label=_('Enabled'),
|
||||
)
|
||||
write_enabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
|
||||
@@ -140,7 +140,7 @@ class UserTokenForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Token
|
||||
fields = [
|
||||
'version', 'token', 'write_enabled', 'expires', 'description', 'allowed_ips',
|
||||
'version', 'token', 'enabled', 'write_enabled', 'expires', 'description', 'allowed_ips',
|
||||
]
|
||||
widgets = {
|
||||
'expires': DateTimePicker(),
|
||||
@@ -177,7 +177,7 @@ class TokenForm(UserTokenForm):
|
||||
|
||||
class Meta(UserTokenForm.Meta):
|
||||
fields = [
|
||||
'version', 'token', 'user', 'write_enabled', 'expires', 'description', 'allowed_ips',
|
||||
'version', 'token', 'user', 'enabled', 'write_enabled', 'expires', 'description', 'allowed_ips',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -5,7 +5,7 @@ import strawberry
|
||||
import strawberry_django
|
||||
from strawberry_django import DatetimeFilterLookup, FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin
|
||||
from netbox.graphql.filters import BaseModelFilter
|
||||
from users import models
|
||||
|
||||
__all__ = (
|
||||
@@ -17,13 +17,13 @@ __all__ = (
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Group, lookups=True)
|
||||
class GroupFilter(BaseObjectTypeFilterMixin):
|
||||
class GroupFilter(BaseModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.User, lookups=True)
|
||||
class UserFilter(BaseObjectTypeFilterMixin):
|
||||
class UserFilter(BaseModelFilter):
|
||||
username: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
first_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
last_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@@ -36,7 +36,7 @@ class UserFilter(BaseObjectTypeFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Owner, lookups=True)
|
||||
class OwnerFilter(BaseObjectTypeFilterMixin):
|
||||
class OwnerFilter(BaseModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group: Annotated['OwnerGroupFilter', strawberry.lazy('users.graphql.filters')] | None = (
|
||||
@@ -49,6 +49,6 @@ class OwnerFilter(BaseObjectTypeFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.OwnerGroup, lookups=True)
|
||||
class OwnerGroupFilter(BaseObjectTypeFilterMixin):
|
||||
class OwnerGroupFilter(BaseModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -9,6 +9,13 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Add a new field to enable/disable tokens
|
||||
migrations.AddField(
|
||||
model_name='token',
|
||||
name='enabled',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
|
||||
# Rename the original key field to "plaintext"
|
||||
migrations.RenameField(
|
||||
model_name='token',
|
||||
@@ -35,7 +42,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
|
||||
# Add version field to distinguish v1 and v2 tokens
|
||||
# Add a version field to distinguish v1 and v2 tokens
|
||||
migrations.AddField(
|
||||
model_name='token',
|
||||
name='version',
|
||||
|
||||
@@ -61,6 +61,11 @@ class Token(models.Model):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
enabled = models.BooleanField(
|
||||
verbose_name=_('enabled'),
|
||||
default=True,
|
||||
help_text=_('Disable to temporarily revoke this token without deleting it.'),
|
||||
)
|
||||
write_enabled = models.BooleanField(
|
||||
verbose_name=_('write enabled'),
|
||||
default=True,
|
||||
@@ -180,6 +185,31 @@ class Token(models.Model):
|
||||
self.key = self.key or self.generate_key()
|
||||
self.update_digest()
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
"""
|
||||
Check whether the token has expired.
|
||||
"""
|
||||
if self.expires is None or timezone.now() < self.expires:
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
"""
|
||||
Check whether the token is active (enabled and not expired).
|
||||
"""
|
||||
return self.enabled and not self.is_expired
|
||||
|
||||
def get_auth_header_prefix(self):
|
||||
"""
|
||||
Return the HTTP Authorization header prefix for this token.
|
||||
"""
|
||||
if self.v1:
|
||||
return 'Token '
|
||||
if self.v2:
|
||||
return f'Bearer {TOKEN_PREFIX}{self.key}.'
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@@ -236,12 +266,6 @@ class Token(models.Model):
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
if self.expires is None or timezone.now() < self.expires:
|
||||
return False
|
||||
return True
|
||||
|
||||
def validate(self, token):
|
||||
"""
|
||||
Validate the given plaintext against the token.
|
||||
|
||||
@@ -25,6 +25,9 @@ class TokenTable(NetBoxTable):
|
||||
verbose_name=_('token'),
|
||||
template_code=TOKEN,
|
||||
)
|
||||
enabled = columns.BooleanColumn(
|
||||
verbose_name=_('Enabled')
|
||||
)
|
||||
write_enabled = columns.BooleanColumn(
|
||||
verbose_name=_('Write Enabled')
|
||||
)
|
||||
@@ -49,10 +52,10 @@ class TokenTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Token
|
||||
fields = (
|
||||
'pk', 'id', 'token', 'version', 'pepper_id', 'user', 'description', 'write_enabled', 'created', 'expires',
|
||||
'last_used', 'allowed_ips',
|
||||
'pk', 'id', 'token', 'version', 'pepper_id', 'user', 'description', 'enabled', 'write_enabled', 'created',
|
||||
'expires', 'last_used', 'allowed_ips',
|
||||
)
|
||||
default_columns = ('token', 'version', 'user', 'write_enabled', 'description', 'allowed_ips')
|
||||
default_columns = ('token', 'version', 'user', 'enabled', 'write_enabled', 'description', 'allowed_ips')
|
||||
|
||||
|
||||
class UserTable(NetBoxTable):
|
||||
|
||||
@@ -195,10 +195,10 @@ class TokenTest(
|
||||
APIViewTestCases.ListObjectsViewTestCase,
|
||||
APIViewTestCases.CreateObjectViewTestCase,
|
||||
APIViewTestCases.UpdateObjectViewTestCase,
|
||||
APIViewTestCases.DeleteObjectViewTestCase
|
||||
APIViewTestCases.DeleteObjectViewTestCase,
|
||||
):
|
||||
model = Token
|
||||
brief_fields = ['description', 'display', 'id', 'key', 'url', 'version', 'write_enabled']
|
||||
brief_fields = ['description', 'display', 'enabled', 'id', 'key', 'url', 'version', 'write_enabled']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -229,12 +229,16 @@ class TokenTest(
|
||||
cls.create_data = [
|
||||
{
|
||||
'user': users[0].pk,
|
||||
'enabled': True,
|
||||
},
|
||||
{
|
||||
'user': users[1].pk,
|
||||
'enabled': False,
|
||||
},
|
||||
{
|
||||
'user': users[2].pk,
|
||||
'enabled': True,
|
||||
'write_enabled': False,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -267,6 +271,8 @@ class TokenTest(
|
||||
self.assertEqual(response.data['expires'], data['expires'])
|
||||
token = Token.objects.get(user=user)
|
||||
self.assertEqual(token.key, response.data['key'])
|
||||
self.assertEqual(token.enabled, response.data['enabled'])
|
||||
self.assertEqual(token.write_enabled, response.data['write_enabled'])
|
||||
|
||||
def test_provision_token_invalid(self):
|
||||
"""
|
||||
|
||||
@@ -285,6 +285,7 @@ class TokenTestCase(TestCase, BaseFilterSetTests):
|
||||
version=1,
|
||||
user=users[0],
|
||||
expires=future_date,
|
||||
enabled=True,
|
||||
write_enabled=True,
|
||||
description='foobar1',
|
||||
),
|
||||
@@ -292,12 +293,14 @@ class TokenTestCase(TestCase, BaseFilterSetTests):
|
||||
version=2,
|
||||
user=users[1],
|
||||
expires=future_date,
|
||||
enabled=False,
|
||||
write_enabled=True,
|
||||
description='foobar2',
|
||||
),
|
||||
Token(
|
||||
version=2,
|
||||
user=users[2],
|
||||
enabled=True,
|
||||
expires=past_date,
|
||||
write_enabled=False,
|
||||
),
|
||||
@@ -339,6 +342,12 @@ class TokenTestCase(TestCase, BaseFilterSetTests):
|
||||
params = {'expires__lte': '2021-01-01T00:00:00'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_enabled(self):
|
||||
params = {'enabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'enabled': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_write_enabled(self):
|
||||
params = {'write_enabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
@@ -20,6 +20,32 @@ class TokenTest(TestCase):
|
||||
"""
|
||||
cls.user = create_test_user('User 1')
|
||||
|
||||
def test_is_active(self):
|
||||
"""
|
||||
Test the is_active property.
|
||||
"""
|
||||
# Token with enabled status and no expiration date
|
||||
token = Token(user=self.user, enabled=True, expires=None)
|
||||
self.assertTrue(token.is_active)
|
||||
|
||||
# Token with disabled status
|
||||
token.enabled = False
|
||||
self.assertFalse(token.is_active)
|
||||
|
||||
# Token with enabled status and future expiration
|
||||
future_date = timezone.now() + timedelta(days=1)
|
||||
token = Token(user=self.user, enabled=True, expires=future_date)
|
||||
self.assertTrue(token.is_active)
|
||||
|
||||
# Token with past expiration
|
||||
token.expires = timezone.now() - timedelta(days=1)
|
||||
self.assertFalse(token.is_active)
|
||||
|
||||
# Token with disabled status and past expiration
|
||||
past_date = timezone.now() - timedelta(days=1)
|
||||
token = Token(user=self.user, enabled=False, expires=past_date)
|
||||
self.assertFalse(token.is_active)
|
||||
|
||||
def test_is_expired(self):
|
||||
"""
|
||||
Test the is_expired property.
|
||||
|
||||
@@ -236,13 +236,14 @@ class TokenTestCase(
|
||||
'token': '4F9DAouzURLbicyoG55htImgqQ0b4UZHP5LUYgl5',
|
||||
'user': users[0].pk,
|
||||
'description': 'Test token',
|
||||
'enabled': True,
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"token,user,description",
|
||||
f"zjebxBPzICiPbWz0Wtx0fTL7bCKXKGTYhNzkgC2S,{users[0].pk},Test token",
|
||||
f"9Z5kGtQWba60Vm226dPDfEAV6BhlTr7H5hAXAfbF,{users[1].pk},Test token",
|
||||
f"njpMnNT6r0k0MDccoUhTYYlvP9BvV3qLzYN2p6Uu,{users[1].pk},Test token",
|
||||
"token,user,description,enabled,write_enabled",
|
||||
f"zjebxBPzICiPbWz0Wtx0fTL7bCKXKGTYhNzkgC2S,{users[0].pk},Test token,true,true",
|
||||
f"9Z5kGtQWba60Vm226dPDfEAV6BhlTr7H5hAXAfbF,{users[1].pk},Test token,true,false",
|
||||
f"njpMnNT6r0k0MDccoUhTYYlvP9BvV3qLzYN2p6Uu,{users[1].pk},Test token,false,true",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
|
||||
0
netbox/users/ui/__init__.py
Normal file
0
netbox/users/ui/__init__.py
Normal file
25
netbox/users/ui/panels.py
Normal file
25
netbox/users/ui/panels.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.ui import actions, attrs, panels
|
||||
|
||||
|
||||
class TokenPanel(panels.ObjectAttributesPanel):
|
||||
version = attrs.NumericAttr('version')
|
||||
key = attrs.TextAttr('key')
|
||||
token = attrs.TextAttr('partial')
|
||||
pepper_id = attrs.NumericAttr('pepper_id')
|
||||
user = attrs.RelatedObjectAttr('user', linkify=True)
|
||||
description = attrs.TextAttr('description')
|
||||
enabled = attrs.BooleanAttr('enabled')
|
||||
write_enabled = attrs.BooleanAttr('write_enabled')
|
||||
expires = attrs.TextAttr('expires')
|
||||
last_used = attrs.TextAttr('last_used')
|
||||
allowed_ips = attrs.TextAttr('allowed_ips')
|
||||
|
||||
|
||||
class TokenExamplePanel(panels.Panel):
|
||||
template_name = 'users/panels/token_example.html'
|
||||
title = _('Example Usage')
|
||||
actions = [
|
||||
actions.CopyContent('token-example')
|
||||
]
|
||||
@@ -3,7 +3,9 @@ from django.db.models import Count
|
||||
from core.models import ObjectChange
|
||||
from core.tables import ObjectChangeTable
|
||||
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
|
||||
from netbox.ui import layout
|
||||
from netbox.views import generic
|
||||
from users.ui import panels
|
||||
from utilities.query import count_related
|
||||
from utilities.views import GetRelatedModelsMixin, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
@@ -26,6 +28,14 @@ class TokenListView(generic.ObjectListView):
|
||||
@register_model_view(Token)
|
||||
class TokenView(generic.ObjectView):
|
||||
queryset = Token.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.TokenPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
panels.TokenExamplePanel(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(Token, 'add', detail=False)
|
||||
|
||||
17
netbox/utilities/filtersets.py
Normal file
17
netbox/utilities/filtersets.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from netbox.registry import registry
|
||||
|
||||
__all__ = (
|
||||
'register_filterset',
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
@@ -17,11 +17,20 @@ __all__ = (
|
||||
'JSONField',
|
||||
'LaxURLField',
|
||||
'MACAddressField',
|
||||
'QueryField',
|
||||
'SlugField',
|
||||
'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):
|
||||
"""
|
||||
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 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__ = (
|
||||
'BulkDeleteForm',
|
||||
@@ -140,11 +141,11 @@ class CSVModelForm(forms.ModelForm):
|
||||
return super().clean()
|
||||
|
||||
|
||||
class FilterForm(forms.Form):
|
||||
class FilterForm(FilterModifierMixin, forms.Form):
|
||||
"""
|
||||
Base Form class for FilterSet forms.
|
||||
"""
|
||||
q = forms.CharField(
|
||||
q = QueryField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
|
||||
@@ -5,13 +5,100 @@ from django import forms
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
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__ = (
|
||||
'BackgroundJobMixin',
|
||||
'CheckLastUpdatedMixin',
|
||||
'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):
|
||||
background_job = forms.BooleanField(
|
||||
label=_('Background job'),
|
||||
@@ -75,3 +162,68 @@ class DistanceValidationMixin(forms.Form):
|
||||
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 .datetime import *
|
||||
from .misc import *
|
||||
from .modifiers 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.urls import NoReverseMatch, reverse
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ObjectType
|
||||
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 netbox.settings import DISK_BASE_UNIT, RAM_BASE_UNIT
|
||||
|
||||
@@ -418,7 +420,20 @@ def applied_filters(context, model, form, query_params):
|
||||
continue
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
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)])
|
||||
|
||||
# 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({
|
||||
'name': filter_name,
|
||||
'value': form.cleaned_data[filter_name],
|
||||
'name': param_name, # Use actual param name for removal link
|
||||
'value': form.cleaned_data.get(filter_name),
|
||||
'link_url': f'?{querydict.urlencode()}',
|
||||
'link_text': f'{bound_field.label}: {display_value}',
|
||||
'link_text': link_text,
|
||||
})
|
||||
|
||||
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 utilities.filtersets 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())
|
||||
@@ -11,9 +11,9 @@ from extras.models import ConfigTemplate
|
||||
from ipam.filtersets import PrimaryIPFilterSet
|
||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
||||
|
||||
from users.filterset_mixins import OwnerFilterMixin
|
||||
from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@@ -27,6 +27,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ClusterTypeFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -34,6 +35,7 @@ class ClusterTypeFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ClusterGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -41,6 +43,7 @@ class ClusterGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet)
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ScopedFilterSet, ContactModelFilterSet):
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
@@ -81,6 +84,7 @@ class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ScopedFilterSet,
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VirtualMachineFilterSet(
|
||||
PrimaryModelFilterSet,
|
||||
TenancyFilterSet,
|
||||
@@ -241,6 +245,7 @@ class VirtualMachineFilterSet(
|
||||
return queryset.exclude(params)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VMInterfaceFilterSet(CommonInterfaceFilterSet, OwnerFilterMixin, NetBoxModelFilterSet):
|
||||
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_machine__cluster',
|
||||
@@ -303,6 +308,7 @@ class VMInterfaceFilterSet(CommonInterfaceFilterSet, OwnerFilterMixin, NetBoxMod
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VirtualDiskFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
||||
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_machine',
|
||||
|
||||
@@ -3,11 +3,9 @@ from typing import Annotated, TYPE_CHECKING
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry import ID
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import FilterLookup
|
||||
|
||||
from netbox.graphql.filter_mixins import NetBoxModelFilterMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .filters import VirtualMachineFilter
|
||||
|
||||
@@ -17,7 +15,7 @@ __all__ = (
|
||||
|
||||
|
||||
@dataclass
|
||||
class VMComponentFilterMixin(NetBoxModelFilterMixin):
|
||||
class VMComponentFilterMixin:
|
||||
virtual_machine: Annotated['VirtualMachineFilter', strawberry.lazy('virtualization.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
@@ -7,11 +7,8 @@ from strawberry_django import BaseFilterLookup, FilterLookup
|
||||
|
||||
from dcim.graphql.filter_mixins import InterfaceBaseFilterMixin, RenderConfigFilterMixin, ScopedFilterMixin
|
||||
from extras.graphql.filter_mixins import ConfigContextFilterMixin
|
||||
from netbox.graphql.filter_mixins import (
|
||||
ImageAttachmentFilterMixin,
|
||||
OrganizationalModelFilterMixin,
|
||||
PrimaryModelFilterMixin,
|
||||
)
|
||||
from netbox.graphql.filter_mixins import ImageAttachmentFilterMixin
|
||||
from netbox.graphql.filters import NetBoxModelFilter, OrganizationalModelFilter, PrimaryModelFilter
|
||||
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
|
||||
from virtualization import models
|
||||
from virtualization.graphql.filter_mixins import VMComponentFilterMixin
|
||||
@@ -40,7 +37,7 @@ __all__ = (
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Cluster, lookups=True)
|
||||
class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
type: Annotated['ClusterTypeFilter', strawberry.lazy('virtualization.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -59,14 +56,14 @@ class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, P
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ClusterGroup, lookups=True)
|
||||
class ClusterGroupFilter(ContactFilterMixin, OrganizationalModelFilterMixin):
|
||||
class ClusterGroupFilter(ContactFilterMixin, OrganizationalModelFilter):
|
||||
vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ClusterType, lookups=True)
|
||||
class ClusterTypeFilter(OrganizationalModelFilterMixin):
|
||||
class ClusterTypeFilter(OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@@ -77,7 +74,7 @@ class VirtualMachineFilter(
|
||||
RenderConfigFilterMixin,
|
||||
ConfigContextFilterMixin,
|
||||
TenancyFilterMixin,
|
||||
PrimaryModelFilterMixin,
|
||||
PrimaryModelFilter,
|
||||
):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
@@ -138,7 +135,7 @@ class VirtualMachineFilter(
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VMInterface, lookups=True)
|
||||
class VMInterfaceFilter(VMComponentFilterMixin, InterfaceBaseFilterMixin):
|
||||
class VMInterfaceFilter(InterfaceBaseFilterMixin, VMComponentFilterMixin, NetBoxModelFilter):
|
||||
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -163,7 +160,7 @@ class VMInterfaceFilter(VMComponentFilterMixin, InterfaceBaseFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualDisk, lookups=True)
|
||||
class VirtualDiskFilter(VMComponentFilterMixin):
|
||||
class VirtualDiskFilter(VMComponentFilterMixin, NetBoxModelFilter):
|
||||
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ from ipam.models import IPAddress, RouteTarget, VLAN
|
||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from .choices import *
|
||||
from .models import *
|
||||
@@ -26,6 +27,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class TunnelGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -33,6 +35,7 @@ class TunnelGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class TunnelFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=TunnelStatusChoices
|
||||
@@ -75,6 +78,7 @@ class TunnelFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class TunnelTerminationFilterSet(NetBoxModelFilterSet):
|
||||
tunnel_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tunnel',
|
||||
@@ -124,6 +128,7 @@ class TunnelTerminationFilterSet(NetBoxModelFilterSet):
|
||||
fields = ('id', 'termination_id')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class IKEProposalFilterSet(PrimaryModelFilterSet):
|
||||
ike_policy_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='ike_policies',
|
||||
@@ -163,6 +168,7 @@ class IKEProposalFilterSet(PrimaryModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class IKEPolicyFilterSet(PrimaryModelFilterSet):
|
||||
version = django_filters.MultipleChoiceFilter(
|
||||
choices=IKEVersionChoices
|
||||
@@ -194,6 +200,7 @@ class IKEPolicyFilterSet(PrimaryModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class IPSecProposalFilterSet(PrimaryModelFilterSet):
|
||||
ipsec_policy_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='ipsec_policies',
|
||||
@@ -227,6 +234,7 @@ class IPSecProposalFilterSet(PrimaryModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class IPSecPolicyFilterSet(PrimaryModelFilterSet):
|
||||
pfs_group = django_filters.MultipleChoiceFilter(
|
||||
choices=DHGroupChoices
|
||||
@@ -255,6 +263,7 @@ class IPSecPolicyFilterSet(PrimaryModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class IPSecProfileFilterSet(PrimaryModelFilterSet):
|
||||
mode = django_filters.MultipleChoiceFilter(
|
||||
choices=IPSecModeChoices
|
||||
@@ -294,6 +303,7 @@ class IPSecProfileFilterSet(PrimaryModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class L2VPNFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=L2VPNTypeChoices,
|
||||
@@ -340,6 +350,7 @@ class L2VPNFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilter
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
|
||||
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=L2VPN.objects.all(),
|
||||
|
||||
@@ -245,7 +245,7 @@ class L2VPNFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFil
|
||||
class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
model = L2VPNTermination
|
||||
fieldsets = (
|
||||
FieldSet('filter_id', 'l2vpn_id'),
|
||||
FieldSet('filter_id', 'tag', 'l2vpn_id'),
|
||||
FieldSet(
|
||||
'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
|
||||
name=_('Assigned Object')
|
||||
@@ -303,3 +303,4 @@ class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
},
|
||||
label=_('Virtual Machine')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -5,9 +5,10 @@ import strawberry_django
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
|
||||
from netbox.graphql.filter_mixins import NetBoxModelFilterMixin, OrganizationalModelFilterMixin, PrimaryModelFilterMixin
|
||||
from netbox.graphql.filters import (
|
||||
ChangeLoggedModelFilter, NetBoxModelFilter, OrganizationalModelFilter, PrimaryModelFilter,
|
||||
)
|
||||
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
|
||||
from vpn import models
|
||||
|
||||
@@ -32,14 +33,12 @@ __all__ = (
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.TunnelGroup, lookups=True)
|
||||
class TunnelGroupFilter(OrganizationalModelFilterMixin):
|
||||
class TunnelGroupFilter(OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.TunnelTermination, lookups=True)
|
||||
class TunnelTerminationFilter(
|
||||
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
|
||||
):
|
||||
class TunnelTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
tunnel: Annotated['TunnelFilter', strawberry.lazy('vpn.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
tunnel_id: ID | None = strawberry_django.filter_field()
|
||||
role: BaseFilterLookup[Annotated['TunnelTerminationRoleEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
|
||||
@@ -59,7 +58,7 @@ class TunnelTerminationFilter(
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Tunnel, lookups=True)
|
||||
class TunnelFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class TunnelFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: BaseFilterLookup[Annotated['TunnelStatusEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -85,7 +84,7 @@ class TunnelFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.IKEProposal, lookups=True)
|
||||
class IKEProposalFilter(PrimaryModelFilterMixin):
|
||||
class IKEProposalFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
authentication_method: (
|
||||
BaseFilterLookup[Annotated['AuthenticationMethodEnum', strawberry.lazy('vpn.graphql.enums')]] | None
|
||||
@@ -114,7 +113,7 @@ class IKEProposalFilter(PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.IKEPolicy, lookups=True)
|
||||
class IKEPolicyFilter(PrimaryModelFilterMixin):
|
||||
class IKEPolicyFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
version: BaseFilterLookup[Annotated['IKEVersionEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -129,7 +128,7 @@ class IKEPolicyFilter(PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.IPSecProposal, lookups=True)
|
||||
class IPSecProposalFilter(PrimaryModelFilterMixin):
|
||||
class IPSecProposalFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
encryption_algorithm: (
|
||||
BaseFilterLookup[Annotated['EncryptionAlgorithmEnum', strawberry.lazy('vpn.graphql.enums')]] | None
|
||||
@@ -155,7 +154,7 @@ class IPSecProposalFilter(PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.IPSecPolicy, lookups=True)
|
||||
class IPSecPolicyFilter(PrimaryModelFilterMixin):
|
||||
class IPSecPolicyFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
proposals: Annotated['IPSecProposalFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -166,7 +165,7 @@ class IPSecPolicyFilter(PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.IPSecProfile, lookups=True)
|
||||
class IPSecProfileFilter(PrimaryModelFilterMixin):
|
||||
class IPSecProfileFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
mode: BaseFilterLookup[Annotated['IPSecModeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -182,7 +181,7 @@ class IPSecProfileFilter(PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.L2VPN, lookups=True)
|
||||
class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
type: BaseFilterLookup[Annotated['L2VPNTypeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
|
||||
@@ -206,7 +205,7 @@ class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixi
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.L2VPNTermination, lookups=True)
|
||||
class L2VPNTerminationFilter(NetBoxModelFilterMixin):
|
||||
class L2VPNTerminationFilter(NetBoxModelFilter):
|
||||
l2vpn: Annotated['L2VPNFilter', strawberry.lazy('vpn.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
l2vpn_id: ID | None = strawberry_django.filter_field()
|
||||
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.choices import LinkStatusChoices
|
||||
from dcim.base_filtersets import ScopedFilterSet
|
||||
from dcim.choices import LinkStatusChoices
|
||||
from dcim.models import Interface
|
||||
from ipam.models import VLAN
|
||||
from netbox.filtersets import NestedGroupModelFilterSet, PrimaryModelFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@@ -18,6 +19,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class WirelessLANGroupFilterSet(NestedGroupModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=WirelessLANGroup.objects.all()
|
||||
@@ -44,6 +46,7 @@ class WirelessLANGroupFilterSet(NestedGroupModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class WirelessLANFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet):
|
||||
group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=WirelessLANGroup.objects.all(),
|
||||
@@ -87,6 +90,7 @@ class WirelessLANFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilter
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class WirelessLinkFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
interface_a_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Interface.objects.all()
|
||||
|
||||
@@ -5,18 +5,16 @@ import strawberry
|
||||
import strawberry_django
|
||||
from strawberry_django import FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseFilterMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .enums import *
|
||||
|
||||
__all__ = (
|
||||
'WirelessAuthenticationBaseFilterMixin',
|
||||
'WirelessAuthenticationFilterMixin',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WirelessAuthenticationBaseFilterMixin(BaseFilterMixin):
|
||||
class WirelessAuthenticationFilterMixin:
|
||||
auth_type: Annotated['WirelessAuthTypeEnum', strawberry.lazy('wireless.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
@@ -6,10 +6,11 @@ from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup
|
||||
|
||||
from dcim.graphql.filter_mixins import ScopedFilterMixin
|
||||
from netbox.graphql.filter_mixins import DistanceFilterMixin, PrimaryModelFilterMixin, NestedGroupModelFilterMixin
|
||||
from netbox.graphql.filter_mixins import DistanceFilterMixin
|
||||
from netbox.graphql.filters import PrimaryModelFilter, NestedGroupModelFilter
|
||||
from tenancy.graphql.filter_mixins import TenancyFilterMixin
|
||||
from wireless import models
|
||||
from .filter_mixins import WirelessAuthenticationBaseFilterMixin
|
||||
from .filter_mixins import WirelessAuthenticationFilterMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dcim.graphql.filters import InterfaceFilter
|
||||
@@ -24,16 +25,16 @@ __all__ = (
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.WirelessLANGroup, lookups=True)
|
||||
class WirelessLANGroupFilter(NestedGroupModelFilterMixin):
|
||||
class WirelessLANGroupFilter(NestedGroupModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.WirelessLAN, lookups=True)
|
||||
class WirelessLANFilter(
|
||||
WirelessAuthenticationBaseFilterMixin,
|
||||
WirelessAuthenticationFilterMixin,
|
||||
ScopedFilterMixin,
|
||||
TenancyFilterMixin,
|
||||
PrimaryModelFilterMixin
|
||||
PrimaryModelFilter
|
||||
):
|
||||
ssid: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: BaseFilterLookup[Annotated['WirelessLANStatusEnum', strawberry.lazy('wireless.graphql.enums')]] | None = (
|
||||
@@ -49,10 +50,10 @@ class WirelessLANFilter(
|
||||
|
||||
@strawberry_django.filter_type(models.WirelessLink, lookups=True)
|
||||
class WirelessLinkFilter(
|
||||
WirelessAuthenticationBaseFilterMixin,
|
||||
WirelessAuthenticationFilterMixin,
|
||||
DistanceFilterMixin,
|
||||
TenancyFilterMixin,
|
||||
PrimaryModelFilterMixin
|
||||
PrimaryModelFilter
|
||||
):
|
||||
interface_a: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
||||
Reference in New Issue
Block a user