Merge branch 'main' into feature

This commit is contained in:
Jeremy Stretch 2026-03-03 15:40:46 -05:00
commit 53ea48efa9
81 changed files with 52392 additions and 51790 deletions

View File

@ -15,7 +15,7 @@ body:
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v4.5.3 placeholder: v4.5.4
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -27,7 +27,7 @@ body:
attributes: attributes:
label: NetBox Version label: NetBox Version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v4.5.3 placeholder: v4.5.4
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -8,7 +8,7 @@ body:
attributes: attributes:
label: NetBox Version label: NetBox Version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v4.5.3 placeholder: v4.5.4
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -0,0 +1,44 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

50
.github/workflows/claude.yml vendored Normal file
View File

@ -0,0 +1,50 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'

84
CLAUDE.md Normal file
View File

@ -0,0 +1,84 @@
# NetBox
Network source-of-truth and infrastructure resource modeling (IRM) tool combining DCIM and IPAM. Built on Django + PostgreSQL + Redis.
## Tech Stack
- Python 3.12+ / Django / Django REST Framework
- PostgreSQL (required), Redis (required for caching/queuing)
- GraphQL via Strawberry, background jobs via RQ
- Docs: MkDocs (in `docs/`)
## Repository Layout
- `netbox/` — Django project root; run all `manage.py` commands from here
- `netbox/netbox/` — Core settings, URLs, WSGI entrypoint
- `netbox/<app>/` — Django apps: `circuits`, `core`, `dcim`, `ipam`, `extras`, `tenancy`, `virtualization`, `wireless`, `users`, `vpn`
- `docs/` — MkDocs documentation source
- `contrib/` — Example configs (systemd, nginx, etc.) and other resources
## Development Setup
```bash
python -m venv ~/.venv/netbox
source ~/.venv/netbox/bin/activate
pip install -r requirements.txt
# Copy and configure
cp netbox/netbox/configuration.example.py netbox/netbox/configuration.py
# Edit configuration.py: set DATABASE, REDIS, SECRET_KEY, ALLOWED_HOSTS
cd netbox/
python manage.py migrate
python manage.py runserver
```
## Key Commands
All commands run from the `netbox/` subdirectory with venv active.
```bash
# Development server
python manage.py runserver
# Run full test suite
export NETBOX_CONFIGURATION=netbox.configuration_testing
python manage.py test
# Faster test runs (no DB rebuild, parallel)
python manage.py test --keepdb --parallel 4
# Migrations
python manage.py makemigrations
python manage.py migrate
# Shell
python manage.py nbshell # NetBox-enhanced shell
```
## Architecture Conventions
- **Apps**: Each Django app owns its models, views, API serializers, filtersets, forms, and tests.
- **REST API**: DRF serializers live in `<app>/api/serializers.py`; viewsets in `<app>/api/views.py`; URLs auto-registered in `<app>/api/urls.py`.
- **GraphQL**: Strawberry types in `<app>/graphql/types.py`.
- **Filtersets**: `<app>/filtersets.py` — used for both UI filtering and API `?filter=` params.
- **Tables**: `django-tables2` used for all object list views (`<app>/tables.py`).
- **Templates**: Django templates in `netbox/templates/<app>/`.
- **Tests**: Mirror the app structure in `<app>/tests/`. Use `netbox.configuration_testing` for test config.
## Coding Standards
- Follow existing Django conventions; don't reinvent patterns already present in the codebase.
- New models must include `created`, `last_updated` fields (inherit from `NetBoxModel` where appropriate).
- Every model exposed in the UI needs: model, serializer, filterset, form, table, views, URL route, and tests.
- API serializers must include a `url` field (absolute URL of the object).
- Use `FeatureQuery` for generic relations (config contexts, custom fields, tags, etc.).
- Avoid adding new dependencies without strong justification.
## Branch & PR Conventions
- Branch naming: `<issue-number>-short-description` (e.g., `1234-device-typerror`)
- Use the `main` branch for patch releases; `feature` tracks work for the upcoming minor/major release.
- Every PR must reference an approved GitHub issue.
- PRs must include tests for new functionality.
## Gotchas
- `configuration.py` is gitignored — never commit it.
- `manage.py` lives in `netbox/`, NOT the repo root. Running from the wrong directory is a common mistake.
- `NETBOX_CONFIGURATION` env var controls which settings module loads; set to `netbox.configuration_testing` for tests.
- The `extras` app is a catch-all for cross-cutting features (custom fields, tags, webhooks, scripts).
- Plugins API: only documented public APIs are stable. Internal NetBox code is subject to change without notice.
- See `docs/development/` for the full contributing guide and code style details.

View File

@ -98,6 +98,10 @@ jsonschema
# https://python-markdown.github.io/changelog/ # https://python-markdown.github.io/changelog/
Markdown Markdown
# MkDocs
# https://github.com/mkdocs/mkdocs/releases
mkdocs<2.0
# MkDocs Material theme (for documentation build) # MkDocs Material theme (for documentation build)
# https://squidfunk.github.io/mkdocs-material/changelog/ # https://squidfunk.github.io/mkdocs-material/changelog/
mkdocs-material mkdocs-material
@ -157,8 +161,7 @@ strawberry-graphql
# Strawberry GraphQL Django extension # Strawberry GraphQL Django extension
# https://github.com/strawberry-graphql/strawberry-django/releases # https://github.com/strawberry-graphql/strawberry-django/releases
# Blocked by #21450 strawberry-graphql-django
strawberry-graphql-django==0.75.0
# SVG image rendering (used for rack elevations) # SVG image rendering (used for rack elevations)
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst # https://github.com/mozman/svgwrite/blob/master/NEWS.rst

View File

