Compare commits

..

36 Commits

Author SHA1 Message Date
Jason Novinger
0c31edb3ca Merge 2f3d7b1c5c into afba5b2791 2025-11-25 21:24:23 +00:00
Jason Novinger
2f3d7b1c5c Fix applied_filters template tag to use field-type-specific lookup labelsresolves
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
E.g. resolves gt="after" for dates vs "greater than" for numbers
2025-11-25 15:24:03 -06:00
Jason Novinger
212ec72f22 Switch to sentence case for filter pill text 2025-11-25 15:24:03 -06:00
Jason Novinger
905a516d23 Removed explicit checks against QueryField and [Null]BooleanField
I did add them to FORM_FIELD_LOOKUPS, though, to underscore that they
were considered and are intentially empty for future devs.
2025-11-25 15:24:03 -06:00
Jason Novinger
d4535df043 Fix filterset registration for doubly-registered models 2025-11-25 15:24:03 -06:00
Jason Novinger
93f916d115 Include MODIFIER_EMPTY_FALSE/_TRUE in __all__
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-11-25 15:24:02 -06:00
Jason Novinger
592a0f9653 Support filter modifiers for ChoiceField 2025-11-25 15:24:02 -06:00
Jason Novinger
d1788c1d2f Enables filter modifiers on APISelect based fields 2025-11-25 15:24:02 -06:00
Jason Novinger
5d309ce85f Remove unused star import, leftover from earlier work 2025-11-25 15:24:02 -06:00
Jason Novinger
ebdf3f98d5 Update app registry for new filtersets store 2025-11-25 15:24:02 -06:00
Jason Novinger
46f9b2ccb4 Remove unneeded imports left from earlier registry work 2025-11-25 15:24:02 -06:00
Jason Novinger
64f38334be Refactor register_filterset to be more generic and simple 2025-11-25 15:24:02 -06:00
Jason Novinger
cf0a13f535 Address PR feedback: refactor brittle test for APISelect useage
Now checks if widget is actually APISelect, rather than trying to infer
from the class name.
2025-11-25 15:24:02 -06:00
Jason Novinger
e2b9317be1 Fix registry pattern to use model identifiers as keys
Changed filterset registration to use model identifiers ('{app_label}.{model_name}')
as registry keys instead of form classes, matching NetBox's pattern for search indexes.
2025-11-25 15:24:02 -06:00
Jason Novinger
316c12c6a9 Address PR feedback: Rename FilterModifierWidget parameter to widget 2025-11-25 15:24:02 -06:00
Jason Novinger
d900bfc312 Address PR feedback: Refactor applied_filters to use FORM_FIELD_LOOKUPS 2025-11-25 15:24:02 -06:00
Jason Novinger
3dcc299cda Address PR feedback: Refactor and consolidate field filtering logic
Consolidated field enhancement logic in FilterModifierMixin by:
- Creating QueryField marker type (CharField subclass) for search fields
- Updating FilterForm and NetBoxModelFilterSetForm to use QueryField for 'q'
- Moving all skip logic into _get_lookup_choices() to return empty list for
  fields that shouldn't be enhanced
- Removing separate _should_skip_field() method
- Removing unused field_name parameter from _get_lookup_choices()
- Replacing hardcoded field name check ('q') with type-based detection
2025-11-25 15:24:02 -06:00
Jason Novinger
626155c5f5 Address PR feedback: Move FORM_FIELD_LOOKUPS to module-level constant
Extracts the field type to lookup mappings from FilterModifierMixin class
attribute to a module-level constant for better reusability.
2025-11-25 15:24:02 -06:00
Jason Novinger
711984a825 Fix filter modifier form submission bug with 'action' field collision
Forms with a field named "action" (e.g., ObjectChangeFilterForm) were causing
the form.action property to be shadowed by the field element, resulting in
[object HTMLSelectElement] appearing in the URL path.

Use form.getAttribute('action') instead of form.action to reliably retrieve
the form's action URL without collision from form fields.

Fixes form submission on /core/changelog/ and any other forms with an 'action'
field using filter modifiers.
2025-11-25 15:24:02 -06:00
Jason Novinger
c298108c1c Address PR feedback: Move FilterModifierMixin into base filter form classes
Incorporates FilterModifierMixin into NetBoxModelFilterSetForm and FilterForm,
making filter modifiers automatic for all filter forms throughout the application.
2025-11-25 15:24:02 -06:00
Jason Novinger
ab70c4b2c1 Address PR feedback: Replace global filterset mappings with registry 2025-11-25 15:24:02 -06:00
Jason Novinger
de96bc8770 Add ChoiceField support to FilterModifierMixin
Enable filter modifiers for single-choice ChoiceFields in addition to the
existing MultipleChoiceField support. ChoiceFields can now display modifier
dropdowns with "Is", "Is Not", "Is Empty", and "Is Not Empty" options when
the corresponding FilterSet defines those lookups.

The mixin correctly verifies lookup availability against the FilterSet, so
modifiers only appear when multiple lookup options are actually supported.
Currently most FilterSets only define 'exact' for single-choice fields, but
this change enables future FilterSet enhancements to expose additional
lookups for ChoiceFields.
2025-11-25 15:24:02 -06:00
Jason Novinger
e9bacf9a2e Enable filter form modifiers on Extras models 2025-11-25 15:24:02 -06:00
Jason Novinger
aa061f958f Enable filter form modifiers on Core models 2025-11-25 15:24:02 -06:00
Jason Novinger
a5b5dd0357 Enable filter form modifiers on Users models 2025-11-25 15:24:02 -06:00
Jason Novinger
f0c6b186ce Enable filter form modifiers on Circuit models 2025-11-25 15:24:02 -06:00
Jason Novinger
cf2608a4ff Enable filter form modifiers on Virtualization models 2025-11-25 15:24:02 -06:00
Jason Novinger
9c436ee0cd Enable filter form modifiers on VPN models 2025-11-25 15:24:02 -06:00
Jason Novinger
4199aeb21e Enable filter form modifiers on IPAM models 2025-11-25 15:24:02 -06:00
Jason Novinger
a7a1a95955 Enable filter form modifiers on Wireless models 2025-11-25 15:24:02 -06:00
Jason Novinger
63ea883717 Enable filter form modifiers on Tenancy models 2025-11-25 15:24:02 -06:00
Jason Novinger
d5f33bd4b4 Enable filter form modifiers on DCIM models 2025-11-25 15:24:02 -06:00
Jason Novinger
8291eb5c13 Fix CircuitFilterForm inheritance 2025-11-25 15:24:02 -06:00
Jason Novinger
93c5c3b846 Fix import order 2025-11-25 15:24:02 -06:00
Jason Novinger
e544976534 Remove extraneous TS comments 2025-11-25 15:24:02 -06:00
Jason Novinger
e19591484c Fixes #7604: Add filter modifier dropdowns for advanced lookup operators
Implements dynamic filter modifier UI that allows users to select lookup operators
(exact, contains, starts with, regex, negation, empty/not empty) directly in filter
forms without manual URL parameter editing.

Supports filters for all scalar types and strings, as well as some
related object filters. Explicitly does not support filters on fields
that use APIWidget. That has been broken out in to follow up work.

**Backend:**
- FilterModifierWidget: Wraps form widgets with lookup modifier dropdown
- FilterModifierMixin: Auto-enhances filterset fields with appropriate lookups
- Extended lookup support: Adds negation (n), regex, iregex, empty_true/false lookups
- Field-type-aware: CharField gets text lookups, IntegerField gets comparison operators, etc.

**Frontend:**
- TypeScript handler syncs modifier dropdown with URL parameters
- Dynamically updates form field names (serial → serial__ic) on modifier change
- Flexible-width modifier dropdowns with semantic CSS classes
2025-11-25 15:23:54 -06:00
66 changed files with 99868 additions and 3670 deletions

View File

@@ -6,17 +6,12 @@ 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=(
@@ -47,7 +42,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
@@ -67,9 +62,7 @@ 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):
@@ -97,9 +90,7 @@ 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()
```
@@ -115,9 +106,7 @@ 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()
```

View File

@@ -325,14 +325,14 @@ class CircuitTypeType(OrganizationalObjectType):
### Change filters.py
Filter classes should inherit from `netbox.graphql.filters.BaseModelFilter`.
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.
```python title="New"
import strawberry
import strawberry_django
from circuits import filtersets, models
from netbox.graphql.filters import BaseModelFilter
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (
'CircuitFilter',
@@ -340,7 +340,8 @@ __all__ = (
@strawberry_django.filter(models.Circuit, lookups=True)
class CircuitFilter(BaseModelFilter):
@autotype_decorator(filtersets.CircuitFilterSet)
class CircuitFilter(BaseFilterMixin):
pass
```

View File

