mirror of
https://github.com/netbox-community/netbox.git
synced 2026-03-04 21:40:05 +01:00
Compare commits
15 Commits
21025-pre-
...
21468-copy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d026d22a22 | ||
|
|
a1d82e45a0 | ||
|
|
e4f7f080b3 | ||
|
|
983ba4fda8 | ||
|
|
54462595a6 | ||
|
|
8ab752b9ad | ||
|
|
b11cc31f9d | ||
|
|
3f02309538 | ||
|
|
53345f194a | ||
|
|
139557b8dd | ||
|
|
fcf02bd8bb | ||
|
|
7d6989ff34 | ||
|
|
3b0b95c265 | ||
|
|
cdc2fb2f06 | ||
|
|
951d856c3c |
@@ -15,7 +15,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.3
|
||||
placeholder: v4.5.4
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -27,7 +27,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.3
|
||||
placeholder: v4.5.4
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
2
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
@@ -8,7 +8,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.3
|
||||
placeholder: v4.5.4
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
84
CLAUDE.md
Normal file
84
CLAUDE.md
Normal 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.
|
||||
@@ -98,6 +98,10 @@ jsonschema
|
||||
# https://python-markdown.github.io/changelog/
|
||||
Markdown
|
||||
|
||||
# MkDocs
|
||||
# https://github.com/mkdocs/mkdocs/releases
|
||||
mkdocs<2.0
|
||||
|
||||
# MkDocs Material theme (for documentation build)
|
||||
# https://squidfunk.github.io/mkdocs-material/changelog/
|
||||
mkdocs-material
|
||||
@@ -157,8 +161,7 @@ strawberry-graphql
|
||||
|
||||
# Strawberry GraphQL Django extension
|
||||
# https://github.com/strawberry-graphql/strawberry-django/releases
|
||||
# Blocked by #21450
|
||||
strawberry-graphql-django==0.75.0
|
||||
strawberry-graphql-django
|
||||
|
||||
# SVG image rendering (used for rack elevations)
|
||||
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"5gbase-t",
|
||||
"10gbase-br-d",
|
||||
"10gbase-br-u",
|
||||
"10gbase-cu",
|
||||
"10gbase-cx4",
|
||||
"10gbase-er",
|
||||
"10gbase-lr",
|
||||
@@ -367,6 +368,7 @@
|
||||
"40gbase-fr4",
|
||||
"40gbase-lr4",
|
||||
"40gbase-sr4",
|
||||
"40gbase-sr4-bd",
|
||||
"50gbase-cr",
|
||||
"50gbase-er",
|
||||
"50gbase-fr",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,34 @@
|
||||
# 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)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
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.graphql.filter_mixins import CircuitTypeFilterMixin
|
||||
@@ -62,9 +62,9 @@ class CircuitTerminationFilter(
|
||||
upstream_speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
xconnect_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
pp_info: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
xconnect_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
pp_info: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
# Cached relations
|
||||
_provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
@@ -92,7 +92,7 @@ class CircuitFilter(
|
||||
TenancyFilterMixin,
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -145,8 +145,8 @@ class CircuitGroupAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, Cha
|
||||
|
||||
@strawberry_django.filter_type(models.Provider, lookups=True)
|
||||
class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[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()
|
||||
circuits: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -159,18 +159,18 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
provider_id: ID | None = strawberry_django.filter_field()
|
||||
account: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
account: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
|
||||
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 = (
|
||||
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)
|
||||
@@ -180,7 +180,7 @@ class VirtualCircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -218,4 +218,4 @@ class VirtualCircuitTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin,
|
||||
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()
|
||||
|
||||
@@ -5,7 +5,7 @@ import strawberry
|
||||
import strawberry_django
|
||||
from django.contrib.contenttypes.models import ContentType as DjangoContentType
|
||||
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 netbox.graphql.filters import BaseModelFilter, PrimaryModelFilter
|
||||
@@ -32,23 +32,23 @@ class DataFileFilter(BaseModelFilter):
|
||||
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 = (
|
||||
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)
|
||||
class DataSourceFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
type: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
source_url: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
type: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
source_url: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: (
|
||||
BaseFilterLookup[Annotated['DataSourceStatusEnum', strawberry.lazy('core.graphql.enums')]] | 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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -62,8 +62,8 @@ class DataSourceFilter(PrimaryModelFilter):
|
||||
class ObjectChangeFilter(BaseModelFilter):
|
||||
time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
user_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
request_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
user_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
request_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
action: (
|
||||
BaseFilterLookup[Annotated['ObjectChangeActionEnum', strawberry.lazy('core.graphql.enums')]] | None
|
||||
) = strawberry_django.filter_field()
|
||||
@@ -76,7 +76,7 @@ class ObjectChangeFilter(BaseModelFilter):
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -87,5 +87,5 @@ class ObjectChangeFilter(BaseModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(DjangoContentType, lookups=True)
|
||||
class ContentTypeFilter(BaseModelFilter):
|
||||
app_label: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
model: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
app_label: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
model: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -84,6 +84,9 @@ class CablePathSerializer(serializers.ModelSerializer):
|
||||
def get_path(self, obj):
|
||||
ret = []
|
||||
for nodes in obj.path_objects:
|
||||
if not nodes:
|
||||
# The path contains an invalid object
|
||||
return []
|
||||
serializer = get_serializer_for_model(nodes[0])
|
||||
context = {'request': self.context['request']}
|
||||
ret.append(serializer(nodes, nested=True, many=True, context=context).data)
|
||||
|
||||
@@ -1386,6 +1386,7 @@ class MACAddressImportForm(PrimaryModelImportForm):
|
||||
|
||||
# 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:
|
||||
interface.snapshot()
|
||||
interface.primary_mac_address = self.instance
|
||||
interface.save()
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ def get_cable_form(a_type, b_type):
|
||||
|
||||
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)):
|
||||
|
||||
# Device component
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup, StrFilterLookup
|
||||
|
||||
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_id: ID | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
label: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
label: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -96,9 +96,9 @@ class ComponentTemplateFilterMixin:
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
device_type_id: ID | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
label: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
label: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -4,7 +4,7 @@ import strawberry
|
||||
import strawberry_django
|
||||
from django.db.models import Q
|
||||
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.constants import *
|
||||
@@ -114,7 +114,7 @@ class CableFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
status: BaseFilterLookup[Annotated['LinkStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -141,6 +141,20 @@ class CableTerminationFilter(ChangeLoggedModelFilter):
|
||||
)
|
||||
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)
|
||||
class ConsolePortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
|
||||
@@ -196,9 +210,9 @@ class DeviceFilter(
|
||||
platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
serial: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
serial: StrFilterLookup[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_id: ID | None = strawberry_django.filter_field()
|
||||
location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
@@ -325,7 +339,7 @@ class InventoryItemTemplateFilter(ComponentTemplateFilterMixin, ChangeLoggedMode
|
||||
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)
|
||||
@@ -342,13 +356,13 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryMod
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
manufacturer_id: ID | None = strawberry_django.filter_field()
|
||||
model: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
model: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
default_platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | 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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -465,7 +479,7 @@ class PortTemplateMappingFilter(BaseModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.MACAddress, lookups=True)
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -511,7 +525,7 @@ class InterfaceFilter(
|
||||
duplex: BaseFilterLookup[Annotated['InterfaceDuplexEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -631,9 +645,9 @@ class InventoryItemFilter(ComponentModelFilterMixin, NetBoxModelFilter):
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
manufacturer_id: ID | None = strawberry_django.filter_field()
|
||||
part_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
serial: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
part_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
serial: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asset_tag: StrFilterLookup[str] | 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 = (
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -680,8 +694,8 @@ class ModuleFilter(ConfigContextFilterMixin, PrimaryModelFilter):
|
||||
status: BaseFilterLookup[Annotated['ModuleStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
serial: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
serial: StrFilterLookup[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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -720,17 +734,17 @@ class ModuleBayFilter(ModularComponentFilterMixin, NetBoxModelFilter):
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
@@ -743,8 +757,8 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryMod
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
profile_id: ID | None = strawberry_django.filter_field()
|
||||
model: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
part_number: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
model: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
part_number: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
instances: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -804,7 +818,7 @@ class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryM
|
||||
power_panel_id: ID | 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()
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -875,7 +889,7 @@ class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryMo
|
||||
location_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | 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.PowerPort, lookups=True)
|
||||
@@ -913,8 +927,8 @@ class RackTypeFilter(ImageAttachmentFilterMixin, RackFilterMixin, WeightFilterMi
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
manufacturer_id: ID | None = strawberry_django.filter_field()
|
||||
model: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
model: StrFilterLookup[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()
|
||||
rack_count: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -935,8 +949,8 @@ class RackFilter(
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
rack_type_id: ID | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
facility_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[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_id: ID | None = strawberry_django.filter_field()
|
||||
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_id: ID | None = strawberry_django.filter_field()
|
||||
serial: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
serial: StrFilterLookup[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 = (
|
||||
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_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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -1020,8 +1034,8 @@ class RegionFilter(ContactFilterMixin, NestedGroupModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.Site, lookups=True)
|
||||
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: BaseFilterLookup[Annotated['SiteStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -1035,11 +1049,11 @@ class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
|
||||
group_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
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()
|
||||
time_zone: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
physical_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
shipping_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
time_zone: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
physical_address: StrFilterLookup[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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -1068,8 +1082,8 @@ class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilter):
|
||||
class VirtualChassisFilter(PrimaryModelFilter):
|
||||
master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
master_id: ID | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
domain: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
domain: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
members: (
|
||||
Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
@@ -1080,7 +1094,7 @@ class VirtualChassisFilter(PrimaryModelFilter):
|
||||
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
device_id: ID | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: (
|
||||
BaseFilterLookup[Annotated['VirtualDeviceContextStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None
|
||||
) = (
|
||||
@@ -1097,7 +1111,7 @@ class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
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: (
|
||||
Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
|
||||
@@ -305,6 +305,50 @@ class Cable(PrimaryModel):
|
||||
except UnsupportedCablePath as 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):
|
||||
data = serialize_object(self, exclude=exclude or [])
|
||||
|
||||
|
||||
@@ -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(
|
||||
APIViewTestCases.GetObjectViewTestCase,
|
||||
|
||||
@@ -2733,6 +2733,7 @@ class DeviceBulkImportView(generic.BulkImportView):
|
||||
# For child devices, save the reverse relation to the parent device bay
|
||||
if parent_bay:
|
||||
device_bay = parent_bay
|
||||
device_bay.snapshot()
|
||||
device_bay.installed_device = obj
|
||||
device_bay.save()
|
||||
|
||||
@@ -3912,19 +3913,6 @@ class CableEditView(generic.ObjectEditView):
|
||||
|
||||
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')
|
||||
class CableDeleteView(generic.ObjectDeleteView):
|
||||
@@ -4099,6 +4087,7 @@ class VirtualChassisEditView(ObjectPermissionRequiredMixin, GetReturnURLMixin, V
|
||||
members = formset.save(commit=False)
|
||||
devices = Device.objects.filter(pk__in=[m.pk for m in members])
|
||||
for device in devices:
|
||||
device.snapshot()
|
||||
device.vc_position = None
|
||||
device.save()
|
||||
for member in members:
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup, StrFilterLookup
|
||||
|
||||
from extras import models
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
|
||||
@@ -50,11 +50,11 @@ __all__ = (
|
||||
|
||||
@strawberry_django.filter_type(models.ConfigContext, lookups=True)
|
||||
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 = (
|
||||
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()
|
||||
regions: Annotated['RegionFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -107,22 +107,22 @@ class ConfigContextFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.ConfigContextProfile, lookups=True)
|
||||
class ConfigContextProfileFilter(SyncedDataFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[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()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ConfigTemplate, lookups=True)
|
||||
class ConfigTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
template_code: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
mime_type: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_extension: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
mime_type: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_extension: StrFilterLookup[str] | 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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
label: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
label: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
required: 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 = (
|
||||
@@ -166,7 +166,7 @@ class CustomFieldFilter(ChangeLoggedModelFilter):
|
||||
validation_maximum: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -182,13 +182,13 @@ class CustomFieldFilter(ChangeLoggedModelFilter):
|
||||
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)
|
||||
class CustomFieldChoiceSetFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
base_choices: (
|
||||
BaseFilterLookup[Annotated['CustomFieldChoiceSetBaseEnum', strawberry.lazy('extras.graphql.enums')]] | None
|
||||
) = (
|
||||
@@ -202,14 +202,14 @@ class CustomFieldChoiceSetFilter(ChangeLoggedModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.CustomLink, lookups=True)
|
||||
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()
|
||||
link_text: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
link_url: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
link_text: StrFilterLookup[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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
group_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
button_class: (
|
||||
BaseFilterLookup[Annotated['CustomLinkButtonClassEnum', strawberry.lazy('extras.graphql.enums')]] | None
|
||||
) = (
|
||||
@@ -220,15 +220,15 @@ class CustomLinkFilter(ChangeLoggedModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.ExportTemplate, lookups=True)
|
||||
class ExportTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
template_code: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
mime_type: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_extension: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
mime_type: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_extension: StrFilterLookup[str] | 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 = (
|
||||
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)
|
||||
@@ -260,22 +260,22 @@ class JournalEntryFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedM
|
||||
kind: BaseFilterLookup[Annotated['JournalEntryKindEnum', strawberry.lazy('extras.graphql.enums')]] | 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.NotificationGroup, lookups=True)
|
||||
class NotificationGroupFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[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()
|
||||
users: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.SavedFilter, lookups=True)
|
||||
class SavedFilterFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[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_id: ID | None = strawberry_django.filter_field()
|
||||
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)
|
||||
class TableConfigFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[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_id: ID | None = strawberry_django.filter_field()
|
||||
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)
|
||||
class TagFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | 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.Webhook, lookups=True)
|
||||
class WebhookFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
payload_url: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
payload_url: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
http_method: (
|
||||
BaseFilterLookup[Annotated['WebhookHttpMethodEnum', strawberry.lazy('extras.graphql.enums')]] | None
|
||||
) = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
http_content_type: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
additional_headers: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
body_template: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
secret: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
http_content_type: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
additional_headers: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
body_template: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
secret: StrFilterLookup[str] | 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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -334,8 +334,8 @@ class WebhookFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelF
|
||||
|
||||
@strawberry_django.filter_type(models.EventRule, lookups=True)
|
||||
class EventRuleFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -346,10 +346,10 @@ class EventRuleFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedMode
|
||||
action_type: BaseFilterLookup[Annotated['EventRuleActionEnum', strawberry.lazy('extras.graphql.enums')]] | None = (
|
||||
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_id: ID | None = strawberry_django.filter_field()
|
||||
action_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -424,6 +424,7 @@ class IPAddressImportForm(PrimaryModelImportForm):
|
||||
# Set as primary for device/VM
|
||||
if self.cleaned_data.get('is_primary') is not None:
|
||||
parent = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
|
||||
parent.snapshot()
|
||||
if self.instance.address.version == 4:
|
||||
parent.primary_ip4 = ipaddress if self.cleaned_data.get('is_primary') else None
|
||||
elif self.instance.address.version == 6:
|
||||
@@ -433,6 +434,7 @@ class IPAddressImportForm(PrimaryModelImportForm):
|
||||
# Set as OOB for device
|
||||
if self.cleaned_data.get('is_oob') is not None:
|
||||
parent = self.cleaned_data.get('device')
|
||||
parent.snapshot()
|
||||
parent.oob_ip = ipaddress if self.cleaned_data.get('is_oob') else None
|
||||
parent.save()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import strawberry_django
|
||||
from django.db.models import Q
|
||||
from netaddr.core import AddrFormatError
|
||||
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.models import Device
|
||||
@@ -70,8 +70,8 @@ class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.ASNRange, lookups=True)
|
||||
class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[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_id: ID | None = strawberry_django.filter_field()
|
||||
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)
|
||||
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_id: ID | 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 = (
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
auth_type: BaseFilterLookup[Annotated['FHRPGroupAuthTypeEnum', strawberry.lazy('ipam.graphql.enums')]] | None = (
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -138,7 +138,7 @@ class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
|
||||
interface_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -174,7 +174,7 @@ class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.IPAddress, lookups=True)
|
||||
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_id: ID | None = strawberry_django.filter_field()
|
||||
status: BaseFilterLookup[Annotated['IPAddressStatusEnum', strawberry.lazy('ipam.graphql.enums')]] | None = (
|
||||
@@ -195,7 +195,7 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
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()
|
||||
def assigned(self, value: bool, prefix) -> Q:
|
||||
@@ -225,8 +225,8 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
|
||||
@strawberry_django.filter_type(models.IPRange, lookups=True)
|
||||
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
start_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
end_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
start_address: StrFilterLookup[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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -279,7 +279,7 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.Prefix, lookups=True)
|
||||
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_id: ID | 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)
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -345,7 +345,7 @@ class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.Service, lookups=True)
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -357,7 +357,7 @@ class ServiceFilter(ContactFilterMixin, ServiceFilterMixin, PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.ServiceTemplate, lookups=True)
|
||||
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)
|
||||
@@ -371,7 +371,7 @@ class VLANFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
vid: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -401,7 +401,7 @@ class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.VLANTranslationPolicy, lookups=True)
|
||||
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)
|
||||
@@ -410,7 +410,7 @@ class VLANTranslationRuleFilter(NetBoxModelFilter):
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -421,8 +421,8 @@ class VLANTranslationRuleFilter(NetBoxModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.VRF, lookups=True)
|
||||
class VRFFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rd: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rd: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
enforce_unique: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
import_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
||||
@@ -432,9 +432,11 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
])
|
||||
available_ips = prefix - child_ips - child_ranges
|
||||
|
||||
# IPv6 /127's, pool, or IPv4 /31-/32 sets are fully usable
|
||||
if (self.family == 6 and self.prefix.prefixlen >= 127) or self.is_pool or (
|
||||
self.family == 4 and self.prefix.prefixlen >= 31
|
||||
# Pool, IPv4 /31-/32 or IPv6 /127-/128 sets are fully usable
|
||||
if (
|
||||
self.is_pool
|
||||
or (self.family == 4 and self.prefix.prefixlen >= 31)
|
||||
or (self.family == 6 and self.prefix.prefixlen >= 127)
|
||||
):
|
||||
return available_ips
|
||||
|
||||
|
||||
@@ -39,3 +39,132 @@ class AnnotatedIPAddressTableTest(TestCase):
|
||||
|
||||
iprange_checkbox_count = html.count(f'name="pk" value="{self.ip_range.pk}"')
|
||||
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)
|
||||
|
||||
@@ -78,12 +78,21 @@ def annotate_ip_space(prefix):
|
||||
records = sorted(records, key=lambda x: x[0])
|
||||
|
||||
# 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
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.prefix.first + 1)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.prefix.last - 1)
|
||||
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)
|
||||
|
||||
if not records:
|
||||
|
||||
@@ -15,6 +15,7 @@ from strawberry_django import (
|
||||
DatetimeFilterLookup,
|
||||
FilterLookup,
|
||||
RangeLookup,
|
||||
StrFilterLookup,
|
||||
TimeFilterLookup,
|
||||
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.')
|
||||
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_comparison_lookup: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
|
||||
float_range_lookup: RangeLookup[float] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Annotated, TypeVar
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup
|
||||
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup, StrFilterLookup
|
||||
|
||||
__all__ = (
|
||||
'DistanceFilterMixin',
|
||||
@@ -48,7 +48,7 @@ class SyncedDataFilterMixin:
|
||||
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()
|
||||
data_synced: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import strawberry_django
|
||||
from strawberry import ID
|
||||
from strawberry_django import ComparisonFilterLookup, FilterLookup
|
||||
from strawberry_django import ComparisonFilterLookup, StrFilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import ChangeLoggingMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, JournalEntriesFilterMixin, TagsFilterMixin
|
||||
@@ -42,21 +42,21 @@ class NetBoxModelFilter(
|
||||
|
||||
@dataclass
|
||||
class NestedGroupModelFilter(NetBoxModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
parent_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrganizationalModelFilter(NetBoxModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrimaryModelFilter(NetBoxModelFilter):
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
2
netbox/project-static/dist/netbox.css
vendored
2
netbox/project-static/dist/netbox.css
vendored
File diff suppressed because one or more lines are too long
8
netbox/project-static/dist/netbox.js
vendored
8
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/netbox.js.map
vendored
6
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -32,19 +32,19 @@
|
||||
"htmx.org": "2.0.8",
|
||||
"query-string": "9.3.1",
|
||||
"sass": "1.97.3",
|
||||
"tom-select": "2.4.3",
|
||||
"tom-select": "2.5.2",
|
||||
"typeface-inter": "3.18.1",
|
||||
"typeface-roboto-mono": "1.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.0.2",
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/eslintrc": "^3.3.4",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/cookie": "^1.0.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
||||
"@typescript-eslint/parser": "^8.56.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"esbuild": "^0.27.3",
|
||||
"esbuild-sass-plugin": "^3.6.0",
|
||||
"eslint": "^9.39.2",
|
||||
@@ -52,12 +52,15 @@
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"globals": "^17.3.0",
|
||||
"globals": "^17.4.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/bootstrap/**/@popperjs/core": "^2.11.6"
|
||||
"@types/bootstrap/**/@popperjs/core": "^2.11.6",
|
||||
"eslint/**/minimatch": "^3.1.3",
|
||||
"eslint-plugin-import/**/minimatch": "^3.1.3",
|
||||
"**/markdown-it": "^14.1.1"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { RecursivePartial, TomOption, TomSettings } from 'tom-select/dist/types/types';
|
||||
import { TomInput } from 'tom-select/dist/cjs/types/core';
|
||||
import type { RecursivePartial, TomOption, TomSettings, TomInput } from 'tom-select/dist/cjs/types';
|
||||
import { addClasses } from 'tom-select/src/vanilla.ts';
|
||||
import queryString from 'query-string';
|
||||
import TomSelect from 'tom-select';
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
dependencies:
|
||||
"@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"
|
||||
resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz"
|
||||
integrity sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==
|
||||
@@ -225,6 +225,21 @@
|
||||
minimatch "^3.1.2"
|
||||
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":
|
||||
version "9.39.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599"
|
||||
@@ -935,100 +950,100 @@
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz#5aec3db807a6b8437ea5d5ebf7bd16b4119aba8d"
|
||||
integrity sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==
|
||||
"@typescript-eslint/eslint-plugin@^8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz#b1ce606d87221daec571e293009675992f0aae76"
|
||||
integrity sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@typescript-eslint/scope-manager" "8.56.0"
|
||||
"@typescript-eslint/type-utils" "8.56.0"
|
||||
"@typescript-eslint/utils" "8.56.0"
|
||||
"@typescript-eslint/visitor-keys" "8.56.0"
|
||||
"@typescript-eslint/scope-manager" "8.56.1"
|
||||
"@typescript-eslint/type-utils" "8.56.1"
|
||||
"@typescript-eslint/utils" "8.56.1"
|
||||
"@typescript-eslint/visitor-keys" "8.56.1"
|
||||
ignore "^7.0.5"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.4.0"
|
||||
|
||||
"@typescript-eslint/parser@^8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.56.0.tgz#8ecff1678b8b1a742d29c446ccf5eeea7f971d72"
|
||||
integrity sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==
|
||||
"@typescript-eslint/parser@^8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.56.1.tgz#21d13b3d456ffb08614c1d68bb9a4f8d9237cdc7"
|
||||
integrity sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.56.0"
|
||||
"@typescript-eslint/types" "8.56.0"
|
||||
"@typescript-eslint/typescript-estree" "8.56.0"
|
||||
"@typescript-eslint/visitor-keys" "8.56.0"
|
||||
"@typescript-eslint/scope-manager" "8.56.1"
|
||||
"@typescript-eslint/types" "8.56.1"
|
||||
"@typescript-eslint/typescript-estree" "8.56.1"
|
||||
"@typescript-eslint/visitor-keys" "8.56.1"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.56.0.tgz#bb8562fecd8f7922e676fc6a1189c20dd7991d73"
|
||||
integrity sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==
|
||||
"@typescript-eslint/project-service@8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.56.1.tgz#65c8d645f028b927bfc4928593b54e2ecd809244"
|
||||
integrity sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.56.0"
|
||||
"@typescript-eslint/types" "^8.56.0"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.56.1"
|
||||
"@typescript-eslint/types" "^8.56.1"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz#604030a4c6433df3728effdd441d47f45a86edb4"
|
||||
integrity sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==
|
||||
"@typescript-eslint/scope-manager@8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz#254df93b5789a871351335dd23e20bc164060f24"
|
||||
integrity sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.56.0"
|
||||
"@typescript-eslint/visitor-keys" "8.56.0"
|
||||
"@typescript-eslint/types" "8.56.1"
|
||||
"@typescript-eslint/visitor-keys" "8.56.1"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.56.0", "@typescript-eslint/tsconfig-utils@^8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz#2538ce83cbc376e685487960cbb24b65fe2abc4e"
|
||||
integrity sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==
|
||||
"@typescript-eslint/tsconfig-utils@8.56.1", "@typescript-eslint/tsconfig-utils@^8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz#1afa830b0fada5865ddcabdc993b790114a879b7"
|
||||
integrity sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==
|
||||
|
||||
"@typescript-eslint/type-utils@8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz#72b4edc1fc73988998f1632b3ec99c2a66eaac6e"
|
||||
integrity sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==
|
||||
"@typescript-eslint/type-utils@8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz#7a6c4fabf225d674644931e004302cbbdd2f2e24"
|
||||
integrity sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.56.0"
|
||||
"@typescript-eslint/typescript-estree" "8.56.0"
|
||||
"@typescript-eslint/utils" "8.56.0"
|
||||
"@typescript-eslint/types" "8.56.1"
|
||||
"@typescript-eslint/typescript-estree" "8.56.1"
|
||||
"@typescript-eslint/utils" "8.56.1"
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.4.0"
|
||||
|
||||
"@typescript-eslint/types@8.56.0", "@typescript-eslint/types@^8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.56.0.tgz#a2444011b9a98ca13d70411d2cbfed5443b3526a"
|
||||
integrity sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==
|
||||
"@typescript-eslint/types@8.56.1", "@typescript-eslint/types@^8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.56.1.tgz#975e5942bf54895291337c91b9191f6eb0632ab9"
|
||||
integrity sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz#fadbc74c14c5bac947db04980ff58bb178701c2e"
|
||||
integrity sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==
|
||||
"@typescript-eslint/typescript-estree@8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz#3b9e57d8129a860c50864c42188f761bdef3eab0"
|
||||
integrity sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.56.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.56.0"
|
||||
"@typescript-eslint/types" "8.56.0"
|
||||
"@typescript-eslint/visitor-keys" "8.56.0"
|
||||
"@typescript-eslint/project-service" "8.56.1"
|
||||
"@typescript-eslint/tsconfig-utils" "8.56.1"
|
||||
"@typescript-eslint/types" "8.56.1"
|
||||
"@typescript-eslint/visitor-keys" "8.56.1"
|
||||
debug "^4.4.3"
|
||||
minimatch "^9.0.5"
|
||||
minimatch "^10.2.2"
|
||||
semver "^7.7.3"
|
||||
tinyglobby "^0.2.15"
|
||||
ts-api-utils "^2.4.0"
|
||||
|
||||
"@typescript-eslint/utils@8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.56.0.tgz#063ce6f702ec603de1b83ee795ed5e877d6f7841"
|
||||
integrity sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==
|
||||
"@typescript-eslint/utils@8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.56.1.tgz#5a86acaf9f1b4c4a85a42effb217f73059f6deb7"
|
||||
integrity sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.9.1"
|
||||
"@typescript-eslint/scope-manager" "8.56.0"
|
||||
"@typescript-eslint/types" "8.56.0"
|
||||
"@typescript-eslint/typescript-estree" "8.56.0"
|
||||
"@typescript-eslint/scope-manager" "8.56.1"
|
||||
"@typescript-eslint/types" "8.56.1"
|
||||
"@typescript-eslint/typescript-estree" "8.56.1"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz#7d6592ab001827d3ce052155edf7ecad19688d7d"
|
||||
integrity sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==
|
||||
"@typescript-eslint/visitor-keys@8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz#50e03475c33a42d123dc99e63acf1841c0231f87"
|
||||
integrity sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.56.0"
|
||||
"@typescript-eslint/types" "8.56.1"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@unrs/resolver-binding-android-arm-eabi@1.11.1":
|
||||
@@ -1148,6 +1163,16 @@ ajv@^6.12.4:
|
||||
json-schema-traverse "^0.4.1"
|
||||
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:
|
||||
version "4.3.0"
|
||||
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"
|
||||
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:
|
||||
version "5.3.7"
|
||||
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"
|
||||
concat-map "0.0.1"
|
||||
|
||||
brace-expansion@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz"
|
||||
integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
|
||||
brace-expansion@^5.0.2:
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.4.tgz#614daaecd0a688f660bbbc909a8748c3d80d4336"
|
||||
integrity sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
balanced-match "^4.0.2"
|
||||
|
||||
braces@^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"
|
||||
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
|
||||
|
||||
globals@^17.3.0:
|
||||
version "17.3.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-17.3.0.tgz#8b96544c2fa91afada02747cc9731c002a96f3b9"
|
||||
integrity sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==
|
||||
globals@^17.4.0:
|
||||
version "17.4.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-17.4.0.tgz#33d7d297ed1536b388a0e2f4bcd0ff19c8ff91b5"
|
||||
integrity sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==
|
||||
|
||||
globalthis@^1.0.3, globalthis@^1.0.4:
|
||||
version "1.0.4"
|
||||
@@ -2749,10 +2779,10 @@ loose-envify@^1.1.0:
|
||||
dependencies:
|
||||
js-tokens "^3.0.0 || ^4.0.0"
|
||||
|
||||
markdown-it@^14.1.0:
|
||||
version "14.1.0"
|
||||
resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz"
|
||||
integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==
|
||||
markdown-it@^14.1.0, markdown-it@^14.1.1:
|
||||
version "14.1.1"
|
||||
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.1.tgz#856f90b66fc39ae70affd25c1b18b581d7deee1f"
|
||||
integrity sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
entities "^4.4.0"
|
||||
@@ -2784,20 +2814,20 @@ micromatch@^4.0.5:
|
||||
braces "^3.0.3"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
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.3:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
|
||||
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^9.0.5:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz"
|
||||
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz"
|
||||
@@ -3455,10 +3485,10 @@ toggle-selection@^1.0.6:
|
||||
resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz"
|
||||
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
|
||||
|
||||
tom-select@2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.4.3.tgz#1daa4131cd317de691f39eb5bf41148265986c1f"
|
||||
integrity sha512-MFFrMxP1bpnAMPbdvPCZk0KwYxLqhYZso39torcdoefeV/NThNyDu8dV96/INJ5XQVTL3O55+GqQ78Pkj5oCfw==
|
||||
tom-select@2.5.2:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.5.2.tgz#77dd4bc780b1ea72905337b24f04ce19dc6d2ca1"
|
||||
integrity sha512-VAlGj5MBWVLMJje2NwA3XSmxa7CUFpp1tdzFZ8wymCkcLeP0NwF4ARmSuUK4BWbmSN1fETlSazWkMIxEpP4GdQ==
|
||||
dependencies:
|
||||
"@orchidjs/sifter" "^1.1.0"
|
||||
"@orchidjs/unicode-variants" "^1.1.2"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version: "4.5.3"
|
||||
version: "4.5.4"
|
||||
edition: "Community"
|
||||
published: "2026-02-17"
|
||||
published: "2026-03-03"
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
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 netbox.graphql.filters import (
|
||||
@@ -60,8 +60,8 @@ __all__ = (
|
||||
|
||||
@strawberry_django.filter_type(models.Tenant, lookups=True)
|
||||
class TenantFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -153,12 +153,12 @@ class TenantGroupFilter(OrganizationalModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.Contact, lookups=True)
|
||||
class ContactFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
title: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
phone: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
email: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
link: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
title: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
phone: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
email: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
address: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
link: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
groups: Annotated['ContactGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-28 05:11+0000\n"
|
||||
"POT-Creation-Date: 2026-03-04 05:17+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -172,8 +172,8 @@ msgstr ""
|
||||
#: netbox/dcim/forms/bulk_edit.py:323 netbox/dcim/forms/bulk_edit.py:673
|
||||
#: netbox/dcim/forms/bulk_edit.py:860 netbox/dcim/forms/bulk_import.py:146
|
||||
#: netbox/dcim/forms/bulk_import.py:247 netbox/dcim/forms/bulk_import.py:349
|
||||
#: netbox/dcim/forms/bulk_import.py:640 netbox/dcim/forms/bulk_import.py:1608
|
||||
#: netbox/dcim/forms/bulk_import.py:1636 netbox/dcim/forms/filtersets.py:106
|
||||
#: netbox/dcim/forms/bulk_import.py:640 netbox/dcim/forms/bulk_import.py:1609
|
||||
#: netbox/dcim/forms/bulk_import.py:1637 netbox/dcim/forms/filtersets.py:106
|
||||
#: netbox/dcim/forms/filtersets.py:256 netbox/dcim/forms/filtersets.py:379
|
||||
#: netbox/dcim/forms/filtersets.py:483 netbox/dcim/forms/filtersets.py:855
|
||||
#: netbox/dcim/forms/filtersets.py:1073 netbox/dcim/forms/filtersets.py:1147
|
||||
@@ -187,7 +187,7 @@ msgstr ""
|
||||
#: netbox/dcim/tables/power.py:90 netbox/dcim/tables/racks.py:111
|
||||
#: netbox/dcim/tables/racks.py:194 netbox/dcim/tables/sites.py:102
|
||||
#: netbox/extras/filtersets.py:707 netbox/ipam/forms/bulk_edit.py:414
|
||||
#: netbox/ipam/forms/bulk_import.py:487 netbox/ipam/forms/filtersets.py:171
|
||||
#: netbox/ipam/forms/bulk_import.py:489 netbox/ipam/forms/filtersets.py:171
|
||||
#: netbox/ipam/forms/filtersets.py:251 netbox/ipam/forms/filtersets.py:476
|
||||
#: netbox/ipam/forms/filtersets.py:573 netbox/ipam/forms/model_forms.py:663
|
||||
#: netbox/ipam/tables/vlans.py:92 netbox/ipam/tables/vlans.py:214
|
||||
@@ -326,7 +326,7 @@ msgstr ""
|
||||
#: netbox/circuits/forms/model_forms.py:162
|
||||
#: netbox/circuits/forms/model_forms.py:260
|
||||
#: netbox/circuits/tables/circuits.py:103
|
||||
#: netbox/circuits/tables/circuits.py:199 netbox/dcim/forms/connections.py:79
|
||||
#: netbox/circuits/tables/circuits.py:199 netbox/dcim/forms/connections.py:83
|
||||
#: netbox/templates/circuits/circuit.html:15
|
||||
#: netbox/templates/circuits/circuitgroupassignment.html:30
|
||||
#: netbox/templates/circuits/circuittermination.html:19
|
||||
@@ -463,7 +463,7 @@ msgstr ""
|
||||
#: netbox/dcim/forms/bulk_edit.py:605 netbox/dcim/forms/bulk_edit.py:803
|
||||
#: netbox/dcim/forms/bulk_edit.py:1057 netbox/dcim/forms/bulk_edit.py:1156
|
||||
#: netbox/dcim/forms/bulk_edit.py:1183 netbox/dcim/forms/bulk_edit.py:1717
|
||||
#: netbox/dcim/forms/bulk_import.py:1483 netbox/dcim/forms/filtersets.py:1220
|
||||
#: netbox/dcim/forms/bulk_import.py:1484 netbox/dcim/forms/filtersets.py:1220
|
||||
#: netbox/dcim/forms/filtersets.py:1545 netbox/dcim/forms/filtersets.py:1761
|
||||
#: netbox/dcim/forms/filtersets.py:1780 netbox/dcim/forms/filtersets.py:1804
|
||||
#: netbox/dcim/forms/filtersets.py:1823 netbox/dcim/tables/devices.py:786
|
||||
@@ -500,8 +500,8 @@ msgstr ""
|
||||
#: netbox/dcim/forms/bulk_import.py:813 netbox/dcim/forms/bulk_import.py:839
|
||||
#: netbox/dcim/forms/bulk_import.py:865 netbox/dcim/forms/bulk_import.py:886
|
||||
#: netbox/dcim/forms/bulk_import.py:972 netbox/dcim/forms/bulk_import.py:1101
|
||||
#: netbox/dcim/forms/bulk_import.py:1120 netbox/dcim/forms/bulk_import.py:1464
|
||||
#: netbox/dcim/forms/bulk_import.py:1673 netbox/dcim/forms/filtersets.py:1104
|
||||
#: netbox/dcim/forms/bulk_import.py:1120 netbox/dcim/forms/bulk_import.py:1465
|
||||
#: netbox/dcim/forms/bulk_import.py:1674 netbox/dcim/forms/filtersets.py:1104
|
||||
#: netbox/dcim/forms/filtersets.py:1205 netbox/dcim/forms/filtersets.py:1333
|
||||
#: netbox/dcim/forms/filtersets.py:1424 netbox/dcim/forms/filtersets.py:1444
|
||||
#: netbox/dcim/forms/filtersets.py:1464 netbox/dcim/forms/filtersets.py:1484
|
||||
@@ -572,8 +572,8 @@ msgstr ""
|
||||
#: netbox/dcim/forms/bulk_import.py:103 netbox/dcim/forms/bulk_import.py:162
|
||||
#: netbox/dcim/forms/bulk_import.py:265 netbox/dcim/forms/bulk_import.py:374
|
||||
#: netbox/dcim/forms/bulk_import.py:605 netbox/dcim/forms/bulk_import.py:765
|
||||
#: netbox/dcim/forms/bulk_import.py:1230 netbox/dcim/forms/bulk_import.py:1452
|
||||
#: netbox/dcim/forms/bulk_import.py:1668 netbox/dcim/forms/bulk_import.py:1731
|
||||
#: netbox/dcim/forms/bulk_import.py:1230 netbox/dcim/forms/bulk_import.py:1453
|
||||
#: netbox/dcim/forms/bulk_import.py:1669 netbox/dcim/forms/bulk_import.py:1732
|
||||
#: netbox/dcim/forms/filtersets.py:208 netbox/dcim/forms/filtersets.py:268
|
||||
#: netbox/dcim/forms/filtersets.py:396 netbox/dcim/forms/filtersets.py:504
|
||||
#: netbox/dcim/forms/filtersets.py:901 netbox/dcim/forms/filtersets.py:1024
|
||||
@@ -588,7 +588,7 @@ msgstr ""
|
||||
#: netbox/ipam/forms/bulk_edit.py:204 netbox/ipam/forms/bulk_edit.py:248
|
||||
#: netbox/ipam/forms/bulk_edit.py:295 netbox/ipam/forms/bulk_edit.py:436
|
||||
#: netbox/ipam/forms/bulk_import.py:198 netbox/ipam/forms/bulk_import.py:262
|
||||
#: netbox/ipam/forms/bulk_import.py:298 netbox/ipam/forms/bulk_import.py:508
|
||||
#: netbox/ipam/forms/bulk_import.py:298 netbox/ipam/forms/bulk_import.py:510
|
||||
#: netbox/ipam/forms/filtersets.py:234 netbox/ipam/forms/filtersets.py:313
|
||||
#: netbox/ipam/forms/filtersets.py:396 netbox/ipam/forms/filtersets.py:585
|
||||
#: netbox/ipam/forms/model_forms.py:503 netbox/ipam/tables/ip.py:182
|
||||
@@ -647,8 +647,8 @@ msgstr ""
|
||||
#: netbox/dcim/forms/bulk_edit.py:793 netbox/dcim/forms/bulk_edit.py:1740
|
||||
#: netbox/dcim/forms/bulk_import.py:122 netbox/dcim/forms/bulk_import.py:167
|
||||
#: netbox/dcim/forms/bulk_import.py:258 netbox/dcim/forms/bulk_import.py:379
|
||||
#: netbox/dcim/forms/bulk_import.py:579 netbox/dcim/forms/bulk_import.py:1470
|
||||
#: netbox/dcim/forms/bulk_import.py:1724 netbox/dcim/forms/filtersets.py:143
|
||||
#: netbox/dcim/forms/bulk_import.py:579 netbox/dcim/forms/bulk_import.py:1471
|
||||
#: netbox/dcim/forms/bulk_import.py:1725 netbox/dcim/forms/filtersets.py:143
|
||||
#: netbox/dcim/forms/filtersets.py:202 netbox/dcim/forms/filtersets.py:235
|
||||
#: netbox/dcim/forms/filtersets.py:363 netbox/dcim/forms/filtersets.py:442
|
||||
#: netbox/dcim/forms/filtersets.py:463 netbox/dcim/forms/filtersets.py:823
|
||||
@@ -665,7 +665,7 @@ msgstr ""
|
||||
#: netbox/ipam/forms/bulk_import.py:102 netbox/ipam/forms/bulk_import.py:122
|
||||
#: netbox/ipam/forms/bulk_import.py:142 netbox/ipam/forms/bulk_import.py:170
|
||||
#: netbox/ipam/forms/bulk_import.py:255 netbox/ipam/forms/bulk_import.py:291
|
||||
#: netbox/ipam/forms/bulk_import.py:468 netbox/ipam/forms/bulk_import.py:501
|
||||
#: netbox/ipam/forms/bulk_import.py:470 netbox/ipam/forms/bulk_import.py:503
|
||||
#: netbox/ipam/forms/filtersets.py:50 netbox/ipam/forms/filtersets.py:71
|
||||
#: netbox/ipam/forms/filtersets.py:109 netbox/ipam/forms/filtersets.py:131
|
||||
#: netbox/ipam/forms/filtersets.py:155 netbox/ipam/forms/filtersets.py:196
|
||||
@@ -922,7 +922,7 @@ msgstr ""
|
||||
|
||||
#: netbox/circuits/forms/bulk_edit.py:192
|
||||
#: netbox/circuits/forms/model_forms.py:170
|
||||
#: netbox/dcim/forms/bulk_import.py:1418 netbox/dcim/forms/bulk_import.py:1443
|
||||
#: netbox/dcim/forms/bulk_import.py:1419 netbox/dcim/forms/bulk_import.py:1444
|
||||
msgid "Termination type"
|
||||
msgstr ""
|
||||
|
||||
@@ -1008,7 +1008,7 @@ msgstr ""
|
||||
#: netbox/ipam/forms/bulk_edit.py:253 netbox/ipam/forms/bulk_edit.py:300
|
||||
#: netbox/ipam/forms/bulk_edit.py:441 netbox/ipam/forms/bulk_import.py:203
|
||||
#: netbox/ipam/forms/bulk_import.py:267 netbox/ipam/forms/bulk_import.py:303
|
||||
#: netbox/ipam/forms/bulk_import.py:513 netbox/ipam/forms/filtersets.py:262
|
||||
#: netbox/ipam/forms/bulk_import.py:515 netbox/ipam/forms/filtersets.py:262
|
||||
#: netbox/ipam/forms/filtersets.py:321 netbox/ipam/forms/filtersets.py:401
|
||||
#: netbox/ipam/forms/filtersets.py:593 netbox/ipam/forms/model_forms.py:189
|
||||
#: netbox/ipam/forms/model_forms.py:215 netbox/ipam/forms/model_forms.py:253
|
||||
@@ -1057,10 +1057,10 @@ msgstr ""
|
||||
#: netbox/dcim/forms/bulk_import.py:105 netbox/dcim/forms/bulk_import.py:164
|
||||
#: netbox/dcim/forms/bulk_import.py:267 netbox/dcim/forms/bulk_import.py:376
|
||||
#: netbox/dcim/forms/bulk_import.py:607 netbox/dcim/forms/bulk_import.py:767
|
||||
#: netbox/dcim/forms/bulk_import.py:1232 netbox/dcim/forms/bulk_import.py:1670
|
||||
#: netbox/dcim/forms/bulk_import.py:1232 netbox/dcim/forms/bulk_import.py:1671
|
||||
#: netbox/ipam/forms/bulk_import.py:200 netbox/ipam/forms/bulk_import.py:264
|
||||
#: netbox/ipam/forms/bulk_import.py:300 netbox/ipam/forms/bulk_import.py:510
|
||||
#: netbox/ipam/forms/bulk_import.py:523
|
||||
#: netbox/ipam/forms/bulk_import.py:300 netbox/ipam/forms/bulk_import.py:512
|
||||
#: netbox/ipam/forms/bulk_import.py:525
|
||||
#: netbox/virtualization/forms/bulk_import.py:57
|
||||
#: netbox/virtualization/forms/bulk_import.py:89
|
||||
#: netbox/vpn/forms/bulk_import.py:38 netbox/vpn/forms/bulk_import.py:265
|
||||
@@ -1073,13 +1073,13 @@ msgstr ""
|
||||
#: netbox/circuits/forms/bulk_import.py:235
|
||||
#: netbox/dcim/forms/bulk_import.py:126 netbox/dcim/forms/bulk_import.py:171
|
||||
#: netbox/dcim/forms/bulk_import.py:383 netbox/dcim/forms/bulk_import.py:583
|
||||
#: netbox/dcim/forms/bulk_import.py:1474 netbox/dcim/forms/bulk_import.py:1665
|
||||
#: netbox/dcim/forms/bulk_import.py:1728 netbox/ipam/forms/bulk_import.py:49
|
||||
#: netbox/dcim/forms/bulk_import.py:1475 netbox/dcim/forms/bulk_import.py:1666
|
||||
#: netbox/dcim/forms/bulk_import.py:1729 netbox/ipam/forms/bulk_import.py:49
|
||||
#: netbox/ipam/forms/bulk_import.py:78 netbox/ipam/forms/bulk_import.py:106
|
||||
#: netbox/ipam/forms/bulk_import.py:126 netbox/ipam/forms/bulk_import.py:146
|
||||
#: netbox/ipam/forms/bulk_import.py:174 netbox/ipam/forms/bulk_import.py:259
|
||||
#: netbox/ipam/forms/bulk_import.py:295 netbox/ipam/forms/bulk_import.py:472
|
||||
#: netbox/ipam/forms/bulk_import.py:505
|
||||
#: netbox/ipam/forms/bulk_import.py:295 netbox/ipam/forms/bulk_import.py:474
|
||||
#: netbox/ipam/forms/bulk_import.py:507
|
||||
#: netbox/virtualization/forms/bulk_import.py:71
|
||||
#: netbox/virtualization/forms/bulk_import.py:132
|
||||
#: netbox/vpn/forms/bulk_import.py:62 netbox/wireless/forms/bulk_import.py:60
|
||||
@@ -1152,8 +1152,8 @@ msgstr ""
|
||||
#: netbox/dcim/forms/bulk_edit.py:439 netbox/dcim/forms/bulk_edit.py:678
|
||||
#: netbox/dcim/forms/bulk_edit.py:727 netbox/dcim/forms/bulk_edit.py:869
|
||||
#: netbox/dcim/forms/bulk_import.py:252 netbox/dcim/forms/bulk_import.py:355
|
||||
#: netbox/dcim/forms/bulk_import.py:646 netbox/dcim/forms/bulk_import.py:1614
|
||||
#: netbox/dcim/forms/bulk_import.py:1648 netbox/dcim/forms/filtersets.py:114
|
||||
#: netbox/dcim/forms/bulk_import.py:646 netbox/dcim/forms/bulk_import.py:1615
|
||||
#: netbox/dcim/forms/bulk_import.py:1649 netbox/dcim/forms/filtersets.py:114
|
||||
#: netbox/dcim/forms/filtersets.py:358 netbox/dcim/forms/filtersets.py:393
|
||||
#: netbox/dcim/forms/filtersets.py:438 netbox/dcim/forms/filtersets.py:491
|
||||
#: netbox/dcim/forms/filtersets.py:820 netbox/dcim/forms/filtersets.py:864
|
||||
@@ -1343,7 +1343,7 @@ msgstr ""
|
||||
#: netbox/dcim/forms/bulk_import.py:115 netbox/dcim/forms/model_forms.py:135
|
||||
#: netbox/dcim/tables/sites.py:69 netbox/extras/forms/filtersets.py:600
|
||||
#: netbox/ipam/filtersets.py:1034 netbox/ipam/forms/bulk_edit.py:423
|
||||
#: netbox/ipam/forms/bulk_import.py:494 netbox/ipam/forms/model_forms.py:561
|
||||
#: netbox/ipam/forms/bulk_import.py:496 netbox/ipam/forms/model_forms.py:561
|
||||
#: netbox/ipam/tables/fhrp.py:64 netbox/ipam/tables/vlans.py:96
|
||||
#: netbox/ipam/tables/vlans.py:219
|
||||
#: netbox/templates/circuits/circuitgroupassignment.html:22
|
||||
@@ -1433,8 +1433,8 @@ msgstr ""
|
||||
#: netbox/dcim/models/modules.py:219 netbox/dcim/models/power.py:95
|
||||
#: netbox/dcim/models/racks.py:301 netbox/dcim/models/racks.py:685
|
||||
#: netbox/dcim/models/sites.py:163 netbox/dcim/models/sites.py:287
|
||||
#: netbox/ipam/models/ip.py:244 netbox/ipam/models/ip.py:526
|
||||
#: netbox/ipam/models/ip.py:755 netbox/ipam/models/vlans.py:228
|
||||
#: netbox/ipam/models/ip.py:244 netbox/ipam/models/ip.py:528
|
||||
#: netbox/ipam/models/ip.py:757 netbox/ipam/models/vlans.py:228
|
||||
#: netbox/virtualization/models/clusters.py:70
|
||||
#: netbox/virtualization/models/virtualmachines.py:80
|
||||
#: netbox/vpn/models/l2vpn.py:36 netbox/vpn/models/tunnels.py:38
|
||||
@@ -1656,7 +1656,7 @@ msgid "virtual circuits"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/circuits/models/virtual_circuits.py:135 netbox/ipam/models/ip.py:201
|
||||
#: netbox/ipam/models/ip.py:762 netbox/vpn/models/tunnels.py:109
|
||||
#: netbox/ipam/models/ip.py:764 netbox/vpn/models/tunnels.py:109
|
||||
msgid "role"
|
||||
msgstr ""
|
||||
|
||||
@@ -1826,7 +1826,7 @@ msgstr ""
|
||||
msgid "Assignments"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/circuits/tables/circuits.py:112 netbox/dcim/forms/connections.py:87
|
||||
#: netbox/circuits/tables/circuits.py:112 netbox/dcim/forms/connections.py:91
|
||||
msgid "Side"
|
||||
msgstr ""
|
||||
|
||||
@@ -1879,7 +1879,7 @@ msgstr ""
|
||||
#: netbox/dcim/forms/bulk_import.py:1096 netbox/dcim/forms/bulk_import.py:1115
|
||||
#: netbox/dcim/forms/bulk_import.py:1134 netbox/dcim/forms/bulk_import.py:1146
|
||||
#: netbox/dcim/forms/bulk_import.py:1194 netbox/dcim/forms/bulk_import.py:1316
|
||||
#: netbox/dcim/forms/bulk_import.py:1718 netbox/dcim/forms/connections.py:30
|
||||
#: netbox/dcim/forms/bulk_import.py:1719 netbox/dcim/forms/connections.py:34
|
||||
#: netbox/dcim/forms/filtersets.py:156 netbox/dcim/forms/filtersets.py:1021
|
||||
#: netbox/dcim/forms/filtersets.py:1054 netbox/dcim/forms/filtersets.py:1202
|
||||
#: netbox/dcim/forms/filtersets.py:1418 netbox/dcim/forms/filtersets.py:1441
|
||||
@@ -2606,7 +2606,7 @@ msgstr ""
|
||||
msgid "last updated"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:300 netbox/dcim/models/cables.py:623
|
||||
#: netbox/core/models/data.py:300 netbox/dcim/models/cables.py:667
|
||||
msgid "path"
|
||||
msgstr ""
|
||||
|
||||
@@ -2614,7 +2614,7 @@ msgstr ""
|
||||
msgid "File path relative to the data source's root"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:307 netbox/ipam/models/ip.py:507
|
||||
#: netbox/core/models/data.py:307 netbox/ipam/models/ip.py:509
|
||||
msgid "size"
|
||||
msgstr ""
|
||||
|
||||
@@ -3141,7 +3141,7 @@ msgstr ""
|
||||
#: netbox/dcim/forms/model_forms.py:1709 netbox/dcim/forms/object_import.py:177
|
||||
#: netbox/dcim/tables/devices.py:702 netbox/dcim/tables/devices.py:737
|
||||
#: netbox/dcim/tables/devices.py:965 netbox/dcim/tables/devices.py:1052
|
||||
#: netbox/dcim/tables/devices.py:1205 netbox/ipam/forms/bulk_import.py:580
|
||||
#: netbox/dcim/tables/devices.py:1205 netbox/ipam/forms/bulk_import.py:582
|
||||
#: netbox/ipam/forms/model_forms.py:758 netbox/ipam/tables/fhrp.py:56
|
||||
#: netbox/ipam/tables/ip.py:329 netbox/ipam/tables/services.py:42
|
||||
#: netbox/netbox/tables/tables.py:329 netbox/netbox/ui/panels.py:203
|
||||
@@ -4065,8 +4065,8 @@ msgstr ""
|
||||
#: netbox/ipam/forms/model_forms.py:203 netbox/ipam/forms/model_forms.py:250
|
||||
#: netbox/ipam/forms/model_forms.py:303 netbox/ipam/forms/model_forms.py:466
|
||||
#: netbox/ipam/forms/model_forms.py:480 netbox/ipam/forms/model_forms.py:494
|
||||
#: netbox/ipam/models/ip.py:224 netbox/ipam/models/ip.py:516
|
||||
#: netbox/ipam/models/ip.py:745 netbox/ipam/models/vrfs.py:61
|
||||
#: netbox/ipam/models/ip.py:224 netbox/ipam/models/ip.py:518
|
||||
#: netbox/ipam/models/ip.py:747 netbox/ipam/models/vrfs.py:61
|
||||
#: netbox/ipam/tables/ip.py:187 netbox/ipam/tables/ip.py:258
|
||||
#: netbox/ipam/tables/ip.py:311 netbox/ipam/tables/ip.py:413
|
||||
#: netbox/templates/dcim/interface.html:165
|
||||
@@ -4447,8 +4447,8 @@ msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_edit.py:438 netbox/dcim/forms/bulk_edit.py:891
|
||||
#: netbox/dcim/forms/bulk_import.py:362 netbox/dcim/forms/bulk_import.py:365
|
||||
#: netbox/dcim/forms/bulk_import.py:653 netbox/dcim/forms/bulk_import.py:1655
|
||||
#: netbox/dcim/forms/bulk_import.py:1659 netbox/dcim/forms/filtersets.py:123
|
||||
#: netbox/dcim/forms/bulk_import.py:653 netbox/dcim/forms/bulk_import.py:1656
|
||||
#: netbox/dcim/forms/bulk_import.py:1660 netbox/dcim/forms/filtersets.py:123
|
||||
#: netbox/dcim/forms/filtersets.py:359 netbox/dcim/forms/filtersets.py:448
|
||||
#: netbox/dcim/forms/filtersets.py:462 netbox/dcim/forms/filtersets.py:501
|
||||
#: netbox/dcim/forms/filtersets.py:874 netbox/dcim/forms/filtersets.py:1086
|
||||
@@ -4510,7 +4510,7 @@ msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_edit.py:549 netbox/dcim/forms/bulk_edit.py:556
|
||||
#: netbox/dcim/forms/bulk_edit.py:787 netbox/dcim/forms/bulk_import.py:460
|
||||
#: netbox/dcim/forms/bulk_import.py:1458 netbox/dcim/forms/filtersets.py:690
|
||||
#: netbox/dcim/forms/bulk_import.py:1459 netbox/dcim/forms/filtersets.py:690
|
||||
#: netbox/dcim/forms/filtersets.py:1215 netbox/dcim/forms/model_forms.py:418
|
||||
#: netbox/dcim/forms/model_forms.py:431 netbox/dcim/tables/modules.py:43
|
||||
#: netbox/extras/forms/filtersets.py:413 netbox/extras/forms/model_forms.py:626
|
||||
@@ -4647,8 +4647,8 @@ msgstr ""
|
||||
msgid "Length"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_edit.py:812 netbox/dcim/forms/bulk_import.py:1477
|
||||
#: netbox/dcim/forms/bulk_import.py:1480 netbox/dcim/forms/filtersets.py:1228
|
||||
#: netbox/dcim/forms/bulk_edit.py:812 netbox/dcim/forms/bulk_import.py:1478
|
||||
#: netbox/dcim/forms/bulk_import.py:1481 netbox/dcim/forms/filtersets.py:1228
|
||||
msgid "Length unit"
|
||||
msgstr ""
|
||||
|
||||
@@ -4657,17 +4657,17 @@ msgstr ""
|
||||
msgid "Domain"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_edit.py:886 netbox/dcim/forms/bulk_import.py:1642
|
||||
#: netbox/dcim/forms/bulk_edit.py:886 netbox/dcim/forms/bulk_import.py:1643
|
||||
#: netbox/dcim/forms/filtersets.py:1316 netbox/dcim/forms/model_forms.py:865
|
||||
msgid "Power panel"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_edit.py:908 netbox/dcim/forms/bulk_import.py:1678
|
||||
#: netbox/dcim/forms/bulk_edit.py:908 netbox/dcim/forms/bulk_import.py:1679
|
||||
#: netbox/dcim/forms/filtersets.py:1338 netbox/templates/dcim/powerfeed.html:83
|
||||
msgid "Supply"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_edit.py:914 netbox/dcim/forms/bulk_import.py:1683
|
||||
#: netbox/dcim/forms/bulk_edit.py:914 netbox/dcim/forms/bulk_import.py:1684
|
||||
#: netbox/dcim/forms/filtersets.py:1343 netbox/templates/dcim/powerfeed.html:95
|
||||
msgid "Phase"
|
||||
msgstr ""
|
||||
@@ -4914,7 +4914,7 @@ msgid "available options"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:149 netbox/dcim/forms/bulk_import.py:643
|
||||
#: netbox/dcim/forms/bulk_import.py:1639 netbox/ipam/forms/bulk_import.py:491
|
||||
#: netbox/dcim/forms/bulk_import.py:1640 netbox/ipam/forms/bulk_import.py:493
|
||||
#: netbox/virtualization/forms/bulk_import.py:64
|
||||
#: netbox/virtualization/forms/bulk_import.py:102
|
||||
msgid "Assigned site"
|
||||
@@ -4977,7 +4977,7 @@ msgstr ""
|
||||
msgid "Parent site"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:359 netbox/dcim/forms/bulk_import.py:1652
|
||||
#: netbox/dcim/forms/bulk_import.py:359 netbox/dcim/forms/bulk_import.py:1653
|
||||
msgid "Rack's location (if any)"
|
||||
msgstr ""
|
||||
|
||||
@@ -5042,7 +5042,7 @@ msgstr ""
|
||||
msgid "Limit platform assignments to this manufacturer"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:576 netbox/dcim/forms/bulk_import.py:1721
|
||||
#: netbox/dcim/forms/bulk_import.py:576 netbox/dcim/forms/bulk_import.py:1722
|
||||
#: netbox/tenancy/forms/bulk_import.py:116
|
||||
msgid "Assigned role"
|
||||
msgstr ""
|
||||
@@ -5245,7 +5245,7 @@ msgid "VDC {vdc} is not assigned to device {device}"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1103 netbox/dcim/forms/bulk_import.py:1121
|
||||
#: netbox/dcim/forms/bulk_import.py:1467
|
||||
#: netbox/dcim/forms/bulk_import.py:1468
|
||||
msgid "Physical medium classification"
|
||||
msgstr ""
|
||||
|
||||
@@ -5329,87 +5329,87 @@ msgstr ""
|
||||
msgid "Must specify the parent device or VM when assigning an interface"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1402
|
||||
#: netbox/dcim/forms/bulk_import.py:1403
|
||||
msgid "Side A site"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1406
|
||||
#: netbox/dcim/forms/bulk_import.py:1407
|
||||
#: netbox/wireless/forms/bulk_import.py:93
|
||||
msgid "Site of parent device A (if any)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1409
|
||||
#: netbox/dcim/forms/bulk_import.py:1410
|
||||
msgid "Side A device"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1412 netbox/dcim/forms/bulk_import.py:1437
|
||||
#: netbox/dcim/forms/bulk_import.py:1413 netbox/dcim/forms/bulk_import.py:1438
|
||||
msgid "Device name"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1415
|
||||
#: netbox/dcim/forms/bulk_import.py:1416
|
||||
msgid "Side A type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1421
|
||||
#: netbox/dcim/forms/bulk_import.py:1422
|
||||
msgid "Side A name"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1422 netbox/dcim/forms/bulk_import.py:1447
|
||||
#: netbox/dcim/forms/bulk_import.py:1423 netbox/dcim/forms/bulk_import.py:1448
|
||||
msgid "Termination name"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1427
|
||||
#: netbox/dcim/forms/bulk_import.py:1428
|
||||
msgid "Side B site"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1431
|
||||
#: netbox/dcim/forms/bulk_import.py:1432
|
||||
#: netbox/wireless/forms/bulk_import.py:114
|
||||
msgid "Site of parent device B (if any)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1434
|
||||
#: netbox/dcim/forms/bulk_import.py:1435
|
||||
msgid "Side B device"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1440
|
||||
#: netbox/dcim/forms/bulk_import.py:1441
|
||||
msgid "Side B type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1446
|
||||
#: netbox/dcim/forms/bulk_import.py:1447
|
||||
msgid "Side B name"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1455
|
||||
#: netbox/dcim/forms/bulk_import.py:1456
|
||||
#: netbox/wireless/forms/bulk_import.py:133
|
||||
msgid "Connection status"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1461
|
||||
#: netbox/dcim/forms/bulk_import.py:1462
|
||||
msgid "Cable connection profile"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1486
|
||||
#: netbox/dcim/forms/bulk_import.py:1487
|
||||
msgid "Color name (e.g. \"Red\") or hex code (e.g. \"f44336\")"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1538
|
||||
#: netbox/dcim/forms/bulk_import.py:1539
|
||||
#, python-brace-format
|
||||
msgid "Side {side_upper}: {device} {termination_object} is already connected"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1544
|
||||
#: netbox/dcim/forms/bulk_import.py:1545
|
||||
#, python-brace-format
|
||||
msgid "{side_upper} side termination not found: {device} {name}"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1565
|
||||
#: netbox/dcim/forms/bulk_import.py:1566
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"{color} did not match any used color name and was longer than six "
|
||||
"characters: invalid hex."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1590 netbox/dcim/forms/model_forms.py:900
|
||||
#: netbox/dcim/forms/bulk_import.py:1591 netbox/dcim/forms/model_forms.py:900
|
||||
#: netbox/dcim/tables/devices.py:1124
|
||||
#: netbox/templates/dcim/panels/virtual_chassis_members.html:10
|
||||
#: netbox/templates/dcim/virtualchassis.html:17
|
||||
@@ -5417,49 +5417,49 @@ msgstr ""
|
||||
msgid "Master"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1594
|
||||
#: netbox/dcim/forms/bulk_import.py:1595
|
||||
msgid "Master device"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1611
|
||||
#: netbox/dcim/forms/bulk_import.py:1612
|
||||
msgid "Name of parent site"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1645
|
||||
#: netbox/dcim/forms/bulk_import.py:1646
|
||||
msgid "Upstream power panel"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1675
|
||||
#: netbox/dcim/forms/bulk_import.py:1676
|
||||
msgid "Primary or redundant"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1680
|
||||
#: netbox/dcim/forms/bulk_import.py:1681
|
||||
msgid "Supply type (AC/DC)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1685
|
||||
#: netbox/dcim/forms/bulk_import.py:1686
|
||||
msgid "Single or three-phase"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1735 netbox/dcim/forms/model_forms.py:1875
|
||||
#: netbox/dcim/forms/bulk_import.py:1736 netbox/dcim/forms/model_forms.py:1875
|
||||
#: netbox/dcim/ui/panels.py:108
|
||||
#: netbox/templates/dcim/virtualdevicecontext.html:30
|
||||
#: netbox/virtualization/ui/panels.py:28
|
||||
msgid "Primary IPv4"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1739
|
||||
#: netbox/dcim/forms/bulk_import.py:1740
|
||||
msgid "IPv4 address with mask, e.g. 1.2.3.4/24"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1742 netbox/dcim/forms/model_forms.py:1884
|
||||
#: netbox/dcim/forms/bulk_import.py:1743 netbox/dcim/forms/model_forms.py:1884
|
||||
#: netbox/dcim/ui/panels.py:113
|
||||
#: netbox/templates/dcim/virtualdevicecontext.html:41
|
||||
#: netbox/virtualization/ui/panels.py:33
|
||||
msgid "Primary IPv6"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_import.py:1746
|
||||
#: netbox/dcim/forms/bulk_import.py:1747
|
||||
msgid "IPv6 address with prefix length, e.g. 2001:db8::1/64"
|
||||
msgstr ""
|
||||
|
||||
@@ -5500,7 +5500,7 @@ msgstr ""
|
||||
msgid "A {model} named {name} already exists"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/connections.py:55 netbox/dcim/forms/model_forms.py:853
|
||||
#: netbox/dcim/forms/connections.py:59 netbox/dcim/forms/model_forms.py:853
|
||||
#: netbox/dcim/tables/power.py:63
|
||||
#: netbox/templates/dcim/inc/cable_termination.html:40
|
||||
#: netbox/templates/dcim/powerfeed.html:24
|
||||
@@ -5509,7 +5509,7 @@ msgstr ""
|
||||
msgid "Power Panel"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/connections.py:64 netbox/dcim/forms/model_forms.py:880
|
||||
#: netbox/dcim/forms/connections.py:68 netbox/dcim/forms/model_forms.py:880
|
||||
#: netbox/templates/dcim/powerfeed.html:21
|
||||
#: netbox/templates/dcim/powerport.html:80
|
||||
msgid "Power Feed"
|
||||
@@ -5722,7 +5722,7 @@ msgstr ""
|
||||
msgid "Please select a {scope_type}."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/mixins.py:122 netbox/ipam/forms/bulk_import.py:462
|
||||
#: netbox/dcim/forms/mixins.py:122 netbox/ipam/forms/bulk_import.py:464
|
||||
msgid "Scope type (app & model)"
|
||||
msgstr ""
|
||||
|
||||
@@ -6056,78 +6056,78 @@ msgstr ""
|
||||
msgid "A and B terminations cannot connect to the same object."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:412 netbox/ipam/models/asns.py:38
|
||||
#: netbox/dcim/models/cables.py:456 netbox/ipam/models/asns.py:38
|
||||
msgid "end"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:483
|
||||
#: netbox/dcim/models/cables.py:527
|
||||
msgid "cable termination"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:484
|
||||
#: netbox/dcim/models/cables.py:528
|
||||
msgid "cable terminations"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:497
|
||||
#: netbox/dcim/models/cables.py:541
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Cannot connect a cable to {obj_parent} > {obj} because it is marked as "
|
||||
"connected."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:514
|
||||
#: netbox/dcim/models/cables.py:558
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Duplicate termination found for {app_label}.{model} {termination_id}: cable "
|
||||
"{cable_pk}"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:524
|
||||
#: netbox/dcim/models/cables.py:568
|
||||
#, python-brace-format
|
||||
msgid "Cables cannot be terminated to {type_display} interfaces"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:531
|
||||
#: netbox/dcim/models/cables.py:575
|
||||
msgid "Circuit terminations attached to a provider network may not be cabled."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:627 netbox/extras/models/configs.py:100
|
||||
#: netbox/dcim/models/cables.py:671 netbox/extras/models/configs.py:100
|
||||
msgid "is active"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:631
|
||||
#: netbox/dcim/models/cables.py:675
|
||||
msgid "is complete"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:635
|
||||
#: netbox/dcim/models/cables.py:679
|
||||
msgid "is split"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:643
|
||||
#: netbox/dcim/models/cables.py:687
|
||||
msgid "cable path"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:644
|
||||
#: netbox/dcim/models/cables.py:688
|
||||
msgid "cable paths"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:731
|
||||
#: netbox/dcim/models/cables.py:775
|
||||
msgid "All originating terminations must be attached to the same link"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:749
|
||||
#: netbox/dcim/models/cables.py:793
|
||||
msgid "All mid-span terminations must have the same termination type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:757
|
||||
#: netbox/dcim/models/cables.py:801
|
||||
msgid "All mid-span terminations must have the same parent object"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:787
|
||||
#: netbox/dcim/models/cables.py:831
|
||||
msgid "All links must be cable or wireless"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/cables.py:789
|
||||
#: netbox/dcim/models/cables.py:833
|
||||
msgid "All links must match first link type"
|
||||
msgstr ""
|
||||
|
||||
@@ -6479,7 +6479,7 @@ msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:661
|
||||
#: netbox/dcim/tables/devices.py:625 netbox/ipam/forms/bulk_edit.py:451
|
||||
#: netbox/ipam/forms/bulk_import.py:526 netbox/ipam/forms/filtersets.py:608
|
||||
#: netbox/ipam/forms/bulk_import.py:528 netbox/ipam/forms/filtersets.py:608
|
||||
#: netbox/ipam/forms/model_forms.py:684 netbox/ipam/tables/vlans.py:111
|
||||
#: netbox/templates/dcim/interface.html:86 netbox/templates/ipam/vlan.html:77
|
||||
#: netbox/virtualization/ui/panels.py:63
|
||||
@@ -7393,7 +7393,7 @@ msgstr ""
|
||||
|
||||
#: netbox/dcim/models/racks.py:312 netbox/ipam/forms/bulk_import.py:207
|
||||
#: netbox/ipam/forms/bulk_import.py:271 netbox/ipam/forms/bulk_import.py:306
|
||||
#: netbox/ipam/forms/bulk_import.py:517
|
||||
#: netbox/ipam/forms/bulk_import.py:519
|
||||
#: netbox/virtualization/forms/bulk_import.py:125
|
||||
msgid "Functional role"
|
||||
msgstr ""
|
||||
@@ -7643,7 +7643,7 @@ msgid "U Height"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devices.py:196 netbox/dcim/tables/devices.py:1161
|
||||
#: netbox/ipam/forms/bulk_import.py:599 netbox/ipam/forms/model_forms.py:309
|
||||
#: netbox/ipam/forms/bulk_import.py:601 netbox/ipam/forms/model_forms.py:309
|
||||
#: netbox/ipam/forms/model_forms.py:321 netbox/ipam/tables/ip.py:307
|
||||
#: netbox/ipam/tables/ip.py:371 netbox/ipam/tables/ip.py:386
|
||||
#: netbox/ipam/tables/ip.py:409 netbox/templates/ipam/ipaddress.html:11
|
||||
@@ -8148,31 +8148,31 @@ msgstr ""
|
||||
msgid "Virtual Machines"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:3531
|
||||
#: netbox/dcim/views.py:3532
|
||||
#, python-brace-format
|
||||
msgid "Installed device {device} in bay {device_bay}."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:3572
|
||||
#: netbox/dcim/views.py:3573
|
||||
#, python-brace-format
|
||||
msgid "Removed device {device} from bay {device_bay}."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:3685 netbox/ipam/tables/ip.py:179
|
||||
#: netbox/dcim/views.py:3686 netbox/ipam/tables/ip.py:179
|
||||
msgid "Children"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:4158
|
||||
#: netbox/dcim/views.py:4147
|
||||
#, python-brace-format
|
||||
msgid "Added member <a href=\"{url}\">{device}</a>"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:4203
|
||||
#: netbox/dcim/views.py:4192
|
||||
#, python-brace-format
|
||||
msgid "Unable to remove master device {device} from the virtual chassis."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:4214
|
||||
#: netbox/dcim/views.py:4203
|
||||
#, python-brace-format
|
||||
msgid "Removed {device} from virtual chassis {chassis}"
|
||||
msgstr ""
|
||||
@@ -10494,7 +10494,7 @@ msgstr ""
|
||||
msgid "IP address (ID)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/filtersets.py:1259 netbox/ipam/models/ip.py:813
|
||||
#: netbox/ipam/filtersets.py:1259 netbox/ipam/models/ip.py:815
|
||||
msgid "IP address"
|
||||
msgstr ""
|
||||
|
||||
@@ -10616,13 +10616,13 @@ msgstr ""
|
||||
msgid "Treat as populated"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/forms/bulk_edit.py:307 netbox/ipam/models/ip.py:797
|
||||
#: netbox/ipam/forms/bulk_edit.py:307 netbox/ipam/models/ip.py:799
|
||||
msgid "DNS name"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/forms/bulk_edit.py:322 netbox/ipam/forms/bulk_edit.py:496
|
||||
#: netbox/ipam/forms/bulk_import.py:444 netbox/ipam/forms/bulk_import.py:563
|
||||
#: netbox/ipam/forms/bulk_import.py:591 netbox/ipam/forms/filtersets.py:432
|
||||
#: netbox/ipam/forms/bulk_import.py:446 netbox/ipam/forms/bulk_import.py:565
|
||||
#: netbox/ipam/forms/bulk_import.py:593 netbox/ipam/forms/filtersets.py:432
|
||||
#: netbox/ipam/forms/filtersets.py:626 netbox/templates/ipam/fhrpgroup.html:22
|
||||
#: netbox/templates/ipam/inc/panels/fhrp_groups.html:24
|
||||
#: netbox/templates/ipam/panels/fhrp_groups.html:10
|
||||
@@ -10667,7 +10667,7 @@ msgstr ""
|
||||
msgid "VLAN ID ranges"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/forms/bulk_edit.py:446 netbox/ipam/forms/bulk_import.py:520
|
||||
#: netbox/ipam/forms/bulk_edit.py:446 netbox/ipam/forms/bulk_import.py:522
|
||||
#: netbox/ipam/forms/filtersets.py:600 netbox/ipam/models/vlans.py:250
|
||||
#: netbox/ipam/tables/vlans.py:108
|
||||
msgid "Q-in-Q role"
|
||||
@@ -10681,7 +10681,7 @@ msgstr ""
|
||||
msgid "Site & Group"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/forms/bulk_edit.py:480 netbox/ipam/forms/bulk_import.py:550
|
||||
#: netbox/ipam/forms/bulk_edit.py:480 netbox/ipam/forms/bulk_import.py:552
|
||||
#: netbox/ipam/forms/model_forms.py:715 netbox/ipam/tables/vlans.py:273
|
||||
#: netbox/templates/ipam/vlantranslationrule.html:14
|
||||
#: netbox/vpn/forms/model_forms.py:319 netbox/vpn/forms/model_forms.py:356
|
||||
@@ -10768,44 +10768,44 @@ msgstr ""
|
||||
msgid "No interface specified; cannot set as out-of-band IP"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/forms/bulk_import.py:448
|
||||
#: netbox/ipam/forms/bulk_import.py:450
|
||||
msgid "Auth type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/forms/bulk_import.py:498
|
||||
#: netbox/ipam/forms/bulk_import.py:500
|
||||
msgid "Assigned VLAN group"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/forms/bulk_import.py:530
|
||||
#: netbox/ipam/forms/bulk_import.py:532
|
||||
msgid "Service VLAN (for Q-in-Q/802.1ad customer VLANs)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/forms/bulk_import.py:553 netbox/ipam/models/vlans.py:369
|
||||
#: netbox/ipam/forms/bulk_import.py:555 netbox/ipam/models/vlans.py:369
|
||||
msgid "VLAN translation policy"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/forms/bulk_import.py:565 netbox/ipam/forms/bulk_import.py:593
|
||||
#: netbox/ipam/forms/bulk_import.py:567 netbox/ipam/forms/bulk_import.py:595
|
||||
msgid "IP protocol"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/forms/bulk_import.py:577
|
||||
#: netbox/ipam/forms/bulk_import.py:579
|
||||
msgid "Parent type (app & model)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/forms/bulk_import.py:584
|
||||
#: netbox/ipam/forms/bulk_import.py:586
|
||||
msgid "Parent object name"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/forms/bulk_import.py:588
|
||||
#: netbox/ipam/forms/bulk_import.py:590
|
||||
msgid "Parent object ID"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/forms/bulk_import.py:640
|
||||
#: netbox/ipam/forms/bulk_import.py:642
|
||||
msgid ""
|
||||
"One of parent or parent_object_id must be included with parent_object_type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/forms/bulk_import.py:653
|
||||
#: netbox/ipam/forms/bulk_import.py:655
|
||||
#, python-brace-format
|
||||
msgid "{ip} is not assigned to this parent."
|
||||
msgstr ""
|
||||
@@ -11160,7 +11160,7 @@ msgstr ""
|
||||
msgid "All IP addresses within this prefix are considered usable"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:261 netbox/ipam/models/ip.py:546
|
||||
#: netbox/ipam/models/ip.py:261 netbox/ipam/models/ip.py:548
|
||||
msgid "mark utilized"
|
||||
msgstr ""
|
||||
|
||||
@@ -11172,12 +11172,12 @@ msgstr ""
|
||||
msgid "Cannot create prefix with /0 mask."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:316 netbox/ipam/models/ip.py:903
|
||||
#: netbox/ipam/models/ip.py:316 netbox/ipam/models/ip.py:905
|
||||
#, python-brace-format
|
||||
msgid "VRF {vrf}"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:316 netbox/ipam/models/ip.py:903
|
||||
#: netbox/ipam/models/ip.py:316 netbox/ipam/models/ip.py:905
|
||||
msgid "global table"
|
||||
msgstr ""
|
||||
|
||||
@@ -11186,136 +11186,136 @@ msgstr ""
|
||||
msgid "Duplicate prefix found in {table}: {prefix}"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:499
|
||||
#: netbox/ipam/models/ip.py:501
|
||||
msgid "start address"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:500 netbox/ipam/models/ip.py:504
|
||||
#: netbox/ipam/models/ip.py:737
|
||||
#: netbox/ipam/models/ip.py:502 netbox/ipam/models/ip.py:506
|
||||
#: netbox/ipam/models/ip.py:739
|
||||
msgid "IPv4 or IPv6 address (with mask)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:503
|
||||
#: netbox/ipam/models/ip.py:505
|
||||
msgid "end address"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:530
|
||||
#: netbox/ipam/models/ip.py:532
|
||||
msgid "Operational status of this range"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:538
|
||||
#: netbox/ipam/models/ip.py:540
|
||||
msgid "The primary function of this range"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:541
|
||||
#: netbox/ipam/models/ip.py:543
|
||||
msgid "mark populated"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:543
|
||||
#: netbox/ipam/models/ip.py:545
|
||||
msgid "Prevent the creation of IP addresses within this range"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:548
|
||||
#: netbox/ipam/models/ip.py:550
|
||||
msgid "Report space as fully utilized"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:557
|
||||
#: netbox/ipam/models/ip.py:559
|
||||
msgid "IP range"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:558
|
||||
#: netbox/ipam/models/ip.py:560
|
||||
msgid "IP ranges"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:571
|
||||
#: netbox/ipam/models/ip.py:573
|
||||
msgid "Starting and ending IP address versions must match"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:577
|
||||
#: netbox/ipam/models/ip.py:579
|
||||
msgid "Starting and ending IP address masks must match"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:584
|
||||
#: netbox/ipam/models/ip.py:586
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Ending address must be greater than the starting address ({start_address})"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:612
|
||||
#: netbox/ipam/models/ip.py:614
|
||||
#, python-brace-format
|
||||
msgid "Defined addresses overlap with range {overlapping_range} in VRF {vrf}"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:621
|
||||
#: netbox/ipam/models/ip.py:623
|
||||
#, python-brace-format
|
||||
msgid "Defined range exceeds maximum supported size ({max_size})"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:736 netbox/tenancy/models/contacts.py:78
|
||||
#: netbox/ipam/models/ip.py:738 netbox/tenancy/models/contacts.py:78
|
||||
msgid "address"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:759
|
||||
#: netbox/ipam/models/ip.py:761
|
||||
msgid "The operational status of this IP"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:767
|
||||
#: netbox/ipam/models/ip.py:769
|
||||
msgid "The functional role of this IP"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:790 netbox/templates/ipam/ipaddress.html:72
|
||||
#: netbox/ipam/models/ip.py:792 netbox/templates/ipam/ipaddress.html:72
|
||||
msgid "NAT (inside)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:791
|
||||
#: netbox/ipam/models/ip.py:793
|
||||
msgid "The IP for which this address is the \"outside\" IP"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:798
|
||||
#: netbox/ipam/models/ip.py:800
|
||||
msgid "Hostname or FQDN (not case-sensitive)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:814 netbox/ipam/models/services.py:86
|
||||
#: netbox/ipam/models/ip.py:816 netbox/ipam/models/services.py:86
|
||||
msgid "IP addresses"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:874
|
||||
#: netbox/ipam/models/ip.py:876
|
||||
msgid "Cannot create IP address with /0 mask."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:880
|
||||
#: netbox/ipam/models/ip.py:882
|
||||
#, python-brace-format
|
||||
msgid "{ip} is a network ID, which may not be assigned to an interface."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:891
|
||||
#: netbox/ipam/models/ip.py:893
|
||||
#, python-brace-format
|
||||
msgid "{ip} is a broadcast address, which may not be assigned to an interface."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:905
|
||||
#: netbox/ipam/models/ip.py:907
|
||||
#, python-brace-format
|
||||
msgid "Duplicate IP address found in {table}: {ipaddress}"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:921
|
||||
#: netbox/ipam/models/ip.py:923
|
||||
#, python-brace-format
|
||||
msgid "Cannot create IP address {ip} inside range {range}."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:942
|
||||
#: netbox/ipam/models/ip.py:944
|
||||
msgid ""
|
||||
"Cannot reassign IP address while it is designated as the primary IP for the "
|
||||
"parent object"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:949
|
||||
#: netbox/ipam/models/ip.py:951
|
||||
msgid ""
|
||||
"Cannot reassign IP address while it is designated as the OOB IP for the "
|
||||
"parent object"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:955
|
||||
#: netbox/ipam/models/ip.py:957
|
||||
msgid "Only IPv6 addresses can be assigned SLAAC status"
|
||||
msgstr ""
|
||||
|
||||
@@ -13824,17 +13824,17 @@ msgstr ""
|
||||
msgid "Not Connected"
|
||||
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
|
||||
msgid "NAT for"
|
||||
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
|
||||
msgid "NAT"
|
||||
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/attrs/numeric.html:9
|
||||
#: netbox/templates/ui/attrs/text.html:4
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ from typing import Annotated
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry_django import DatetimeFilterLookup, FilterLookup
|
||||
from strawberry_django import DatetimeFilterLookup, FilterLookup, StrFilterLookup
|
||||
|
||||
from netbox.graphql.filters import BaseModelFilter
|
||||
from users import models
|
||||
@@ -18,16 +18,16 @@ __all__ = (
|
||||
|
||||
@strawberry_django.filter_type(models.Group, lookups=True)
|
||||
class GroupFilter(BaseModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.User, lookups=True)
|
||||
class UserFilter(BaseModelFilter):
|
||||
username: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
first_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
last_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
email: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
username: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
first_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
last_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
email: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
is_superuser: 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()
|
||||
@@ -37,8 +37,8 @@ class UserFilter(BaseModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.Owner, lookups=True)
|
||||
class OwnerFilter(BaseModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group: Annotated['OwnerGroupFilter', strawberry.lazy('users.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -50,5 +50,5 @@ class OwnerFilter(BaseModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.OwnerGroup, lookups=True)
|
||||
class OwnerGroupFilter(BaseModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -38,6 +38,7 @@ FILTER_TREENODE_NEGATION_LOOKUP_MAP = dict(
|
||||
# HTTP Request META safe copy
|
||||
#
|
||||
|
||||
# Non-HTTP_ META keys to include when copying a request (whitelist)
|
||||
HTTP_REQUEST_META_SAFE_COPY = [
|
||||
'CONTENT_LENGTH',
|
||||
'CONTENT_TYPE',
|
||||
@@ -61,6 +62,13 @@ HTTP_REQUEST_META_SAFE_COPY = [
|
||||
'SERVER_PORT',
|
||||
]
|
||||
|
||||
# HTTP_ META keys known to carry sensitive data; excluded when copying a request (denylist)
|
||||
HTTP_REQUEST_META_SENSITIVE = {
|
||||
'HTTP_AUTHORIZATION',
|
||||
'HTTP_COOKIE',
|
||||
'HTTP_PROXY_AUTHORIZATION',
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# CSV-style format delimiters
|
||||
|
||||
@@ -8,7 +8,7 @@ from netaddr import AddrFormatError, IPAddress
|
||||
|
||||
from netbox.registry import registry
|
||||
|
||||
from .constants import HTTP_REQUEST_META_SAFE_COPY
|
||||
from .constants import HTTP_REQUEST_META_SAFE_COPY, HTTP_REQUEST_META_SENSITIVE
|
||||
|
||||
__all__ = (
|
||||
'NetBoxFakeRequest',
|
||||
@@ -45,11 +45,14 @@ def copy_safe_request(request, include_files=True):
|
||||
request: The original request object
|
||||
include_files: Whether to include request.FILES.
|
||||
"""
|
||||
meta = {
|
||||
k: request.META[k]
|
||||
for k in HTTP_REQUEST_META_SAFE_COPY
|
||||
if k in request.META and isinstance(request.META[k], str)
|
||||
}
|
||||
meta = {}
|
||||
for k, v in request.META.items():
|
||||
if not isinstance(v, str):
|
||||
continue
|
||||
if k in HTTP_REQUEST_META_SAFE_COPY:
|
||||
meta[k] = v
|
||||
elif k.startswith('HTTP_') and k not in HTTP_REQUEST_META_SENSITIVE:
|
||||
meta[k] = v
|
||||
data = {
|
||||
'META': meta,
|
||||
'COOKIES': request.COOKIES,
|
||||
|
||||
@@ -1,7 +1,42 @@
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.test import RequestFactory, TestCase
|
||||
from netaddr import IPAddress
|
||||
|
||||
from utilities.request import get_client_ip
|
||||
from utilities.request import copy_safe_request, get_client_ip
|
||||
|
||||
|
||||
class CopySafeRequestTests(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def _make_request(self, **kwargs):
|
||||
request = self.factory.get('/', **kwargs)
|
||||
request.user = AnonymousUser()
|
||||
return request
|
||||
|
||||
def test_standard_meta_keys_copied(self):
|
||||
request = self._make_request(HTTP_USER_AGENT='TestAgent/1.0')
|
||||
fake = copy_safe_request(request)
|
||||
self.assertEqual(fake.META.get('HTTP_USER_AGENT'), 'TestAgent/1.0')
|
||||
|
||||
def test_arbitrary_http_headers_copied(self):
|
||||
"""Arbitrary HTTP_ headers (e.g. X-NetBox-*) should be included."""
|
||||
request = self._make_request(HTTP_X_NETBOX_BRANCH='my-branch')
|
||||
fake = copy_safe_request(request)
|
||||
self.assertEqual(fake.META.get('HTTP_X_NETBOX_BRANCH'), 'my-branch')
|
||||
|
||||
def test_sensitive_headers_excluded(self):
|
||||
"""Authorization and Cookie headers must not be copied."""
|
||||
request = self._make_request(HTTP_AUTHORIZATION='Bearer secret')
|
||||
fake = copy_safe_request(request)
|
||||
self.assertNotIn('HTTP_AUTHORIZATION', fake.META)
|
||||
|
||||
def test_non_string_meta_values_excluded(self):
|
||||
"""Non-string META values must not be copied."""
|
||||
request = self._make_request()
|
||||
request.META['HTTP_X_CUSTOM_INT'] = 42
|
||||
fake = copy_safe_request(request)
|
||||
self.assertNotIn('HTTP_X_CUSTOM_INT', fake.META)
|
||||
|
||||
|
||||
class GetClientIPTests(TestCase):
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import FilterLookup
|
||||
from strawberry_django import StrFilterLookup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .filters import VirtualMachineFilter
|
||||
@@ -20,5 +20,5 @@ class VMComponentFilterMixin:
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
virtual_machine_id: ID | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
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 extras.graphql.filter_mixins import ConfigContextFilterMixin
|
||||
@@ -39,7 +39,7 @@ __all__ = (
|
||||
|
||||
@strawberry_django.filter_type(models.Cluster, lookups=True)
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -77,7 +77,7 @@ class VirtualMachineFilter(
|
||||
TenancyFilterMixin,
|
||||
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_id: ID | None = strawberry_django.filter_field()
|
||||
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 = (
|
||||
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()
|
||||
virtual_disk_count: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
interfaces: Annotated['VMInterfaceFilter', strawberry.lazy('virtualization.graphql.filters')] | None = (
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
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 netbox.graphql.filters import (
|
||||
@@ -63,7 +63,7 @@ class TunnelTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLo
|
||||
|
||||
@strawberry_django.filter_type(models.Tunnel, lookups=True)
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -89,7 +89,7 @@ class TunnelFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.IKEProposal, lookups=True)
|
||||
class IKEProposalFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
authentication_method: (
|
||||
BaseFilterLookup[Annotated['AuthenticationMethodEnum', strawberry.lazy('vpn.graphql.enums')]] | None
|
||||
) = (
|
||||
@@ -118,7 +118,7 @@ class IKEProposalFilter(PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.IKEPolicy, lookups=True)
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -128,12 +128,12 @@ class IKEPolicyFilter(PrimaryModelFilter):
|
||||
proposals: Annotated['IKEProposalFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
|
||||
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)
|
||||
class IPSecProposalFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
encryption_algorithm: (
|
||||
BaseFilterLookup[Annotated['EncryptionAlgorithmEnum', strawberry.lazy('vpn.graphql.enums')]] | None
|
||||
) = (
|
||||
@@ -159,7 +159,7 @@ class IPSecProposalFilter(PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.IPSecPolicy, lookups=True)
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -170,7 +170,7 @@ class IPSecPolicyFilter(PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.IPSecProfile, lookups=True)
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -186,8 +186,8 @@ class IPSecProfileFilter(PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.L2VPN, lookups=True)
|
||||
class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
type: BaseFilterLookup[Annotated['L2VPNTypeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry_django import FilterLookup
|
||||
from strawberry_django import StrFilterLookup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .enums import *
|
||||
@@ -21,4 +21,4 @@ class WirelessAuthenticationFilterMixin:
|
||||
auth_cipher: Annotated['WirelessAuthCipherEnum', strawberry.lazy('wireless.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
auth_psk: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
auth_psk: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
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 netbox.graphql.filter_mixins import DistanceFilterMixin
|
||||
@@ -38,7 +38,7 @@ class WirelessLANFilter(
|
||||
TenancyFilterMixin,
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -65,7 +65,7 @@ class WirelessLinkFilter(
|
||||
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 = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
[project]
|
||||
name = "netbox"
|
||||
version = "4.5.3"
|
||||
version = "4.5.4"
|
||||
requires-python = ">=3.12"
|
||||
description = "The premier source of truth powering network automation."
|
||||
readme = "README.md"
|
||||
|
||||
@@ -17,27 +17,28 @@ django-taggit==6.1.0
|
||||
django-timezone-field==7.2.1
|
||||
djangorestframework==3.16.1
|
||||
drf-spectacular==0.29.0
|
||||
drf-spectacular-sidecar==2026.1.1
|
||||
drf-spectacular-sidecar==2026.3.1
|
||||
feedparser==6.0.12
|
||||
gunicorn==25.0.3
|
||||
gunicorn==25.1.0
|
||||
Jinja2==3.1.6
|
||||
jsonschema==4.26.0
|
||||
Markdown==3.10.2
|
||||
mkdocs-material==9.7.1
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.7.3
|
||||
mkdocstrings==1.0.3
|
||||
mkdocstrings-python==2.0.2
|
||||
mkdocstrings-python==2.0.3
|
||||
netaddr==1.3.0
|
||||
nh3==0.3.3
|
||||
Pillow==12.1.1
|
||||
psycopg[c,pool]==3.3.2
|
||||
psycopg[c,pool]==3.3.3
|
||||
PyYAML==6.0.3
|
||||
requests==2.32.5
|
||||
rq==2.6.1
|
||||
rq==2.7.0
|
||||
social-auth-app-django==5.7.0
|
||||
social-auth-core==4.8.5
|
||||
sorl-thumbnail==13.0.0
|
||||
strawberry-graphql==0.295.0
|
||||
strawberry-graphql-django==0.75.0
|
||||
strawberry-graphql==0.307.1
|
||||
strawberry-graphql-django==0.79.0
|
||||
svgwrite==1.4.3
|
||||
tablib==3.9.0
|
||||
tzdata==2025.3
|
||||
|
||||
Reference in New Issue
Block a user