@ -349,6 +349,7 @@
"5gbase-t", "5gbase-t",
"10gbase-br-d", "10gbase-br-d",
"10gbase-br-u", "10gbase-br-u",
"10gbase-cu",
"10gbase-cx4", "10gbase-cx4",
"10gbase-er", "10gbase-er",
"10gbase-lr", "10gbase-lr",
@ -367,6 +368,7 @@
"40gbase-fr4", "40gbase-fr4",
"40gbase-lr4", "40gbase-lr4",
"40gbase-sr4", "40gbase-sr4",
"40gbase-sr4-bd",
"50gbase-cr", "50gbase-cr",
"50gbase-er", "50gbase-er",
"50gbase-fr", "50gbase-fr",

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,34 @@
# NetBox v4.5 # NetBox v4.5
## v4.5.4 (2026-03-03)
### Enhancements
* [#21369](https://github.com/netbox-community/netbox/issues/21369) - Support lazy-loading of image attachments
* [#21385](https://github.com/netbox-community/netbox/issues/21385) - Add contact assignment support for virtual circuits
* [#21394](https://github.com/netbox-community/netbox/issues/21394) - Add 10GBASE-CU and 40GBASE-SR4 BiDi interface types
* [#21477](https://github.com/netbox-community/netbox/issues/21477) - Extend GraphQL API filters for cables
### Performance Improvements
* [#21456](https://github.com/netbox-community/netbox/issues/21456) - Improve performance of config context resolution via GraphQL API
* [#21459](https://github.com/netbox-community/netbox/issues/21459) - Avoid prefetching data for hidden table columns
### Bug Fixes
* [#20490](https://github.com/netbox-community/netbox/issues/20490) - Restrict visibility of scripts in list view to users with view permission
* [#20911](https://github.com/netbox-community/netbox/issues/20911) - Sort module bay options alphabetically when installing a module
* [#21347](https://github.com/netbox-community/netbox/issues/21347) - The allocation of IPv6 addresses from a non-pool prefix should start at one, not zero
* [#21429](https://github.com/netbox-community/netbox/issues/21429) - Termination type should persist when employing "create & add another" workflow for cables
* [#21478](https://github.com/netbox-community/netbox/issues/21478) - Fix GraphQL union type resolution for connected console ports
* [#21481](https://github.com/netbox-community/netbox/issues/21481) - Fix display of facility ID on rack view
* [#21518](https://github.com/netbox-community/netbox/issues/21518) - Fix decimal custom field displaying as unset when value is zero
* [#21524](https://github.com/netbox-community/netbox/issues/21524) - Avoid `IndexError` exception when encountering stale cable paths
* [#21527](https://github.com/netbox-community/netbox/issues/21527) - Fix display of primary IP address with associated NAT IP on device view
* [#21550](https://github.com/netbox-community/netbox/issues/21550) - Ensure pre-change snapshots are recorded for related objects
---
## v4.5.3 (2026-02-17) ## v4.5.3 (2026-02-17)
### Enhancements ### Enhancements

View File

@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Annotated
import strawberry import strawberry
import strawberry_django import strawberry_django
from strawberry.scalars import ID from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, DateFilterLookup, FilterLookup from strawberry_django import BaseFilterLookup, DateFilterLookup, StrFilterLookup
from circuits import models from circuits import models
from circuits.graphql.filter_mixins import CircuitTypeFilterMixin from circuits.graphql.filter_mixins import CircuitTypeFilterMixin
@ -62,9 +62,9 @@ class CircuitTerminationFilter(
upstream_speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( upstream_speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
xconnect_id: FilterLookup[str] | None = strawberry_django.filter_field() xconnect_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
pp_info: FilterLookup[str] | None = strawberry_django.filter_field() pp_info: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
# Cached relations # Cached relations
_provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( _provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
@ -92,7 +92,7 @@ class CircuitFilter(
TenancyFilterMixin, TenancyFilterMixin,
PrimaryModelFilter PrimaryModelFilter
): ):
cid: FilterLookup[str] | None = strawberry_django.filter_field() cid: StrFilterLookup[str] | None = strawberry_django.filter_field()
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -145,8 +145,8 @@ class CircuitGroupAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, Cha
@strawberry_django.filter_type(models.Provider, lookups=True) @strawberry_django.filter_type(models.Provider, lookups=True)
class ProviderFilter(ContactFilterMixin, PrimaryModelFilter): class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
circuits: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( circuits: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -159,18 +159,18 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilter):
strawberry_django.filter_field() strawberry_django.filter_field()
) )
provider_id: ID | None = strawberry_django.filter_field() provider_id: ID | None = strawberry_django.filter_field()
account: FilterLookup[str] | None = strawberry_django.filter_field() account: StrFilterLookup[str] | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True) @strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
class ProviderNetworkFilter(PrimaryModelFilter): class ProviderNetworkFilter(PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
provider_id: ID | None = strawberry_django.filter_field() provider_id: ID | None = strawberry_django.filter_field()
service_id: FilterLookup[str] | None = strawberry_django.filter_field() service_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True) @strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
@ -180,7 +180,7 @@ class VirtualCircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True) @strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter): class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
cid: FilterLookup[str] | None = strawberry_django.filter_field() cid: StrFilterLookup[str] | None = strawberry_django.filter_field()
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -218,4 +218,4 @@ class VirtualCircuitTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin,
strawberry_django.filter_field() strawberry_django.filter_field()
) )
interface_id: ID | None = strawberry_django.filter_field() interface_id: ID | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()

View File

@ -2,6 +2,7 @@ import re
import typing import typing
from collections import OrderedDict from collections import OrderedDict
from drf_spectacular.contrib.django_filters import DjangoFilterExtension
from drf_spectacular.extensions import OpenApiSerializerExtension, OpenApiSerializerFieldExtension, _SchemaType from drf_spectacular.extensions import OpenApiSerializerExtension, OpenApiSerializerFieldExtension, _SchemaType
from drf_spectacular.openapi import AutoSchema from drf_spectacular.openapi import AutoSchema
from drf_spectacular.plumbing import ( from drf_spectacular.plumbing import (
@ -9,6 +10,7 @@ from drf_spectacular.plumbing import (
build_choice_field, build_choice_field,
build_media_type_object, build_media_type_object,
build_object_type, build_object_type,
follow_field_source,
get_doc, get_doc,
) )
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
@ -23,6 +25,29 @@ BULK_ACTIONS = ("bulk_destroy", "bulk_partial_update", "bulk_update")
WRITABLE_ACTIONS = ("PATCH", "POST", "PUT") WRITABLE_ACTIONS = ("PATCH", "POST", "PUT")
class NetBoxDjangoFilterExtension(DjangoFilterExtension):
"""
Overrides drf-spectacular's DjangoFilterExtension to fix a regression in v0.29.0 where
_get_model_field() incorrectly double-appends to_field_name when field_name already ends
with that value (e.g. field_name='tags__slug', to_field_name='slug' produces the invalid
path ['tags', 'slug', 'slug']). This caused hundreds of spurious warnings during schema
generation for filters such as TagFilter, TenancyFilterSet.tenant, and OwnerFilterMixin.owner.
See: https://github.com/netbox-community/netbox/issues/20787
https://github.com/tfranzel/drf-spectacular/issues/1475
"""
priority = 1
def _get_model_field(self, filter_field, model):
if not filter_field.field_name:
return None
path = filter_field.field_name.split('__')
to_field_name = filter_field.extra.get('to_field_name')
if to_field_name is not None and path[-1] != to_field_name:
path.append(to_field_name)
return follow_field_source(model, path, emit_warnings=False)
class FixTimeZoneSerializerField(OpenApiSerializerFieldExtension): class FixTimeZoneSerializerField(OpenApiSerializerFieldExtension):
target_class = 'timezone_field.rest_framework.TimeZoneSerializerField' target_class = 'timezone_field.rest_framework.TimeZoneSerializerField'

View File

@ -5,7 +5,7 @@ import strawberry
import strawberry_django import strawberry_django
from django.contrib.contenttypes.models import ContentType as DjangoContentType from django.contrib.contenttypes.models import ContentType as DjangoContentType
from strawberry.scalars import ID from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup, StrFilterLookup
from core import models from core import models
from netbox.graphql.filters import BaseModelFilter, PrimaryModelFilter from netbox.graphql.filters import BaseModelFilter, PrimaryModelFilter
@ -32,23 +32,23 @@ class DataFileFilter(BaseModelFilter):
strawberry_django.filter_field() strawberry_django.filter_field()
) )
source_id: ID | None = strawberry_django.filter_field() source_id: ID | None = strawberry_django.filter_field()
path: FilterLookup[str] | None = strawberry_django.filter_field() path: StrFilterLookup[str] | None = strawberry_django.filter_field()
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
hash: FilterLookup[str] | None = strawberry_django.filter_field() hash: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.DataSource, lookups=True) @strawberry_django.filter_type(models.DataSource, lookups=True)
class DataSourceFilter(PrimaryModelFilter): class DataSourceFilter(PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
type: FilterLookup[str] | None = strawberry_django.filter_field() type: StrFilterLookup[str] | None = strawberry_django.filter_field()
source_url: FilterLookup[str] | None = strawberry_django.filter_field() source_url: StrFilterLookup[str] | None = strawberry_django.filter_field()
status: ( status: (
BaseFilterLookup[Annotated['DataSourceStatusEnum', strawberry.lazy('core.graphql.enums')]] | None BaseFilterLookup[Annotated['DataSourceStatusEnum', strawberry.lazy('core.graphql.enums')]] | None
) = strawberry_django.filter_field() ) = strawberry_django.filter_field()
enabled: FilterLookup[bool] | None = strawberry_django.filter_field() enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
ignore_rules: FilterLookup[str] | None = strawberry_django.filter_field() ignore_rules: StrFilterLookup[str] | None = strawberry_django.filter_field()
parameters: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( parameters: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -62,8 +62,8 @@ class DataSourceFilter(PrimaryModelFilter):
class ObjectChangeFilter(BaseModelFilter): class ObjectChangeFilter(BaseModelFilter):
time: DatetimeFilterLookup[datetime] | 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: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
user_name: FilterLookup[str] | None = strawberry_django.filter_field() user_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
request_id: FilterLookup[str] | None = strawberry_django.filter_field() request_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
action: ( action: (
BaseFilterLookup[Annotated['ObjectChangeActionEnum', strawberry.lazy('core.graphql.enums')]] | None BaseFilterLookup[Annotated['ObjectChangeActionEnum', strawberry.lazy('core.graphql.enums')]] | None
) = strawberry_django.filter_field() ) = strawberry_django.filter_field()
@ -76,7 +76,7 @@ class ObjectChangeFilter(BaseModelFilter):
strawberry_django.filter_field() strawberry_django.filter_field()
) )
related_object_id: ID | None = strawberry_django.filter_field() related_object_id: ID | None = strawberry_django.filter_field()
object_repr: FilterLookup[str] | None = strawberry_django.filter_field() object_repr: StrFilterLookup[str] | None = strawberry_django.filter_field()
prechange_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( prechange_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -87,5 +87,5 @@ class ObjectChangeFilter(BaseModelFilter):
@strawberry_django.filter_type(DjangoContentType, lookups=True) @strawberry_django.filter_type(DjangoContentType, lookups=True)
class ContentTypeFilter(BaseModelFilter): class ContentTypeFilter(BaseModelFilter):
app_label: FilterLookup[str] | None = strawberry_django.filter_field() app_label: StrFilterLookup[str] | None = strawberry_django.filter_field()
model: FilterLookup[str] | None = strawberry_django.filter_field() model: StrFilterLookup[str] | None = strawberry_django.filter_field()

View File

@ -84,6 +84,9 @@ class CablePathSerializer(serializers.ModelSerializer):
def get_path(self, obj): def get_path(self, obj):
ret = [] ret = []
for nodes in obj.path_objects: for nodes in obj.path_objects:
if not nodes:
# The path contains an invalid object
return []
serializer = get_serializer_for_model(nodes[0]) serializer = get_serializer_for_model(nodes[0])
context = {'request': self.context['request']} context = {'request': self.context['request']}
ret.append(serializer(nodes, nested=True, many=True, context=context).data) ret.append(serializer(nodes, nested=True, many=True, context=context).data)

View File

@ -921,6 +921,7 @@ class InterfaceTypeChoices(ChoiceSet):
# 10 Gbps Ethernet # 10 Gbps Ethernet
TYPE_10GE_BR_D = '10gbase-br-d' TYPE_10GE_BR_D = '10gbase-br-d'
TYPE_10GE_BR_U = '10gbase-br-u' TYPE_10GE_BR_U = '10gbase-br-u'
TYPE_10GE_CU = '10gbase-cu'
TYPE_10GE_CX4 = '10gbase-cx4' TYPE_10GE_CX4 = '10gbase-cx4'
TYPE_10GE_ER = '10gbase-er' TYPE_10GE_ER = '10gbase-er'
TYPE_10GE_LR = '10gbase-lr' TYPE_10GE_LR = '10gbase-lr'
@ -943,6 +944,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_40GE_FR4 = '40gbase-fr4' TYPE_40GE_FR4 = '40gbase-fr4'
TYPE_40GE_LR4 = '40gbase-lr4' TYPE_40GE_LR4 = '40gbase-lr4'
TYPE_40GE_SR4 = '40gbase-sr4' TYPE_40GE_SR4 = '40gbase-sr4'
TYPE_40GE_SR4_BD = '40gbase-sr4-bd'
# 50 Gbps Ethernet # 50 Gbps Ethernet
TYPE_50GE_CR = '50gbase-cr' TYPE_50GE_CR = '50gbase-cr'
@ -1192,6 +1194,7 @@ class InterfaceTypeChoices(ChoiceSet):
( (
(TYPE_10GE_BR_D, '10GBASE-BR-D (10GE BiDi Down)'), (TYPE_10GE_BR_D, '10GBASE-BR-D (10GE BiDi Down)'),
(TYPE_10GE_BR_U, '10GBASE-BR-U (10GE BiDi Up)'), (TYPE_10GE_BR_U, '10GBASE-BR-U (10GE BiDi Up)'),
(TYPE_10GE_CU, '10GBASE-CU (10GE DAC Passive Twinax)'),
(TYPE_10GE_CX4, '10GBASE-CX4 (10GE DAC)'), (TYPE_10GE_CX4, '10GBASE-CX4 (10GE DAC)'),
(TYPE_10GE_ER, '10GBASE-ER (10GE)'), (TYPE_10GE_ER, '10GBASE-ER (10GE)'),
(TYPE_10GE_LR, '10GBASE-LR (10GE)'), (TYPE_10GE_LR, '10GBASE-LR (10GE)'),
@ -1220,6 +1223,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_40GE_FR4, '40GBASE-FR4 (40GE)'), (TYPE_40GE_FR4, '40GBASE-FR4 (40GE)'),
(TYPE_40GE_LR4, '40GBASE-LR4 (40GE)'), (TYPE_40GE_LR4, '40GBASE-LR4 (40GE)'),
(TYPE_40GE_SR4, '40GBASE-SR4 (40GE)'), (TYPE_40GE_SR4, '40GBASE-SR4 (40GE)'),
(TYPE_40GE_SR4_BD, '40GBASE-SR4 (40GE BiDi)'),
) )
), ),
( (

View File

@ -1386,6 +1386,7 @@ class MACAddressImportForm(PrimaryModelImportForm):
# Assign the MAC address as primary for its interface, if designated as such # Assign the MAC address as primary for its interface, if designated as such
if interface and self.cleaned_data['is_primary'] and self.instance.pk: if interface and self.cleaned_data['is_primary'] and self.instance.pk:
interface.snapshot()
interface.primary_mac_address = self.instance interface.primary_mac_address = self.instance
interface.save() interface.save()

View File

@ -15,6 +15,10 @@ def get_cable_form(a_type, b_type):
def __new__(mcs, name, bases, attrs): def __new__(mcs, name, bases, attrs):
# NOTE: Cable.clone() mirrors the parent selector mapping below:
# termination_{end}_device / termination_{end}_powerpanel / termination_{end}_circuit
# This supports both the "Clone" and "Create & Add Another" workflows.
# If you change the mapping here, update Cable.clone() accordingly.
for cable_end, term_cls in (('a', a_type), ('b', b_type)): for cable_end, term_cls in (('a', a_type), ('b', b_type)):
# Device component # Device component

View File

@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Annotated
import strawberry import strawberry
import strawberry_django import strawberry_django
from strawberry import ID from strawberry import ID
from strawberry_django import BaseFilterLookup, FilterLookup from strawberry_django import BaseFilterLookup, FilterLookup, StrFilterLookup
from core.graphql.filters import ContentTypeFilter from core.graphql.filters import ContentTypeFilter
@ -66,9 +66,9 @@ class ComponentModelFilterMixin:
) )
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
device_id: ID | None = strawberry_django.filter_field() device_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
label: FilterLookup[str] | None = strawberry_django.filter_field() label: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
@dataclass @dataclass
@ -96,9 +96,9 @@ class ComponentTemplateFilterMixin:
strawberry_django.filter_field() strawberry_django.filter_field()
) )
device_type_id: ID | None = strawberry_django.filter_field() device_type_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
label: FilterLookup[str] | None = strawberry_django.filter_field() label: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
@dataclass @dataclass

View File

@ -4,7 +4,7 @@ import strawberry
import strawberry_django import strawberry_django
from django.db.models import Q from django.db.models import Q
from strawberry.scalars import ID from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, FilterLookup from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, FilterLookup, StrFilterLookup
from dcim import models from dcim import models
from dcim.constants import * from dcim.constants import *
@ -114,7 +114,7 @@ class CableFilter(TenancyFilterMixin, PrimaryModelFilter):
status: BaseFilterLookup[Annotated['LinkStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = ( status: BaseFilterLookup[Annotated['LinkStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
label: FilterLookup[str] | None = strawberry_django.filter_field() label: StrFilterLookup[str] | None = strawberry_django.filter_field()
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = ( color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -141,6 +141,20 @@ class CableTerminationFilter(ChangeLoggedModelFilter):
) )
termination_id: ID | None = strawberry_django.filter_field() termination_id: ID | None = strawberry_django.filter_field()
# Cached relations
_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field(
name='device'
)
_rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field(
name='rack'
)
_location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='location')
)
_site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field(
name='site'
)
@strawberry_django.filter_type(models.ConsolePort, lookups=True) @strawberry_django.filter_type(models.ConsolePort, lookups=True)
class ConsolePortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter): class ConsolePortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
@ -196,9 +210,9 @@ class DeviceFilter(
platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
serial: FilterLookup[str] | None = strawberry_django.filter_field() serial: StrFilterLookup[str] | None = strawberry_django.filter_field()
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field() asset_tag: StrFilterLookup[str] | None = strawberry_django.filter_field()
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field() site_id: ID | None = strawberry_django.filter_field()
location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
@ -325,7 +339,7 @@ class InventoryItemTemplateFilter(ComponentTemplateFilterMixin, ChangeLoggedMode
strawberry_django.filter_field() strawberry_django.filter_field()
) )
manufacturer_id: ID | None = strawberry_django.filter_field() manufacturer_id: ID | None = strawberry_django.filter_field()
part_id: FilterLookup[str] | None = strawberry_django.filter_field() part_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.DeviceRole, lookups=True) @strawberry_django.filter_type(models.DeviceRole, lookups=True)
@ -342,13 +356,13 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryMod
strawberry_django.filter_field() strawberry_django.filter_field()
) )
manufacturer_id: ID | None = strawberry_django.filter_field() manufacturer_id: ID | None = strawberry_django.filter_field()
model: FilterLookup[str] | None = strawberry_django.filter_field() model: StrFilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
default_platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( default_platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
default_platform_id: ID | None = strawberry_django.filter_field() default_platform_id: ID | None = strawberry_django.filter_field()
part_number: FilterLookup[str] | None = strawberry_django.filter_field() part_number: StrFilterLookup[str] | None = strawberry_django.filter_field()
instances: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( instances: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -465,7 +479,7 @@ class PortTemplateMappingFilter(BaseModelFilter):
@strawberry_django.filter_type(models.MACAddress, lookups=True) @strawberry_django.filter_type(models.MACAddress, lookups=True)
class MACAddressFilter(PrimaryModelFilter): class MACAddressFilter(PrimaryModelFilter):
mac_address: FilterLookup[str] | None = strawberry_django.filter_field() mac_address: StrFilterLookup[str] | None = strawberry_django.filter_field()
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -511,7 +525,7 @@ class InterfaceFilter(
duplex: BaseFilterLookup[Annotated['InterfaceDuplexEnum', strawberry.lazy('dcim.graphql.enums')]] | None = ( duplex: BaseFilterLookup[Annotated['InterfaceDuplexEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
wwn: FilterLookup[str] | None = strawberry_django.filter_field() wwn: StrFilterLookup[str] | None = strawberry_django.filter_field()
parent: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( parent: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -631,9 +645,9 @@ class InventoryItemFilter(ComponentModelFilterMixin, NetBoxModelFilter):
strawberry_django.filter_field() strawberry_django.filter_field()
) )
manufacturer_id: ID | None = strawberry_django.filter_field() manufacturer_id: ID | None = strawberry_django.filter_field()
part_id: FilterLookup[str] | None = strawberry_django.filter_field() part_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
serial: FilterLookup[str] | None = strawberry_django.filter_field() serial: StrFilterLookup[str] | None = strawberry_django.filter_field()
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field() asset_tag: StrFilterLookup[str] | None = strawberry_django.filter_field()
discovered: FilterLookup[bool] | None = strawberry_django.filter_field() discovered: FilterLookup[bool] | None = strawberry_django.filter_field()
@ -651,7 +665,7 @@ class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilt
status: BaseFilterLookup[Annotated['LocationStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = ( status: BaseFilterLookup[Annotated['LocationStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
facility: FilterLookup[str] | None = strawberry_django.filter_field() facility: StrFilterLookup[str] | None = strawberry_django.filter_field()
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -680,8 +694,8 @@ class ModuleFilter(ConfigContextFilterMixin, PrimaryModelFilter):
status: BaseFilterLookup[Annotated['ModuleStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = ( status: BaseFilterLookup[Annotated['ModuleStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
serial: FilterLookup[str] | None = strawberry_django.filter_field() serial: StrFilterLookup[str] | None = strawberry_django.filter_field()
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field() asset_tag: StrFilterLookup[str] | None = strawberry_django.filter_field()
console_ports: Annotated['ConsolePortFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( console_ports: Annotated['ConsolePortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -720,17 +734,17 @@ class ModuleBayFilter(ModularComponentFilterMixin, NetBoxModelFilter):
strawberry_django.filter_field() strawberry_django.filter_field()
) )
parent_id: ID | None = strawberry_django.filter_field() parent_id: ID | None = strawberry_django.filter_field()
position: FilterLookup[str] | None = strawberry_django.filter_field() position: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ModuleBayTemplate, lookups=True) @strawberry_django.filter_type(models.ModuleBayTemplate, lookups=True)
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter): class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
position: FilterLookup[str] | None = strawberry_django.filter_field() position: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ModuleTypeProfile, lookups=True) @strawberry_django.filter_type(models.ModuleTypeProfile, lookups=True)
class ModuleTypeProfileFilter(PrimaryModelFilter): class ModuleTypeProfileFilter(PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ModuleType, lookups=True) @strawberry_django.filter_type(models.ModuleType, lookups=True)
@ -743,8 +757,8 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryMod
strawberry_django.filter_field() strawberry_django.filter_field()
) )
profile_id: ID | None = strawberry_django.filter_field() profile_id: ID | None = strawberry_django.filter_field()
model: FilterLookup[str] | None = strawberry_django.filter_field() model: StrFilterLookup[str] | None = strawberry_django.filter_field()
part_number: FilterLookup[str] | None = strawberry_django.filter_field() part_number: StrFilterLookup[str] | None = strawberry_django.filter_field()
instances: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( instances: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -804,7 +818,7 @@ class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryM
power_panel_id: ID | None = strawberry_django.filter_field() power_panel_id: ID | None = strawberry_django.filter_field()
rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
rack_id: ID | None = strawberry_django.filter_field() rack_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
status: BaseFilterLookup[Annotated['PowerFeedStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = ( status: BaseFilterLookup[Annotated['PowerFeedStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -875,7 +889,7 @@ class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryMo
location_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( location_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.PowerPort, lookups=True) @strawberry_django.filter_type(models.PowerPort, lookups=True)
@ -913,8 +927,8 @@ class RackTypeFilter(ImageAttachmentFilterMixin, RackFilterMixin, WeightFilterMi
strawberry_django.filter_field() strawberry_django.filter_field()
) )
manufacturer_id: ID | None = strawberry_django.filter_field() manufacturer_id: ID | None = strawberry_django.filter_field()
model: FilterLookup[str] | None = strawberry_django.filter_field() model: StrFilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
racks: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() racks: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
rack_count: ComparisonFilterLookup[int] | None = strawberry_django.filter_field() rack_count: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
@ -935,8 +949,8 @@ class RackFilter(
strawberry_django.filter_field() strawberry_django.filter_field()
) )
rack_type_id: ID | None = strawberry_django.filter_field() rack_type_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
facility_id: FilterLookup[str] | None = strawberry_django.filter_field() facility_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field() site_id: ID | None = strawberry_django.filter_field()
location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
@ -950,8 +964,8 @@ class RackFilter(
) )
role: Annotated['RackRoleFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() role: Annotated['RackRoleFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
role_id: ID | None = strawberry_django.filter_field() role_id: ID | None = strawberry_django.filter_field()
serial: FilterLookup[str] | None = strawberry_django.filter_field() serial: StrFilterLookup[str] | None = strawberry_django.filter_field()
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field() asset_tag: StrFilterLookup[str] | None = strawberry_django.filter_field()
airflow: BaseFilterLookup[Annotated['RackAirflowEnum', strawberry.lazy('dcim.graphql.enums')]] | None = ( airflow: BaseFilterLookup[Annotated['RackAirflowEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -969,7 +983,7 @@ class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilter):
) )
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
user_id: ID | None = strawberry_django.filter_field() user_id: ID | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
status: BaseFilterLookup[Annotated['RackReservationStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = ( status: BaseFilterLookup[Annotated['RackReservationStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -1020,8 +1034,8 @@ class RegionFilter(ContactFilterMixin, NestedGroupModelFilter):
@strawberry_django.filter_type(models.Site, lookups=True) @strawberry_django.filter_type(models.Site, lookups=True)
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilter): class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
status: BaseFilterLookup[Annotated['SiteStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = ( status: BaseFilterLookup[Annotated['SiteStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -1035,11 +1049,11 @@ class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
group_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( group_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
facility: FilterLookup[str] | None = strawberry_django.filter_field() facility: StrFilterLookup[str] | None = strawberry_django.filter_field()
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
time_zone: FilterLookup[str] | None = strawberry_django.filter_field() time_zone: StrFilterLookup[str] | None = strawberry_django.filter_field()
physical_address: FilterLookup[str] | None = strawberry_django.filter_field() physical_address: StrFilterLookup[str] | None = strawberry_django.filter_field()
shipping_address: FilterLookup[str] | None = strawberry_django.filter_field() shipping_address: StrFilterLookup[str] | None = strawberry_django.filter_field()
latitude: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( latitude: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -1068,8 +1082,8 @@ class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilter):
class VirtualChassisFilter(PrimaryModelFilter): class VirtualChassisFilter(PrimaryModelFilter):
master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
master_id: ID | None = strawberry_django.filter_field() master_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
domain: FilterLookup[str] | None = strawberry_django.filter_field() domain: StrFilterLookup[str] | None = strawberry_django.filter_field()
members: ( members: (
Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field() ) = strawberry_django.filter_field()
@ -1080,7 +1094,7 @@ class VirtualChassisFilter(PrimaryModelFilter):
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilter): class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilter):
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
device_id: ID | None = strawberry_django.filter_field() device_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
status: ( status: (
BaseFilterLookup[Annotated['VirtualDeviceContextStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None BaseFilterLookup[Annotated['VirtualDeviceContextStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None
) = ( ) = (
@ -1097,7 +1111,7 @@ class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilter):
strawberry_django.filter_field() strawberry_django.filter_field()
) )
primary_ip6_id: ID | None = strawberry_django.filter_field() primary_ip6_id: ID | None = strawberry_django.filter_field()
comments: FilterLookup[str] | None = strawberry_django.filter_field() comments: StrFilterLookup[str] | None = strawberry_django.filter_field()
interfaces: ( interfaces: (
Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None
) = strawberry_django.filter_field() ) = strawberry_django.filter_field()

View File

@ -305,6 +305,50 @@ class Cable(PrimaryModel):
except UnsupportedCablePath as e: except UnsupportedCablePath as e:
raise AbortRequest(e) raise AbortRequest(e)
def clone(self):
"""
Return attributes suitable for cloning this cable.
In addition to the fields defined in `clone_fields`, include the termination
type and parent selector fields used by dcim.forms.connections.get_cable_form().
"""
attrs = super().clone()
# Mirror dcim.forms.connections.get_cable_form() parent-field logic
for cable_end, terminations in (('a', self.a_terminations), ('b', self.b_terminations)):
if not terminations:
continue
term_cls = type(terminations[0])
term_label = term_cls._meta.label_lower
# Matches CableForm choices: "<app_label>.<model>"
attrs[f'{cable_end}_terminations_type'] = term_label
# Device component
if hasattr(term_cls, 'device'):
device_ids = sorted({t.device_id for t in terminations if t.device_id})
if device_ids:
attrs[f'termination_{cable_end}_device'] = device_ids
# PowerFeed
elif term_label == 'dcim.powerfeed':
powerpanel_ids = sorted({t.power_panel_id for t in terminations if t.power_panel_id})
if powerpanel_ids:
attrs[f'termination_{cable_end}_powerpanel'] = powerpanel_ids
# CircuitTermination
elif term_label == 'circuits.circuittermination':
circuit_ids = sorted({t.circuit_id for t in terminations if t.circuit_id})
if circuit_ids:
attrs[f'termination_{cable_end}_circuit'] = circuit_ids
# Never clone the actual terminations, as they are already occupied
attrs.pop('a_terminations', None)
attrs.pop('b_terminations', None)
return attrs
def serialize_object(self, exclude=None): def serialize_object(self, exclude=None):
data = serialize_object(self, exclude=exclude or []) data = serialize_object(self, exclude=exclude or [])

View File

@ -2614,6 +2614,126 @@ class CableTest(APIViewTestCases.APIViewTestCase):
}, },
] ]
def test_graphql_cable_termination_cached_filters(self):
"""
Validate filtering cables by cached CableTermination relations via GraphQL:
cable_list(filters: { terminations: { <relation>: {...}, DISTINCT: true } })
Also asserts deduplication when both ends match (cable between two interfaces
on the same device/rack/location/site).
"""
self.add_permissions(
'dcim.view_cable',
'dcim.view_device',
'dcim.view_interface',
'dcim.view_rack',
'dcim.view_location',
'dcim.view_site',
)
# Reuse existing fixtures from setUpTestData()
devicetype = DeviceType.objects.get(slug='device-type-1')
role = DeviceRole.objects.get(slug='device-role-1')
# Create an isolated topology for this test
site_a = Site.objects.create(name='GQL Site A', slug='gql-site-a')
site_b = Site.objects.create(name='GQL Site B', slug='gql-site-b')
location_a = Location.objects.create(
site=site_a,
name='GQL Location A',
slug='gql-location-a',
status=LocationStatusChoices.STATUS_ACTIVE,
)
location_b = Location.objects.create(
site=site_b,
name='GQL Location B',
slug='gql-location-b',
status=LocationStatusChoices.STATUS_ACTIVE,
)
rack_a = Rack.objects.create(site=site_a, location=location_a, name='GQL Rack A', u_height=42)
rack_b = Rack.objects.create(site=site_b, location=location_b, name='GQL Rack B', u_height=42)
device_a = Device.objects.create(
device_type=devicetype,
role=role,
name='GQL Device A',
site=site_a,
location=location_a,
rack=rack_a,
)
device_b = Device.objects.create(
device_type=devicetype,
role=role,
name='GQL Device B',
site=site_b,
location=location_b,
rack=rack_b,
)
a0 = Interface.objects.create(device=device_a, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth0')
a1 = Interface.objects.create(device=device_a, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth1')
a2 = Interface.objects.create(device=device_a, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth2')
b0 = Interface.objects.create(device=device_b, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth0')
# Both ends on Device A (duplication risk without DISTINCT)
cable_same_device = Cable(a_terminations=[a0], b_terminations=[a1], label='GQL Cable Same Device')
cable_same_device.save()
# Cross to Device B
cable_cross = Cable(a_terminations=[a2], b_terminations=[b0], label='GQL Cable Cross')
cable_cross.save()
expected_a = {str(cable_same_device.pk), str(cable_cross.pk)}
expected_b = {str(cable_cross.pk)}
url = reverse('graphql')
test_cases = (
# Device (ID + name)
(f'device: {{ id: {{ exact: "{device_a.pk}" }} }}', expected_a),
(f'device: {{ name: {{ exact: "{device_a.name}" }} }}', expected_a),
(f'device: {{ id: {{ exact: "{device_b.pk}" }} }}', expected_b),
(f'device: {{ name: {{ exact: "{device_b.name}" }} }}', expected_b),
# Rack (ID + name)
(f'rack: {{ id: {{ exact: "{rack_a.pk}" }} }}', expected_a),
(f'rack: {{ name: {{ exact: "{rack_a.name}" }} }}', expected_a),
(f'rack: {{ id: {{ exact: "{rack_b.pk}" }} }}', expected_b),
(f'rack: {{ name: {{ exact: "{rack_b.name}" }} }}', expected_b),
# Location (ID + name)
(f'location: {{ id: {{ exact: "{location_a.pk}" }} }}', expected_a),
(f'location: {{ name: {{ exact: "{location_a.name}" }} }}', expected_a),
(f'location: {{ id: {{ exact: "{location_b.pk}" }} }}', expected_b),
(f'location: {{ name: {{ exact: "{location_b.name}" }} }}', expected_b),
# Site (ID + slug)
(f'site: {{ id: {{ exact: "{site_a.pk}" }} }}', expected_a),
(f'site: {{ slug: {{ exact: "{site_a.slug}" }} }}', expected_a),
(f'site: {{ id: {{ exact: "{site_b.pk}" }} }}', expected_b),
(f'site: {{ slug: {{ exact: "{site_b.slug}" }} }}', expected_b),
)
for inner_filter, expected in test_cases:
with self.subTest(filter=inner_filter):
query = f"""{{
cable_list(filters: {{ terminations: {{ {inner_filter} DISTINCT: true }} }})
{{ id }}
}}"""
response = self.client.post(url, data={'query': query}, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
data = response.json()
self.assertNotIn('errors', data)
rows = data['data']['cable_list']
ids = [row['id'] for row in rows]
# Ensure DISTINCT is actually effective (no duplicate cables when both ends match)
self.assertEqual(len(ids), len(set(ids)), f'Duplicate cables returned for: {inner_filter}')
self.assertSetEqual(set(ids), expected)
class CableTerminationTest( class CableTerminationTest(
APIViewTestCases.GetObjectViewTestCase, APIViewTestCases.GetObjectViewTestCase,

View File

@ -2733,6 +2733,7 @@ class DeviceBulkImportView(generic.BulkImportView):
# For child devices, save the reverse relation to the parent device bay # For child devices, save the reverse relation to the parent device bay
if parent_bay: if parent_bay:
device_bay = parent_bay device_bay = parent_bay
device_bay.snapshot()
device_bay.installed_device = obj device_bay.installed_device = obj
device_bay.save() device_bay.save()
@ -3912,19 +3913,6 @@ class CableEditView(generic.ObjectEditView):
return super().alter_object(obj, request, url_args, url_kwargs) return super().alter_object(obj, request, url_args, url_kwargs)
def get_extra_addanother_params(self, request):
params = {
'a_terminations_type': request.GET.get('a_terminations_type'),
'b_terminations_type': request.GET.get('b_terminations_type')
}
for key in request.POST:
if 'device' in key or 'power_panel' in key or 'circuit' in key:
params.update({key: request.POST.get(key)})
return params
@register_model_view(Cable, 'delete') @register_model_view(Cable, 'delete')
class CableDeleteView(generic.ObjectDeleteView): class CableDeleteView(generic.ObjectDeleteView):
@ -4099,6 +4087,7 @@ class VirtualChassisEditView(ObjectPermissionRequiredMixin, GetReturnURLMixin, V
members = formset.save(commit=False) members = formset.save(commit=False)
devices = Device.objects.filter(pk__in=[m.pk for m in members]) devices = Device.objects.filter(pk__in=[m.pk for m in members])
for device in devices: for device in devices:
device.snapshot()
device.vc_position = None device.vc_position = None
device.save() device.save()
for member in members: for member in members:

View File

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
import strawberry import strawberry
import strawberry_django import strawberry_django
from strawberry.scalars import ID from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, FilterLookup from strawberry_django import BaseFilterLookup, FilterLookup, StrFilterLookup
from extras import models from extras import models
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
@ -50,11 +50,11 @@ __all__ = (
@strawberry_django.filter_type(models.ConfigContext, lookups=True) @strawberry_django.filter_type(models.ConfigContext, lookups=True)
class ConfigContextFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter): class ConfigContextFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
is_active: FilterLookup[bool] | None = strawberry_django.filter_field() is_active: FilterLookup[bool] | None = strawberry_django.filter_field()
regions: Annotated['RegionFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( regions: Annotated['RegionFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -107,22 +107,22 @@ class ConfigContextFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.ConfigContextProfile, lookups=True) @strawberry_django.filter_type(models.ConfigContextProfile, lookups=True)
class ConfigContextProfileFilter(SyncedDataFilterMixin, PrimaryModelFilter): class ConfigContextProfileFilter(SyncedDataFilterMixin, PrimaryModelFilter):
name: FilterLookup[str] = strawberry_django.filter_field() name: StrFilterLookup[str] = strawberry_django.filter_field()
description: FilterLookup[str] = strawberry_django.filter_field() description: StrFilterLookup[str] = strawberry_django.filter_field()
tags: Annotated['TagFilter', strawberry.lazy('extras.graphql.filters')] | None = 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) @strawberry_django.filter_type(models.ConfigTemplate, lookups=True)
class ConfigTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter): class ConfigTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
template_code: FilterLookup[str] | None = strawberry_django.filter_field() template_code: StrFilterLookup[str] | None = strawberry_django.filter_field()
environment_params: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( environment_params: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
mime_type: FilterLookup[str] | None = strawberry_django.filter_field() mime_type: StrFilterLookup[str] | None = strawberry_django.filter_field()
file_name: FilterLookup[str] | None = strawberry_django.filter_field() file_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
file_extension: FilterLookup[str] | None = strawberry_django.filter_field() file_extension: StrFilterLookup[str] | None = strawberry_django.filter_field()
as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field() as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field()
@ -137,10 +137,10 @@ class CustomFieldFilter(ChangeLoggedModelFilter):
related_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( related_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
label: FilterLookup[str] | None = strawberry_django.filter_field() label: StrFilterLookup[str] | None = strawberry_django.filter_field()
group_name: FilterLookup[str] | None = strawberry_django.filter_field() group_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
required: FilterLookup[bool] | None = strawberry_django.filter_field() required: FilterLookup[bool] | None = strawberry_django.filter_field()
unique: FilterLookup[bool] | None = strawberry_django.filter_field() unique: FilterLookup[bool] | None = strawberry_django.filter_field()
search_weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( search_weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
@ -166,7 +166,7 @@ class CustomFieldFilter(ChangeLoggedModelFilter):
validation_maximum: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( validation_maximum: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
validation_regex: FilterLookup[str] | None = strawberry_django.filter_field() validation_regex: StrFilterLookup[str] | None = strawberry_django.filter_field()
choice_set: Annotated['CustomFieldChoiceSetFilter', strawberry.lazy('extras.graphql.filters')] | None = ( choice_set: Annotated['CustomFieldChoiceSetFilter', strawberry.lazy('extras.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -182,13 +182,13 @@ class CustomFieldFilter(ChangeLoggedModelFilter):
strawberry_django.filter_field() strawberry_django.filter_field()
) )
is_cloneable: FilterLookup[bool] | None = strawberry_django.filter_field() is_cloneable: FilterLookup[bool] | None = strawberry_django.filter_field()
comments: FilterLookup[str] | None = strawberry_django.filter_field() comments: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.CustomFieldChoiceSet, lookups=True) @strawberry_django.filter_type(models.CustomFieldChoiceSet, lookups=True)
class CustomFieldChoiceSetFilter(ChangeLoggedModelFilter): class CustomFieldChoiceSetFilter(ChangeLoggedModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
base_choices: ( base_choices: (
BaseFilterLookup[Annotated['CustomFieldChoiceSetBaseEnum', strawberry.lazy('extras.graphql.enums')]] | None BaseFilterLookup[Annotated['CustomFieldChoiceSetBaseEnum', strawberry.lazy('extras.graphql.enums')]] | None
) = ( ) = (
@ -202,14 +202,14 @@ class CustomFieldChoiceSetFilter(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.CustomLink, lookups=True) @strawberry_django.filter_type(models.CustomLink, lookups=True)
class CustomLinkFilter(ChangeLoggedModelFilter): class CustomLinkFilter(ChangeLoggedModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
enabled: FilterLookup[bool] | None = strawberry_django.filter_field() enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
link_text: FilterLookup[str] | None = strawberry_django.filter_field() link_text: StrFilterLookup[str] | None = strawberry_django.filter_field()
link_url: FilterLookup[str] | None = strawberry_django.filter_field() link_url: StrFilterLookup[str] | None = strawberry_django.filter_field()
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
group_name: FilterLookup[str] | None = strawberry_django.filter_field() group_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
button_class: ( button_class: (
BaseFilterLookup[Annotated['CustomLinkButtonClassEnum', strawberry.lazy('extras.graphql.enums')]] | None BaseFilterLookup[Annotated['CustomLinkButtonClassEnum', strawberry.lazy('extras.graphql.enums')]] | None
) = ( ) = (
@ -220,15 +220,15 @@ class CustomLinkFilter(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.ExportTemplate, lookups=True) @strawberry_django.filter_type(models.ExportTemplate, lookups=True)
class ExportTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter): class ExportTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
template_code: FilterLookup[str] | None = strawberry_django.filter_field() template_code: StrFilterLookup[str] | None = strawberry_django.filter_field()
environment_params: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( environment_params: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
mime_type: FilterLookup[str] | None = strawberry_django.filter_field() mime_type: StrFilterLookup[str] | None = strawberry_django.filter_field()
file_name: FilterLookup[str] | None = strawberry_django.filter_field() file_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
file_extension: FilterLookup[str] | None = strawberry_django.filter_field() file_extension: StrFilterLookup[str] | None = strawberry_django.filter_field()
as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field() as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field()
@ -244,7 +244,7 @@ class ImageAttachmentFilter(ChangeLoggedModelFilter):
image_width: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( image_width: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.JournalEntry, lookups=True) @strawberry_django.filter_type(models.JournalEntry, lookups=True)
@ -260,22 +260,22 @@ class JournalEntryFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedM
kind: BaseFilterLookup[Annotated['JournalEntryKindEnum', strawberry.lazy('extras.graphql.enums')]] | None = ( kind: BaseFilterLookup[Annotated['JournalEntryKindEnum', strawberry.lazy('extras.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
comments: FilterLookup[str] | None = strawberry_django.filter_field() comments: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.NotificationGroup, lookups=True) @strawberry_django.filter_type(models.NotificationGroup, lookups=True)
class NotificationGroupFilter(ChangeLoggedModelFilter): class NotificationGroupFilter(ChangeLoggedModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
groups: Annotated['GroupFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() groups: Annotated['GroupFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
users: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() users: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.SavedFilter, lookups=True) @strawberry_django.filter_type(models.SavedFilter, lookups=True)
class SavedFilterFilter(ChangeLoggedModelFilter): class SavedFilterFilter(ChangeLoggedModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
user_id: ID | None = strawberry_django.filter_field() user_id: ID | None = strawberry_django.filter_field()
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
@ -290,8 +290,8 @@ class SavedFilterFilter(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.TableConfig, lookups=True) @strawberry_django.filter_type(models.TableConfig, lookups=True)
class TableConfigFilter(ChangeLoggedModelFilter): class TableConfigFilter(ChangeLoggedModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
user_id: ID | None = strawberry_django.filter_field() user_id: ID | None = strawberry_django.filter_field()
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
@ -303,30 +303,30 @@ class TableConfigFilter(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.Tag, lookups=True) @strawberry_django.filter_type(models.Tag, lookups=True)
class TagFilter(ChangeLoggedModelFilter): class TagFilter(ChangeLoggedModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = ( color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.Webhook, lookups=True) @strawberry_django.filter_type(models.Webhook, lookups=True)
class WebhookFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter): class WebhookFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
payload_url: FilterLookup[str] | None = strawberry_django.filter_field() payload_url: StrFilterLookup[str] | None = strawberry_django.filter_field()
http_method: ( http_method: (
BaseFilterLookup[Annotated['WebhookHttpMethodEnum', strawberry.lazy('extras.graphql.enums')]] | None BaseFilterLookup[Annotated['WebhookHttpMethodEnum', strawberry.lazy('extras.graphql.enums')]] | None
) = ( ) = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
http_content_type: FilterLookup[str] | None = strawberry_django.filter_field() http_content_type: StrFilterLookup[str] | None = strawberry_django.filter_field()
additional_headers: FilterLookup[str] | None = strawberry_django.filter_field() additional_headers: StrFilterLookup[str] | None = strawberry_django.filter_field()
body_template: FilterLookup[str] | None = strawberry_django.filter_field() body_template: StrFilterLookup[str] | None = strawberry_django.filter_field()
secret: FilterLookup[str] | None = strawberry_django.filter_field() secret: StrFilterLookup[str] | None = strawberry_django.filter_field()
ssl_verification: FilterLookup[bool] | None = strawberry_django.filter_field() ssl_verification: FilterLookup[bool] | None = strawberry_django.filter_field()
ca_file_path: FilterLookup[str] | None = strawberry_django.filter_field() ca_file_path: StrFilterLookup[str] | None = strawberry_django.filter_field()
events: Annotated['EventRuleFilter', strawberry.lazy('extras.graphql.filters')] | None = ( events: Annotated['EventRuleFilter', strawberry.lazy('extras.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -334,8 +334,8 @@ class WebhookFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelF
@strawberry_django.filter_type(models.EventRule, lookups=True) @strawberry_django.filter_type(models.EventRule, lookups=True)
class EventRuleFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter): class EventRuleFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
event_types: Annotated['StringArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( event_types: Annotated['StringArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -346,10 +346,10 @@ class EventRuleFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedMode
action_type: BaseFilterLookup[Annotated['EventRuleActionEnum', strawberry.lazy('extras.graphql.enums')]] | None = ( action_type: BaseFilterLookup[Annotated['EventRuleActionEnum', strawberry.lazy('extras.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
action_object_type: FilterLookup[str] | None = strawberry_django.filter_field() action_object_type: StrFilterLookup[str] | None = strawberry_django.filter_field()
action_object_type_id: ID | None = strawberry_django.filter_field() action_object_type_id: ID | None = strawberry_django.filter_field()
action_object_id: ID | None = strawberry_django.filter_field() action_object_id: ID | None = strawberry_django.filter_field()
action_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( action_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
comments: FilterLookup[str] | None = strawberry_django.filter_field() comments: StrFilterLookup[str] | None = strawberry_django.filter_field()

View File

@ -22,7 +22,19 @@ if TYPE_CHECKING:
@strawberry.type @strawberry.type
class ConfigContextMixin: class ConfigContextMixin:
@strawberry_django.field @classmethod
def get_queryset(cls, queryset, info: Info, **kwargs):
queryset = super().get_queryset(queryset, info, **kwargs)
# If `config_context` is requested, call annotate_config_context_data() on the queryset
selected = {f.name for f in info.selected_fields[0].selections}
if 'config_context' in selected and hasattr(queryset, 'annotate_config_context_data'):
return queryset.annotate_config_context_data()
return queryset
# Ensure `local_context_data` is fetched when `config_context` is requested
@strawberry_django.field(only=['local_context_data'])
def config_context(self) -> strawberry.scalars.JSON: def config_context(self) -> strawberry.scalars.JSON:
return self.get_config_context() return self.get_config_context()

View File

@ -424,6 +424,7 @@ class IPAddressImportForm(PrimaryModelImportForm):
# Set as primary for device/VM # Set as primary for device/VM
if self.cleaned_data.get('is_primary') is not None: if self.cleaned_data.get('is_primary') is not None:
parent = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine') parent = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
parent.snapshot()
if self.instance.address.version == 4: if self.instance.address.version == 4:
parent.primary_ip4 = ipaddress if self.cleaned_data.get('is_primary') else None parent.primary_ip4 = ipaddress if self.cleaned_data.get('is_primary') else None
elif self.instance.address.version == 6: elif self.instance.address.version == 6:
@ -433,6 +434,7 @@ class IPAddressImportForm(PrimaryModelImportForm):
# Set as OOB for device # Set as OOB for device
if self.cleaned_data.get('is_oob') is not None: if self.cleaned_data.get('is_oob') is not None:
parent = self.cleaned_data.get('device') parent = self.cleaned_data.get('device')
parent.snapshot()
parent.oob_ip = ipaddress if self.cleaned_data.get('is_oob') else None parent.oob_ip = ipaddress if self.cleaned_data.get('is_oob') else None
parent.save() parent.save()

View File

@ -7,7 +7,7 @@ import strawberry_django
from django.db.models import Q from django.db.models import Q
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from strawberry.scalars import ID from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, DateFilterLookup, FilterLookup from strawberry_django import BaseFilterLookup, DateFilterLookup, FilterLookup, StrFilterLookup
from dcim.graphql.filter_mixins import ScopedFilterMixin from dcim.graphql.filter_mixins import ScopedFilterMixin
from dcim.models import Device from dcim.models import Device
@ -70,8 +70,8 @@ class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.ASNRange, lookups=True) @strawberry_django.filter_type(models.ASNRange, lookups=True)
class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter): class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | 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() rir_id: ID | None = strawberry_django.filter_field()
start: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( start: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
@ -84,7 +84,7 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
@strawberry_django.filter_type(models.Aggregate, lookups=True) @strawberry_django.filter_type(models.Aggregate, lookups=True)
class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter): class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
prefix: FilterLookup[str] | None = strawberry_django.filter_field() prefix: StrFilterLookup[str] | None = strawberry_django.filter_field()
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | 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() rir_id: ID | None = strawberry_django.filter_field()
date_added: DateFilterLookup[date] | None = strawberry_django.filter_field() date_added: DateFilterLookup[date] | None = strawberry_django.filter_field()
@ -120,14 +120,14 @@ class FHRPGroupFilter(PrimaryModelFilter):
group_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( group_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
protocol: BaseFilterLookup[Annotated['FHRPGroupProtocolEnum', strawberry.lazy('ipam.graphql.enums')]] | None = ( protocol: BaseFilterLookup[Annotated['FHRPGroupProtocolEnum', strawberry.lazy('ipam.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
auth_type: BaseFilterLookup[Annotated['FHRPGroupAuthTypeEnum', strawberry.lazy('ipam.graphql.enums')]] | None = ( auth_type: BaseFilterLookup[Annotated['FHRPGroupAuthTypeEnum', strawberry.lazy('ipam.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
auth_key: FilterLookup[str] | None = strawberry_django.filter_field() auth_key: StrFilterLookup[str] | None = strawberry_django.filter_field()
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -138,7 +138,7 @@ class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
interface_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( interface_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
interface_id: FilterLookup[str] | None = strawberry_django.filter_field() interface_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
group: Annotated['FHRPGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( group: Annotated['FHRPGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -174,7 +174,7 @@ class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.IPAddress, lookups=True) @strawberry_django.filter_type(models.IPAddress, lookups=True)
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter): class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
address: FilterLookup[str] | None = strawberry_django.filter_field() address: StrFilterLookup[str] | None = strawberry_django.filter_field()
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | 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() vrf_id: ID | None = strawberry_django.filter_field()
status: BaseFilterLookup[Annotated['IPAddressStatusEnum', strawberry.lazy('ipam.graphql.enums')]] | None = ( status: BaseFilterLookup[Annotated['IPAddressStatusEnum', strawberry.lazy('ipam.graphql.enums')]] | None = (
@ -195,7 +195,7 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
strawberry_django.filter_field() strawberry_django.filter_field()
) )
nat_outside_id: ID | None = strawberry_django.filter_field() nat_outside_id: ID | None = strawberry_django.filter_field()
dns_name: FilterLookup[str] | None = strawberry_django.filter_field() dns_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_field() @strawberry_django.filter_field()
def assigned(self, value: bool, prefix) -> Q: def assigned(self, value: bool, prefix) -> Q:
@ -225,8 +225,8 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
@strawberry_django.filter_type(models.IPRange, lookups=True) @strawberry_django.filter_type(models.IPRange, lookups=True)
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter): class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
start_address: FilterLookup[str] | None = strawberry_django.filter_field() start_address: StrFilterLookup[str] | None = strawberry_django.filter_field()
end_address: FilterLookup[str] | None = strawberry_django.filter_field() end_address: StrFilterLookup[str] | None = strawberry_django.filter_field()
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -279,7 +279,7 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.Prefix, lookups=True) @strawberry_django.filter_type(models.Prefix, lookups=True)
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter): class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
prefix: FilterLookup[str] | None = strawberry_django.filter_field() prefix: StrFilterLookup[str] | None = strawberry_django.filter_field()
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | 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() vrf_id: ID | None = strawberry_django.filter_field()
vlan: Annotated['VLANFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() vlan: Annotated['VLANFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
@ -328,7 +328,7 @@ class RoleFilter(OrganizationalModelFilter):
@strawberry_django.filter_type(models.RouteTarget, lookups=True) @strawberry_django.filter_type(models.RouteTarget, lookups=True)
class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilter): class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
importing_vrfs: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( importing_vrfs: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -345,7 +345,7 @@ class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.Service, lookups=True) @strawberry_django.filter_type(models.Service, lookups=True)
class ServiceFilter(ContactFilterMixin, ServiceFilterMixin, PrimaryModelFilter): class ServiceFilter(ContactFilterMixin, ServiceFilterMixin, PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -357,7 +357,7 @@ class ServiceFilter(ContactFilterMixin, ServiceFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.ServiceTemplate, lookups=True) @strawberry_django.filter_type(models.ServiceTemplate, lookups=True)
class ServiceTemplateFilter(ServiceFilterMixin, PrimaryModelFilter): class ServiceTemplateFilter(ServiceFilterMixin, PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.VLAN, lookups=True) @strawberry_django.filter_type(models.VLAN, lookups=True)
@ -371,7 +371,7 @@ class VLANFilter(TenancyFilterMixin, PrimaryModelFilter):
vid: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( vid: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
status: BaseFilterLookup[Annotated['VLANStatusEnum', strawberry.lazy('ipam.graphql.enums')]] | None = ( status: BaseFilterLookup[Annotated['VLANStatusEnum', strawberry.lazy('ipam.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -401,7 +401,7 @@ class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilter):
@strawberry_django.filter_type(models.VLANTranslationPolicy, lookups=True) @strawberry_django.filter_type(models.VLANTranslationPolicy, lookups=True)
class VLANTranslationPolicyFilter(PrimaryModelFilter): class VLANTranslationPolicyFilter(PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.VLANTranslationRule, lookups=True) @strawberry_django.filter_type(models.VLANTranslationRule, lookups=True)
@ -410,7 +410,7 @@ class VLANTranslationRuleFilter(NetBoxModelFilter):
strawberry_django.filter_field() strawberry_django.filter_field()
) )
policy_id: ID | None = strawberry_django.filter_field() policy_id: ID | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
local_vid: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( local_vid: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -421,8 +421,8 @@ class VLANTranslationRuleFilter(NetBoxModelFilter):
@strawberry_django.filter_type(models.VRF, lookups=True) @strawberry_django.filter_type(models.VRF, lookups=True)
class VRFFilter(TenancyFilterMixin, PrimaryModelFilter): class VRFFilter(TenancyFilterMixin, PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
rd: FilterLookup[str] | None = strawberry_django.filter_field() rd: StrFilterLookup[str] | None = strawberry_django.filter_field()
enforce_unique: FilterLookup[bool] | None = strawberry_django.filter_field() enforce_unique: FilterLookup[bool] | None = strawberry_django.filter_field()
import_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( import_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()

View File

@ -432,9 +432,11 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
]) ])
available_ips = prefix - child_ips - child_ranges available_ips = prefix - child_ips - child_ranges
# IPv6 /127's, pool, or IPv4 /31-/32 sets are fully usable # Pool, IPv4 /31-/32 or IPv6 /127-/128 sets are fully usable
if (self.family == 6 and self.prefix.prefixlen >= 127) or self.is_pool or ( if (
self.family == 4 and self.prefix.prefixlen >= 31 self.is_pool
or (self.family == 4 and self.prefix.prefixlen >= 31)
or (self.family == 6 and self.prefix.prefixlen >= 127)
): ):
return available_ips return available_ips

View File

@ -39,3 +39,132 @@ class AnnotatedIPAddressTableTest(TestCase):
iprange_checkbox_count = html.count(f'name="pk" value="{self.ip_range.pk}"') iprange_checkbox_count = html.count(f'name="pk" value="{self.ip_range.pk}"')
self.assertEqual(iprange_checkbox_count, 0) self.assertEqual(iprange_checkbox_count, 0)
def test_annotate_ip_space_ipv4_non_pool_excludes_network_and_broadcast(self):
prefix = Prefix.objects.create(
prefix=IPNetwork('192.0.2.0/29'), # 8 addresses total
status='active',
is_pool=False,
)
data = annotate_ip_space(prefix)
self.assertEqual(len(data), 1)
available = data[0]
# /29 non-pool: exclude .0 (network) and .7 (broadcast)
self.assertEqual(available.first_ip, '192.0.2.1/29')
self.assertEqual(available.size, 6)
def test_annotate_ip_space_ipv4_pool_includes_network_and_broadcast(self):
prefix = Prefix.objects.create(
prefix=IPNetwork('192.0.2.8/29'), # 8 addresses total
status='active',
is_pool=True,
)
data = annotate_ip_space(prefix)
self.assertEqual(len(data), 1)
available = data[0]
# Pool: all addresses are usable, including network/broadcast
self.assertEqual(available.first_ip, '192.0.2.8/29')
self.assertEqual(available.size, 8)
def test_annotate_ip_space_ipv4_31_includes_all_ips(self):
prefix = Prefix.objects.create(
prefix=IPNetwork('192.0.2.16/31'), # 2 addresses total
status='active',
is_pool=False,
)
data = annotate_ip_space(prefix)
self.assertEqual(len(data), 1)
available = data[0]
# /31: fully usable
self.assertEqual(available.first_ip, '192.0.2.16/31')
self.assertEqual(available.size, 2)
def test_annotate_ip_space_ipv4_32_includes_single_ip(self):
prefix = Prefix.objects.create(
prefix=IPNetwork('192.0.2.100/32'), # 1 address total
status='active',
is_pool=False,
)
data = annotate_ip_space(prefix)
self.assertEqual(len(data), 1)
available = data[0]
# /32: single usable address
self.assertEqual(available.first_ip, '192.0.2.100/32')
self.assertEqual(available.size, 1)
def test_annotate_ip_space_ipv6_non_pool_excludes_anycast_first_ip(self):
prefix = Prefix.objects.create(
prefix=IPNetwork('2001:db8::/126'), # 4 addresses total
status='active',
is_pool=False,
)
data = annotate_ip_space(prefix)
# No child records -> expect one AvailableIPSpace entry
self.assertEqual(len(data), 1)
available = data[0]
# For IPv6 non-pool prefixes (except /127-/128), the first address is reserved (subnet-router anycast)
self.assertEqual(available.first_ip, '2001:db8::1/126')
self.assertEqual(available.size, 3) # 4 total - 1 reserved anycast
def test_annotate_ip_space_ipv6_127_includes_all_ips(self):
prefix = Prefix.objects.create(
prefix=IPNetwork('2001:db8::/127'), # 2 addresses total
status='active',
is_pool=False,
)
data = annotate_ip_space(prefix)
self.assertEqual(len(data), 1)
available = data[0]
# /127 is fully usable (no anycast exclusion)
self.assertEqual(available.first_ip, '2001:db8::/127')
self.assertEqual(available.size, 2)
def test_annotate_ip_space_ipv6_128_includes_single_ip(self):
prefix = Prefix.objects.create(
prefix=IPNetwork('2001:db8::1/128'), # 1 address total
status='active',
is_pool=False,
)
data = annotate_ip_space(prefix)
self.assertEqual(len(data), 1)
available = data[0]
# /128 is fully usable (single host address)
self.assertEqual(available.first_ip, '2001:db8::1/128')
self.assertEqual(available.size, 1)
def test_annotate_ip_space_ipv6_pool_includes_anycast_first_ip(self):
prefix = Prefix.objects.create(
prefix=IPNetwork('2001:db8:1::/126'), # 4 addresses total
status='active',
is_pool=True,
)
data = annotate_ip_space(prefix)
self.assertEqual(len(data), 1)
available = data[0]
# Pools are fully usable
self.assertEqual(available.first_ip, '2001:db8:1::/126')
self.assertEqual(available.size, 4)

View File

@ -78,12 +78,21 @@ def annotate_ip_space(prefix):
records = sorted(records, key=lambda x: x[0]) records = sorted(records, key=lambda x: x[0])
# Determine the first & last valid IP addresses in the prefix # Determine the first & last valid IP addresses in the prefix
if prefix.family == 4 and prefix.mask_length < 31 and not prefix.is_pool: if (
prefix.is_pool
or (prefix.family == 4 and prefix.mask_length >= 31)
or (prefix.family == 6 and prefix.mask_length >= 127)
):
# Pool, IPv4 /31-/32 or IPv6 /127-/128 sets are fully usable
first_ip_in_prefix = netaddr.IPAddress(prefix.prefix.first)
last_ip_in_prefix = netaddr.IPAddress(prefix.prefix.last)
elif prefix.family == 4:
# Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31 # Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31
first_ip_in_prefix = netaddr.IPAddress(prefix.prefix.first + 1) first_ip_in_prefix = netaddr.IPAddress(prefix.prefix.first + 1)
last_ip_in_prefix = netaddr.IPAddress(prefix.prefix.last - 1) last_ip_in_prefix = netaddr.IPAddress(prefix.prefix.last - 1)
else: else:
first_ip_in_prefix = netaddr.IPAddress(prefix.prefix.first) # For IPv6 prefixes, omit the Subnet-Router anycast address (RFC 4291)
first_ip_in_prefix = netaddr.IPAddress(prefix.prefix.first + 1)
last_ip_in_prefix = netaddr.IPAddress(prefix.prefix.last) last_ip_in_prefix = netaddr.IPAddress(prefix.prefix.last)
if not records: if not records:

View File

@ -15,6 +15,7 @@ from strawberry_django import (
DatetimeFilterLookup, DatetimeFilterLookup,
FilterLookup, FilterLookup,
RangeLookup, RangeLookup,
StrFilterLookup,
TimeFilterLookup, TimeFilterLookup,
process_filters, process_filters,
) )
@ -40,7 +41,7 @@ SKIP_MSG = 'Filter will be skipped on `null` value'
@strawberry.input(one_of=True, description='Lookup for JSON field. Only one of the lookup fields can be set.') @strawberry.input(one_of=True, description='Lookup for JSON field. Only one of the lookup fields can be set.')
class JSONLookup: class JSONLookup:
string_lookup: FilterLookup[str] | None = strawberry_django.filter_field() string_lookup: StrFilterLookup[str] | None = strawberry_django.filter_field()
int_range_lookup: RangeLookup[int] | None = strawberry_django.filter_field() int_range_lookup: RangeLookup[int] | None = strawberry_django.filter_field()
int_comparison_lookup: ComparisonFilterLookup[int] | None = strawberry_django.filter_field() int_comparison_lookup: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
float_range_lookup: RangeLookup[float] | None = strawberry_django.filter_field() float_range_lookup: RangeLookup[float] | None = strawberry_django.filter_field()

View File

@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Annotated, TypeVar
import strawberry import strawberry
import strawberry_django import strawberry_django
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup, StrFilterLookup
__all__ = ( __all__ = (
'DistanceFilterMixin', 'DistanceFilterMixin',
@ -48,7 +48,7 @@ class SyncedDataFilterMixin:
strawberry_django.filter_field() strawberry_django.filter_field()
) )
data_file_id: FilterLookup[int] | None = strawberry_django.filter_field() data_file_id: FilterLookup[int] | None = strawberry_django.filter_field()
data_path: FilterLookup[str] | None = strawberry_django.filter_field() data_path: StrFilterLookup[str] | None = strawberry_django.filter_field()
auto_sync_enabled: FilterLookup[bool] | None = strawberry_django.filter_field() auto_sync_enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
data_synced: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() data_synced: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()

View File

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
import strawberry_django import strawberry_django
from strawberry import ID from strawberry import ID
from strawberry_django import ComparisonFilterLookup, FilterLookup from strawberry_django import ComparisonFilterLookup, StrFilterLookup
from core.graphql.filter_mixins import ChangeLoggingMixin from core.graphql.filter_mixins import ChangeLoggingMixin
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, JournalEntriesFilterMixin, TagsFilterMixin from extras.graphql.filter_mixins import CustomFieldsFilterMixin, JournalEntriesFilterMixin, TagsFilterMixin
@ -42,21 +42,21 @@ class NetBoxModelFilter(
@dataclass @dataclass
class NestedGroupModelFilter(NetBoxModelFilter): class NestedGroupModelFilter(NetBoxModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
parent_id: ID | None = strawberry_django.filter_field() parent_id: ID | None = strawberry_django.filter_field()
@dataclass @dataclass
class OrganizationalModelFilter(NetBoxModelFilter): class OrganizationalModelFilter(NetBoxModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
comments: FilterLookup[str] | None = strawberry_django.filter_field() comments: StrFilterLookup[str] | None = strawberry_django.filter_field()
@dataclass @dataclass
class PrimaryModelFilter(NetBoxModelFilter): class PrimaryModelFilter(NetBoxModelFilter):
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
comments: FilterLookup[str] | None = strawberry_django.filter_field() comments: StrFilterLookup[str] | None = strawberry_django.filter_field()

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

@ -32,19 +32,19 @@
"htmx.org": "2.0.8", "htmx.org": "2.0.8",
"query-string": "9.3.1", "query-string": "9.3.1",
"sass": "1.97.3", "sass": "1.97.3",
"tom-select": "2.4.3", "tom-select": "2.5.2",
"typeface-inter": "3.18.1", "typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13" "typeface-roboto-mono": "1.1.13"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^2.0.2", "@eslint/compat": "^2.0.2",
"@eslint/eslintrc": "^3.3.3", "@eslint/eslintrc": "^3.3.4",
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@types/bootstrap": "5.2.10", "@types/bootstrap": "5.2.10",
"@types/cookie": "^1.0.0", "@types/cookie": "^1.0.0",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@typescript-eslint/eslint-plugin": "^8.56.0", "@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.0", "@typescript-eslint/parser": "^8.56.1",
"esbuild": "^0.27.3", "esbuild": "^0.27.3",
"esbuild-sass-plugin": "^3.6.0", "esbuild-sass-plugin": "^3.6.0",
"eslint": "^9.39.2", "eslint": "^9.39.2",
@ -52,7 +52,7 @@
"eslint-import-resolver-typescript": "^4.4.4", "eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.5", "eslint-plugin-prettier": "^5.5.5",
"globals": "^17.3.0", "globals": "^17.4.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },

View File

@ -1,5 +1,4 @@
import { RecursivePartial, TomOption, TomSettings } from 'tom-select/dist/types/types'; import type { RecursivePartial, TomOption, TomSettings, TomInput } from 'tom-select/dist/cjs/types';
import { TomInput } from 'tom-select/dist/cjs/types/core';
import { addClasses } from 'tom-select/src/vanilla.ts'; import { addClasses } from 'tom-select/src/vanilla.ts';
import queryString from 'query-string'; import queryString from 'query-string';
import TomSelect from 'tom-select'; import TomSelect from 'tom-select';

View File

@ -210,7 +210,7 @@
dependencies: dependencies:
"@types/json-schema" "^7.0.15" "@types/json-schema" "^7.0.15"
"@eslint/eslintrc@^3.3.1", "@eslint/eslintrc@^3.3.3": "@eslint/eslintrc@^3.3.1":
version "3.3.3" version "3.3.3"
resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz" resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz"
integrity sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ== integrity sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==
@ -225,6 +225,21 @@
minimatch "^3.1.2" minimatch "^3.1.2"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@eslint/eslintrc@^3.3.4":
version "3.3.4"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.4.tgz#e402b1920f7c1f5a15342caa432b1348cacbb641"
integrity sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==
dependencies:
ajv "^6.14.0"
debug "^4.3.2"
espree "^10.0.1"
globals "^14.0.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.1.1"
minimatch "^3.1.3"
strip-json-comments "^3.1.1"
"@eslint/js@9.39.2", "@eslint/js@^9.39.2": "@eslint/js@9.39.2", "@eslint/js@^9.39.2":
version "9.39.2" version "9.39.2"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599"
@ -935,100 +950,100 @@
dependencies: dependencies:
"@types/estree" "*" "@types/estree" "*"
"@typescript-eslint/eslint-plugin@^8.56.0": "@typescript-eslint/eslint-plugin@^8.56.1":
version "8.56.0" version "8.56.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz#5aec3db807a6b8437ea5d5ebf7bd16b4119aba8d" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz#b1ce606d87221daec571e293009675992f0aae76"
integrity sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw== integrity sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==
dependencies: dependencies:
"@eslint-community/regexpp" "^4.12.2" "@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.56.0" "@typescript-eslint/scope-manager" "8.56.1"
"@typescript-eslint/type-utils" "8.56.0" "@typescript-eslint/type-utils" "8.56.1"
"@typescript-eslint/utils" "8.56.0" "@typescript-eslint/utils" "8.56.1"
"@typescript-eslint/visitor-keys" "8.56.0" "@typescript-eslint/visitor-keys" "8.56.1"
ignore "^7.0.5" ignore "^7.0.5"
natural-compare "^1.4.0" natural-compare "^1.4.0"
ts-api-utils "^2.4.0" ts-api-utils "^2.4.0"
"@typescript-eslint/parser@^8.56.0": "@typescript-eslint/parser@^8.56.1":
version "8.56.0" version "8.56.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.56.0.tgz#8ecff1678b8b1a742d29c446ccf5eeea7f971d72" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.56.1.tgz#21d13b3d456ffb08614c1d68bb9a4f8d9237cdc7"
integrity sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg== integrity sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==
dependencies: dependencies:
"@typescript-eslint/scope-manager" "8.56.0" "@typescript-eslint/scope-manager" "8.56.1"
"@typescript-eslint/types" "8.56.0" "@typescript-eslint/types" "8.56.1"
"@typescript-eslint/typescript-estree" "8.56.0" "@typescript-eslint/typescript-estree" "8.56.1"
"@typescript-eslint/visitor-keys" "8.56.0" "@typescript-eslint/visitor-keys" "8.56.1"
debug "^4.4.3" debug "^4.4.3"
"@typescript-eslint/project-service@8.56.0": "@typescript-eslint/project-service@8.56.1":
version "8.56.0" version "8.56.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.56.0.tgz#bb8562fecd8f7922e676fc6a1189c20dd7991d73" resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.56.1.tgz#65c8d645f028b927bfc4928593b54e2ecd809244"
integrity sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg== integrity sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==
dependencies: dependencies:
"@typescript-eslint/tsconfig-utils" "^8.56.0" "@typescript-eslint/tsconfig-utils" "^8.56.1"
"@typescript-eslint/types" "^8.56.0" "@typescript-eslint/types" "^8.56.1"
debug "^4.4.3" debug "^4.4.3"
"@typescript-eslint/scope-manager@8.56.0": "@typescript-eslint/scope-manager@8.56.1":
version "8.56.0" version "8.56.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz#604030a4c6433df3728effdd441d47f45a86edb4" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz#254df93b5789a871351335dd23e20bc164060f24"
integrity sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w== integrity sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==
dependencies: dependencies:
"@typescript-eslint/types" "8.56.0" "@typescript-eslint/types" "8.56.1"
"@typescript-eslint/visitor-keys" "8.56.0" "@typescript-eslint/visitor-keys" "8.56.1"
"@typescript-eslint/tsconfig-utils@8.56.0", "@typescript-eslint/tsconfig-utils@^8.56.0": "@typescript-eslint/tsconfig-utils@8.56.1", "@typescript-eslint/tsconfig-utils@^8.56.1":
version "8.56.0" version "8.56.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz#2538ce83cbc376e685487960cbb24b65fe2abc4e" resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz#1afa830b0fada5865ddcabdc993b790114a879b7"
integrity sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg== integrity sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==
"@typescript-eslint/type-utils@8.56.0": "@typescript-eslint/type-utils@8.56.1":
version "8.56.0" version "8.56.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz#72b4edc1fc73988998f1632b3ec99c2a66eaac6e" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz#7a6c4fabf225d674644931e004302cbbdd2f2e24"
integrity sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA== integrity sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==
dependencies: dependencies:
"@typescript-eslint/types" "8.56.0" "@typescript-eslint/types" "8.56.1"
"@typescript-eslint/typescript-estree" "8.56.0" "@typescript-eslint/typescript-estree" "8.56.1"
"@typescript-eslint/utils" "8.56.0" "@typescript-eslint/utils" "8.56.1"
debug "^4.4.3" debug "^4.4.3"
ts-api-utils "^2.4.0" ts-api-utils "^2.4.0"
"@typescript-eslint/types@8.56.0", "@typescript-eslint/types@^8.56.0": "@typescript-eslint/types@8.56.1", "@typescript-eslint/types@^8.56.1":
version "8.56.0" version "8.56.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.56.0.tgz#a2444011b9a98ca13d70411d2cbfed5443b3526a" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.56.1.tgz#975e5942bf54895291337c91b9191f6eb0632ab9"
integrity sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ== integrity sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==
"@typescript-eslint/typescript-estree@8.56.0": "@typescript-eslint/typescript-estree@8.56.1":
version "8.56.0" version "8.56.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz#fadbc74c14c5bac947db04980ff58bb178701c2e" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz#3b9e57d8129a860c50864c42188f761bdef3eab0"
integrity sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q== integrity sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==
dependencies: dependencies:
"@typescript-eslint/project-service" "8.56.0" "@typescript-eslint/project-service" "8.56.1"
"@typescript-eslint/tsconfig-utils" "8.56.0" "@typescript-eslint/tsconfig-utils" "8.56.1"
"@typescript-eslint/types" "8.56.0" "@typescript-eslint/types" "8.56.1"
"@typescript-eslint/visitor-keys" "8.56.0" "@typescript-eslint/visitor-keys" "8.56.1"
debug "^4.4.3" debug "^4.4.3"
minimatch "^9.0.5" minimatch "^10.2.2"
semver "^7.7.3" semver "^7.7.3"
tinyglobby "^0.2.15" tinyglobby "^0.2.15"
ts-api-utils "^2.4.0" ts-api-utils "^2.4.0"
"@typescript-eslint/utils@8.56.0": "@typescript-eslint/utils@8.56.1":
version "8.56.0" version "8.56.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.56.0.tgz#063ce6f702ec603de1b83ee795ed5e877d6f7841" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.56.1.tgz#5a86acaf9f1b4c4a85a42effb217f73059f6deb7"
integrity sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ== integrity sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==
dependencies: dependencies:
"@eslint-community/eslint-utils" "^4.9.1" "@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.56.0" "@typescript-eslint/scope-manager" "8.56.1"
"@typescript-eslint/types" "8.56.0" "@typescript-eslint/types" "8.56.1"
"@typescript-eslint/typescript-estree" "8.56.0" "@typescript-eslint/typescript-estree" "8.56.1"
"@typescript-eslint/visitor-keys@8.56.0": "@typescript-eslint/visitor-keys@8.56.1":
version "8.56.0" version "8.56.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz#7d6592ab001827d3ce052155edf7ecad19688d7d" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz#50e03475c33a42d123dc99e63acf1841c0231f87"
integrity sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg== integrity sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==
dependencies: dependencies:
"@typescript-eslint/types" "8.56.0" "@typescript-eslint/types" "8.56.1"
eslint-visitor-keys "^5.0.0" eslint-visitor-keys "^5.0.0"
"@unrs/resolver-binding-android-arm-eabi@1.11.1": "@unrs/resolver-binding-android-arm-eabi@1.11.1":
@ -1148,6 +1163,16 @@ ajv@^6.12.4:
json-schema-traverse "^0.4.1" json-schema-traverse "^0.4.1"
uri-js "^4.2.2" uri-js "^4.2.2"
ajv@^6.14.0:
version "6.14.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a"
integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ansi-styles@^4.1.0: ansi-styles@^4.1.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz"
@ -1274,6 +1299,11 @@ balanced-match@^1.0.0:
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
balanced-match@^4.0.2:
version "4.0.4"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a"
integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==
bootstrap@5.3.7: bootstrap@5.3.7:
version "5.3.7" version "5.3.7"
resolved "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz" resolved "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz"
@ -1292,12 +1322,12 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0" balanced-match "^1.0.0"
concat-map "0.0.1" concat-map "0.0.1"
brace-expansion@^2.0.1: brace-expansion@^5.0.2:
version "2.0.2" version "5.0.4"
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.4.tgz#614daaecd0a688f660bbbc909a8748c3d80d4336"
integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== integrity sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==
dependencies: dependencies:
balanced-match "^1.0.0" balanced-match "^4.0.2"
braces@^3.0.3: braces@^3.0.3:
version "3.0.3" version "3.0.3"
@ -2189,10 +2219,10 @@ globals@^14.0.0:
resolved "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz" resolved "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz"
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
globals@^17.3.0: globals@^17.4.0:
version "17.3.0" version "17.4.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-17.3.0.tgz#8b96544c2fa91afada02747cc9731c002a96f3b9" resolved "https://registry.yarnpkg.com/globals/-/globals-17.4.0.tgz#33d7d297ed1536b388a0e2f4bcd0ff19c8ff91b5"
integrity sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw== integrity sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==
globalthis@^1.0.3, globalthis@^1.0.4: globalthis@^1.0.3, globalthis@^1.0.4:
version "1.0.4" version "1.0.4"
@ -2784,6 +2814,13 @@ micromatch@^4.0.5:
braces "^3.0.3" braces "^3.0.3"
picomatch "^2.3.1" picomatch "^2.3.1"
minimatch@^10.2.2:
version "10.2.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde"
integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==
dependencies:
brace-expansion "^5.0.2"
minimatch@^3.1.2: minimatch@^3.1.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
@ -2791,12 +2828,12 @@ minimatch@^3.1.2:
dependencies: dependencies:
brace-expansion "^1.1.7" brace-expansion "^1.1.7"
minimatch@^9.0.5: minimatch@^3.1.3:
version "9.0.5" version "3.1.5"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
dependencies: dependencies:
brace-expansion "^2.0.1" brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.6: minimist@^1.2.0, minimist@^1.2.6:
version "1.2.8" version "1.2.8"
@ -3455,10 +3492,10 @@ toggle-selection@^1.0.6:
resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz" resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz"
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
tom-select@2.4.3: tom-select@2.5.2:
version "2.4.3" version "2.5.2"
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.4.3.tgz#1daa4131cd317de691f39eb5bf41148265986c1f" resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.5.2.tgz#77dd4bc780b1ea72905337b24f04ce19dc6d2ca1"
integrity sha512-MFFrMxP1bpnAMPbdvPCZk0KwYxLqhYZso39torcdoefeV/NThNyDu8dV96/INJ5XQVTL3O55+GqQ78Pkj5oCfw== integrity sha512-VAlGj5MBWVLMJje2NwA3XSmxa7CUFpp1tdzFZ8wymCkcLeP0NwF4ARmSuUK4BWbmSN1fETlSazWkMIxEpP4GdQ==
dependencies: dependencies:
"@orchidjs/sifter" "^1.1.0" "@orchidjs/sifter" "^1.1.0"
"@orchidjs/unicode-variants" "^1.1.2" "@orchidjs/unicode-variants" "^1.1.2"

View File

@ -1,3 +1,3 @@
version: "4.5.3" version: "4.5.4"
edition: "Community" edition: "Community"
published: "2026-02-17" published: "2026-03-03"

View File

@ -1,10 +1,12 @@
{% load i18n %} {% load i18n %}
<a href="{{ value.get_absolute_url }}"{% if name %} id="attr_{{ name }}"{% endif %}>{{ value.address.ip }}</a> <span>
{% if value.nat_inside %} <a href="{{ value.get_absolute_url }}"{% if name %} id="attr_{{ name }}"{% endif %}>{{ value.address.ip }}</a>
({% trans "NAT for" %} <a href="{{ value.nat_inside.get_absolute_url }}">{{ value.nat_inside.address.ip }}</a>) {% if value.nat_inside %}
{% elif value.nat_outside.exists %} ({% trans "NAT for" %} <a href="{{ value.nat_inside.get_absolute_url }}">{{ value.nat_inside.address.ip }}</a>)
({% trans "NAT" %}: {% for nat in value.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}) {% elif value.nat_outside.exists %}
{% endif %} ({% trans "NAT" %}: {% for nat in value.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %}
</span>
<a class="btn btn-sm btn-primary copy-content" data-clipboard-target="#attr_{{ name }}" title="{% trans "Copy to clipboard" %}"> <a class="btn btn-sm btn-primary copy-content" data-clipboard-target="#attr_{{ name }}" title="{% trans "Copy to clipboard" %}">
<i class="mdi mdi-content-copy"></i> <i class="mdi mdi-content-copy"></i>
</a> </a>

View File

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
import strawberry import strawberry
import strawberry_django import strawberry_django
from strawberry.scalars import ID from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, FilterLookup from strawberry_django import BaseFilterLookup, StrFilterLookup
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
from netbox.graphql.filters import ( from netbox.graphql.filters import (
@ -60,8 +60,8 @@ __all__ = (
@strawberry_django.filter_type(models.Tenant, lookups=True) @strawberry_django.filter_type(models.Tenant, lookups=True)
class TenantFilter(ContactFilterMixin, PrimaryModelFilter): class TenantFilter(ContactFilterMixin, PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
group: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( group: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -153,12 +153,12 @@ class TenantGroupFilter(OrganizationalModelFilter):
@strawberry_django.filter_type(models.Contact, lookups=True) @strawberry_django.filter_type(models.Contact, lookups=True)
class ContactFilter(PrimaryModelFilter): class ContactFilter(PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
title: FilterLookup[str] | None = strawberry_django.filter_field() title: StrFilterLookup[str] | None = strawberry_django.filter_field()
phone: FilterLookup[str] | None = strawberry_django.filter_field() phone: StrFilterLookup[str] | None = strawberry_django.filter_field()
email: FilterLookup[str] | None = strawberry_django.filter_field() email: StrFilterLookup[str] | None = strawberry_django.filter_field()
address: FilterLookup[str] | None = strawberry_django.filter_field() address: StrFilterLookup[str] | None = strawberry_django.filter_field()
link: FilterLookup[str] | None = strawberry_django.filter_field() link: StrFilterLookup[str] | None = strawberry_django.filter_field()
groups: Annotated['ContactGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( groups: Annotated['ContactGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-21 05:16+0000\n" "POT-Creation-Date: 2026-03-03 05:20+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -41,9 +41,9 @@ msgstr ""
#: netbox/circuits/choices.py:21 netbox/dcim/choices.py:20 #: netbox/circuits/choices.py:21 netbox/dcim/choices.py:20
#: netbox/dcim/choices.py:102 netbox/dcim/choices.py:204 #: netbox/dcim/choices.py:102 netbox/dcim/choices.py:204
#: netbox/dcim/choices.py:257 netbox/dcim/choices.py:1929 #: netbox/dcim/choices.py:257 netbox/dcim/choices.py:1933
#: netbox/dcim/choices.py:1987 netbox/dcim/choices.py:2054 #: netbox/dcim/choices.py:1991 netbox/dcim/choices.py:2058
#: netbox/dcim/choices.py:2076 netbox/virtualization/choices.py:20 #: netbox/dcim/choices.py:2080 netbox/virtualization/choices.py:20
#: netbox/virtualization/choices.py:46 netbox/vpn/choices.py:18 #: netbox/virtualization/choices.py:46 netbox/vpn/choices.py:18
#: netbox/vpn/choices.py:281 #: netbox/vpn/choices.py:281
msgid "Planned" msgid "Planned"
@ -57,8 +57,8 @@ msgstr ""
#: netbox/core/tables/tasks.py:23 netbox/dcim/choices.py:22 #: netbox/core/tables/tasks.py:23 netbox/dcim/choices.py:22
#: netbox/dcim/choices.py:103 netbox/dcim/choices.py:155 #: netbox/dcim/choices.py:103 netbox/dcim/choices.py:155
#: netbox/dcim/choices.py:203 netbox/dcim/choices.py:256 #: netbox/dcim/choices.py:203 netbox/dcim/choices.py:256
#: netbox/dcim/choices.py:1986 netbox/dcim/choices.py:2053 #: netbox/dcim/choices.py:1990 netbox/dcim/choices.py:2057
#: netbox/dcim/choices.py:2075 netbox/extras/tables/tables.py:642 #: netbox/dcim/choices.py:2079 netbox/extras/tables/tables.py:642
#: netbox/ipam/choices.py:31 netbox/ipam/choices.py:49 #: netbox/ipam/choices.py:31 netbox/ipam/choices.py:49
#: netbox/ipam/choices.py:69 netbox/ipam/choices.py:154 #: netbox/ipam/choices.py:69 netbox/ipam/choices.py:154
#: netbox/templates/extras/configcontext.html:29 #: netbox/templates/extras/configcontext.html:29
@ -70,8 +70,8 @@ msgid "Active"
msgstr "" msgstr ""
#: netbox/circuits/choices.py:24 netbox/dcim/choices.py:202 #: netbox/circuits/choices.py:24 netbox/dcim/choices.py:202
#: netbox/dcim/choices.py:255 netbox/dcim/choices.py:1985 #: netbox/dcim/choices.py:255 netbox/dcim/choices.py:1989
#: netbox/dcim/choices.py:2055 netbox/dcim/choices.py:2074 #: netbox/dcim/choices.py:2059 netbox/dcim/choices.py:2078
#: netbox/virtualization/choices.py:24 netbox/virtualization/choices.py:44 #: netbox/virtualization/choices.py:24 netbox/virtualization/choices.py:44
msgid "Offline" msgid "Offline"
msgstr "" msgstr ""
@ -84,7 +84,7 @@ msgstr ""
msgid "Decommissioned" msgid "Decommissioned"
msgstr "" msgstr ""
#: netbox/circuits/choices.py:90 netbox/dcim/choices.py:1998 #: netbox/circuits/choices.py:90 netbox/dcim/choices.py:2002
#: netbox/dcim/tables/devices.py:1208 netbox/templates/dcim/interface.html:148 #: netbox/dcim/tables/devices.py:1208 netbox/templates/dcim/interface.html:148
#: netbox/tenancy/choices.py:17 #: netbox/tenancy/choices.py:17
msgid "Primary" msgid "Primary"
@ -1995,7 +1995,7 @@ msgstr ""
#: netbox/core/choices.py:22 netbox/core/choices.py:59 #: netbox/core/choices.py:22 netbox/core/choices.py:59
#: netbox/core/constants.py:21 netbox/core/tables/tasks.py:35 #: netbox/core/constants.py:21 netbox/core/tables/tasks.py:35
#: netbox/dcim/choices.py:206 netbox/dcim/choices.py:259 #: netbox/dcim/choices.py:206 netbox/dcim/choices.py:259
#: netbox/dcim/choices.py:1988 netbox/dcim/choices.py:2078 #: netbox/dcim/choices.py:1992 netbox/dcim/choices.py:2082
#: netbox/virtualization/choices.py:48 #: netbox/virtualization/choices.py:48
msgid "Failed" msgid "Failed"
msgstr "" msgstr ""
@ -2181,7 +2181,7 @@ msgid "User name"
msgstr "" msgstr ""
#: netbox/core/forms/bulk_edit.py:25 netbox/core/forms/filtersets.py:47 #: netbox/core/forms/bulk_edit.py:25 netbox/core/forms/filtersets.py:47
#: netbox/core/tables/data.py:28 netbox/dcim/choices.py:2036 #: netbox/core/tables/data.py:28 netbox/dcim/choices.py:2040
#: netbox/dcim/forms/bulk_edit.py:1105 netbox/dcim/forms/bulk_edit.py:1386 #: netbox/dcim/forms/bulk_edit.py:1105 netbox/dcim/forms/bulk_edit.py:1386
#: netbox/dcim/forms/filtersets.py:1619 netbox/dcim/forms/filtersets.py:1712 #: netbox/dcim/forms/filtersets.py:1619 netbox/dcim/forms/filtersets.py:1712
#: netbox/dcim/tables/devices.py:581 netbox/dcim/tables/devicetypes.py:233 #: netbox/dcim/tables/devices.py:581 netbox/dcim/tables/devicetypes.py:233
@ -2375,7 +2375,7 @@ msgstr ""
msgid "Rack Elevations" msgid "Rack Elevations"
msgstr "" msgstr ""
#: netbox/core/forms/model_forms.py:160 netbox/dcim/choices.py:1907 #: netbox/core/forms/model_forms.py:160 netbox/dcim/choices.py:1911
#: netbox/dcim/forms/bulk_edit.py:944 netbox/dcim/forms/bulk_edit.py:1340 #: netbox/dcim/forms/bulk_edit.py:944 netbox/dcim/forms/bulk_edit.py:1340
#: netbox/dcim/forms/bulk_edit.py:1361 netbox/dcim/tables/racks.py:144 #: netbox/dcim/forms/bulk_edit.py:1361 netbox/dcim/tables/racks.py:144
#: netbox/netbox/navigation/menu.py:316 netbox/netbox/navigation/menu.py:320 #: netbox/netbox/navigation/menu.py:316 netbox/netbox/navigation/menu.py:320
@ -3041,8 +3041,8 @@ msgid "Staging"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:23 netbox/dcim/choices.py:208 #: netbox/dcim/choices.py:23 netbox/dcim/choices.py:208
#: netbox/dcim/choices.py:260 netbox/dcim/choices.py:1930 #: netbox/dcim/choices.py:260 netbox/dcim/choices.py:1934
#: netbox/dcim/choices.py:2079 netbox/virtualization/choices.py:23 #: netbox/dcim/choices.py:2083 netbox/virtualization/choices.py:23
#: netbox/virtualization/choices.py:49 netbox/vpn/choices.py:282 #: netbox/virtualization/choices.py:49 netbox/vpn/choices.py:282
msgid "Decommissioning" msgid "Decommissioning"
msgstr "" msgstr ""
@ -3108,7 +3108,7 @@ msgstr ""
msgid "Millimeters" msgid "Millimeters"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:115 netbox/dcim/choices.py:1952 #: netbox/dcim/choices.py:115 netbox/dcim/choices.py:1956
msgid "Inches" msgid "Inches"
msgstr "" msgstr ""
@ -3186,7 +3186,7 @@ msgid "Rear"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:205 netbox/dcim/choices.py:258 #: netbox/dcim/choices.py:205 netbox/dcim/choices.py:258
#: netbox/dcim/choices.py:2077 netbox/virtualization/choices.py:47 #: netbox/dcim/choices.py:2081 netbox/virtualization/choices.py:47
msgid "Staged" msgid "Staged"
msgstr "" msgstr ""
@ -3219,7 +3219,7 @@ msgid "Top to bottom"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:235 netbox/dcim/choices.py:280 #: netbox/dcim/choices.py:235 netbox/dcim/choices.py:280
#: netbox/dcim/choices.py:1562 #: netbox/dcim/choices.py:1566
msgid "Passive" msgid "Passive"
msgstr "" msgstr ""
@ -3248,8 +3248,8 @@ msgid "Proprietary"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:606 netbox/dcim/choices.py:853 #: netbox/dcim/choices.py:606 netbox/dcim/choices.py:853
#: netbox/dcim/choices.py:1474 netbox/dcim/choices.py:1476 #: netbox/dcim/choices.py:1478 netbox/dcim/choices.py:1480
#: netbox/dcim/choices.py:1712 netbox/dcim/choices.py:1714 #: netbox/dcim/choices.py:1716 netbox/dcim/choices.py:1718
#: netbox/netbox/navigation/menu.py:212 #: netbox/netbox/navigation/menu.py:212
msgid "Other" msgid "Other"
msgstr "" msgstr ""
@ -3262,11 +3262,11 @@ msgstr ""
msgid "Physical" msgid "Physical"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:884 netbox/dcim/choices.py:1151 #: netbox/dcim/choices.py:884 netbox/dcim/choices.py:1153
msgid "Virtual" msgid "Virtual"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:885 netbox/dcim/choices.py:1351 #: netbox/dcim/choices.py:885 netbox/dcim/choices.py:1355
#: netbox/dcim/forms/bulk_edit.py:1546 netbox/dcim/forms/filtersets.py:1577 #: netbox/dcim/forms/bulk_edit.py:1546 netbox/dcim/forms/filtersets.py:1577
#: netbox/dcim/forms/filtersets.py:1703 netbox/dcim/forms/model_forms.py:1125 #: netbox/dcim/forms/filtersets.py:1703 netbox/dcim/forms/model_forms.py:1125
#: netbox/dcim/forms/model_forms.py:1589 netbox/netbox/navigation/menu.py:150 #: netbox/dcim/forms/model_forms.py:1589 netbox/netbox/navigation/menu.py:150
@ -3275,11 +3275,11 @@ msgstr ""
msgid "Wireless" msgid "Wireless"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1149 #: netbox/dcim/choices.py:1151
msgid "Virtual interfaces" msgid "Virtual interfaces"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1152 netbox/dcim/forms/bulk_edit.py:1399 #: netbox/dcim/choices.py:1154 netbox/dcim/forms/bulk_edit.py:1399
#: netbox/dcim/forms/bulk_import.py:949 netbox/dcim/forms/model_forms.py:1107 #: netbox/dcim/forms/bulk_import.py:949 netbox/dcim/forms/model_forms.py:1107
#: netbox/dcim/tables/devices.py:741 netbox/templates/dcim/interface.html:112 #: netbox/dcim/tables/devices.py:741 netbox/templates/dcim/interface.html:112
#: netbox/virtualization/forms/bulk_edit.py:177 #: netbox/virtualization/forms/bulk_edit.py:177
@ -3288,67 +3288,67 @@ msgstr ""
msgid "Bridge" msgid "Bridge"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1153 #: netbox/dcim/choices.py:1155
msgid "Link Aggregation Group (LAG)" msgid "Link Aggregation Group (LAG)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1157 #: netbox/dcim/choices.py:1159
msgid "FastEthernet (100 Mbps)" msgid "FastEthernet (100 Mbps)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1166 #: netbox/dcim/choices.py:1168
msgid "GigabitEthernet (1 Gbps)" msgid "GigabitEthernet (1 Gbps)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1184 #: netbox/dcim/choices.py:1186
msgid "2.5/5 Gbps Ethernet" msgid "2.5/5 Gbps Ethernet"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1191 #: netbox/dcim/choices.py:1193
msgid "10 Gbps Ethernet" msgid "10 Gbps Ethernet"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1206 #: netbox/dcim/choices.py:1209
msgid "25 Gbps Ethernet" msgid "25 Gbps Ethernet"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1216 #: netbox/dcim/choices.py:1219
msgid "40 Gbps Ethernet" msgid "40 Gbps Ethernet"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1226 #: netbox/dcim/choices.py:1230
msgid "50 Gbps Ethernet" msgid "50 Gbps Ethernet"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1236 #: netbox/dcim/choices.py:1240
msgid "100 Gbps Ethernet" msgid "100 Gbps Ethernet"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1257 #: netbox/dcim/choices.py:1261
msgid "200 Gbps Ethernet" msgid "200 Gbps Ethernet"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1271 #: netbox/dcim/choices.py:1275
msgid "400 Gbps Ethernet" msgid "400 Gbps Ethernet"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1289 #: netbox/dcim/choices.py:1293
msgid "800 Gbps Ethernet" msgid "800 Gbps Ethernet"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1298 #: netbox/dcim/choices.py:1302
msgid "Pluggable transceivers" msgid "Pluggable transceivers"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1335 #: netbox/dcim/choices.py:1339
msgid "Backplane Ethernet" msgid "Backplane Ethernet"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1367 #: netbox/dcim/choices.py:1371
msgid "Cellular" msgid "Cellular"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1419 netbox/dcim/forms/filtersets.py:425 #: netbox/dcim/choices.py:1423 netbox/dcim/forms/filtersets.py:425
#: netbox/dcim/forms/filtersets.py:911 netbox/dcim/forms/filtersets.py:1112 #: netbox/dcim/forms/filtersets.py:911 netbox/dcim/forms/filtersets.py:1112
#: netbox/dcim/forms/filtersets.py:1910 #: netbox/dcim/forms/filtersets.py:1910
#: netbox/templates/dcim/inventoryitem.html:56 #: netbox/templates/dcim/inventoryitem.html:56
@ -3356,255 +3356,255 @@ msgstr ""
msgid "Serial" msgid "Serial"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1434 #: netbox/dcim/choices.py:1438
msgid "Coaxial" msgid "Coaxial"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1455 #: netbox/dcim/choices.py:1459
msgid "Stacking" msgid "Stacking"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1507 #: netbox/dcim/choices.py:1511
msgid "Half" msgid "Half"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1508 #: netbox/dcim/choices.py:1512
msgid "Full" msgid "Full"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1509 netbox/netbox/preferences.py:32 #: netbox/dcim/choices.py:1513 netbox/netbox/preferences.py:32
#: netbox/wireless/choices.py:480 #: netbox/wireless/choices.py:480
msgid "Auto" msgid "Auto"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1521 #: netbox/dcim/choices.py:1525
msgid "Access" msgid "Access"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1522 netbox/ipam/tables/vlans.py:150 #: netbox/dcim/choices.py:1526 netbox/ipam/tables/vlans.py:150
#: netbox/ipam/tables/vlans.py:210 #: netbox/ipam/tables/vlans.py:210
#: netbox/templates/dcim/inc/interface_vlans_table.html:7 #: netbox/templates/dcim/inc/interface_vlans_table.html:7
msgid "Tagged" msgid "Tagged"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1523 #: netbox/dcim/choices.py:1527
msgid "Tagged (All)" msgid "Tagged (All)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1524 netbox/templates/ipam/vlan_edit.html:26 #: netbox/dcim/choices.py:1528 netbox/templates/ipam/vlan_edit.html:26
msgid "Q-in-Q (802.1ad)" msgid "Q-in-Q (802.1ad)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1553 #: netbox/dcim/choices.py:1557
msgid "IEEE Standard" msgid "IEEE Standard"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1564 #: netbox/dcim/choices.py:1568
msgid "Passive 24V (2-pair)" msgid "Passive 24V (2-pair)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1565 #: netbox/dcim/choices.py:1569
msgid "Passive 24V (4-pair)" msgid "Passive 24V (4-pair)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1566 #: netbox/dcim/choices.py:1570
msgid "Passive 48V (2-pair)" msgid "Passive 48V (2-pair)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1567 #: netbox/dcim/choices.py:1571
msgid "Passive 48V (4-pair)" msgid "Passive 48V (4-pair)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1640 #: netbox/dcim/choices.py:1644
msgid "Copper" msgid "Copper"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1663 #: netbox/dcim/choices.py:1667
msgid "Fiber Optic" msgid "Fiber Optic"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1699 netbox/dcim/choices.py:1913 #: netbox/dcim/choices.py:1703 netbox/dcim/choices.py:1917
msgid "USB" msgid "USB"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1755 #: netbox/dcim/choices.py:1759
msgid "Single" msgid "Single"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1757 #: netbox/dcim/choices.py:1761
msgid "1C1P" msgid "1C1P"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1758 #: netbox/dcim/choices.py:1762
msgid "1C2P" msgid "1C2P"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1759 #: netbox/dcim/choices.py:1763
msgid "1C4P" msgid "1C4P"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1760 #: netbox/dcim/choices.py:1764
msgid "1C6P" msgid "1C6P"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1761 #: netbox/dcim/choices.py:1765
msgid "1C8P" msgid "1C8P"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1762 #: netbox/dcim/choices.py:1766
msgid "1C12P" msgid "1C12P"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1763 #: netbox/dcim/choices.py:1767
msgid "1C16P" msgid "1C16P"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1767 #: netbox/dcim/choices.py:1771
msgid "Trunk" msgid "Trunk"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1769 #: netbox/dcim/choices.py:1773
msgid "2C1P trunk" msgid "2C1P trunk"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1770 #: netbox/dcim/choices.py:1774
msgid "2C2P trunk" msgid "2C2P trunk"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1771 #: netbox/dcim/choices.py:1775
msgid "2C4P trunk" msgid "2C4P trunk"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1772 #: netbox/dcim/choices.py:1776
msgid "2C4P trunk (shuffle)" msgid "2C4P trunk (shuffle)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1773 #: netbox/dcim/choices.py:1777
msgid "2C6P trunk" msgid "2C6P trunk"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1774 #: netbox/dcim/choices.py:1778
msgid "2C8P trunk" msgid "2C8P trunk"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1775 #: netbox/dcim/choices.py:1779
msgid "2C12P trunk" msgid "2C12P trunk"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1776 #: netbox/dcim/choices.py:1780
msgid "4C1P trunk" msgid "4C1P trunk"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1777 #: netbox/dcim/choices.py:1781
msgid "4C2P trunk" msgid "4C2P trunk"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1778 #: netbox/dcim/choices.py:1782
msgid "4C4P trunk" msgid "4C4P trunk"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1779 #: netbox/dcim/choices.py:1783
msgid "4C4P trunk (shuffle)" msgid "4C4P trunk (shuffle)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1780 #: netbox/dcim/choices.py:1784
msgid "4C6P trunk" msgid "4C6P trunk"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1781 #: netbox/dcim/choices.py:1785
msgid "4C8P trunk" msgid "4C8P trunk"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1782 #: netbox/dcim/choices.py:1786
msgid "8C4P trunk" msgid "8C4P trunk"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1786 #: netbox/dcim/choices.py:1790
msgid "Breakout" msgid "Breakout"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1788 #: netbox/dcim/choices.py:1792
msgid "1C4P:4C1P breakout" msgid "1C4P:4C1P breakout"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1789 #: netbox/dcim/choices.py:1793
msgid "1C6P:6C1P breakout" msgid "1C6P:6C1P breakout"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1790 #: netbox/dcim/choices.py:1794
msgid "2C4P:8C1P breakout (shuffle)" msgid "2C4P:8C1P breakout (shuffle)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1848 #: netbox/dcim/choices.py:1852
msgid "Copper - Twisted Pair (UTP/STP)" msgid "Copper - Twisted Pair (UTP/STP)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1862 #: netbox/dcim/choices.py:1866
msgid "Copper - Twinax (DAC)" msgid "Copper - Twinax (DAC)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1869 #: netbox/dcim/choices.py:1873
msgid "Copper - Coaxial" msgid "Copper - Coaxial"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1884 #: netbox/dcim/choices.py:1888
msgid "Fiber - Multimode" msgid "Fiber - Multimode"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1895 #: netbox/dcim/choices.py:1899
msgid "Fiber - Single-mode" msgid "Fiber - Single-mode"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1903 #: netbox/dcim/choices.py:1907
msgid "Fiber - Other" msgid "Fiber - Other"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1928 netbox/dcim/forms/filtersets.py:1402 #: netbox/dcim/choices.py:1932 netbox/dcim/forms/filtersets.py:1402
msgid "Connected" msgid "Connected"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1947 netbox/netbox/choices.py:177 #: netbox/dcim/choices.py:1951 netbox/netbox/choices.py:177
msgid "Kilometers" msgid "Kilometers"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1948 netbox/netbox/choices.py:178 #: netbox/dcim/choices.py:1952 netbox/netbox/choices.py:178
#: netbox/templates/dcim/cable_trace.html:65 #: netbox/templates/dcim/cable_trace.html:65
msgid "Meters" msgid "Meters"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1949 #: netbox/dcim/choices.py:1953
msgid "Centimeters" msgid "Centimeters"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1950 netbox/netbox/choices.py:179 #: netbox/dcim/choices.py:1954 netbox/netbox/choices.py:179
msgid "Miles" msgid "Miles"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1951 netbox/netbox/choices.py:180 #: netbox/dcim/choices.py:1955 netbox/netbox/choices.py:180
#: netbox/templates/dcim/cable_trace.html:66 #: netbox/templates/dcim/cable_trace.html:66
msgid "Feet" msgid "Feet"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1999 #: netbox/dcim/choices.py:2003
msgid "Redundant" msgid "Redundant"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:2020 #: netbox/dcim/choices.py:2024
msgid "Single phase" msgid "Single phase"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:2021 #: netbox/dcim/choices.py:2025
msgid "Three-phase" msgid "Three-phase"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:2037 netbox/extras/choices.py:53 #: netbox/dcim/choices.py:2041 netbox/extras/choices.py:53
#: netbox/netbox/preferences.py:45 netbox/netbox/preferences.py:70 #: netbox/netbox/preferences.py:45 netbox/netbox/preferences.py:70
#: netbox/templates/extras/customfield.html:78 netbox/vpn/choices.py:20 #: netbox/templates/extras/customfield.html:78 netbox/vpn/choices.py:20
#: netbox/wireless/choices.py:27 #: netbox/wireless/choices.py:27
msgid "Disabled" msgid "Disabled"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:2038 #: netbox/dcim/choices.py:2042
msgid "Faulty" msgid "Faulty"
msgstr "" msgstr ""
@ -13824,17 +13824,17 @@ msgstr ""
msgid "Not Connected" msgid "Not Connected"
msgstr "" msgstr ""
#: netbox/templates/dcim/device/attrs/ipaddress.html:4 #: netbox/templates/dcim/device/attrs/ipaddress.html:5
#: netbox/templates/virtualization/virtualmachine/attrs/ipaddress.html:4 #: netbox/templates/virtualization/virtualmachine/attrs/ipaddress.html:4
msgid "NAT for" msgid "NAT for"
msgstr "" msgstr ""
#: netbox/templates/dcim/device/attrs/ipaddress.html:6 #: netbox/templates/dcim/device/attrs/ipaddress.html:7
#: netbox/templates/virtualization/virtualmachine/attrs/ipaddress.html:6 #: netbox/templates/virtualization/virtualmachine/attrs/ipaddress.html:6
msgid "NAT" msgid "NAT"
msgstr "" msgstr ""
#: netbox/templates/dcim/device/attrs/ipaddress.html:8 #: netbox/templates/dcim/device/attrs/ipaddress.html:10
#: netbox/templates/ui/actions/copy_content.html:2 #: netbox/templates/ui/actions/copy_content.html:2
#: netbox/templates/ui/attrs/numeric.html:9 #: netbox/templates/ui/attrs/numeric.html:9
#: netbox/templates/ui/attrs/text.html:4 #: netbox/templates/ui/attrs/text.html:4
@ -16677,7 +16677,7 @@ msgstr ""
msgid "A column named {name} is already defined for table {table_name}" msgid "A column named {name} is already defined for table {table_name}"
msgstr "" msgstr ""
#: netbox/utilities/templates/builtins/customfield_value.html:30 #: netbox/utilities/templates/builtins/customfield_value.html:32
msgid "Not defined" msgid "Not defined"
msgstr "" msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ from typing import Annotated
import strawberry import strawberry
import strawberry_django import strawberry_django
from strawberry_django import DatetimeFilterLookup, FilterLookup from strawberry_django import DatetimeFilterLookup, FilterLookup, StrFilterLookup
from netbox.graphql.filters import BaseModelFilter from netbox.graphql.filters import BaseModelFilter
from users import models from users import models
@ -18,16 +18,16 @@ __all__ = (
@strawberry_django.filter_type(models.Group, lookups=True) @strawberry_django.filter_type(models.Group, lookups=True)
class GroupFilter(BaseModelFilter): class GroupFilter(BaseModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.User, lookups=True) @strawberry_django.filter_type(models.User, lookups=True)
class UserFilter(BaseModelFilter): class UserFilter(BaseModelFilter):
username: FilterLookup[str] | None = strawberry_django.filter_field() username: StrFilterLookup[str] | None = strawberry_django.filter_field()
first_name: FilterLookup[str] | None = strawberry_django.filter_field() first_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
last_name: FilterLookup[str] | None = strawberry_django.filter_field() last_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
email: FilterLookup[str] | None = strawberry_django.filter_field() email: StrFilterLookup[str] | None = strawberry_django.filter_field()
is_superuser: FilterLookup[bool] | None = strawberry_django.filter_field() is_superuser: FilterLookup[bool] | None = strawberry_django.filter_field()
is_active: FilterLookup[bool] | None = strawberry_django.filter_field() is_active: FilterLookup[bool] | None = strawberry_django.filter_field()
date_joined: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() date_joined: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
@ -37,8 +37,8 @@ class UserFilter(BaseModelFilter):
@strawberry_django.filter_type(models.Owner, lookups=True) @strawberry_django.filter_type(models.Owner, lookups=True)
class OwnerFilter(BaseModelFilter): class OwnerFilter(BaseModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()
group: Annotated['OwnerGroupFilter', strawberry.lazy('users.graphql.filters')] | None = ( group: Annotated['OwnerGroupFilter', strawberry.lazy('users.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -50,5 +50,5 @@ class OwnerFilter(BaseModelFilter):
@strawberry_django.filter_type(models.OwnerGroup, lookups=True) @strawberry_django.filter_type(models.OwnerGroup, lookups=True)
class OwnerGroupFilter(BaseModelFilter): class OwnerGroupFilter(BaseModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()

View File

@ -2,6 +2,8 @@
{% load i18n %} {% load i18n %}
{% if customfield.type == 'integer' and value is not None %} {% if customfield.type == 'integer' and value is not None %}
{{ value }} {{ value }}
{% elif customfield.type == 'decimal' and value is not None %}
{{ value }}
{% elif customfield.type == 'longtext' and value %} {% elif customfield.type == 'longtext' and value %}
{{ value|markdown }} {{ value|markdown }}
{% elif customfield.type == 'boolean' and value == True %} {% elif customfield.type == 'boolean' and value == True %}

View File

@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Annotated
import strawberry import strawberry
import strawberry_django import strawberry_django
from strawberry.scalars import ID from strawberry.scalars import ID
from strawberry_django import FilterLookup from strawberry_django import StrFilterLookup
if TYPE_CHECKING: if TYPE_CHECKING:
from .filters import VirtualMachineFilter from .filters import VirtualMachineFilter
@ -20,5 +20,5 @@ class VMComponentFilterMixin:
strawberry_django.filter_field() strawberry_django.filter_field()
) )
virtual_machine_id: ID | None = strawberry_django.filter_field() virtual_machine_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: StrFilterLookup[str] | None = strawberry_django.filter_field()

View File

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
import strawberry import strawberry
import strawberry_django import strawberry_django
from strawberry.scalars import ID from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, FilterLookup from strawberry_django import BaseFilterLookup, FilterLookup, StrFilterLookup
from dcim.graphql.filter_mixins import InterfaceBaseFilterMixin, RenderConfigFilterMixin, ScopedFilterMixin from dcim.graphql.filter_mixins import InterfaceBaseFilterMixin, RenderConfigFilterMixin, ScopedFilterMixin
from extras.graphql.filter_mixins import ConfigContextFilterMixin from extras.graphql.filter_mixins import ConfigContextFilterMixin
@ -39,7 +39,7 @@ __all__ = (
@strawberry_django.filter_type(models.Cluster, lookups=True) @strawberry_django.filter_type(models.Cluster, lookups=True)
class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter): class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
type: Annotated['ClusterTypeFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( type: Annotated['ClusterTypeFilter', strawberry.lazy('virtualization.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -77,7 +77,7 @@ class VirtualMachineFilter(
TenancyFilterMixin, TenancyFilterMixin,
PrimaryModelFilter, PrimaryModelFilter,
): ):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field() site_id: ID | None = strawberry_django.filter_field()
cluster: Annotated['ClusterFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( cluster: Annotated['ClusterFilter', strawberry.lazy('virtualization.graphql.filters')] | None = (
@ -116,7 +116,7 @@ class VirtualMachineFilter(
disk: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( disk: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
serial: FilterLookup[str] | None = strawberry_django.filter_field() serial: StrFilterLookup[str] | None = strawberry_django.filter_field()
interface_count: FilterLookup[int] | None = strawberry_django.filter_field() interface_count: FilterLookup[int] | None = strawberry_django.filter_field()
virtual_disk_count: FilterLookup[int] | None = strawberry_django.filter_field() virtual_disk_count: FilterLookup[int] | None = strawberry_django.filter_field()
interfaces: Annotated['VMInterfaceFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( interfaces: Annotated['VMInterfaceFilter', strawberry.lazy('virtualization.graphql.filters')] | None = (

View File

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
import strawberry import strawberry
import strawberry_django import strawberry_django
from strawberry.scalars import ID from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, FilterLookup from strawberry_django import BaseFilterLookup, StrFilterLookup
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
from netbox.graphql.filters import ( from netbox.graphql.filters import (
@ -63,7 +63,7 @@ class TunnelTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLo
@strawberry_django.filter_type(models.Tunnel, lookups=True) @strawberry_django.filter_type(models.Tunnel, lookups=True)
class TunnelFilter(TenancyFilterMixin, PrimaryModelFilter): class TunnelFilter(TenancyFilterMixin, PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
status: BaseFilterLookup[Annotated['TunnelStatusEnum', strawberry.lazy('vpn.graphql.enums')]] | None = ( status: BaseFilterLookup[Annotated['TunnelStatusEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -89,7 +89,7 @@ class TunnelFilter(TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.IKEProposal, lookups=True) @strawberry_django.filter_type(models.IKEProposal, lookups=True)
class IKEProposalFilter(PrimaryModelFilter): class IKEProposalFilter(PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
authentication_method: ( authentication_method: (
BaseFilterLookup[Annotated['AuthenticationMethodEnum', strawberry.lazy('vpn.graphql.enums')]] | None BaseFilterLookup[Annotated['AuthenticationMethodEnum', strawberry.lazy('vpn.graphql.enums')]] | None
) = ( ) = (
@ -118,7 +118,7 @@ class IKEProposalFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.IKEPolicy, lookups=True) @strawberry_django.filter_type(models.IKEPolicy, lookups=True)
class IKEPolicyFilter(PrimaryModelFilter): class IKEPolicyFilter(PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
version: BaseFilterLookup[Annotated['IKEVersionEnum', strawberry.lazy('vpn.graphql.enums')]] | None = ( version: BaseFilterLookup[Annotated['IKEVersionEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -128,12 +128,12 @@ class IKEPolicyFilter(PrimaryModelFilter):
proposals: Annotated['IKEProposalFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( proposals: Annotated['IKEProposalFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
preshared_key: FilterLookup[str] | None = strawberry_django.filter_field() preshared_key: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.IPSecProposal, lookups=True) @strawberry_django.filter_type(models.IPSecProposal, lookups=True)
class IPSecProposalFilter(PrimaryModelFilter): class IPSecProposalFilter(PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
encryption_algorithm: ( encryption_algorithm: (
BaseFilterLookup[Annotated['EncryptionAlgorithmEnum', strawberry.lazy('vpn.graphql.enums')]] | None BaseFilterLookup[Annotated['EncryptionAlgorithmEnum', strawberry.lazy('vpn.graphql.enums')]] | None
) = ( ) = (
@ -159,7 +159,7 @@ class IPSecProposalFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.IPSecPolicy, lookups=True) @strawberry_django.filter_type(models.IPSecPolicy, lookups=True)
class IPSecPolicyFilter(PrimaryModelFilter): class IPSecPolicyFilter(PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
proposals: Annotated['IPSecProposalFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( proposals: Annotated['IPSecProposalFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -170,7 +170,7 @@ class IPSecPolicyFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.IPSecProfile, lookups=True) @strawberry_django.filter_type(models.IPSecProfile, lookups=True)
class IPSecProfileFilter(PrimaryModelFilter): class IPSecProfileFilter(PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
mode: BaseFilterLookup[Annotated['IPSecModeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = ( mode: BaseFilterLookup[Annotated['IPSecModeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -186,8 +186,8 @@ class IPSecProfileFilter(PrimaryModelFilter):
@strawberry_django.filter_type(models.L2VPN, lookups=True) @strawberry_django.filter_type(models.L2VPN, lookups=True)
class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter): class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: StrFilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
type: BaseFilterLookup[Annotated['L2VPNTypeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = ( type: BaseFilterLookup[Annotated['L2VPNTypeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )

View File

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
import strawberry import strawberry
import strawberry_django import strawberry_django
from strawberry_django import FilterLookup from strawberry_django import StrFilterLookup
if TYPE_CHECKING: if TYPE_CHECKING:
from .enums import * from .enums import *
@ -21,4 +21,4 @@ class WirelessAuthenticationFilterMixin:
auth_cipher: Annotated['WirelessAuthCipherEnum', strawberry.lazy('wireless.graphql.enums')] | None = ( auth_cipher: Annotated['WirelessAuthCipherEnum', strawberry.lazy('wireless.graphql.enums')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
auth_psk: FilterLookup[str] | None = strawberry_django.filter_field() auth_psk: StrFilterLookup[str] | None = strawberry_django.filter_field()

View File

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
import strawberry import strawberry
import strawberry_django import strawberry_django
from strawberry.scalars import ID from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, FilterLookup from strawberry_django import BaseFilterLookup, StrFilterLookup
from dcim.graphql.filter_mixins import ScopedFilterMixin from dcim.graphql.filter_mixins import ScopedFilterMixin
from netbox.graphql.filter_mixins import DistanceFilterMixin from netbox.graphql.filter_mixins import DistanceFilterMixin
@ -38,7 +38,7 @@ class WirelessLANFilter(
TenancyFilterMixin, TenancyFilterMixin,
PrimaryModelFilter PrimaryModelFilter
): ):
ssid: FilterLookup[str] | None = strawberry_django.filter_field() ssid: StrFilterLookup[str] | None = strawberry_django.filter_field()
status: BaseFilterLookup[Annotated['WirelessLANStatusEnum', strawberry.lazy('wireless.graphql.enums')]] | None = ( status: BaseFilterLookup[Annotated['WirelessLANStatusEnum', strawberry.lazy('wireless.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@ -65,7 +65,7 @@ class WirelessLinkFilter(
strawberry_django.filter_field() strawberry_django.filter_field()
) )
interface_b_id: ID | None = strawberry_django.filter_field() interface_b_id: ID | None = strawberry_django.filter_field()
ssid: FilterLookup[str] | None = strawberry_django.filter_field() ssid: StrFilterLookup[str] | None = strawberry_django.filter_field()
status: BaseFilterLookup[Annotated['WirelessLANStatusEnum', strawberry.lazy('wireless.graphql.enums')]] | None = ( status: BaseFilterLookup[Annotated['WirelessLANStatusEnum', strawberry.lazy('wireless.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )

View File

@ -3,7 +3,7 @@
[project] [project]
name = "netbox" name = "netbox"
version = "4.5.3" version = "4.5.4"
requires-python = ">=3.12" requires-python = ">=3.12"
description = "The premier source of truth powering network automation." description = "The premier source of truth powering network automation."
readme = "README.md" readme = "README.md"

View File

@ -17,27 +17,28 @@ django-taggit==6.1.0
django-timezone-field==7.2.1 django-timezone-field==7.2.1
djangorestframework==3.16.1 djangorestframework==3.16.1
drf-spectacular==0.29.0 drf-spectacular==0.29.0
drf-spectacular-sidecar==2026.1.1 drf-spectacular-sidecar==2026.3.1
feedparser==6.0.12 feedparser==6.0.12
gunicorn==25.0.3 gunicorn==25.1.0
Jinja2==3.1.6 Jinja2==3.1.6
jsonschema==4.26.0 jsonschema==4.26.0
Markdown==3.10.2 Markdown==3.10.2
mkdocs-material==9.7.1 mkdocs==1.6.1
mkdocs-material==9.7.3
mkdocstrings==1.0.3 mkdocstrings==1.0.3
mkdocstrings-python==2.0.2 mkdocstrings-python==2.0.3
netaddr==1.3.0 netaddr==1.3.0
nh3==0.3.3 nh3==0.3.3
Pillow==12.1.1 Pillow==12.1.1
psycopg[c,pool]==3.3.2 psycopg[c,pool]==3.3.3
PyYAML==6.0.3 PyYAML==6.0.3
requests==2.32.5 requests==2.32.5
rq==2.6.1 rq==2.7.0
social-auth-app-django==5.7.0 social-auth-app-django==5.7.0
social-auth-core==4.8.5 social-auth-core==4.8.5
sorl-thumbnail==13.0.0 sorl-thumbnail==13.0.0
strawberry-graphql==0.295.0 strawberry-graphql==0.307.1
strawberry-graphql-django==0.75.0 strawberry-graphql-django==0.79.0
svgwrite==1.4.3 svgwrite==1.4.3
tablib==3.9.0 tablib==3.9.0
tzdata==2025.3 tzdata==2025.3