@@ -1,111 +0,0 @@
## v4.5.0 (FUTURE)
### Breaking Changes
* Python 3.10 and 3.11 are no longer supported. NetBox now requires Python 3.12 or later.
* GraphQL API queries which filter by object IDs or enums must now specify a filter lookup similar to other fields. (For example, `id: 123` becomes `id: {exact: 123 }`.)
* Rendering a device or virtual machine configuration is now restricted to users with the `render_config` permission for the applicable object type.
* Retrieval of API token plaintexts is no longer supported. The `ALLOW_TOKEN_RETRIEVAL` config parameter has been removed.
* The owner of an API token can no longer be changed once it has been created.
* Config contexts now apply to all child platforms of a parent platform.
* The `/api/extras/object-types/` REST API endpoint has been removed. (Use `/api/core/object-types/` instead.)
* The `/api/dcim/cable-terminations/` REST API endpoint is now read-only. Cable terminations must be set on cables directly.
* The UI view dedicated to swaping A/Z circuit terminations has been removed.
* Webhooks no longer specify a `model` in payload data. (Reference `object_type` instead, which includes the parent app label.)
* The obsolete module `core.models.contenttypes` has been removed (replaced in v4.4 by `core.models.object_types`).
* The `load_yaml()` and `load_json()` utility methods have been removed from the base class for custom scripts.
* The experimental HTMX navigation feature has been removed.
* The obsolete field `is_staff` has been removed from the `User` model.
### New Features
#### Lookup Modifiers in Filter Forms ([#7604](https://github.com/netbox-community/netbox/issues/7604))
#### Improved API Authentication Tokens ([#20210](https://github.com/netbox-community/netbox/issues/20210))
#### Object Ownership ([#20304](https://github.com/netbox-community/netbox/issues/20304))
#### Advanced Port Mappings ([#20564](https://github.com/netbox-community/netbox/issues/20564))
#### Cable Profiles ([#20788](https://github.com/netbox-community/netbox/issues/20788))
### Enhancements
* [#16681](https://github.com/netbox-community/netbox/issues/16681) - Introduce a `render_config` permission, which is noq required to render a device or virtual machine configuration
* [#18658](https://github.com/netbox-community/netbox/issues/18658) - Add a `start_on_boot` choice field for virtual machines
* [#19095](https://github.com/netbox-community/netbox/issues/19095) - Add support for Python 3.13 and 3.14
* [#19338](https://github.com/netbox-community/netbox/issues/19338) - Enable filter lookups for object IDs and enums in GraphQL API queries
* [#19523](https://github.com/netbox-community/netbox/issues/19523) - Cache the number of instances for device, module, and rack types, and enable filtering by these counts
* [#20417](https://github.com/netbox-community/netbox/issues/20417) - Add an optional `color` field for device type power outlets
* [#20476](https://github.com/netbox-community/netbox/issues/20476) - Once provisioned, the owner of an API token cannot be changed
* [#20492](https://github.com/netbox-community/netbox/issues/20492) - Completely disabled the means to retrieve legacy API token plaintexts (removed the `ALLOW_TOKEN_RETRIEVAL` config parameter)
* [#20639](https://github.com/netbox-community/netbox/issues/20639) - Apply config contexts to devices/VMs assigned any child platform of the parent platform
* [#20834](https://github.com/netbox-community/netbox/issues/20834) - Add an `enabled` boolean field to API tokens
* [#20917](https://github.com/netbox-community/netbox/issues/20917) - Include usage reference on API token views
* [#20925](https://github.com/netbox-community/netbox/issues/20925) - Add optional `comments` field to all subclasses of `OrganizationalModel`
* [#20936](https://github.com/netbox-community/netbox/issues/20936) - Introduce the `/api/authentication-check/` REST API endpoint for validating authentication tokens
### Plugins
* [#13182](https://github.com/netbox-community/netbox/issues/13182) - Added `PrimaryModel`, `OrganizationalModel`, and `NestedGroupModel` to the plugins API, as well as their respective base classes for various resources
### Other Changes
* [#16137](https://github.com/netbox-community/netbox/issues/16137) - Remove the obsolete boolean field `is_staff` from the `User` model
* [#17571](https://github.com/netbox-community/netbox/issues/17571) - Remove the experimental HTMX navigation feature
* [#17936](https://github.com/netbox-community/netbox/issues/17936) - Introduce a dedicated `GFKSerializerField` for representing generic foreign keys in API serializers
* [#19889](https://github.com/netbox-community/netbox/issues/19889) - Drop support for Python 3.10 and 3.11
* [#19898](https://github.com/netbox-community/netbox/issues/19898) - Remove the obsolete REST API endpoint `/api/extras/object-types/`
* [#20088](https://github.com/netbox-community/netbox/issues/20088) - Remove the non-deterministic `model` key from webhook payload data
* [#20095](https://github.com/netbox-community/netbox/issues/20095) - Remove the obsolete module `core.models.contenttypes`
* [#20096](https://github.com/netbox-community/netbox/issues/20096) - Remove the `load_yaml()` and `load_json()` utility methods from the `BaseScript` class
* [#20204](https://github.com/netbox-community/netbox/issues/20204) - Started migrating object views from custom HTML templates to declarative layouts
* [#20617](https://github.com/netbox-community/netbox/issues/20617) - Introduce `BaseModel` as the global base class for models
* [#20683](https://github.com/netbox-community/netbox/issues/20683) - Remove the UI view dedicated to swaping A/Z circuit terminations
* [#20926](https://github.com/netbox-community/netbox/issues/20926) - Standardize naming of GraphQL filters
### REST API Changes
* Most objects now include an optional `owner` foreign key field.
* The `/api/dcim/cable-terminations` endpoint is now read-only.
* Introduced the `/api/authentication-check/` endpoint.
* `circuits.CircuitGroup`
* Add optional `comments` field
* `circuits.CircuitType`
* Add optional `comments` field
* `circuits.VirtualCircuitType`
* Add optional `comments` field
* `dcim.Cable`
* Add the optional `profile` choice field
* `dcim.InventoryItemRole`
* Add optional `comments` field
* `dcim.Manufacturer`
* Add optional `comments` field
* `dcim.ModuleType`
* Add read-only `module_count` integer field
* `dcim.PowerOutletTemplate`
* Add optional `color` field
* `dcim.RackRole`
* Add optional `comments` field
* `dcim.RackType`
* Add read-only `rack_count` integer field
* `ipam.ASNRange`
* Add optional `comments` field
* `ipam.RIR`
* Add optional `comments` field
* `ipam.Role`
* Add optional `comments` field
* `ipam.VLANGroup`
* Add optional `comments` field
* `tenancy.ContactRole`
* Add optional `comments` field
* `users.Token`
* Add `enabled` boolean field
* `virtualization.ClusterGroup`
* Add optional `comments` field
* `virtualization.ClusterType`
* Add optional `comments` field
* `virtualization.VirtualMachine`
* Add optional `start_on_boot` choice field
* `vpn.TunnelGroup`
* Add optional `comments` field

View File

@@ -25,12 +25,10 @@ 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
@@ -344,21 +342,12 @@ 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,
})

View File

@@ -3,18 +3,17 @@ from typing import Annotated, TYPE_CHECKING
import strawberry
import strawberry_django
from strawberry_django import BaseFilterLookup
from netbox.graphql.filter_mixins import OrganizationalModelFilterMixin
if TYPE_CHECKING:
from netbox.graphql.enums import ColorEnum
__all__ = (
'CircuitTypeFilterMixin',
'BaseCircuitTypeFilterMixin',
)
@dataclass
class CircuitTypeFilterMixin:
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
class BaseCircuitTypeFilterMixin(OrganizationalModelFilterMixin):
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()

View File

@@ -7,12 +7,17 @@ from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, FilterLookup, DateFilterLookup
from circuits import models
from circuits.graphql.filter_mixins import CircuitTypeFilterMixin
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin
from dcim.graphql.filter_mixins import CabledObjectModelFilterMixin
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
from netbox.graphql.filter_mixins import DistanceFilterMixin, ImageAttachmentFilterMixin
from netbox.graphql.filters import ChangeLoggedModelFilter, OrganizationalModelFilter, PrimaryModelFilter
from netbox.graphql.filter_mixins import (
DistanceFilterMixin,
ImageAttachmentFilterMixin,
OrganizationalModelFilterMixin,
PrimaryModelFilterMixin,
)
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
from .filter_mixins import BaseCircuitTypeFilterMixin
if TYPE_CHECKING:
from core.graphql.filters import ContentTypeFilter
@@ -38,9 +43,10 @@ __all__ = (
@strawberry_django.filter_type(models.CircuitTermination, lookups=True)
class CircuitTerminationFilter(
BaseObjectTypeFilterMixin,
CustomFieldsFilterMixin,
TagsFilterMixin,
ChangeLoggedModelFilter,
ChangeLogFilterMixin,
CabledObjectModelFilterMixin,
):
circuit: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
@@ -89,7 +95,7 @@ class CircuitFilter(
ImageAttachmentFilterMixin,
DistanceFilterMixin,
TenancyFilterMixin,
PrimaryModelFilter
PrimaryModelFilterMixin
):
cid: FilterLookup[str] | None = strawberry_django.filter_field()
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
@@ -118,17 +124,19 @@ class CircuitFilter(
@strawberry_django.filter_type(models.CircuitType, lookups=True)
class CircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter):
class CircuitTypeFilter(BaseCircuitTypeFilterMixin):
pass
@strawberry_django.filter_type(models.CircuitGroup, lookups=True)
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilter):
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
pass
@strawberry_django.filter_type(models.CircuitGroupAssignment, lookups=True)
class CircuitGroupAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
class CircuitGroupAssignmentFilter(
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
):
member_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -143,7 +151,7 @@ class CircuitGroupAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, Cha
@strawberry_django.filter_type(models.Provider, lookups=True)
class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
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()
@@ -153,7 +161,7 @@ class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.ProviderAccount, lookups=True)
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilter):
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -163,7 +171,7 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
class ProviderNetworkFilter(PrimaryModelFilter):
class ProviderNetworkFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -173,12 +181,12 @@ class ProviderNetworkFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
class VirtualCircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter):
class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin):
pass
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
cid: FilterLookup[str] | None = strawberry_django.filter_field()
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -201,7 +209,9 @@ class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.VirtualCircuitTermination, lookups=True)
class VirtualCircuitTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
class VirtualCircuitTerminationFilter(
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
):
virtual_circuit: Annotated['VirtualCircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)

View File

@@ -4,18 +4,31 @@ from typing import Annotated, TYPE_CHECKING
import strawberry
import strawberry_django
from strawberry_django import DatetimeFilterLookup
from strawberry import ID
from strawberry_django import FilterLookup, DatetimeFilterLookup
if TYPE_CHECKING:
from .filters import *
__all__ = (
'ChangeLoggingMixin',
'BaseFilterMixin',
'BaseObjectTypeFilterMixin',
'ChangeLogFilterMixin',
)
# @strawberry.input
class BaseFilterMixin: ...
@dataclass
class ChangeLoggingMixin:
class BaseObjectTypeFilterMixin(BaseFilterMixin):
id: FilterLookup[ID] | None = strawberry_django.filter_field()
@dataclass
class ChangeLogFilterMixin(BaseFilterMixin):
id: FilterLookup[ID] | None = strawberry_django.filter_field()
# 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()

View File

@@ -8,7 +8,8 @@ from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup
from core import models
from netbox.graphql.filters import BaseModelFilter, PrimaryModelFilter
from core.graphql.filter_mixins import BaseFilterMixin
from netbox.graphql.filter_mixins import PrimaryModelFilterMixin
from .enums import *
if TYPE_CHECKING:
@@ -24,7 +25,8 @@ __all__ = (
@strawberry_django.filter_type(models.DataFile, lookups=True)
class DataFileFilter(BaseModelFilter):
class DataFileFilter(BaseFilterMixin):
id: FilterLookup[ID] | None = strawberry_django.filter_field()
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 = (
@@ -39,7 +41,7 @@ class DataFileFilter(BaseModelFilter):
@strawberry_django.filter_type(models.DataSource, lookups=True)
class DataSourceFilter(PrimaryModelFilter):
class DataSourceFilter(PrimaryModelFilterMixin):
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()
@@ -58,7 +60,8 @@ class DataSourceFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.ObjectChange, lookups=True)
class ObjectChangeFilter(BaseModelFilter):
class ObjectChangeFilter(BaseFilterMixin):
id: FilterLookup[ID] | None = strawberry_django.filter_field()
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()
@@ -85,6 +88,7 @@ class ObjectChangeFilter(BaseModelFilter):
@strawberry_django.filter_type(DjangoContentType, lookups=True)
class ContentTypeFilter(BaseModelFilter):
class ContentTypeFilter(BaseFilterMixin):
id: FilterLookup[ID] | None = strawberry_django.filter_field()
app_label: FilterLookup[str] | None = strawberry_django.filter_field()
model: FilterLookup[str] | None = strawberry_django.filter_field()

View File

@@ -6,7 +6,9 @@ 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:
@@ -20,16 +22,16 @@ __all__ = (
'ComponentModelFilterMixin',
'ComponentTemplateFilterMixin',
'InterfaceBaseFilterMixin',
'ModularComponentFilterMixin',
'ModularComponentModelFilterMixin',
'ModularComponentTemplateFilterMixin',
'RackFilterMixin',
'RackBaseFilterMixin',
'RenderConfigFilterMixin',
'ScopedFilterMixin',
)
@dataclass
class ScopedFilterMixin:
class ScopedFilterMixin(BaseFilterMixin):
scope_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -37,7 +39,7 @@ class ScopedFilterMixin:
@dataclass
class ComponentModelFilterMixin:
class ComponentModelFilterMixin(NetBoxModelFilterMixin):
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()
@@ -46,7 +48,7 @@ class ComponentModelFilterMixin:
@dataclass
class ModularComponentFilterMixin(ComponentModelFilterMixin):
class ModularComponentModelFilterMixin(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 = (
@@ -55,7 +57,7 @@ class ModularComponentFilterMixin(ComponentModelFilterMixin):
@dataclass
class CabledObjectModelFilterMixin:
class CabledObjectModelFilterMixin(BaseFilterMixin):
cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
cable_id: ID | None = strawberry_django.filter_field()
cable_end: (
@@ -65,7 +67,7 @@ class CabledObjectModelFilterMixin:
@dataclass
class ComponentTemplateFilterMixin:
class ComponentTemplateFilterMixin(ChangeLogFilterMixin):
device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -83,7 +85,7 @@ class ModularComponentTemplateFilterMixin(ComponentTemplateFilterMixin):
@dataclass
class RenderConfigFilterMixin:
class RenderConfigFilterMixin(BaseFilterMixin):
config_template: Annotated['ConfigTemplateFilter', strawberry.lazy('extras.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -91,7 +93,7 @@ class RenderConfigFilterMixin:
@dataclass
class InterfaceBaseFilterMixin:
class InterfaceBaseFilterMixin(BaseFilterMixin):
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
mtu: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
@@ -122,7 +124,7 @@ class InterfaceBaseFilterMixin:
@dataclass
class RackFilterMixin:
class RackBaseFilterMixin(WeightFilterMixin, PrimaryModelFilterMixin):
width: BaseFilterLookup[Annotated['RackWidthEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)

View File

@@ -6,24 +6,29 @@ import strawberry_django
from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, FilterLookup
from core.graphql.filter_mixins import 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 ImageAttachmentFilterMixin, WeightFilterMixin
from netbox.graphql.filters import (
ChangeLoggedModelFilter, NestedGroupModelFilter, OrganizationalModelFilter, PrimaryModelFilter, NetBoxModelFilter,
from netbox.graphql.filter_mixins import (
PrimaryModelFilterMixin,
OrganizationalModelFilterMixin,
NestedGroupModelFilterMixin,
ImageAttachmentFilterMixin,
WeightFilterMixin,
)
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,
)
@@ -91,7 +96,7 @@ __all__ = (
@strawberry_django.filter_type(models.Cable, lookups=True)
class CableFilter(TenancyFilterMixin, PrimaryModelFilter):
class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
type: BaseFilterLookup[Annotated['CableTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -114,7 +119,7 @@ class CableFilter(TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.CableTermination, lookups=True)
class CableTerminationFilter(ChangeLoggedModelFilter):
class CableTerminationFilter(ChangeLogFilterMixin):
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 = (
@@ -127,7 +132,7 @@ class CableTerminationFilter(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.ConsolePort, lookups=True)
class ConsolePortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -137,14 +142,14 @@ class ConsolePortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixi
@strawberry_django.filter_type(models.ConsolePortTemplate, lookups=True)
class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin):
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.ConsoleServerPort, lookups=True)
class ConsoleServerPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -154,7 +159,7 @@ class ConsoleServerPortFilter(ModularComponentFilterMixin, CabledObjectModelFilt
@strawberry_django.filter_type(models.ConsoleServerPortTemplate, lookups=True)
class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -167,7 +172,7 @@ class DeviceFilter(
ImageAttachmentFilterMixin,
RenderConfigFilterMixin,
ConfigContextFilterMixin,
PrimaryModelFilter,
PrimaryModelFilterMixin,
):
device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -280,7 +285,7 @@ class DeviceFilter(
@strawberry_django.filter_type(models.DeviceBay, lookups=True)
class DeviceBayFilter(ComponentModelFilterMixin, NetBoxModelFilter):
class DeviceBayFilter(ComponentModelFilterMixin):
installed_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -288,12 +293,12 @@ class DeviceBayFilter(ComponentModelFilterMixin, NetBoxModelFilter):
@strawberry_django.filter_type(models.DeviceBayTemplate, lookups=True)
class DeviceBayTemplateFilter(ComponentTemplateFilterMixin, ChangeLoggedModelFilter):
class DeviceBayTemplateFilter(ComponentTemplateFilterMixin):
pass
@strawberry_django.filter_type(models.InventoryItemTemplate, lookups=True)
class InventoryItemTemplateFilter(ComponentTemplateFilterMixin, ChangeLoggedModelFilter):
class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
parent: Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -313,7 +318,7 @@ class InventoryItemTemplateFilter(ComponentTemplateFilterMixin, ChangeLoggedMode
@strawberry_django.filter_type(models.DeviceRole, lookups=True)
class DeviceRoleFilter(RenderConfigFilterMixin, OrganizationalModelFilter):
class DeviceRoleFilter(OrganizationalModelFilterMixin, RenderConfigFilterMixin):
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -321,7 +326,7 @@ class DeviceRoleFilter(RenderConfigFilterMixin, OrganizationalModelFilter):
@strawberry_django.filter_type(models.DeviceType, lookups=True)
class DeviceTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryModelFilter):
class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -397,7 +402,7 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryMod
@strawberry_django.filter_type(models.FrontPort, lookups=True)
class FrontPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -414,7 +419,7 @@ class FrontPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin,
@strawberry_django.filter_type(models.FrontPortTemplate, lookups=True)
class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -431,7 +436,7 @@ class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedM
@strawberry_django.filter_type(models.MACAddress, lookups=True)
class MACAddressFilter(PrimaryModelFilter):
class MACAddressFilter(PrimaryModelFilterMixin):
mac_address: FilterLookup[str] | None = strawberry_django.filter_field()
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -458,12 +463,7 @@ class MACAddressFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.Interface, lookups=True)
class InterfaceFilter(
ModularComponentFilterMixin,
InterfaceBaseFilterMixin,
CabledObjectModelFilterMixin,
NetBoxModelFilter
):
class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin, CabledObjectModelFilterMixin):
vcdcs: Annotated['VirtualDeviceContextFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -553,7 +553,7 @@ class InterfaceFilter(
@strawberry_django.filter_type(models.InterfaceTemplate, lookups=True)
class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
type: BaseFilterLookup[Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -575,7 +575,7 @@ class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedM
@strawberry_django.filter_type(models.InventoryItem, lookups=True)
class InventoryItemFilter(ComponentModelFilterMixin, NetBoxModelFilter):
class InventoryItemFilter(ComponentModelFilterMixin):
parent: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -602,14 +602,14 @@ class InventoryItemFilter(ComponentModelFilterMixin, NetBoxModelFilter):
@strawberry_django.filter_type(models.InventoryItemRole, lookups=True)
class InventoryItemRoleFilter(OrganizationalModelFilter):
class InventoryItemRoleFilter(OrganizationalModelFilterMixin):
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, NestedGroupModelFilter):
class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilterMixin):
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 = (
@@ -625,12 +625,12 @@ class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilt
@strawberry_django.filter_type(models.Manufacturer, lookups=True)
class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilter):
class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilterMixin):
pass
@strawberry_django.filter_type(models.Module, lookups=True)
class ModuleFilter(ConfigContextFilterMixin, PrimaryModelFilter):
class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
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 = (
@@ -679,7 +679,7 @@ class ModuleFilter(ConfigContextFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.ModuleBay, lookups=True)
class ModuleBayFilter(ModularComponentFilterMixin, NetBoxModelFilter):
class ModuleBayFilter(ModularComponentModelFilterMixin):
parent: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -688,17 +688,17 @@ class ModuleBayFilter(ModularComponentFilterMixin, NetBoxModelFilter):
@strawberry_django.filter_type(models.ModuleBayTemplate, lookups=True)
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin):
position: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ModuleTypeProfile, lookups=True)
class ModuleTypeProfileFilter(PrimaryModelFilter):
class ModuleTypeProfileFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ModuleType, lookups=True)
class ModuleTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryModelFilter):
class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -749,7 +749,7 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryMod
@strawberry_django.filter_type(models.Platform, lookups=True)
class PlatformFilter(OrganizationalModelFilter):
class PlatformFilter(OrganizationalModelFilterMixin):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -761,7 +761,7 @@ class PlatformFilter(OrganizationalModelFilter):
@strawberry_django.filter_type(models.PowerFeed, lookups=True)
class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
power_panel: Annotated['PowerPanelFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -796,7 +796,7 @@ class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryM
@strawberry_django.filter_type(models.PowerOutlet, lookups=True)
class PowerOutletFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: BaseFilterLookup[Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -816,7 +816,7 @@ class PowerOutletFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixi
@strawberry_django.filter_type(models.PowerOutletTemplate, lookups=True)
class PowerOutletTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
type: BaseFilterLookup[Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -830,7 +830,7 @@ class PowerOutletTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLogge
@strawberry_django.filter_type(models.PowerPanel, lookups=True)
class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilter):
class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilterMixin):
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 = (
@@ -843,7 +843,7 @@ class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryMo
@strawberry_django.filter_type(models.PowerPort, lookups=True)
class PowerPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: BaseFilterLookup[Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -856,7 +856,7 @@ class PowerPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin,
@strawberry_django.filter_type(models.PowerPortTemplate, lookups=True)
class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: BaseFilterLookup[Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -869,7 +869,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedM
@strawberry_django.filter_type(models.RackType, lookups=True)
class RackTypeFilter(RackFilterMixin, WeightFilterMixin, PrimaryModelFilter):
class RackTypeFilter(RackBaseFilterMixin):
form_factor: BaseFilterLookup[Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -884,14 +884,7 @@ class RackTypeFilter(RackFilterMixin, WeightFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.Rack, lookups=True)
class RackFilter(
ContactFilterMixin,
ImageAttachmentFilterMixin,
TenancyFilterMixin,
WeightFilterMixin,
RackFilterMixin,
PrimaryModelFilter
):
class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, RackBaseFilterMixin):
form_factor: BaseFilterLookup[Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -925,7 +918,7 @@ class RackFilter(
@strawberry_django.filter_type(models.RackReservation, lookups=True)
class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilter):
class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
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 = (
@@ -940,14 +933,14 @@ class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.RackRole, lookups=True)
class RackRoleFilter(OrganizationalModelFilter):
class RackRoleFilter(OrganizationalModelFilterMixin):
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.RearPort, lookups=True)
class RearPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -960,7 +953,7 @@ class RearPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin,
@strawberry_django.filter_type(models.RearPortTemplate, lookups=True)
class RearPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -973,7 +966,7 @@ class RearPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedMo
@strawberry_django.filter_type(models.Region, lookups=True)
class RegionFilter(ContactFilterMixin, NestedGroupModelFilter):
class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -983,7 +976,7 @@ class RegionFilter(ContactFilterMixin, NestedGroupModelFilter):
@strawberry_django.filter_type(models.Site, lookups=True)
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
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 = (
@@ -1019,7 +1012,7 @@ class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
@strawberry_django.filter_type(models.SiteGroup, lookups=True)
class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilter):
class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -1029,7 +1022,7 @@ class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilter):
@strawberry_django.filter_type(models.VirtualChassis, lookups=True)
class VirtualChassisFilter(PrimaryModelFilter):
class VirtualChassisFilter(PrimaryModelFilterMixin):
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()
@@ -1041,7 +1034,7 @@ class VirtualChassisFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.VirtualDeviceContext, lookups=True)
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilter):
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
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()

View File

@@ -3,6 +3,9 @@ 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
@@ -13,30 +16,37 @@ __all__ = (
'JournalEntriesFilterMixin',
'TagsFilterMixin',
'ConfigContextFilterMixin',
'TagBaseFilterMixin',
)
@dataclass
class CustomFieldsFilterMixin:
class CustomFieldsFilterMixin(BaseFilterMixin):
custom_field_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@dataclass
class JournalEntriesFilterMixin:
class JournalEntriesFilterMixin(BaseFilterMixin):
journal_entries: Annotated['JournalEntryFilter', strawberry.lazy('extras.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@dataclass
class TagsFilterMixin:
class TagsFilterMixin(BaseFilterMixin):
tags: Annotated['TagFilter', strawberry.lazy('extras.graphql.filters')] | None = strawberry_django.filter_field()
@dataclass
class ConfigContextFilterMixin:
class ConfigContextFilterMixin(BaseFilterMixin):
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()

View File

@@ -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 CustomFieldsFilterMixin, TagsFilterMixin
from netbox.graphql.filter_mixins import SyncedDataFilterMixin
from netbox.graphql.filters import ChangeLoggedModelFilter, PrimaryModelFilter
from extras.graphql.filter_mixins import TagBaseFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin
from netbox.graphql.filter_mixins import PrimaryModelFilterMixin, SyncedDataFilterMixin
if TYPE_CHECKING:
from core.graphql.filters import ContentTypeFilter
@@ -42,7 +42,7 @@ __all__ = (
@strawberry_django.filter_type(models.ConfigContext, lookups=True)
class ConfigContextFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
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(SyncedDataFilterMixin, ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.ConfigContextProfile, lookups=True)
class ConfigContextProfileFilter(SyncedDataFilterMixin, PrimaryModelFilter):
class ConfigContextProfileFilter(SyncedDataFilterMixin, PrimaryModelFilterMixin):
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(SyncedDataFilterMixin, ChangeLoggedModelFilter):
class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
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(SyncedDataFilterMixin, ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.CustomField, lookups=True)
class CustomFieldFilter(ChangeLoggedModelFilter):
class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
type: BaseFilterLookup[Annotated['CustomFieldTypeEnum', strawberry.lazy('extras.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -179,7 +179,7 @@ class CustomFieldFilter(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.CustomFieldChoiceSet, lookups=True)
class CustomFieldChoiceSetFilter(ChangeLoggedModelFilter):
class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field()
base_choices: (
@@ -194,7 +194,7 @@ class CustomFieldChoiceSetFilter(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.CustomLink, lookups=True)
class CustomLinkFilter(ChangeLoggedModelFilter):
class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
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(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.ExportTemplate, lookups=True)
class ExportTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
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(SyncedDataFilterMixin, ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.ImageAttachment, lookups=True)
class ImageAttachmentFilter(ChangeLoggedModelFilter):
class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -241,7 +241,7 @@ class ImageAttachmentFilter(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.JournalEntry, lookups=True)
class JournalEntryFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -257,7 +257,7 @@ class JournalEntryFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedM
@strawberry_django.filter_type(models.NotificationGroup, lookups=True)
class NotificationGroupFilter(ChangeLoggedModelFilter):
class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
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(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.SavedFilter, lookups=True)
class SavedFilterFilter(ChangeLoggedModelFilter):
class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
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(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.TableConfig, lookups=True)
class TableConfigFilter(ChangeLoggedModelFilter):
class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
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,9 +295,7 @@ class TableConfigFilter(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.Tag, lookups=True)
class TagFilter(ChangeLoggedModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field()
class TagFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin, TagBaseFilterMixin):
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@@ -305,7 +303,7 @@ class TagFilter(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.Webhook, lookups=True)
class WebhookFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
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()
@@ -326,7 +324,7 @@ class WebhookFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelF
@strawberry_django.filter_type(models.EventRule, lookups=True)
class EventRuleFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
class EventRuleFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
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 = (

View File

@@ -46,10 +46,6 @@ 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),
@@ -57,7 +53,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__in=platforms) | Q(platforms=None),
Q(platforms=obj.platform) | 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),
@@ -107,6 +103,7 @@ 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),
@@ -170,15 +167,6 @@ 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

View File

@@ -1,5 +1,6 @@
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
@@ -9,12 +10,12 @@ 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,
)

View File

@@ -3,20 +3,21 @@ from typing import Annotated, TYPE_CHECKING
import strawberry
import strawberry_django
from strawberry_django import BaseFilterLookup
from core.graphql.filter_mixins import BaseFilterMixin
if TYPE_CHECKING:
from netbox.graphql.filter_lookups import IntegerLookup
from .enums import *
__all__ = (
'ServiceFilterMixin',
'ServiceBaseFilterMixin',
)
@dataclass
class ServiceFilterMixin:
protocol: BaseFilterLookup[Annotated['ServiceProtocolEnum', strawberry.lazy('ipam.graphql.enums')]] | None = (
class ServiceBaseFilterMixin(BaseFilterMixin):
protocol: Annotated['ServiceProtocolEnum', strawberry.lazy('ipam.graphql.enums')] | None = (
strawberry_django.filter_field()
)
ports: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (

View File

@@ -9,13 +9,12 @@ 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 ServiceFilterMixin
from netbox.graphql.filters import (
ChangeLoggedModelFilter, NetBoxModelFilter, OrganizationalModelFilter, PrimaryModelFilter,
)
from ipam.graphql.filter_mixins import ServiceBaseFilterMixin
from netbox.graphql.filter_mixins import NetBoxModelFilterMixin, OrganizationalModelFilterMixin, PrimaryModelFilterMixin
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
from virtualization.models import VMInterface
@@ -50,7 +49,7 @@ __all__ = (
@strawberry_django.filter_type(models.ASN, lookups=True)
class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
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 = (
@@ -65,7 +64,7 @@ class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.ASNRange, lookups=True)
class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
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()
@@ -79,7 +78,7 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
@strawberry_django.filter_type(models.Aggregate, lookups=True)
class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
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()
@@ -112,7 +111,7 @@ class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
@strawberry_django.filter_type(models.FHRPGroup, lookups=True)
class FHRPGroupFilter(PrimaryModelFilter):
class FHRPGroupFilter(PrimaryModelFilterMixin):
group_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@@ -130,7 +129,7 @@ class FHRPGroupFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.FHRPGroupAssignment, lookups=True)
class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
interface_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -169,7 +168,7 @@ class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.IPAddress, lookups=True)
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
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()
@@ -220,7 +219,7 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
@strawberry_django.filter_type(models.IPRange, lookups=True)
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
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 = (
@@ -274,7 +273,7 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.Prefix, lookups=True)
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
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()
@@ -311,19 +310,19 @@ class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, Pr
@strawberry_django.filter_type(models.RIR, lookups=True)
class RIRFilter(OrganizationalModelFilter):
class RIRFilter(OrganizationalModelFilterMixin):
is_private: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Role, lookups=True)
class RoleFilter(OrganizationalModelFilter):
class RoleFilter(OrganizationalModelFilterMixin):
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, PrimaryModelFilter):
class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
importing_vrfs: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -340,7 +339,7 @@ class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.Service, lookups=True)
class ServiceFilter(ContactFilterMixin, ServiceFilterMixin, PrimaryModelFilter):
class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -352,12 +351,12 @@ class ServiceFilter(ContactFilterMixin, ServiceFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.ServiceTemplate, lookups=True)
class ServiceTemplateFilter(ServiceFilterMixin, PrimaryModelFilter):
class ServiceTemplateFilter(ServiceBaseFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.VLAN, lookups=True)
class VLANFilter(TenancyFilterMixin, PrimaryModelFilter):
class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
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 = (
@@ -389,19 +388,19 @@ class VLANFilter(TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.VLANGroup, lookups=True)
class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilter):
class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilterMixin):
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(PrimaryModelFilter):
class VLANTranslationPolicyFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.VLANTranslationRule, lookups=True)
class VLANTranslationRuleFilter(NetBoxModelFilter):
class VLANTranslationRuleFilter(NetBoxModelFilterMixin):
policy: Annotated['VLANTranslationPolicyFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -416,7 +415,7 @@ class VLANTranslationRuleFilter(NetBoxModelFilter):
@strawberry_django.filter_type(models.VRF, lookups=True)
class VRFFilter(TenancyFilterMixin, PrimaryModelFilter):
class VRFFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
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()

View File

@@ -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,21 +75,17 @@ 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:

View File

@@ -5,7 +5,6 @@ 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
@@ -13,7 +12,6 @@ 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
@@ -64,15 +62,3 @@ 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)

View File

@@ -4,11 +4,19 @@ 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',
)
@@ -22,15 +30,51 @@ if TYPE_CHECKING:
from extras.graphql.filters import *
class NetBoxModelFilterMixin(
ChangeLogFilterMixin,
CustomFieldsFilterMixin,
JournalEntriesFilterMixin,
TagsFilterMixin,
BaseObjectTypeFilterMixin,
):
pass
@dataclass
class ImageAttachmentFilterMixin:
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):
images: Annotated['ImageAttachmentFilter', strawberry.lazy('extras.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@dataclass
class WeightFilterMixin:
class WeightFilterMixin(BaseFilterMixin):
weight: FilterLookup[float] | None = strawberry_django.filter_field()
weight_unit: BaseFilterLookup[Annotated['WeightUnitEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
strawberry_django.filter_field()
@@ -38,7 +82,7 @@ class WeightFilterMixin:
@dataclass
class SyncedDataFilterMixin:
class SyncedDataFilterMixin(BaseFilterMixin):
data_source: Annotated['DataSourceFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -53,7 +97,7 @@ class SyncedDataFilterMixin:
@dataclass
class DistanceFilterMixin:
class DistanceFilterMixin(BaseFilterMixin):
distance: FilterLookup[float] | None = strawberry_django.filter_field()
distance_unit: BaseFilterLookup[Annotated['DistanceUnitEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
strawberry_django.filter_field()

View File

@@ -1,61 +0,0 @@
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()

View File

@@ -32,18 +32,6 @@ 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):

View File

@@ -66,32 +66,6 @@ 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')

View File

@@ -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, AuthenticationCheckView, StatusView
from netbox.api.views import APIRootView, StatusView
from netbox.graphql.schema import schema
from netbox.graphql.views import NetBoxGraphQLView
from netbox.plugins.urls import plugin_patterns, plugin_api_patterns
@@ -53,7 +53,6 @@ _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(

View File

@@ -0,0 +1,3 @@
dist
node_modules
.cache

View File

@@ -0,0 +1,53 @@
{
"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
}
]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,86 +0,0 @@
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,
},
],
},
},
]);

View File

@@ -1,14 +1,14 @@
{
"name": "netbox-graphiql",
"version": "4.5.0",
"version": "4.3.0",
"description": "NetBox GraphiQL Custom Front End",
"main": "dist/graphiql.js",
"license": "Apache-2.0",
"private": true,
"dependencies": {
"@graphiql/plugin-explorer": "3.2.6",
"graphiql": "4.1.2",
"graphql": "16.12.0",
"@graphiql/plugin-explorer": "3.2.5",
"graphiql": "3.8.3",
"graphql": "16.10.0",
"js-cookie": "3.0.5",
"react": "18.3.1",
"react-dom": "18.3.1"

View File

@@ -1,7 +1,6 @@
{
"name": "netbox",
"type": "module",
"version": "4.5.0",
"version": "4.4.0",
"main": "dist/netbox.js",
"license": "Apache-2.0",
"private": true,
@@ -9,14 +8,14 @@
"netbox-graphiql"
],
"scripts": {
"bundle": "node bundle.cjs",
"bundle:styles": "node bundle.cjs --styles",
"bundle:scripts": "node bundle.cjs --scripts",
"bundle": "node bundle.js",
"bundle:styles": "node bundle.js --styles",
"bundle:scripts": "node bundle.js --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 ./src/**/*.ts",
"validate:lint": "eslint -c .eslintrc ./src/**/*.ts",
"validate:types": "tsc --noEmit",
"validate:formatting": "yarn validate:formatting:scripts && yarn validate:formatting:styles",
"validate:formatting:styles": "prettier -c styles/**/*.scss",
@@ -37,24 +36,20 @@
"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": "^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",
"@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",
"esbuild-sass-plugin": "^3.3.1",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint": "<9.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.1",
"globals": "^16.5.0",
"prettier": "^3.7.3",
"typescript": "^5.9.3"
"prettier": "^3.3.3",
"typescript": "<5.5"
},
"resolutions": {
"@types/bootstrap/**/@popperjs/core": "^2.11.6"

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +0,0 @@
{% 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>&lt;TOKEN&gt;</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 %}

View File

@@ -1,4 +1,69 @@
{% 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 %}

View File

@@ -5,6 +5,8 @@ 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
@@ -16,14 +18,14 @@ __all__ = (
@dataclass
class ContactFilterMixin:
class ContactFilterMixin(BaseFilterMixin):
contacts: Annotated['ContactAssignmentFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@dataclass
class TenancyFilterMixin:
class TenancyFilterMixin(BaseFilterMixin):
tenant: Annotated['TenantFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
strawberry_django.filter_field()
)

View File

@@ -5,9 +5,12 @@ 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.filters import (
ChangeLoggedModelFilter, NestedGroupModelFilter, OrganizationalModelFilter, PrimaryModelFilter,
from netbox.graphql.filter_mixins import (
NestedGroupModelFilterMixin,
OrganizationalModelFilterMixin,
PrimaryModelFilterMixin,
)
from tenancy import models
from .filter_mixins import ContactFilterMixin
@@ -54,7 +57,7 @@ __all__ = (
@strawberry_django.filter_type(models.Tenant, lookups=True)
class TenantFilter(ContactFilterMixin, PrimaryModelFilter):
class TenantFilter(PrimaryModelFilterMixin, ContactFilterMixin):
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 = (
@@ -133,7 +136,7 @@ class TenantFilter(ContactFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.TenantGroup, lookups=True)
class TenantGroupFilter(OrganizationalModelFilter):
class TenantGroupFilter(OrganizationalModelFilterMixin):
parent: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -147,7 +150,7 @@ class TenantGroupFilter(OrganizationalModelFilter):
@strawberry_django.filter_type(models.Contact, lookups=True)
class ContactFilter(PrimaryModelFilter):
class ContactFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
title: FilterLookup[str] | None = strawberry_django.filter_field()
phone: FilterLookup[str] | None = strawberry_django.filter_field()
@@ -163,19 +166,19 @@ class ContactFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.ContactRole, lookups=True)
class ContactRoleFilter(OrganizationalModelFilter):
class ContactRoleFilter(OrganizationalModelFilterMixin):
pass
@strawberry_django.filter_type(models.ContactGroup, lookups=True)
class ContactGroupFilter(NestedGroupModelFilter):
class ContactGroupFilter(NestedGroupModelFilterMixin):
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, ChangeLoggedModelFilter):
class ContactAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field()
)

View File

@@ -32,10 +32,10 @@ class TokenSerializer(ValidatedModelSerializer):
model = Token
fields = (
'id', 'url', 'display_url', 'display', 'version', 'key', 'user', 'description', 'created', 'expires',
'last_used', 'enabled', 'write_enabled', 'pepper_id', 'allowed_ips', 'token',
'last_used', 'write_enabled', 'pepper_id', 'allowed_ips', 'token',
)
read_only_fields = ('key',)
brief_fields = ('id', 'url', 'display', 'version', 'key', 'enabled', 'write_enabled', 'description')
brief_fields = ('id', 'url', 'display', 'version', 'key', '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',
'enabled', 'write_enabled', 'description', 'allowed_ips', 'username', 'password', 'token',
'write_enabled', 'description', 'allowed_ips', 'username', 'password', 'token',
)
def validate(self, data):

View File

@@ -1,4 +1,5 @@
import django_filters
from django.db.models import Q
from django.utils.translation import gettext as _
@@ -170,8 +171,7 @@ class TokenFilterSet(BaseFilterSet):
class Meta:
model = Token
fields = (
'id', 'version', 'key', 'pepper_id', 'enabled', 'write_enabled',
'description', 'created', 'expires', 'last_used',
'id', 'version', 'key', 'pepper_id', 'write_enabled', 'description', 'created', 'expires', 'last_used',
)
def search(self, queryset, name, value):

View File

@@ -99,11 +99,6 @@ 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,
@@ -127,7 +122,7 @@ class TokenBulkEditForm(BulkEditForm):
model = Token
fieldsets = (
FieldSet('enabled', 'write_enabled', 'description', 'expires', 'allowed_ips'),
FieldSet('write_enabled', 'description', 'expires', 'allowed_ips'),
)
nullable_fields = (
'expires', 'description', 'allowed_ips',

View File

@@ -52,7 +52,7 @@ class TokenImportForm(CSVModelForm):
class Meta:
model = Token
fields = ('user', 'version', 'token', 'enabled', 'write_enabled', 'expires', 'description',)
fields = ('user', 'version', 'token', 'write_enabled', 'expires', 'description',)
class OwnerGroupImportForm(CSVModelForm):

View File

@@ -114,7 +114,7 @@ class TokenFilterForm(SavedFiltersMixin, FilterForm):
model = Token
fieldsets = (
FieldSet('q', 'filter_id',),
FieldSet('version', 'user_id', 'enabled', 'write_enabled', 'expires', 'last_used', name=_('Token')),
FieldSet('version', 'user_id', 'write_enabled', 'expires', 'last_used', name=_('Token')),
)
version = forms.ChoiceField(
choices=add_blank_choice(TokenVersionChoices),
@@ -125,13 +125,6 @@ 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(

View File

@@ -140,7 +140,7 @@ class UserTokenForm(forms.ModelForm):
class Meta:
model = Token
fields = [
'version', 'token', 'enabled', 'write_enabled', 'expires', 'description', 'allowed_ips',
'version', 'token', 'write_enabled', 'expires', 'description', 'allowed_ips',
]
widgets = {
'expires': DateTimePicker(),
@@ -177,7 +177,7 @@ class TokenForm(UserTokenForm):
class Meta(UserTokenForm.Meta):
fields = [
'version', 'token', 'user', 'enabled', 'write_enabled', 'expires', 'description', 'allowed_ips',
'version', 'token', 'user', 'write_enabled', 'expires', 'description', 'allowed_ips',
]
def __init__(self, *args, **kwargs):

View File

@@ -5,7 +5,7 @@ import strawberry
import strawberry_django
from strawberry_django import DatetimeFilterLookup, FilterLookup
from netbox.graphql.filters import BaseModelFilter
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin
from users import models
__all__ = (
@@ -17,13 +17,13 @@ __all__ = (
@strawberry_django.filter_type(models.Group, lookups=True)
class GroupFilter(BaseModelFilter):
class GroupFilter(BaseObjectTypeFilterMixin):
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(BaseModelFilter):
class UserFilter(BaseObjectTypeFilterMixin):
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(BaseModelFilter):
@strawberry_django.filter_type(models.Owner, lookups=True)
class OwnerFilter(BaseModelFilter):
class OwnerFilter(BaseObjectTypeFilterMixin):
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(BaseModelFilter):
@strawberry_django.filter_type(models.OwnerGroup, lookups=True)
class OwnerGroupFilter(BaseModelFilter):
class OwnerGroupFilter(BaseObjectTypeFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field()

View File

@@ -9,13 +9,6 @@ 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',
@@ -42,7 +35,7 @@ class Migration(migrations.Migration):
),
),
# Add a version field to distinguish v1 and v2 tokens
# Add version field to distinguish v1 and v2 tokens
migrations.AddField(
model_name='token',
name='version',

View File

@@ -61,11 +61,6 @@ 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,
@@ -185,31 +180,6 @@ 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()
@@ -266,6 +236,12 @@ 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.

View File

@@ -25,9 +25,6 @@ class TokenTable(NetBoxTable):
verbose_name=_('token'),
template_code=TOKEN,
)
enabled = columns.BooleanColumn(
verbose_name=_('Enabled')
)
write_enabled = columns.BooleanColumn(
verbose_name=_('Write Enabled')
)
@@ -52,10 +49,10 @@ class TokenTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Token
fields = (
'pk', 'id', 'token', 'version', 'pepper_id', 'user', 'description', 'enabled', 'write_enabled', 'created',
'expires', 'last_used', 'allowed_ips',
'pk', 'id', 'token', 'version', 'pepper_id', 'user', 'description', 'write_enabled', 'created', 'expires',
'last_used', 'allowed_ips',
)
default_columns = ('token', 'version', 'user', 'enabled', 'write_enabled', 'description', 'allowed_ips')
default_columns = ('token', 'version', 'user', 'write_enabled', 'description', 'allowed_ips')
class UserTable(NetBoxTable):

View File

@@ -195,10 +195,10 @@ class TokenTest(
APIViewTestCases.ListObjectsViewTestCase,
APIViewTestCases.CreateObjectViewTestCase,
APIViewTestCases.UpdateObjectViewTestCase,
APIViewTestCases.DeleteObjectViewTestCase,
APIViewTestCases.DeleteObjectViewTestCase
):
model = Token
brief_fields = ['description', 'display', 'enabled', 'id', 'key', 'url', 'version', 'write_enabled']
brief_fields = ['description', 'display', 'id', 'key', 'url', 'version', 'write_enabled']
bulk_update_data = {
'description': 'New description',
}
@@ -229,16 +229,12 @@ 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,
},
]
@@ -271,8 +267,6 @@ 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):
"""

View File

@@ -285,7 +285,6 @@ class TokenTestCase(TestCase, BaseFilterSetTests):
version=1,
user=users[0],
expires=future_date,
enabled=True,
write_enabled=True,
description='foobar1',
),
@@ -293,14 +292,12 @@ 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,
),
@@ -342,12 +339,6 @@ 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)

View File

@@ -20,32 +20,6 @@ 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.

View File

@@ -236,14 +236,13 @@ class TokenTestCase(
'token': '4F9DAouzURLbicyoG55htImgqQ0b4UZHP5LUYgl5',
'user': users[0].pk,
'description': 'Test token',
'enabled': True,
}
cls.csv_data = (
"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",
"token,user,description",
f"zjebxBPzICiPbWz0Wtx0fTL7bCKXKGTYhNzkgC2S,{users[0].pk},Test token",
f"9Z5kGtQWba60Vm226dPDfEAV6BhlTr7H5hAXAfbF,{users[1].pk},Test token",
f"njpMnNT6r0k0MDccoUhTYYlvP9BvV3qLzYN2p6Uu,{users[1].pk},Test token",
)
cls.csv_update_data = (

View File

@@ -1,25 +0,0 @@
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')
]

View File

@@ -3,9 +3,7 @@ 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
@@ -28,14 +26,6 @@ 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)

View File

@@ -1,9 +1,5 @@
from netbox.registry import registry
__all__ = (
'register_filterset',
)
def register_filterset(filterset_class):
"""

View File

@@ -39,20 +39,20 @@ FORM_FIELD_LOOKUPS = {
forms.IntegerField: [
('exact', _('is')),
('n', _('is not')),
('gt', _('greater than')),
('gte', _('at least')),
('lt', _('less than')),
('lte', _('at most')),
('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')),
('gt', _('greater than (>)')),
('gte', _('at least (≥)')),
('lt', _('less than (<)')),
('lte', _('at most (≤)')),
(MODIFIER_EMPTY_TRUE, _('is empty')),
(MODIFIER_EMPTY_FALSE, _('is not empty')),
],
@@ -196,11 +196,11 @@ class FilterModifierMixin:
if filterset:
lookups = self._verify_lookups_with_filterset(field_name, lookups, filterset)
if len(lookups) > 1:
field.widget = FilterModifierWidget(
widget=field.widget,
lookups=lookups
)
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.

View File

@@ -1,15 +1,13 @@
<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 %}
{# 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>
{# Original widget - rendered exactly as it would be without our wrapper #}
<div class="ms-2 flex-grow-1 filter-value-container">

View File

@@ -1,14 +1,10 @@
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
@@ -16,28 +12,6 @@ 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."""
@@ -111,8 +85,10 @@ class FilterModifierWidgetTest(TestCase):
# 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)
self.assertIn('value="exact"', html)
self.assertIn('>Is</option>', html)
self.assertIn('value="ic"', html)
self.assertIn('>Contains</option>', html)
# Should contain original input
self.assertIn('type="text"', html)
@@ -126,78 +102,127 @@ class FilterModifierMixinTest(TestCase):
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
name = forms.CharField(required=False)
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)
self.assertIsInstance(form.fields['name'].widget, FilterModifierWidget)
self.assertGreater(len(form.fields['name'].widget.lookups), 1)
# Should have exact, ic, isw, iew
lookup_codes = [lookup[0] for lookup in form.fields['name'].widget.lookups]
self.assertIn('exact', lookup_codes)
self.assertIn('ic', lookup_codes)
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
active = forms.BooleanField(required=False)
form = TestForm()
self.assertNotIsInstance(form.fields['boolean_field'].widget, FilterModifierWidget)
self.assertNotIsInstance(form.fields['active'].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)
self.assertIn('exact', tag_lookups)
self.assertIn('n', tag_lookups)
def test_mixin_enhances_multi_choice_field(self):
"""Plain MultipleChoiceField should be enhanced with choice-appropriate lookups."""
class TestForm(FilterModifierMixin, forms.Form):
status = forms.MultipleChoiceField(choices=[('a', 'A'), ('b', 'B')], required=False)
form = TestForm()
self.assertIsInstance(form.fields['status'].widget, FilterModifierWidget)
status_lookups = [lookup[0] for lookup in form.fields['status'].widget.lookups]
# Should have choice-based lookups (not text-based like contains/regex)
self.assertIn('exact', status_lookups)
self.assertIn('n', status_lookups)
self.assertIn('empty_true', status_lookups)
# Should NOT have text-based lookups
self.assertNotIn('ic', status_lookups)
self.assertNotIn('regex', status_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
count = forms.IntegerField(required=False)
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)
self.assertIsInstance(form.fields['count'].widget, FilterModifierWidget)
lookup_codes = [lookup[0] for lookup in form.fields['count'].widget.lookups]
self.assertIn('gte', lookup_codes)
self.assertIn('lte', lookup_codes)
def test_mixin_enhances_decimal_field(self):
"""DecimalField should be enhanced with comparison modifiers."""
def test_mixin_adds_isnull_lookup_to_all_fields(self):
"""All field types should include isnull (empty/not empty) lookup."""
class TestForm(FilterModifierMixin, forms.Form):
decimal_field = forms.DecimalField(required=False)
model = TestModel
name = forms.CharField(required=False)
count = forms.IntegerField(required=False)
created = forms.DateField(required=False)
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)
# CharField should have empty_true and empty_false
char_lookups = [lookup[0] for lookup in form.fields['name'].widget.lookups]
self.assertIn('empty_true', char_lookups)
self.assertIn('empty_false', char_lookups)
def test_mixin_enhances_date_field(self):
"""DateField should be enhanced with date-appropriate modifiers."""
# IntegerField should have empty_true and empty_false
int_lookups = [lookup[0] for lookup in form.fields['count'].widget.lookups]
self.assertIn('empty_true', int_lookups)
self.assertIn('empty_false', int_lookups)
# DateField should have empty_true and empty_false
date_lookups = [lookup[0] for lookup in form.fields['created'].widget.lookups]
self.assertIn('empty_true', date_lookups)
self.assertIn('empty_false', date_lookups)
def test_char_field_includes_extended_lookups(self):
"""CharField should include negation, iexact, and regex lookups."""
class TestForm(FilterModifierMixin, forms.Form):
date_field = forms.DateField(required=False)
model = TestModel
name = forms.CharField(required=False)
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)
char_lookups = [lookup[0] for lookup in form.fields['name'].widget.lookups]
self.assertIn('n', char_lookups) # negation
self.assertIn('ie', char_lookups) # iexact
self.assertIn('regex', char_lookups) # regex
self.assertIn('iregex', char_lookups) # case-insensitive regex
def test_numeric_fields_include_negation(self):
"""IntegerField and DecimalField should include negation lookup."""
class TestForm(FilterModifierMixin, forms.Form):
count = forms.IntegerField(required=False)
weight = forms.DecimalField(required=False)
form = TestForm()
int_lookups = [lookup[0] for lookup in form.fields['count'].widget.lookups]
self.assertIn('n', int_lookups)
decimal_lookups = [lookup[0] for lookup in form.fields['weight'].widget.lookups]
self.assertIn('n', decimal_lookups)
def test_date_field_includes_negation(self):
"""DateField should include negation lookup."""
class TestForm(FilterModifierMixin, forms.Form):
created = forms.DateField(required=False)
form = TestForm()
date_lookups = [lookup[0] for lookup in form.fields['created'].widget.lookups]
self.assertIn('n', date_lookups)
class ExtendedLookupFilterPillsTest(TestCase):
@@ -236,26 +261,6 @@ class ExtendedLookupFilterPillsTest(TestCase):
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."""

View File

@@ -11,6 +11,7 @@ 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

View File

@@ -3,9 +3,11 @@ from typing import Annotated, TYPE_CHECKING
import strawberry
import strawberry_django
from strawberry.scalars import ID
from strawberry import ID
from strawberry_django import FilterLookup
from netbox.graphql.filter_mixins import NetBoxModelFilterMixin
if TYPE_CHECKING:
from .filters import VirtualMachineFilter
@@ -15,7 +17,7 @@ __all__ = (
@dataclass
class VMComponentFilterMixin:
class VMComponentFilterMixin(NetBoxModelFilterMixin):
virtual_machine: Annotated['VirtualMachineFilter', strawberry.lazy('virtualization.graphql.filters')] | None = (
strawberry_django.filter_field()
)

View File

@@ -7,8 +7,11 @@ 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
from netbox.graphql.filters import NetBoxModelFilter, OrganizationalModelFilter, PrimaryModelFilter
from netbox.graphql.filter_mixins import (
ImageAttachmentFilterMixin,
OrganizationalModelFilterMixin,
PrimaryModelFilterMixin,
)
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
from virtualization import models
from virtualization.graphql.filter_mixins import VMComponentFilterMixin
@@ -37,7 +40,7 @@ __all__ = (
@strawberry_django.filter_type(models.Cluster, lookups=True)
class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
type: Annotated['ClusterTypeFilter', strawberry.lazy('virtualization.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -56,14 +59,14 @@ class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, P
@strawberry_django.filter_type(models.ClusterGroup, lookups=True)
class ClusterGroupFilter(ContactFilterMixin, OrganizationalModelFilter):
class ClusterGroupFilter(ContactFilterMixin, OrganizationalModelFilterMixin):
vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.ClusterType, lookups=True)
class ClusterTypeFilter(OrganizationalModelFilter):
class ClusterTypeFilter(OrganizationalModelFilterMixin):
pass
@@ -74,7 +77,7 @@ class VirtualMachineFilter(
RenderConfigFilterMixin,
ConfigContextFilterMixin,
TenancyFilterMixin,
PrimaryModelFilter,
PrimaryModelFilterMixin,
):
name: FilterLookup[str] | None = strawberry_django.filter_field()
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
@@ -135,7 +138,7 @@ class VirtualMachineFilter(
@strawberry_django.filter_type(models.VMInterface, lookups=True)
class VMInterfaceFilter(InterfaceBaseFilterMixin, VMComponentFilterMixin, NetBoxModelFilter):
class VMInterfaceFilter(VMComponentFilterMixin, InterfaceBaseFilterMixin):
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -160,7 +163,7 @@ class VMInterfaceFilter(InterfaceBaseFilterMixin, VMComponentFilterMixin, NetBox
@strawberry_django.filter_type(models.VirtualDisk, lookups=True)
class VirtualDiskFilter(VMComponentFilterMixin, NetBoxModelFilter):
class VirtualDiskFilter(VMComponentFilterMixin):
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)

View File

@@ -5,10 +5,9 @@ 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.filters import (
ChangeLoggedModelFilter, NetBoxModelFilter, OrganizationalModelFilter, PrimaryModelFilter,
)
from netbox.graphql.filter_mixins import NetBoxModelFilterMixin, OrganizationalModelFilterMixin, PrimaryModelFilterMixin
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
from vpn import models
@@ -33,12 +32,14 @@ __all__ = (
@strawberry_django.filter_type(models.TunnelGroup, lookups=True)
class TunnelGroupFilter(OrganizationalModelFilter):
class TunnelGroupFilter(OrganizationalModelFilterMixin):
pass
@strawberry_django.filter_type(models.TunnelTermination, lookups=True)
class TunnelTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
class TunnelTerminationFilter(
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
):
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 = (
@@ -58,7 +59,7 @@ class TunnelTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLo
@strawberry_django.filter_type(models.Tunnel, lookups=True)
class TunnelFilter(TenancyFilterMixin, PrimaryModelFilter):
class TunnelFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
status: BaseFilterLookup[Annotated['TunnelStatusEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
strawberry_django.filter_field()
@@ -84,7 +85,7 @@ class TunnelFilter(TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.IKEProposal, lookups=True)
class IKEProposalFilter(PrimaryModelFilter):
class IKEProposalFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
authentication_method: (
BaseFilterLookup[Annotated['AuthenticationMethodEnum', strawberry.lazy('vpn.graphql.enums')]] | None
@@ -113,7 +114,7 @@ class IKEProposalFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.IKEPolicy, lookups=True)
class IKEPolicyFilter(PrimaryModelFilter):
class IKEPolicyFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
version: BaseFilterLookup[Annotated['IKEVersionEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
strawberry_django.filter_field()
@@ -128,7 +129,7 @@ class IKEPolicyFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.IPSecProposal, lookups=True)
class IPSecProposalFilter(PrimaryModelFilter):
class IPSecProposalFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
encryption_algorithm: (
BaseFilterLookup[Annotated['EncryptionAlgorithmEnum', strawberry.lazy('vpn.graphql.enums')]] | None
@@ -154,7 +155,7 @@ class IPSecProposalFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.IPSecPolicy, lookups=True)
class IPSecPolicyFilter(PrimaryModelFilter):
class IPSecPolicyFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
proposals: Annotated['IPSecProposalFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -165,7 +166,7 @@ class IPSecPolicyFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.IPSecProfile, lookups=True)
class IPSecProfileFilter(PrimaryModelFilter):
class IPSecProfileFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
mode: BaseFilterLookup[Annotated['IPSecModeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
strawberry_django.filter_field()
@@ -181,7 +182,7 @@ class IPSecProfileFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.L2VPN, lookups=True)
class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
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 = (
@@ -205,7 +206,7 @@ class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.L2VPNTermination, lookups=True)
class L2VPNTerminationFilter(NetBoxModelFilter):
class L2VPNTerminationFilter(NetBoxModelFilterMixin):
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 = (

View File

@@ -1,8 +1,8 @@
import django_filters
from django.db.models import Q
from dcim.base_filtersets import ScopedFilterSet
from dcim.choices import LinkStatusChoices
from dcim.base_filtersets import ScopedFilterSet
from dcim.models import Interface
from ipam.models import VLAN
from netbox.filtersets import NestedGroupModelFilterSet, PrimaryModelFilterSet

View File

@@ -5,16 +5,18 @@ import strawberry
import strawberry_django
from strawberry_django import FilterLookup
from core.graphql.filter_mixins import BaseFilterMixin
if TYPE_CHECKING:
from .enums import *
__all__ = (
'WirelessAuthenticationFilterMixin',
'WirelessAuthenticationBaseFilterMixin',
)
@dataclass
class WirelessAuthenticationFilterMixin:
class WirelessAuthenticationBaseFilterMixin(BaseFilterMixin):
auth_type: Annotated['WirelessAuthTypeEnum', strawberry.lazy('wireless.graphql.enums')] | None = (
strawberry_django.filter_field()
)

View File

@@ -6,11 +6,10 @@ 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
from netbox.graphql.filters import PrimaryModelFilter, NestedGroupModelFilter
from netbox.graphql.filter_mixins import DistanceFilterMixin, PrimaryModelFilterMixin, NestedGroupModelFilterMixin
from tenancy.graphql.filter_mixins import TenancyFilterMixin
from wireless import models
from .filter_mixins import WirelessAuthenticationFilterMixin
from .filter_mixins import WirelessAuthenticationBaseFilterMixin
if TYPE_CHECKING:
from dcim.graphql.filters import InterfaceFilter
@@ -25,16 +24,16 @@ __all__ = (
@strawberry_django.filter_type(models.WirelessLANGroup, lookups=True)
class WirelessLANGroupFilter(NestedGroupModelFilter):
class WirelessLANGroupFilter(NestedGroupModelFilterMixin):
pass
@strawberry_django.filter_type(models.WirelessLAN, lookups=True)
class WirelessLANFilter(
WirelessAuthenticationFilterMixin,
WirelessAuthenticationBaseFilterMixin,
ScopedFilterMixin,
TenancyFilterMixin,
PrimaryModelFilter
PrimaryModelFilterMixin
):
ssid: FilterLookup[str] | None = strawberry_django.filter_field()
status: BaseFilterLookup[Annotated['WirelessLANStatusEnum', strawberry.lazy('wireless.graphql.enums')]] | None = (
@@ -50,10 +49,10 @@ class WirelessLANFilter(
@strawberry_django.filter_type(models.WirelessLink, lookups=True)
class WirelessLinkFilter(
WirelessAuthenticationFilterMixin,
WirelessAuthenticationBaseFilterMixin,
DistanceFilterMixin,
TenancyFilterMixin,
PrimaryModelFilter
PrimaryModelFilterMixin
):
interface_a: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()