merge feature
This commit is contained in:
commit
113c8b7ae6
|
|
@ -15,7 +15,7 @@ body:
|
|||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.4.7
|
||||
placeholder: v4.4.8
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ body:
|
|||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.4.7
|
||||
placeholder: v4.4.8
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ jobs:
|
|||
NETBOX_CONFIGURATION: netbox.configuration_testing
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.12', '3.13']
|
||||
python-version: ['3.12', '3.13', '3.14']
|
||||
node-version: ['20.x']
|
||||
services:
|
||||
redis:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "NetBox REST API",
|
||||
"version": "4.4.7",
|
||||
"version": "4.4.8",
|
||||
"license": {
|
||||
"name": "Apache v2 License"
|
||||
}
|
||||
|
|
@ -27997,6 +27997,58 @@
|
|||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "type",
|
||||
|
|
@ -31608,6 +31660,58 @@
|
|||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "type",
|
||||
|
|
@ -35022,6 +35126,58 @@
|
|||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "updated_by_request",
|
||||
|
|
@ -47623,6 +47779,58 @@
|
|||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "type",
|
||||
|
|
@ -53692,6 +53900,58 @@
|
|||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tx_power",
|
||||
|
|
@ -60311,6 +60571,58 @@
|
|||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "updated_by_request",
|
||||
|
|
@ -68666,6 +68978,58 @@
|
|||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "updated_by_request",
|
||||
|
|
@ -81283,6 +81647,58 @@
|
|||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "type",
|
||||
|
|
@ -86612,6 +87028,58 @@
|
|||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "type",
|
||||
|
|
@ -99835,6 +100303,58 @@
|
|||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (slug)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "tenant_id__n",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Tenant (ID)",
|
||||
"explode": true,
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "type",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ Depending on its classification, each NetBox model may support various features
|
|||
|
||||
| Feature | Feature Mixin | Registry Key | Description |
|
||||
|------------------------------------------------------------|-------------------------|---------------------|-----------------------------------------------------------------------------------------|
|
||||
| [Bookmarks](../features/customization.md#bookmarks) | `BookmarksMixin` | `bookmarks` | These models can be bookmarked natively in the user interface |
|
||||
| [Bookmarks](../features/user-preferences.md#bookmarks) | `BookmarksMixin` | `bookmarks` | These models can be bookmarked natively in the user interface |
|
||||
| [Change logging](../features/change-logging.md) | `ChangeLoggingMixin` | `change_logging` | Changes to these objects are automatically recorded in the change log |
|
||||
| Cloning | `CloningMixin` | `cloning` | Provides the `clone()` method to prepare a copy |
|
||||
| [Contacts](../features/contacts.md) | `ContactsMixin` | `contacts` | Contacts can be associated with these models |
|
||||
|
|
|
|||
|
|
@ -325,14 +325,14 @@ class CircuitTypeType(OrganizationalObjectType):
|
|||
|
||||
### Change filters.py
|
||||
|
||||
Strawberry currently doesn't directly support django-filter, so an explicit filters.py file will need to be created. NetBox includes a new `autotype_decorator` used to automatically wrap FilterSets to reduce the required code to a minimum.
|
||||
Filter classes should inherit from `netbox.graphql.filters.BaseModelFilter`.
|
||||
|
||||
```python title="New"
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from circuits import filtersets, models
|
||||
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
from netbox.graphql.filters import BaseModelFilter
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilter',
|
||||
|
|
@ -340,8 +340,7 @@ __all__ = (
|
|||
|
||||
|
||||
@strawberry_django.filter(models.Circuit, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitFilterSet)
|
||||
class CircuitFilter(BaseFilterMixin):
|
||||
class CircuitFilter(BaseModelFilter):
|
||||
pass
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,5 +1,22 @@
|
|||
# NetBox v4.4
|
||||
|
||||
## v4.4.8 (2025-12-09)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#20068](https://github.com/netbox-community/netbox/issues/20068) - Support the assignment of module type profile attributes via bulk import
|
||||
* [#20914](https://github.com/netbox-community/netbox/issues/20914) - Enable filtering device components by tenant assigned to device
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#19918](https://github.com/netbox-community/netbox/issues/19918) - Fix support for `{module}` resolution of components of child modules
|
||||
* [#20759](https://github.com/netbox-community/netbox/issues/20759) - Improve legibility of object types in permissions form
|
||||
* [#20860](https://github.com/netbox-community/netbox/issues/20860) - Ensure user-provided changelog message is recorded when creating device components via the UI
|
||||
* [#20878](https://github.com/netbox-community/netbox/issues/20878) - Use the active database connection when executing custom scripts
|
||||
* [#20888](https://github.com/netbox-community/netbox/issues/20888) - Resolve warnings about non-decimal values for min/max latitude & longitude fields
|
||||
|
||||
---
|
||||
|
||||
## v4.4.7 (2025-11-25)
|
||||
|
||||
### Enhancements
|
||||
|
|
|
|||
|
|
@ -3,17 +3,18 @@ from typing import Annotated, TYPE_CHECKING
|
|||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from netbox.graphql.filter_mixins import OrganizationalModelFilterMixin
|
||||
from strawberry_django import BaseFilterLookup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from netbox.graphql.enums import ColorEnum
|
||||
|
||||
__all__ = (
|
||||
'BaseCircuitTypeFilterMixin',
|
||||
'CircuitTypeFilterMixin',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseCircuitTypeFilterMixin(OrganizationalModelFilterMixin):
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
class CircuitTypeFilterMixin:
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,17 +7,12 @@ from strawberry.scalars import ID
|
|||
from strawberry_django import BaseFilterLookup, FilterLookup, DateFilterLookup
|
||||
|
||||
from circuits import models
|
||||
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
||||
from circuits.graphql.filter_mixins import CircuitTypeFilterMixin
|
||||
from dcim.graphql.filter_mixins import CabledObjectModelFilterMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
|
||||
from netbox.graphql.filter_mixins import (
|
||||
DistanceFilterMixin,
|
||||
ImageAttachmentFilterMixin,
|
||||
OrganizationalModelFilterMixin,
|
||||
PrimaryModelFilterMixin,
|
||||
)
|
||||
from netbox.graphql.filter_mixins import DistanceFilterMixin, ImageAttachmentFilterMixin
|
||||
from netbox.graphql.filters import ChangeLoggedModelFilter, OrganizationalModelFilter, PrimaryModelFilter
|
||||
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
|
||||
from .filter_mixins import BaseCircuitTypeFilterMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.graphql.filters import ContentTypeFilter
|
||||
|
|
@ -43,10 +38,9 @@ __all__ = (
|
|||
|
||||
@strawberry_django.filter_type(models.CircuitTermination, lookups=True)
|
||||
class CircuitTerminationFilter(
|
||||
BaseObjectTypeFilterMixin,
|
||||
CustomFieldsFilterMixin,
|
||||
TagsFilterMixin,
|
||||
ChangeLogFilterMixin,
|
||||
ChangeLoggedModelFilter,
|
||||
CabledObjectModelFilterMixin,
|
||||
):
|
||||
circuit: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
|
|
@ -95,7 +89,7 @@ class CircuitFilter(
|
|||
ImageAttachmentFilterMixin,
|
||||
DistanceFilterMixin,
|
||||
TenancyFilterMixin,
|
||||
PrimaryModelFilterMixin
|
||||
PrimaryModelFilter
|
||||
):
|
||||
cid: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
|
|
@ -124,19 +118,17 @@ class CircuitFilter(
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.CircuitType, lookups=True)
|
||||
class CircuitTypeFilter(BaseCircuitTypeFilterMixin):
|
||||
class CircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.CircuitGroup, lookups=True)
|
||||
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
|
||||
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.CircuitGroupAssignment, lookups=True)
|
||||
class CircuitGroupAssignmentFilter(
|
||||
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
|
||||
):
|
||||
class CircuitGroupAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
member_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -151,7 +143,7 @@ class CircuitGroupAssignmentFilter(
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Provider, lookups=True)
|
||||
class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
|
|
@ -161,7 +153,7 @@ class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ProviderAccount, lookups=True)
|
||||
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -171,7 +163,7 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
|
||||
class ProviderNetworkFilter(PrimaryModelFilterMixin):
|
||||
class ProviderNetworkFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
|
@ -181,12 +173,12 @@ class ProviderNetworkFilter(PrimaryModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
|
||||
class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin):
|
||||
class VirtualCircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
|
||||
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
cid: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
|
@ -209,9 +201,7 @@ class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualCircuitTermination, lookups=True)
|
||||
class VirtualCircuitTerminationFilter(
|
||||
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
|
||||
):
|
||||
class VirtualCircuitTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
virtual_circuit: Annotated['VirtualCircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,31 +4,18 @@ from typing import Annotated, TYPE_CHECKING
|
|||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry import ID
|
||||
from strawberry_django import FilterLookup, DatetimeFilterLookup
|
||||
from strawberry_django import DatetimeFilterLookup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .filters import *
|
||||
|
||||
__all__ = (
|
||||
'BaseFilterMixin',
|
||||
'BaseObjectTypeFilterMixin',
|
||||
'ChangeLogFilterMixin',
|
||||
'ChangeLoggingMixin',
|
||||
)
|
||||
|
||||
|
||||
# @strawberry.input
|
||||
class BaseFilterMixin: ...
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseObjectTypeFilterMixin(BaseFilterMixin):
|
||||
id: FilterLookup[ID] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChangeLogFilterMixin(BaseFilterMixin):
|
||||
id: FilterLookup[ID] | None = strawberry_django.filter_field()
|
||||
class ChangeLoggingMixin:
|
||||
# TODO: "changelog" is not a valid field name; needs to be updated for ObjectChange
|
||||
changelog: Annotated['ObjectChangeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ from strawberry.scalars import ID
|
|||
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup
|
||||
|
||||
from core import models
|
||||
from core.graphql.filter_mixins import BaseFilterMixin
|
||||
from netbox.graphql.filter_mixins import PrimaryModelFilterMixin
|
||||
from netbox.graphql.filters import BaseModelFilter, PrimaryModelFilter
|
||||
from .enums import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -25,8 +24,7 @@ __all__ = (
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.DataFile, lookups=True)
|
||||
class DataFileFilter(BaseFilterMixin):
|
||||
id: FilterLookup[ID] | None = strawberry_django.filter_field()
|
||||
class DataFileFilter(BaseModelFilter):
|
||||
created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
last_updated: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
source: Annotated['DataSourceFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
|
|
@ -41,7 +39,7 @@ class DataFileFilter(BaseFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.DataSource, lookups=True)
|
||||
class DataSourceFilter(PrimaryModelFilterMixin):
|
||||
class DataSourceFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
type: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
source_url: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
@ -60,8 +58,7 @@ class DataSourceFilter(PrimaryModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ObjectChange, lookups=True)
|
||||
class ObjectChangeFilter(BaseFilterMixin):
|
||||
id: FilterLookup[ID] | None = strawberry_django.filter_field()
|
||||
class ObjectChangeFilter(BaseModelFilter):
|
||||
time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
user_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
@ -88,7 +85,6 @@ class ObjectChangeFilter(BaseFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(DjangoContentType, lookups=True)
|
||||
class ContentTypeFilter(BaseFilterMixin):
|
||||
id: FilterLookup[ID] | None = strawberry_django.filter_field()
|
||||
class ContentTypeFilter(BaseModelFilter):
|
||||
app_label: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
model: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ from drf_spectacular.types import OpenApiTypes
|
|||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.models import FrontPort, FrontPortTemplate, PortMapping, PortTemplateMapping, RearPort, RearPortTemplate
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
__all__ = (
|
||||
'ConnectedEndpointsSerializer',
|
||||
'PortSerializer',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -35,3 +37,53 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
|||
@extend_schema_field(serializers.BooleanField)
|
||||
def get_connected_endpoints_reachable(self, obj):
|
||||
return obj._path and obj._path.is_complete and obj._path.is_active
|
||||
|
||||
|
||||
class PortSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Base serializer for front & rear port and port templates.
|
||||
"""
|
||||
@property
|
||||
def _mapper(self):
|
||||
"""
|
||||
Return the model and ForeignKey field name used to track port mappings for this model.
|
||||
"""
|
||||
if self.Meta.model is FrontPort:
|
||||
return PortMapping, 'front_port'
|
||||
if self.Meta.model is RearPort:
|
||||
return PortMapping, 'rear_port'
|
||||
if self.Meta.model is FrontPortTemplate:
|
||||
return PortTemplateMapping, 'front_port'
|
||||
if self.Meta.model is RearPortTemplate:
|
||||
return PortTemplateMapping, 'rear_port'
|
||||
raise ValueError(f"Could not determine mapping details for {self.__class__}")
|
||||
|
||||
def create(self, validated_data):
|
||||
mappings = validated_data.pop('mappings', [])
|
||||
instance = super().create(validated_data)
|
||||
|
||||
# Create port mappings
|
||||
mapping_model, fk_name = self._mapper
|
||||
for attrs in mappings:
|
||||
mapping_model.objects.create(**{
|
||||
fk_name: instance,
|
||||
**attrs,
|
||||
})
|
||||
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
mappings = validated_data.pop('mappings', None)
|
||||
instance = super().update(instance, validated_data)
|
||||
|
||||
if mappings is not None:
|
||||
# Update port mappings
|
||||
mapping_model, fk_name = self._mapper
|
||||
mapping_model.objects.filter(**{fk_name: instance}).delete()
|
||||
for attrs in mappings:
|
||||
mapping_model.objects.create(**{
|
||||
fk_name: instance,
|
||||
**attrs,
|
||||
})
|
||||
|
||||
return instance
|
||||
|
|
|
|||
|
|
@ -5,21 +5,21 @@ from rest_framework import serializers
|
|||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
|
||||
RearPort, VirtualDeviceContext,
|
||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PortMapping,
|
||||
PowerOutlet, PowerPort, RearPort, VirtualDeviceContext,
|
||||
)
|
||||
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
|
||||
from ipam.api.serializers_.vrfs import VRFSerializer
|
||||
from ipam.models import VLAN
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
from netbox.api.gfk_fields import GFKSerializerField
|
||||
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
|
||||
from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
|
||||
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
|
||||
from wireless.choices import *
|
||||
from wireless.models import WirelessLAN
|
||||
from .base import ConnectedEndpointsSerializer
|
||||
from .base import ConnectedEndpointsSerializer, PortSerializer
|
||||
from .cables import CabledObjectSerializer
|
||||
from .devices import DeviceSerializer, MACAddressSerializer, ModuleSerializer, VirtualDeviceContextSerializer
|
||||
from .manufacturers import ManufacturerSerializer
|
||||
|
|
@ -294,7 +294,20 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||
return super().validate(data)
|
||||
|
||||
|
||||
class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
class RearPortMappingSerializer(serializers.ModelSerializer):
|
||||
position = serializers.IntegerField(
|
||||
source='rear_port_position'
|
||||
)
|
||||
front_port = serializers.PrimaryKeyRelatedField(
|
||||
queryset=FrontPort.objects.all(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PortMapping
|
||||
fields = ('position', 'front_port', 'front_port_position')
|
||||
|
||||
|
||||
class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, PortSerializer):
|
||||
device = DeviceSerializer(nested=True)
|
||||
module = ModuleSerializer(
|
||||
nested=True,
|
||||
|
|
@ -303,28 +316,36 @@ class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
|||
allow_null=True
|
||||
)
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
front_ports = RearPortMappingSerializer(
|
||||
source='mappings',
|
||||
many=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
|
||||
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
'front_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||
|
||||
|
||||
class FrontPortRearPortSerializer(WritableNestedSerializer):
|
||||
"""
|
||||
NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device)
|
||||
"""
|
||||
class FrontPortMappingSerializer(serializers.ModelSerializer):
|
||||
position = serializers.IntegerField(
|
||||
source='front_port_position'
|
||||
)
|
||||
rear_port = serializers.PrimaryKeyRelatedField(
|
||||
queryset=RearPort.objects.all(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'label', 'description']
|
||||
model = PortMapping
|
||||
fields = ('position', 'rear_port', 'rear_port_position')
|
||||
|
||||
|
||||
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, PortSerializer):
|
||||
device = DeviceSerializer(nested=True)
|
||||
module = ModuleSerializer(
|
||||
nested=True,
|
||||
|
|
@ -333,14 +354,18 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
|||
allow_null=True
|
||||
)
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
rear_port = FrontPortRearPortSerializer()
|
||||
rear_ports = FrontPortMappingSerializer(
|
||||
source='mappings',
|
||||
many=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
|
||||
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
||||
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
|
||||
'rear_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ from dcim.choices import *
|
|||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
|
||||
InventoryItemTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||
InventoryItemTemplate, ModuleBayTemplate, PortTemplateMapping, PowerOutletTemplate, PowerPortTemplate,
|
||||
RearPortTemplate,
|
||||
)
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.gfk_fields import GFKSerializerField
|
||||
from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
|
||||
from wireless.choices import *
|
||||
from .base import PortSerializer
|
||||
from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer
|
||||
from .manufacturers import ManufacturerSerializer
|
||||
from .nested import NestedInterfaceTemplateSerializer
|
||||
|
|
@ -205,7 +207,20 @@ class InterfaceTemplateSerializer(ComponentTemplateSerializer):
|
|||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class RearPortTemplateSerializer(ComponentTemplateSerializer):
|
||||
class RearPortTemplateMappingSerializer(serializers.ModelSerializer):
|
||||
position = serializers.IntegerField(
|
||||
source='rear_port_position'
|
||||
)
|
||||
front_port = serializers.PrimaryKeyRelatedField(
|
||||
queryset=FrontPortTemplate.objects.all(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PortTemplateMapping
|
||||
fields = ('position', 'front_port', 'front_port_position')
|
||||
|
||||
|
||||
class RearPortTemplateSerializer(ComponentTemplateSerializer, PortSerializer):
|
||||
device_type = DeviceTypeSerializer(
|
||||
required=False,
|
||||
nested=True,
|
||||
|
|
@ -219,17 +234,35 @@ class RearPortTemplateSerializer(ComponentTemplateSerializer):
|
|||
default=None
|
||||
)
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
front_ports = RearPortTemplateMappingSerializer(
|
||||
source='mappings',
|
||||
many=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
|
||||
'positions', 'description', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
|
||||
'front_ports', 'description', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class FrontPortTemplateSerializer(ComponentTemplateSerializer):
|
||||
class FrontPortTemplateMappingSerializer(serializers.ModelSerializer):
|
||||
position = serializers.IntegerField(
|
||||
source='front_port_position'
|
||||
)
|
||||
rear_port = serializers.PrimaryKeyRelatedField(
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PortTemplateMapping
|
||||
fields = ('position', 'rear_port', 'rear_port_position')
|
||||
|
||||
|
||||
class FrontPortTemplateSerializer(ComponentTemplateSerializer, PortSerializer):
|
||||
device_type = DeviceTypeSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
|
|
@ -243,13 +276,17 @@ class FrontPortTemplateSerializer(ComponentTemplateSerializer):
|
|||
default=None
|
||||
)
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
rear_port = RearPortTemplateSerializer(nested=True)
|
||||
rear_ports = FrontPortTemplateMappingSerializer(
|
||||
source='mappings',
|
||||
many=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
|
||||
'rear_port', 'rear_port_position', 'description', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
|
||||
'rear_ports', 'description', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ CABLE_POSITION_MAX = 1024
|
|||
# RearPorts
|
||||
#
|
||||
|
||||
REARPORT_POSITIONS_MIN = 1
|
||||
REARPORT_POSITIONS_MAX = 1024
|
||||
PORT_POSITION_MIN = 1
|
||||
PORT_POSITION_MAX = 1024
|
||||
|
||||
|
||||
#
|
||||
|
|
|
|||
|
|
@ -904,12 +904,15 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
|||
null_value=None
|
||||
)
|
||||
rear_port_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RearPort.objects.all()
|
||||
field_name='mappings__rear_port',
|
||||
queryset=RearPort.objects.all(),
|
||||
to_field_name='rear_port',
|
||||
label=_('Rear port (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = ('id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description')
|
||||
fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
|
|
@ -918,6 +921,12 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
|
|||
choices=PortTypeChoices,
|
||||
null_value=None
|
||||
)
|
||||
front_port_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='mappings__front_port',
|
||||
queryset=FrontPort.objects.all(),
|
||||
to_field_name='front_port',
|
||||
label=_('Front port (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
|
|
@ -1664,6 +1673,17 @@ class DeviceComponentFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
|||
choices=DeviceStatusChoices,
|
||||
field_name='device__status',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label=_('Tenant (ID)'),
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Tenant (slug)'),
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
|
@ -2137,13 +2157,16 @@ class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet)
|
|||
null_value=None
|
||||
)
|
||||
rear_port_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RearPort.objects.all()
|
||||
field_name='mappings__rear_port',
|
||||
queryset=RearPort.objects.all(),
|
||||
to_field_name='rear_port',
|
||||
label=_('Rear port (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = (
|
||||
'id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description', 'mark_connected', 'cable_end',
|
||||
'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end',
|
||||
'cable_position',
|
||||
)
|
||||
|
||||
|
|
@ -2154,6 +2177,12 @@ class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
|
|||
choices=PortTypeChoices,
|
||||
null_value=None
|
||||
)
|
||||
front_port_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='mappings__front_port',
|
||||
queryset=FrontPort.objects.all(),
|
||||
to_field_name='front_port',
|
||||
label=_('Front port (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
|
|
|
|||
|
|
@ -476,14 +476,30 @@ class ModuleTypeImportForm(PrimaryModelImportForm):
|
|||
required=False,
|
||||
help_text=_('Unit for module weight')
|
||||
)
|
||||
attribute_data = forms.JSONField(
|
||||
label=_('Attributes'),
|
||||
required=False,
|
||||
help_text=_('Attribute values for the assigned profile, passed as a dictionary')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
fields = [
|
||||
'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'profile',
|
||||
'owner', 'comments', 'tags'
|
||||
'attribute_data', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Attribute data may be included only if a profile is specified
|
||||
if self.cleaned_data.get('attribute_data') and not self.cleaned_data.get('profile'):
|
||||
raise forms.ValidationError(_("Profile must be specified if attribute data is provided."))
|
||||
|
||||
# Default attribute_data to an empty dictionary if a profile is specified (to enforce schema validation)
|
||||
if self.cleaned_data.get('profile') and not self.cleaned_data.get('attribute_data'):
|
||||
self.cleaned_data['attribute_data'] = {}
|
||||
|
||||
|
||||
class DeviceRoleImportForm(NestedGroupModelImportForm):
|
||||
parent = CSVModelChoiceField(
|
||||
|
|
@ -1075,12 +1091,6 @@ class FrontPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
|
|||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
rear_port = CSVModelChoiceField(
|
||||
label=_('Rear port'),
|
||||
queryset=RearPort.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('Corresponding rear port')
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
label=_('Type'),
|
||||
choices=PortTypeChoices,
|
||||
|
|
@ -1090,32 +1100,9 @@ class FrontPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
|
|||
class Meta:
|
||||
model = FrontPort
|
||||
fields = (
|
||||
'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
|
||||
'description', 'owner', 'tags'
|
||||
'device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description', 'owner', 'tags'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit RearPort choices to those belonging to this device (or VC master)
|
||||
if self.is_bound and 'device' in self.data:
|
||||
try:
|
||||
device = self.fields['device'].to_python(self.data['device'])
|
||||
except forms.ValidationError:
|
||||
device = None
|
||||
else:
|
||||
try:
|
||||
device = self.instance.device
|
||||
except Device.DoesNotExist:
|
||||
device = None
|
||||
|
||||
if device:
|
||||
self.fields['rear_port'].queryset = RearPort.objects.filter(
|
||||
device__in=[device, device.get_vc_master()]
|
||||
)
|
||||
else:
|
||||
self.fields['rear_port'].queryset = RearPort.objects.none()
|
||||
|
||||
|
||||
class RearPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
|
||||
device = CSVModelChoiceField(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from netbox.forms import (
|
|||
PrimaryModelFilterSetForm,
|
||||
)
|
||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||
from tenancy.models import Tenant
|
||||
from users.models import Owner, User
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||
from utilities.forms.fields import ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
|
||||
|
|
@ -123,6 +124,11 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
|||
required=False,
|
||||
label=_('Device role')
|
||||
)
|
||||
tenant_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
label=_('Tenant')
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
|
|
@ -131,7 +137,8 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
|||
'location_id': '$location_id',
|
||||
'virtual_chassis_id': '$virtual_chassis_id',
|
||||
'device_type_id': '$device_type_id',
|
||||
'role_id': '$role_id'
|
||||
'role_id': '$role_id',
|
||||
'tenant_id': '$tenant_id'
|
||||
},
|
||||
label=_('Device')
|
||||
)
|
||||
|
|
@ -1360,7 +1367,8 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
|
|
@ -1384,7 +1392,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
|
|||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
|
|
@ -1409,7 +1417,8 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
|
|
@ -1428,7 +1437,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||
FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
|
|
@ -1461,7 +1470,8 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||
FieldSet('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', name=_('Wireless')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', 'vdc_id',
|
||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
'vdc_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
|
|
@ -1582,7 +1592,8 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
|||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||
)
|
||||
|
|
@ -1606,7 +1617,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
|||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||
|
|
@ -1630,7 +1641,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
|
|||
FieldSet('name', 'label', 'position', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
)
|
||||
|
|
@ -1648,7 +1659,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
|||
FieldSet('name', 'label', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
)
|
||||
|
|
@ -1665,7 +1676,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
|||
),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.db import connection
|
||||
from django.db.models.signals import post_save
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.constants import LOCATION_SCOPE_TYPES
|
||||
from dcim.models import Site
|
||||
from dcim.models import PortMapping, PortTemplateMapping, Site
|
||||
from utilities.forms import get_field_value
|
||||
from utilities.forms.fields import (
|
||||
ContentTypeChoiceField, CSVContentTypeField, DynamicModelChoiceField,
|
||||
|
|
@ -13,6 +15,7 @@ from utilities.templatetags.builtins.filters import bettertitle
|
|||
from utilities.forms.widgets import HTMXSelect
|
||||
|
||||
__all__ = (
|
||||
'FrontPortFormMixin',
|
||||
'ScopedBulkEditForm',
|
||||
'ScopedForm',
|
||||
'ScopedImportForm',
|
||||
|
|
@ -128,3 +131,75 @@ class ScopedImportForm(forms.Form):
|
|||
"Please select a {scope_type}."
|
||||
).format(scope_type=scope_type.model_class()._meta.model_name)
|
||||
})
|
||||
|
||||
|
||||
class FrontPortFormMixin(forms.Form):
|
||||
rear_ports = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label=_('Rear ports'),
|
||||
widget=forms.SelectMultiple(attrs={'size': 8})
|
||||
)
|
||||
|
||||
port_mapping_model = PortMapping
|
||||
parent_field = 'device'
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check that the total number of FrontPorts and positions matches the selected number of RearPort:position
|
||||
# mappings. Note that `name` will be a list under FrontPortCreateForm, in which cases we multiply the number of
|
||||
# FrontPorts being creation by the number of positions.
|
||||
positions = self.cleaned_data['positions']
|
||||
frontport_count = len(self.cleaned_data['name']) if type(self.cleaned_data['name']) is list else 1
|
||||
rearport_count = len(self.cleaned_data['rear_ports'])
|
||||
if frontport_count * positions != rearport_count:
|
||||
raise forms.ValidationError({
|
||||
'rear_ports': _(
|
||||
"The total number of front port positions ({frontport_count}) must match the selected number of "
|
||||
"rear port positions ({rearport_count})."
|
||||
).format(
|
||||
frontport_count=frontport_count,
|
||||
rearport_count=rearport_count
|
||||
)
|
||||
})
|
||||
|
||||
def _save_m2m(self):
|
||||
super()._save_m2m()
|
||||
|
||||
# TODO: Can this be made more efficient?
|
||||
# Delete existing rear port mappings
|
||||
self.port_mapping_model.objects.filter(front_port_id=self.instance.pk).delete()
|
||||
|
||||
# Create new rear port mappings
|
||||
mappings = []
|
||||
if self.port_mapping_model is PortTemplateMapping:
|
||||
params = {
|
||||
'device_type_id': self.instance.device_type_id,
|
||||
'module_type_id': self.instance.module_type_id,
|
||||
}
|
||||
else:
|
||||
params = {
|
||||
'device_id': self.instance.device_id,
|
||||
}
|
||||
for i, rp_position in enumerate(self.cleaned_data['rear_ports'], start=1):
|
||||
rear_port_id, rear_port_position = rp_position.split(':')
|
||||
mappings.append(
|
||||
self.port_mapping_model(**{
|
||||
**params,
|
||||
'front_port_id': self.instance.pk,
|
||||
'front_port_position': i,
|
||||
'rear_port_id': rear_port_id,
|
||||
'rear_port_position': rear_port_position,
|
||||
})
|
||||
)
|
||||
self.port_mapping_model.objects.bulk_create(mappings)
|
||||
# Send post_save signals
|
||||
for mapping in mappings:
|
||||
post_save.send(
|
||||
sender=PortMapping,
|
||||
instance=mapping,
|
||||
created=True,
|
||||
raw=False,
|
||||
using=connection,
|
||||
update_fields=None
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from timezone_field import TimeZoneFormField
|
|||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.forms.mixins import FrontPortFormMixin
|
||||
from dcim.models import *
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.choices import VLANQinQRoleChoices
|
||||
|
|
@ -1112,34 +1113,66 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
|
|||
]
|
||||
|
||||
|
||||
class FrontPortTemplateForm(ModularComponentTemplateForm):
|
||||
rear_port = DynamicModelChoiceField(
|
||||
label=_('Rear port'),
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_type_id': '$device_type',
|
||||
'module_type_id': '$module_type',
|
||||
}
|
||||
)
|
||||
|
||||
class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
FieldSet('device_type', name=_('Device Type')),
|
||||
FieldSet('module_type', name=_('Module Type')),
|
||||
),
|
||||
'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||
'name', 'label', 'type', 'positions', 'rear_ports', 'description',
|
||||
),
|
||||
)
|
||||
|
||||
# Override FrontPortFormMixin attrs
|
||||
port_mapping_model = PortTemplateMapping
|
||||
parent_field = 'device_type'
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
|
||||
'description',
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if device_type_id := self.data.get('device_type') or self.initial.get('device_type'):
|
||||
device_type = DeviceType.objects.get(pk=device_type_id)
|
||||
else:
|
||||
return
|
||||
|
||||
# Populate rear port choices
|
||||
self.fields['rear_ports'].choices = self._get_rear_port_choices(device_type, self.instance)
|
||||
|
||||
# Set initial rear port mappings
|
||||
if self.instance.pk:
|
||||
self.initial['rear_ports'] = [
|
||||
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
|
||||
for mapping in PortTemplateMapping.objects.filter(front_port_id=self.instance.pk)
|
||||
]
|
||||
|
||||
def _get_rear_port_choices(self, device_type, front_port):
|
||||
"""
|
||||
Return a list of choices representing each available rear port & position pair on the device type, excluding
|
||||
those assigned to the specified instance.
|
||||
"""
|
||||
occupied_rear_port_positions = [
|
||||
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
|
||||
for mapping in device_type.port_mappings.exclude(front_port=front_port.pk)
|
||||
]
|
||||
|
||||
choices = []
|
||||
for rear_port in RearPortTemplate.objects.filter(device_type=device_type):
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
pair_id = f'{rear_port.pk}:{i}'
|
||||
if pair_id not in occupied_rear_port_positions:
|
||||
pair_label = f'{rear_port.name}:{i}'
|
||||
choices.append(
|
||||
(pair_id, pair_label)
|
||||
)
|
||||
return choices
|
||||
|
||||
|
||||
class RearPortTemplateForm(ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
|
|
@ -1578,17 +1611,10 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
|||
}
|
||||
|
||||
|
||||
class FrontPortForm(ModularDeviceComponentForm):
|
||||
rear_port = DynamicModelChoiceField(
|
||||
queryset=RearPort.objects.all(),
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
|
||||
class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
|
||||
'description', 'tags',
|
||||
),
|
||||
)
|
||||
|
|
@ -1596,10 +1622,49 @@ class FrontPortForm(ModularDeviceComponentForm):
|
|||
class Meta:
|
||||
model = FrontPort
|
||||
fields = [
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
|
||||
'description', 'owner', 'tags',
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner',
|
||||
'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if device_id := self.data.get('device') or self.initial.get('device'):
|
||||
device = Device.objects.get(pk=device_id)
|
||||
else:
|
||||
return
|
||||
|
||||
# Populate rear port choices
|
||||
self.fields['rear_ports'].choices = self._get_rear_port_choices(device, self.instance)
|
||||
|
||||
# Set initial rear port mappings
|
||||
if self.instance.pk:
|
||||
self.initial['rear_ports'] = [
|
||||
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
|
||||
for mapping in PortMapping.objects.filter(front_port_id=self.instance.pk)
|
||||
]
|
||||
|
||||
def _get_rear_port_choices(self, device, front_port):
|
||||
"""
|
||||
Return a list of choices representing each available rear port & position pair on the device, excluding those
|
||||
assigned to the specified instance.
|
||||
"""
|
||||
occupied_rear_port_positions = [
|
||||
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
|
||||
for mapping in device.port_mappings.exclude(front_port=front_port.pk)
|
||||
]
|
||||
|
||||
choices = []
|
||||
for rear_port in RearPort.objects.filter(device=device):
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
pair_id = f'{rear_port.pk}:{i}'
|
||||
if pair_id not in occupied_rear_port_positions:
|
||||
pair_label = f'{rear_port.name}:{i}'
|
||||
choices.append(
|
||||
(pair_id, pair_label)
|
||||
)
|
||||
return choices
|
||||
|
||||
|
||||
class RearPortForm(ModularDeviceComponentForm):
|
||||
fieldsets = (
|
||||
|
|
|
|||
|
|
@ -109,85 +109,30 @@ class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemp
|
|||
|
||||
|
||||
class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
|
||||
rear_port = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label=_('Rear ports'),
|
||||
help_text=_('Select one rear port assignment for each front port being created.'),
|
||||
widget=forms.SelectMultiple(attrs={'size': 6})
|
||||
)
|
||||
|
||||
# Override fieldsets from FrontPortTemplateForm to omit rear_port_position
|
||||
# Override fieldsets from FrontPortTemplateForm
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
FieldSet('device_type', name=_('Device Type')),
|
||||
FieldSet('module_type', name=_('Module Type')),
|
||||
),
|
||||
'name', 'label', 'type', 'color', 'rear_port', 'description',
|
||||
'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'description',
|
||||
),
|
||||
)
|
||||
|
||||
class Meta(model_forms.FrontPortTemplateForm.Meta):
|
||||
exclude = ('name', 'label', 'rear_port', 'rear_port_position')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# TODO: This needs better validation
|
||||
if 'device_type' in self.initial or self.data.get('device_type'):
|
||||
parent = DeviceType.objects.get(
|
||||
pk=self.initial.get('device_type') or self.data.get('device_type')
|
||||
)
|
||||
elif 'module_type' in self.initial or self.data.get('module_type'):
|
||||
parent = ModuleType.objects.get(
|
||||
pk=self.initial.get('module_type') or self.data.get('module_type')
|
||||
)
|
||||
else:
|
||||
return
|
||||
|
||||
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
|
||||
occupied_port_positions = [
|
||||
(front_port.rear_port_id, front_port.rear_port_position)
|
||||
for front_port in parent.frontporttemplates.all()
|
||||
]
|
||||
|
||||
# Populate rear port choices
|
||||
choices = []
|
||||
rear_ports = parent.rearporttemplates.all()
|
||||
for rear_port in rear_ports:
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
if (rear_port.pk, i) not in occupied_port_positions:
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
|
||||
# positions
|
||||
frontport_count = len(self.cleaned_data['name'])
|
||||
rearport_count = len(self.cleaned_data['rear_port'])
|
||||
if frontport_count != rearport_count:
|
||||
raise forms.ValidationError({
|
||||
'rear_port': _(
|
||||
"The number of front port templates to be created ({frontport_count}) must match the selected "
|
||||
"number of rear port positions ({rearport_count})."
|
||||
).format(
|
||||
frontport_count=frontport_count,
|
||||
rearport_count=rearport_count
|
||||
)
|
||||
})
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = (
|
||||
'device_type', 'module_type', 'type', 'color', 'positions', 'description',
|
||||
)
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
|
||||
positions = self.cleaned_data['positions']
|
||||
offset = positions * iteration
|
||||
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
'rear_port_position': int(position),
|
||||
'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -269,74 +214,26 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
|||
}
|
||||
)
|
||||
)
|
||||
rear_port = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label=_('Rear ports'),
|
||||
help_text=_('Select one rear port assignment for each front port being created.'),
|
||||
widget=forms.SelectMultiple(attrs={'size': 6})
|
||||
)
|
||||
|
||||
# Override fieldsets from FrontPortForm to omit rear_port_position
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags',
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
|
||||
'description', 'tags',
|
||||
),
|
||||
)
|
||||
|
||||
class Meta(model_forms.FrontPortForm.Meta):
|
||||
exclude = ('name', 'label', 'rear_port', 'rear_port_position')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if device_id := self.data.get('device') or self.initial.get('device'):
|
||||
device = Device.objects.get(pk=device_id)
|
||||
else:
|
||||
return
|
||||
|
||||
# Determine which rear port positions are occupied. These will be excluded from the list of available
|
||||
# mappings.
|
||||
occupied_port_positions = [
|
||||
(front_port.rear_port_id, front_port.rear_port_position)
|
||||
for front_port in device.frontports.all()
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = [
|
||||
'device', 'module', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner', 'tags',
|
||||
]
|
||||
|
||||
# Populate rear port choices
|
||||
choices = []
|
||||
rear_ports = RearPort.objects.filter(device=device)
|
||||
for rear_port in rear_ports:
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
if (rear_port.pk, i) not in occupied_port_positions:
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check that the number of FrontPorts to be created matches the selected number of RearPort positions
|
||||
frontport_count = len(self.cleaned_data['name'])
|
||||
rearport_count = len(self.cleaned_data['rear_port'])
|
||||
if frontport_count != rearport_count:
|
||||
raise forms.ValidationError({
|
||||
'rear_port': _(
|
||||
"The number of front ports to be created ({frontport_count}) must match the selected number of "
|
||||
"rear port positions ({rearport_count})."
|
||||
).format(
|
||||
frontport_count=frontport_count,
|
||||
rearport_count=rearport_count
|
||||
)
|
||||
})
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
|
||||
|
||||
positions = self.cleaned_data['positions']
|
||||
offset = positions * iteration
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
'rear_port_position': int(position),
|
||||
'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ __all__ = (
|
|||
'InterfaceTemplateImportForm',
|
||||
'InventoryItemTemplateImportForm',
|
||||
'ModuleBayTemplateImportForm',
|
||||
'PortTemplateMappingImportForm',
|
||||
'PowerOutletTemplateImportForm',
|
||||
'PowerPortTemplateImportForm',
|
||||
'RearPortTemplateImportForm',
|
||||
|
|
@ -113,31 +114,11 @@ class FrontPortTemplateImportForm(forms.ModelForm):
|
|||
label=_('Type'),
|
||||
choices=PortTypeChoices.CHOICES
|
||||
)
|
||||
rear_port = forms.ModelChoiceField(
|
||||
label=_('Rear port'),
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
|
||||
def clean_device_type(self):
|
||||
if device_type := self.cleaned_data['device_type']:
|
||||
rear_port = self.fields['rear_port']
|
||||
rear_port.queryset = rear_port.queryset.filter(device_type=device_type)
|
||||
|
||||
return device_type
|
||||
|
||||
def clean_module_type(self):
|
||||
if module_type := self.cleaned_data['module_type']:
|
||||
rear_port = self.fields['rear_port']
|
||||
rear_port.queryset = rear_port.queryset.filter(module_type=module_type)
|
||||
|
||||
return module_type
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'device_type', 'module_type', 'name', 'type', 'color', 'rear_port', 'rear_port_position', 'label',
|
||||
'description',
|
||||
'device_type', 'module_type', 'name', 'type', 'color', 'positions', 'label', 'description',
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -154,6 +135,25 @@ class RearPortTemplateImportForm(forms.ModelForm):
|
|||
]
|
||||
|
||||
|
||||
class PortTemplateMappingImportForm(forms.ModelForm):
|
||||
front_port = forms.ModelChoiceField(
|
||||
label=_('Front port'),
|
||||
queryset=FrontPortTemplate.objects.all(),
|
||||
to_field_name='name',
|
||||
)
|
||||
rear_port = forms.ModelChoiceField(
|
||||
label=_('Rear port'),
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
to_field_name='name',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PortTemplateMapping
|
||||
fields = [
|
||||
'front_port', 'front_port_position', 'rear_port', 'rear_port_position',
|
||||
]
|
||||
|
||||
|
||||
class ModuleBayTemplateImportForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ import strawberry_django
|
|||
from strawberry import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseFilterMixin, ChangeLogFilterMixin
|
||||
from core.graphql.filters import ContentTypeFilter
|
||||
from netbox.graphql.filter_mixins import NetBoxModelFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin
|
||||
from .enums import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -22,16 +20,16 @@ __all__ = (
|
|||
'ComponentModelFilterMixin',
|
||||
'ComponentTemplateFilterMixin',
|
||||
'InterfaceBaseFilterMixin',
|
||||
'ModularComponentModelFilterMixin',
|
||||
'ModularComponentFilterMixin',
|
||||
'ModularComponentTemplateFilterMixin',
|
||||
'RackBaseFilterMixin',
|
||||
'RackFilterMixin',
|
||||
'RenderConfigFilterMixin',
|
||||
'ScopedFilterMixin',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScopedFilterMixin(BaseFilterMixin):
|
||||
class ScopedFilterMixin:
|
||||
scope_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -39,7 +37,7 @@ class ScopedFilterMixin(BaseFilterMixin):
|
|||
|
||||
|
||||
@dataclass
|
||||
class ComponentModelFilterMixin(NetBoxModelFilterMixin):
|
||||
class ComponentModelFilterMixin:
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
device_id: ID | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
@ -48,7 +46,7 @@ class ComponentModelFilterMixin(NetBoxModelFilterMixin):
|
|||
|
||||
|
||||
@dataclass
|
||||
class ModularComponentModelFilterMixin(ComponentModelFilterMixin):
|
||||
class ModularComponentFilterMixin(ComponentModelFilterMixin):
|
||||
module: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
module_id: ID | None = strawberry_django.filter_field()
|
||||
inventory_items: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
|
|
@ -57,7 +55,7 @@ class ModularComponentModelFilterMixin(ComponentModelFilterMixin):
|
|||
|
||||
|
||||
@dataclass
|
||||
class CabledObjectModelFilterMixin(BaseFilterMixin):
|
||||
class CabledObjectModelFilterMixin:
|
||||
cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
cable_id: ID | None = strawberry_django.filter_field()
|
||||
cable_end: (
|
||||
|
|
@ -67,7 +65,7 @@ class CabledObjectModelFilterMixin(BaseFilterMixin):
|
|||
|
||||
|
||||
@dataclass
|
||||
class ComponentTemplateFilterMixin(ChangeLogFilterMixin):
|
||||
class ComponentTemplateFilterMixin:
|
||||
device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -85,7 +83,7 @@ class ModularComponentTemplateFilterMixin(ComponentTemplateFilterMixin):
|
|||
|
||||
|
||||
@dataclass
|
||||
class RenderConfigFilterMixin(BaseFilterMixin):
|
||||
class RenderConfigFilterMixin:
|
||||
config_template: Annotated['ConfigTemplateFilter', strawberry.lazy('extras.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -93,7 +91,7 @@ class RenderConfigFilterMixin(BaseFilterMixin):
|
|||
|
||||
|
||||
@dataclass
|
||||
class InterfaceBaseFilterMixin(BaseFilterMixin):
|
||||
class InterfaceBaseFilterMixin:
|
||||
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
mtu: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
|
@ -124,7 +122,7 @@ class InterfaceBaseFilterMixin(BaseFilterMixin):
|
|||
|
||||
|
||||
@dataclass
|
||||
class RackBaseFilterMixin(WeightFilterMixin, PrimaryModelFilterMixin):
|
||||
class RackFilterMixin:
|
||||
width: BaseFilterLookup[Annotated['RackWidthEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,29 +6,25 @@ import strawberry_django
|
|||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import ChangeLogFilterMixin
|
||||
from dcim import models
|
||||
from dcim.constants import *
|
||||
from dcim.graphql.enums import InterfaceKindEnum
|
||||
from dcim.graphql.filter_mixins import (
|
||||
ComponentModelFilterMixin, ComponentTemplateFilterMixin, ModularComponentFilterMixin,
|
||||
ModularComponentTemplateFilterMixin, RackFilterMixin,
|
||||
)
|
||||
from extras.graphql.filter_mixins import ConfigContextFilterMixin
|
||||
from netbox.graphql.filter_mixins import (
|
||||
PrimaryModelFilterMixin,
|
||||
OrganizationalModelFilterMixin,
|
||||
NestedGroupModelFilterMixin,
|
||||
ImageAttachmentFilterMixin,
|
||||
WeightFilterMixin,
|
||||
from netbox.graphql.filter_mixins import ImageAttachmentFilterMixin, WeightFilterMixin
|
||||
from netbox.graphql.filters import (
|
||||
BaseModelFilter, ChangeLoggedModelFilter, NestedGroupModelFilter, OrganizationalModelFilter, PrimaryModelFilter,
|
||||
NetBoxModelFilter,
|
||||
)
|
||||
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
|
||||
from virtualization.models import VMInterface
|
||||
|
||||
from .filter_mixins import (
|
||||
CabledObjectModelFilterMixin,
|
||||
ComponentModelFilterMixin,
|
||||
ComponentTemplateFilterMixin,
|
||||
InterfaceBaseFilterMixin,
|
||||
ModularComponentModelFilterMixin,
|
||||
ModularComponentTemplateFilterMixin,
|
||||
RackBaseFilterMixin,
|
||||
RenderConfigFilterMixin,
|
||||
)
|
||||
|
||||
|
|
@ -75,6 +71,8 @@ __all__ = (
|
|||
'ModuleTypeFilter',
|
||||
'ModuleTypeProfileFilter',
|
||||
'PlatformFilter',
|
||||
'PortMappingFilter',
|
||||
'PortTemplateMappingFilter',
|
||||
'PowerFeedFilter',
|
||||
'PowerOutletFilter',
|
||||
'PowerOutletTemplateFilter',
|
||||
|
|
@ -96,7 +94,7 @@ __all__ = (
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Cable, lookups=True)
|
||||
class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
|
||||
class CableFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
type: BaseFilterLookup[Annotated['CableTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -119,7 +117,7 @@ class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.CableTermination, lookups=True)
|
||||
class CableTerminationFilter(ChangeLogFilterMixin):
|
||||
class CableTerminationFilter(ChangeLoggedModelFilter):
|
||||
cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
cable_id: ID | None = strawberry_django.filter_field()
|
||||
cable_end: BaseFilterLookup[Annotated['CableEndEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
|
|
@ -132,7 +130,7 @@ class CableTerminationFilter(ChangeLogFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ConsolePort, lookups=True)
|
||||
class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
class ConsolePortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
|
||||
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -142,14 +140,14 @@ class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ConsolePortTemplate, lookups=True)
|
||||
class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ConsoleServerPort, lookups=True)
|
||||
class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
class ConsoleServerPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
|
||||
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -159,7 +157,7 @@ class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectMode
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ConsoleServerPortTemplate, lookups=True)
|
||||
class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -172,7 +170,7 @@ class DeviceFilter(
|
|||
ImageAttachmentFilterMixin,
|
||||
RenderConfigFilterMixin,
|
||||
ConfigContextFilterMixin,
|
||||
PrimaryModelFilterMixin,
|
||||
PrimaryModelFilter,
|
||||
):
|
||||
device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
|
@ -285,7 +283,7 @@ class DeviceFilter(
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.DeviceBay, lookups=True)
|
||||
class DeviceBayFilter(ComponentModelFilterMixin):
|
||||
class DeviceBayFilter(ComponentModelFilterMixin, NetBoxModelFilter):
|
||||
installed_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -293,12 +291,12 @@ class DeviceBayFilter(ComponentModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.DeviceBayTemplate, lookups=True)
|
||||
class DeviceBayTemplateFilter(ComponentTemplateFilterMixin):
|
||||
class DeviceBayTemplateFilter(ComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.InventoryItemTemplate, lookups=True)
|
||||
class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
|
||||
class InventoryItemTemplateFilter(ComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
parent: Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -318,7 +316,7 @@ class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.DeviceRole, lookups=True)
|
||||
class DeviceRoleFilter(OrganizationalModelFilterMixin, RenderConfigFilterMixin):
|
||||
class DeviceRoleFilter(RenderConfigFilterMixin, OrganizationalModelFilter):
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -326,7 +324,7 @@ class DeviceRoleFilter(OrganizationalModelFilterMixin, RenderConfigFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.DeviceType, lookups=True)
|
||||
class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
|
||||
class DeviceTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryModelFilter):
|
||||
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -402,41 +400,58 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.FrontPort, lookups=True)
|
||||
class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
class FrontPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
rear_port: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
rear_port_id: ID | None = strawberry_django.filter_field()
|
||||
rear_port_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.FrontPortTemplate, lookups=True)
|
||||
class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.PortMapping, lookups=True)
|
||||
class PortMappingFilter(BaseModelFilter):
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
front_port: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
rear_port: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
front_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
rear_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.PortTemplateMapping, lookups=True)
|
||||
class PortTemplateMappingFilter(BaseModelFilter):
|
||||
device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
module_type: Annotated['ModuleTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
front_port: Annotated['FrontPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
rear_port: Annotated['RearPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
rear_port_id: ID | None = strawberry_django.filter_field()
|
||||
rear_port_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
front_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
rear_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.MACAddress, lookups=True)
|
||||
class MACAddressFilter(PrimaryModelFilterMixin):
|
||||
class MACAddressFilter(PrimaryModelFilter):
|
||||
mac_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
|
@ -463,7 +478,12 @@ class MACAddressFilter(PrimaryModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Interface, lookups=True)
|
||||
class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin, CabledObjectModelFilterMixin):
|
||||
class InterfaceFilter(
|
||||
ModularComponentFilterMixin,
|
||||
InterfaceBaseFilterMixin,
|
||||
CabledObjectModelFilterMixin,
|
||||
NetBoxModelFilter
|
||||
):
|
||||
vcdcs: Annotated['VirtualDeviceContextFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -553,7 +573,7 @@ class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.InterfaceTemplate, lookups=True)
|
||||
class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -575,7 +595,7 @@ class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.InventoryItem, lookups=True)
|
||||
class InventoryItemFilter(ComponentModelFilterMixin):
|
||||
class InventoryItemFilter(ComponentModelFilterMixin, NetBoxModelFilter):
|
||||
parent: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -602,14 +622,14 @@ class InventoryItemFilter(ComponentModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.InventoryItemRole, lookups=True)
|
||||
class InventoryItemRoleFilter(OrganizationalModelFilterMixin):
|
||||
class InventoryItemRoleFilter(OrganizationalModelFilter):
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Location, lookups=True)
|
||||
class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilterMixin):
|
||||
class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilter):
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
status: BaseFilterLookup[Annotated['LocationStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
|
|
@ -625,12 +645,12 @@ class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilt
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Manufacturer, lookups=True)
|
||||
class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilterMixin):
|
||||
class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Module, lookups=True)
|
||||
class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
|
||||
class ModuleFilter(ConfigContextFilterMixin, PrimaryModelFilter):
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
device_id: ID | None = strawberry_django.filter_field()
|
||||
module_bay: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
|
|
@ -679,7 +699,7 @@ class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ModuleBay, lookups=True)
|
||||
class ModuleBayFilter(ModularComponentModelFilterMixin):
|
||||
class ModuleBayFilter(ModularComponentFilterMixin, NetBoxModelFilter):
|
||||
parent: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -688,17 +708,17 @@ class ModuleBayFilter(ModularComponentModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ModuleBayTemplate, lookups=True)
|
||||
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
position: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ModuleTypeProfile, lookups=True)
|
||||
class ModuleTypeProfileFilter(PrimaryModelFilterMixin):
|
||||
class ModuleTypeProfileFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ModuleType, lookups=True)
|
||||
class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
|
||||
class ModuleTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryModelFilter):
|
||||
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -749,7 +769,7 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Platform, lookups=True)
|
||||
class PlatformFilter(OrganizationalModelFilterMixin):
|
||||
class PlatformFilter(OrganizationalModelFilter):
|
||||
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -761,7 +781,7 @@ class PlatformFilter(OrganizationalModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.PowerFeed, lookups=True)
|
||||
class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
power_panel: Annotated['PowerPanelFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -796,7 +816,7 @@ class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryM
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.PowerOutlet, lookups=True)
|
||||
class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
class PowerOutletFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -816,7 +836,7 @@ class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.PowerOutletTemplate, lookups=True)
|
||||
class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
|
||||
class PowerOutletTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -830,7 +850,7 @@ class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.PowerPanel, lookups=True)
|
||||
class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilterMixin):
|
||||
class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilter):
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
|
|
@ -843,7 +863,7 @@ class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryMo
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.PowerPort, lookups=True)
|
||||
class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
class PowerPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -856,7 +876,7 @@ class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.PowerPortTemplate, lookups=True)
|
||||
class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -869,7 +889,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.RackType, lookups=True)
|
||||
class RackTypeFilter(RackBaseFilterMixin):
|
||||
class RackTypeFilter(RackFilterMixin, WeightFilterMixin, PrimaryModelFilter):
|
||||
form_factor: BaseFilterLookup[Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -884,7 +904,14 @@ class RackTypeFilter(RackBaseFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Rack, lookups=True)
|
||||
class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, RackBaseFilterMixin):
|
||||
class RackFilter(
|
||||
ContactFilterMixin,
|
||||
ImageAttachmentFilterMixin,
|
||||
TenancyFilterMixin,
|
||||
WeightFilterMixin,
|
||||
RackFilterMixin,
|
||||
PrimaryModelFilter
|
||||
):
|
||||
form_factor: BaseFilterLookup[Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -918,7 +945,7 @@ class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.RackReservation, lookups=True)
|
||||
class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rack_id: ID | None = strawberry_django.filter_field()
|
||||
units: Annotated['IntegerArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
|
|
@ -933,14 +960,14 @@ class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.RackRole, lookups=True)
|
||||
class RackRoleFilter(OrganizationalModelFilterMixin):
|
||||
class RackRoleFilter(OrganizationalModelFilter):
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.RearPort, lookups=True)
|
||||
class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
class RearPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -953,7 +980,7 @@ class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMi
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.RearPortTemplate, lookups=True)
|
||||
class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
class RearPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -966,7 +993,7 @@ class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Region, lookups=True)
|
||||
class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
||||
class RegionFilter(ContactFilterMixin, NestedGroupModelFilter):
|
||||
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -976,7 +1003,7 @@ class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Site, lookups=True)
|
||||
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: BaseFilterLookup[Annotated['SiteStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
|
|
@ -1012,7 +1039,7 @@ class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.SiteGroup, lookups=True)
|
||||
class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
||||
class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilter):
|
||||
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -1022,7 +1049,7 @@ class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualChassis, lookups=True)
|
||||
class VirtualChassisFilter(PrimaryModelFilterMixin):
|
||||
class VirtualChassisFilter(PrimaryModelFilter):
|
||||
master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
master_id: ID | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
@ -1034,7 +1061,7 @@ class VirtualChassisFilter(PrimaryModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualDeviceContext, lookups=True)
|
||||
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
device_id: ID | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
|
|||
|
|
@ -385,7 +385,8 @@ class DeviceTypeType(PrimaryObjectType):
|
|||
)
|
||||
class FrontPortType(ModularComponentType, CabledObjectMixin):
|
||||
color: str
|
||||
rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]
|
||||
|
||||
mappings: List[Annotated["PortMappingType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
|
|
@ -396,7 +397,8 @@ class FrontPortType(ModularComponentType, CabledObjectMixin):
|
|||
)
|
||||
class FrontPortTemplateType(ModularComponentTemplateType):
|
||||
color: str
|
||||
rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
|
||||
|
||||
mappings: List[Annotated["PortMappingTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
|
|
@ -636,6 +638,28 @@ class PlatformType(NestedGroupObjectType):
|
|||
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.PortMapping,
|
||||
fields='__all__',
|
||||
filters=PortMappingFilter,
|
||||
pagination=True
|
||||
)
|
||||
class PortMappingType(ModularComponentTemplateType):
|
||||
front_port: Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]
|
||||
rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.PortTemplateMapping,
|
||||
fields='__all__',
|
||||
filters=PortTemplateMappingFilter,
|
||||
pagination=True
|
||||
)
|
||||
class PortMappingTemplateType(ModularComponentTemplateType):
|
||||
front_port: Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]
|
||||
rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.PowerFeed,
|
||||
exclude=['_path'],
|
||||
|
|
@ -768,7 +792,7 @@ class RackRoleType(OrganizationalObjectType):
|
|||
class RearPortType(ModularComponentType, CabledObjectMixin):
|
||||
color: str
|
||||
|
||||
frontports: List[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]]
|
||||
mappings: List[Annotated["PortMappingType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
|
|
@ -780,7 +804,7 @@ class RearPortType(ModularComponentType, CabledObjectMixin):
|
|||
class RearPortTemplateType(ModularComponentTemplateType):
|
||||
color: str
|
||||
|
||||
frontport_templates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
mappings: List[Annotated["PortMappingTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import decimal
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
|
@ -17,8 +19,8 @@ class Migration(migrations.Migration):
|
|||
max_digits=8,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(-90.0),
|
||||
django.core.validators.MaxValueValidator(90.0),
|
||||
django.core.validators.MinValueValidator(decimal.Decimal('-90.0')),
|
||||
django.core.validators.MaxValueValidator(decimal.Decimal('90.0'))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -31,8 +33,8 @@ class Migration(migrations.Migration):
|
|||
max_digits=9,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(-180.0),
|
||||
django.core.validators.MaxValueValidator(180.0),
|
||||
django.core.validators.MinValueValidator(decimal.Decimal('-180.0')),
|
||||
django.core.validators.MaxValueValidator(decimal.Decimal('180.0'))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -45,8 +47,8 @@ class Migration(migrations.Migration):
|
|||
max_digits=8,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(-90.0),
|
||||
django.core.validators.MaxValueValidator(90.0),
|
||||
django.core.validators.MinValueValidator(decimal.Decimal('-90.0')),
|
||||
django.core.validators.MaxValueValidator(decimal.Decimal('90.0'))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -59,8 +61,8 @@ class Migration(migrations.Migration):
|
|||
max_digits=9,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(-180.0),
|
||||
django.core.validators.MaxValueValidator(180.0),
|
||||
django.core.validators.MinValueValidator(decimal.Decimal('-180.0')),
|
||||
django.core.validators.MaxValueValidator(decimal.Decimal('180.0'))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,219 @@
|
|||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
from itertools import islice
|
||||
|
||||
|
||||
def chunked(iterable, size):
|
||||
"""
|
||||
Yield successive chunks of a given size from an iterator.
|
||||
"""
|
||||
iterator = iter(iterable)
|
||||
while chunk := list(islice(iterator, size)):
|
||||
yield chunk
|
||||
|
||||
|
||||
def populate_port_template_mappings(apps, schema_editor):
|
||||
FrontPortTemplate = apps.get_model('dcim', 'FrontPortTemplate')
|
||||
PortTemplateMapping = apps.get_model('dcim', 'PortTemplateMapping')
|
||||
|
||||
front_ports = FrontPortTemplate.objects.iterator(chunk_size=1000)
|
||||
|
||||
def generate_copies():
|
||||
for front_port in front_ports:
|
||||
yield PortTemplateMapping(
|
||||
device_type_id=front_port.device_type_id,
|
||||
module_type_id=front_port.module_type_id,
|
||||
front_port_id=front_port.pk,
|
||||
front_port_position=1,
|
||||
rear_port_id=front_port.rear_port_id,
|
||||
rear_port_position=front_port.rear_port_position,
|
||||
)
|
||||
|
||||
# Bulk insert in streaming batches
|
||||
for chunk in chunked(generate_copies(), 1000):
|
||||
PortTemplateMapping.objects.bulk_create(chunk, batch_size=1000)
|
||||
|
||||
|
||||
def populate_port_mappings(apps, schema_editor):
|
||||
FrontPort = apps.get_model('dcim', 'FrontPort')
|
||||
PortMapping = apps.get_model('dcim', 'PortMapping')
|
||||
|
||||
front_ports = FrontPort.objects.iterator(chunk_size=1000)
|
||||
|
||||
def generate_copies():
|
||||
for front_port in front_ports:
|
||||
yield PortMapping(
|
||||
device_id=front_port.device_id,
|
||||
front_port_id=front_port.pk,
|
||||
front_port_position=1,
|
||||
rear_port_id=front_port.rear_port_id,
|
||||
rear_port_position=front_port.rear_port_position,
|
||||
)
|
||||
|
||||
# Bulk insert in streaming batches
|
||||
for chunk in chunked(generate_copies(), 1000):
|
||||
PortMapping.objects.bulk_create(chunk, batch_size=1000)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0221_cable_position'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Create PortTemplateMapping model (for DeviceTypes)
|
||||
migrations.CreateModel(
|
||||
name='PortTemplateMapping',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
(
|
||||
'front_port_position',
|
||||
models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024)
|
||||
]
|
||||
)
|
||||
),
|
||||
(
|
||||
'rear_port_position',
|
||||
models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024)
|
||||
]
|
||||
)
|
||||
),
|
||||
(
|
||||
'device_type',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='dcim.devicetype',
|
||||
related_name='port_mappings',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
),
|
||||
(
|
||||
'module_type',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='dcim.moduletype',
|
||||
related_name='port_mappings',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
),
|
||||
(
|
||||
'front_port',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='dcim.frontporttemplate',
|
||||
related_name='mappings'
|
||||
)
|
||||
),
|
||||
(
|
||||
'rear_port',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='dcim.rearporttemplate',
|
||||
related_name='mappings'
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='porttemplatemapping',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('front_port', 'front_port_position'),
|
||||
name='dcim_porttemplatemapping_unique_front_port_position'
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='porttemplatemapping',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('rear_port', 'rear_port_position'),
|
||||
name='dcim_porttemplatemapping_unique_rear_port_position'
|
||||
),
|
||||
),
|
||||
|
||||
# Create PortMapping model (for Devices)
|
||||
migrations.CreateModel(
|
||||
name='PortMapping',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
(
|
||||
'front_port_position',
|
||||
models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024)
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
'rear_port_position',
|
||||
models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
'device',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='dcim.device',
|
||||
related_name='port_mappings'
|
||||
)
|
||||
),
|
||||
(
|
||||
'front_port',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='dcim.frontport',
|
||||
related_name='mappings'
|
||||
)
|
||||
),
|
||||
(
|
||||
'rear_port',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='dcim.rearport',
|
||||
related_name='mappings'
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='portmapping',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('front_port', 'front_port_position'),
|
||||
name='dcim_portmapping_unique_front_port_position'
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='portmapping',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('rear_port', 'rear_port_position'),
|
||||
name='dcim_portmapping_unique_rear_port_position'
|
||||
),
|
||||
),
|
||||
|
||||
# Data migration
|
||||
migrations.RunPython(
|
||||
code=populate_port_template_mappings,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=populate_port_mappings,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0222_port_mappings'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Remove rear_port & rear_port_position from FrontPortTemplate
|
||||
migrations.RemoveConstraint(
|
||||
model_name='frontporttemplate',
|
||||
name='dcim_frontporttemplate_unique_rear_port_position',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='frontporttemplate',
|
||||
name='rear_port',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='frontporttemplate',
|
||||
name='rear_port_position',
|
||||
),
|
||||
|
||||
# Add positions on FrontPortTemplate
|
||||
migrations.AddField(
|
||||
model_name='frontporttemplate',
|
||||
name='positions',
|
||||
field=models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024)
|
||||
]
|
||||
),
|
||||
),
|
||||
|
||||
# Remove rear_port & rear_port_position from FrontPort
|
||||
migrations.RemoveConstraint(
|
||||
model_name='frontport',
|
||||
name='dcim_frontport_unique_rear_port_position',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='frontport',
|
||||
name='rear_port',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='frontport',
|
||||
name='rear_port_position',
|
||||
),
|
||||
|
||||
# Add positions on FrontPort
|
||||
migrations.AddField(
|
||||
model_name='frontport',
|
||||
name='positions',
|
||||
field=models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024)
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -6,7 +6,7 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0221_cable_position'),
|
||||
('dcim', '0223_frontport_positions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.constants import PORT_POSITION_MAX, PORT_POSITION_MIN
|
||||
|
||||
__all__ = (
|
||||
'PortMappingBase',
|
||||
)
|
||||
|
||||
|
||||
class PortMappingBase(models.Model):
|
||||
"""
|
||||
Base class for PortMapping and PortTemplateMapping
|
||||
"""
|
||||
front_port_position = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=(
|
||||
MinValueValidator(PORT_POSITION_MIN),
|
||||
MaxValueValidator(PORT_POSITION_MAX),
|
||||
),
|
||||
)
|
||||
rear_port_position = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=(
|
||||
MinValueValidator(PORT_POSITION_MIN),
|
||||
MaxValueValidator(PORT_POSITION_MAX),
|
||||
),
|
||||
)
|
||||
|
||||
_netbox_private = True
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('front_port', 'front_port_position'),
|
||||
name='%(app_label)s_%(class)s_unique_front_port_position'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('rear_port', 'rear_port_position'),
|
||||
name='%(app_label)s_%(class)s_unique_rear_port_position'
|
||||
),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate rear port position
|
||||
if self.rear_port_position > self.rear_port.positions:
|
||||
raise ValidationError({
|
||||
"rear_port_position": _(
|
||||
"Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
|
||||
"positions."
|
||||
).format(
|
||||
rear_port_position=self.rear_port_position,
|
||||
name=self.rear_port.name,
|
||||
positions=self.rear_port.positions
|
||||
)
|
||||
})
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import itertools
|
||||
import logging
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
|
@ -22,7 +23,7 @@ from utilities.fields import ColorField, GenericArrayForeignKey
|
|||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.serialization import deserialize_object, serialize_object
|
||||
from wireless.models import WirelessLink
|
||||
from .device_components import FrontPort, PathEndpoint, RearPort
|
||||
from .device_components import FrontPort, PathEndpoint, PortMapping, RearPort
|
||||
|
||||
__all__ = (
|
||||
'Cable',
|
||||
|
|
@ -30,6 +31,8 @@ __all__ = (
|
|||
'CableTermination',
|
||||
)
|
||||
|
||||
logger = logging.getLogger(f'netbox.{__name__}')
|
||||
|
||||
trace_paths = Signal()
|
||||
|
||||
|
||||
|
|
@ -666,7 +669,13 @@ class CablePath(models.Model):
|
|||
is_active = True
|
||||
is_split = False
|
||||
|
||||
logger.debug(f'Tracing cable path from {terminations}...')
|
||||
|
||||
segment = 0
|
||||
while terminations:
|
||||
segment += 1
|
||||
logger.debug(f'[Path segment #{segment}] Position stack: {position_stack}')
|
||||
logger.debug(f'[Path segment #{segment}] Local terminations: {terminations}')
|
||||
|
||||
# Terminations must all be of the same type
|
||||
if not all(isinstance(t, type(terminations[0])) for t in terminations[1:]):
|
||||
|
|
@ -697,7 +706,10 @@ class CablePath(models.Model):
|
|||
position_stack.append([terminations[0].cable_position])
|
||||
|
||||
# Step 2: Determine the attached links (Cable or WirelessLink), if any
|
||||
links = [termination.link for termination in terminations if termination.link is not None]
|
||||
links = list(dict.fromkeys(
|
||||
termination.link for termination in terminations if termination.link is not None
|
||||
))
|
||||
logger.debug(f'[Path segment #{segment}] Links: {links}')
|
||||
if len(links) == 0:
|
||||
if len(path) == 1:
|
||||
# If this is the start of the path and no link exists, return None
|
||||
|
|
@ -760,10 +772,13 @@ class CablePath(models.Model):
|
|||
link.interface_b if link.interface_a is terminations[0] else link.interface_a for link in links
|
||||
]
|
||||
|
||||
logger.debug(f'[Path segment #{segment}] Remote terminations: {remote_terminations}')
|
||||
|
||||
# Remote Terminations must all be of the same type, otherwise return a split path
|
||||
if not all(isinstance(t, type(remote_terminations[0])) for t in remote_terminations[1:]):
|
||||
is_complete = False
|
||||
is_split = True
|
||||
logger.debug('Remote termination types differ; aborting trace.')
|
||||
break
|
||||
|
||||
# Step 7: Record the far-end termination object(s)
|
||||
|
|
@ -777,58 +792,53 @@ class CablePath(models.Model):
|
|||
|
||||
if isinstance(remote_terminations[0], FrontPort):
|
||||
# Follow FrontPorts to their corresponding RearPorts
|
||||
rear_ports = RearPort.objects.filter(
|
||||
pk__in=[t.rear_port_id for t in remote_terminations]
|
||||
)
|
||||
if len(rear_ports) > 1 or rear_ports[0].positions > 1:
|
||||
position_stack.append([fp.rear_port_position for fp in remote_terminations])
|
||||
|
||||
terminations = rear_ports
|
||||
|
||||
elif isinstance(remote_terminations[0], RearPort):
|
||||
if len(remote_terminations) == 1 and remote_terminations[0].positions == 1:
|
||||
front_ports = FrontPort.objects.filter(
|
||||
rear_port_id__in=[rp.pk for rp in remote_terminations],
|
||||
rear_port_position=1
|
||||
)
|
||||
# Obtain the individual front ports based on the termination and all positions
|
||||
elif len(remote_terminations) > 1 and position_stack:
|
||||
if remote_terminations[0].positions > 1 and position_stack:
|
||||
positions = position_stack.pop()
|
||||
|
||||
# Ensure we have a number of positions equal to the amount of remote terminations
|
||||
if len(remote_terminations) != len(positions):
|
||||
raise UnsupportedCablePath(
|
||||
_("All positions counts within the path on opposite ends of links must match")
|
||||
)
|
||||
|
||||
# Get our front ports
|
||||
q_filter = Q()
|
||||
for rt in remote_terminations:
|
||||
position = positions.pop()
|
||||
q_filter |= Q(rear_port_id=rt.pk, rear_port_position=position)
|
||||
if q_filter is Q():
|
||||
raise UnsupportedCablePath(_("Remote termination position filter is missing"))
|
||||
front_ports = FrontPort.objects.filter(q_filter)
|
||||
# Obtain the individual front ports based on the termination and position
|
||||
elif position_stack:
|
||||
front_ports = FrontPort.objects.filter(
|
||||
rear_port_id=remote_terminations[0].pk,
|
||||
rear_port_position__in=position_stack.pop()
|
||||
)
|
||||
# If all rear ports have a single position, we can just get the front ports
|
||||
elif all([rp.positions == 1 for rp in remote_terminations]):
|
||||
front_ports = FrontPort.objects.filter(rear_port_id__in=[rp.pk for rp in remote_terminations])
|
||||
|
||||
if len(front_ports) != len(remote_terminations):
|
||||
# Some rear ports does not have a front port
|
||||
is_split = True
|
||||
break
|
||||
else:
|
||||
# No position indicated: path has split, so we stop at the RearPorts
|
||||
q_filter |= Q(front_port=rt, front_port_position__in=positions)
|
||||
port_mappings = PortMapping.objects.filter(q_filter)
|
||||
elif remote_terminations[0].positions > 1:
|
||||
is_split = True
|
||||
logger.debug(
|
||||
'Encountered front port mapped to multiple rear ports but position stack is empty; aborting '
|
||||
'trace.'
|
||||
)
|
||||
break
|
||||
else:
|
||||
port_mappings = PortMapping.objects.filter(front_port__in=remote_terminations)
|
||||
if not port_mappings:
|
||||
break
|
||||
|
||||
terminations = front_ports
|
||||
# Compile the list of RearPorts without duplication or altering their ordering
|
||||
terminations = list(dict.fromkeys(mapping.rear_port for mapping in port_mappings))
|
||||
if any(t.positions > 1 for t in terminations):
|
||||
position_stack.append([mapping.rear_port_position for mapping in port_mappings])
|
||||
|
||||
elif isinstance(remote_terminations[0], RearPort):
|
||||
# Follow RearPorts to their corresponding FrontPorts
|
||||
if remote_terminations[0].positions > 1 and position_stack:
|
||||
positions = position_stack.pop()
|
||||
q_filter = Q()
|
||||
for rt in remote_terminations:
|
||||
q_filter |= Q(rear_port=rt, rear_port_position__in=positions)
|
||||
port_mappings = PortMapping.objects.filter(q_filter)
|
||||
elif remote_terminations[0].positions > 1:
|
||||
is_split = True
|
||||
logger.debug(
|
||||
'Encountered rear port mapped to multiple front ports but position stack is empty; aborting '
|
||||
'trace.'
|
||||
)
|
||||
break
|
||||
else:
|
||||
port_mappings = PortMapping.objects.filter(rear_port__in=remote_terminations)
|
||||
if not port_mappings:
|
||||
break
|
||||
|
||||
# Compile the list of FrontPorts without duplication or altering their ordering
|
||||
terminations = list(dict.fromkeys(mapping.front_port for mapping in port_mappings))
|
||||
if any(t.positions > 1 for t in terminations):
|
||||
position_stack.append([mapping.front_port_position for mapping in port_mappings])
|
||||
|
||||
elif isinstance(remote_terminations[0], CircuitTermination):
|
||||
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
|
||||
|
|
@ -876,6 +886,7 @@ class CablePath(models.Model):
|
|||
# Unsupported topology, mark as split and exit
|
||||
is_complete = False
|
||||
is_split = True
|
||||
logger.warning('Encountered an unsupported topology; aborting trace.')
|
||||
break
|
||||
|
||||
return cls(
|
||||
|
|
@ -954,16 +965,23 @@ class CablePath(models.Model):
|
|||
|
||||
# RearPort splitting to multiple FrontPorts with no stack position
|
||||
if type(nodes[0]) is RearPort:
|
||||
return FrontPort.objects.filter(rear_port__in=nodes)
|
||||
return [
|
||||
mapping.front_port for mapping in
|
||||
PortMapping.objects.filter(rear_port__in=nodes).prefetch_related('front_port')
|
||||
]
|
||||
# Cable terminating to multiple FrontPorts mapped to different
|
||||
# RearPorts connected to different cables
|
||||
elif type(nodes[0]) is FrontPort:
|
||||
return RearPort.objects.filter(pk__in=[fp.rear_port_id for fp in nodes])
|
||||
if type(nodes[0]) is FrontPort:
|
||||
return [
|
||||
mapping.rear_port for mapping in
|
||||
PortMapping.objects.filter(front_port__in=nodes).prefetch_related('rear_port')
|
||||
]
|
||||
# Cable terminating to multiple CircuitTerminations
|
||||
elif type(nodes[0]) is CircuitTermination:
|
||||
if type(nodes[0]) is CircuitTermination:
|
||||
return [
|
||||
ct.get_peer_termination() for ct in nodes
|
||||
]
|
||||
return []
|
||||
|
||||
def get_asymmetric_nodes(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from mptt.models import MPTTModel, TreeForeignKey
|
|||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models.base import PortMappingBase
|
||||
from dcim.models.mixins import InterfaceValidationMixin
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
|
|
@ -28,6 +29,7 @@ __all__ = (
|
|||
'InterfaceTemplate',
|
||||
'InventoryItemTemplate',
|
||||
'ModuleBayTemplate',
|
||||
'PortTemplateMapping',
|
||||
'PowerOutletTemplate',
|
||||
'PowerPortTemplate',
|
||||
'RearPortTemplate',
|
||||
|
|
@ -518,6 +520,53 @@ class InterfaceTemplate(InterfaceValidationMixin, ModularComponentTemplateModel)
|
|||
}
|
||||
|
||||
|
||||
class PortTemplateMapping(PortMappingBase):
|
||||
"""
|
||||
Maps a FrontPortTemplate & position to a RearPortTemplate & position.
|
||||
"""
|
||||
device_type = models.ForeignKey(
|
||||
to='dcim.DeviceType',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='port_mappings',
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
module_type = models.ForeignKey(
|
||||
to='dcim.ModuleType',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='port_mappings',
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
front_port = models.ForeignKey(
|
||||
to='dcim.FrontPortTemplate',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='mappings',
|
||||
)
|
||||
rear_port = models.ForeignKey(
|
||||
to='dcim.RearPortTemplate',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='mappings',
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate rear port assignment
|
||||
if self.front_port.device_type_id != self.rear_port.device_type_id:
|
||||
raise ValidationError({
|
||||
"rear_port": _("Rear port ({rear_port}) must belong to the same device type").format(
|
||||
rear_port=self.rear_port
|
||||
)
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Associate the mapping with the parent DeviceType/ModuleType
|
||||
self.device_type = self.front_port.device_type
|
||||
self.module_type = self.front_port.module_type
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class FrontPortTemplate(ModularComponentTemplateModel):
|
||||
"""
|
||||
Template for a pass-through port on the front of a new Device.
|
||||
|
|
@ -531,18 +580,13 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
rear_port = models.ForeignKey(
|
||||
to='dcim.RearPortTemplate',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='frontport_templates'
|
||||
)
|
||||
rear_port_position = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('rear port position'),
|
||||
positions = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('positions'),
|
||||
default=1,
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
]
|
||||
MinValueValidator(PORT_POSITION_MIN),
|
||||
MaxValueValidator(PORT_POSITION_MAX)
|
||||
],
|
||||
)
|
||||
|
||||
component_model = FrontPort
|
||||
|
|
@ -557,10 +601,6 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||
fields=('module_type', 'name'),
|
||||
name='%(app_label)s_%(class)s_unique_module_type_name'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('rear_port', 'rear_port_position'),
|
||||
name='%(app_label)s_%(class)s_unique_rear_port_position'
|
||||
),
|
||||
)
|
||||
verbose_name = _('front port template')
|
||||
verbose_name_plural = _('front port templates')
|
||||
|
|
@ -568,40 +608,23 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
try:
|
||||
|
||||
# Validate rear port assignment
|
||||
if self.rear_port.device_type != self.device_type:
|
||||
raise ValidationError(
|
||||
_("Rear port ({name}) must belong to the same device type").format(name=self.rear_port)
|
||||
)
|
||||
|
||||
# Validate rear port position assignment
|
||||
if self.rear_port_position > self.rear_port.positions:
|
||||
raise ValidationError(
|
||||
_("Invalid rear port position ({position}); rear port {name} has only {count} positions").format(
|
||||
position=self.rear_port_position,
|
||||
name=self.rear_port.name,
|
||||
count=self.rear_port.positions
|
||||
)
|
||||
)
|
||||
|
||||
except RearPortTemplate.DoesNotExist:
|
||||
pass
|
||||
# Check that positions is greater than or equal to the number of associated RearPortTemplates
|
||||
if not self._state.adding:
|
||||
mapping_count = self.mappings.count()
|
||||
if self.positions < mapping_count:
|
||||
raise ValidationError({
|
||||
"positions": _(
|
||||
"The number of positions cannot be less than the number of mapped rear port templates ({count})"
|
||||
).format(count=mapping_count)
|
||||
})
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
if self.rear_port:
|
||||
rear_port_name = self.rear_port.resolve_name(kwargs.get('module'))
|
||||
rear_port = RearPort.objects.get(name=rear_port_name, **kwargs)
|
||||
else:
|
||||
rear_port = None
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
color=self.color,
|
||||
rear_port=rear_port,
|
||||
rear_port_position=self.rear_port_position,
|
||||
positions=self.positions,
|
||||
**kwargs
|
||||
)
|
||||
instantiate.do_not_call_in_templates = True
|
||||
|
|
@ -611,8 +634,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||
'name': self.name,
|
||||
'type': self.type,
|
||||
'color': self.color,
|
||||
'rear_port': self.rear_port.name,
|
||||
'rear_port_position': self.rear_port_position,
|
||||
'positions': self.positions,
|
||||
'label': self.label,
|
||||
'description': self.description,
|
||||
}
|
||||
|
|
@ -635,9 +657,9 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||
verbose_name=_('positions'),
|
||||
default=1,
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
]
|
||||
MinValueValidator(PORT_POSITION_MIN),
|
||||
MaxValueValidator(PORT_POSITION_MAX)
|
||||
],
|
||||
)
|
||||
|
||||
component_model = RearPort
|
||||
|
|
@ -646,6 +668,20 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||
verbose_name = _('rear port template')
|
||||
verbose_name_plural = _('rear port templates')
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check that positions is greater than or equal to the number of associated FrontPortTemplates
|
||||
if not self._state.adding:
|
||||
mapping_count = self.mappings.count()
|
||||
if self.positions < mapping_count:
|
||||
raise ValidationError({
|
||||
"positions": _(
|
||||
"The number of positions cannot be less than the number of mapped front port templates "
|
||||
"({count})"
|
||||
).format(count=mapping_count)
|
||||
})
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
|
|
@ -687,8 +723,8 @@ class ModuleBayTemplate(ModularComponentTemplateModel):
|
|||
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.name,
|
||||
label=self.label,
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
position=self.position,
|
||||
**kwargs
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from mptt.models import MPTTModel, TreeForeignKey
|
|||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.fields import WWNField
|
||||
from dcim.models.base import PortMappingBase
|
||||
from dcim.models.mixins import InterfaceValidationMixin
|
||||
from netbox.choices import ColorChoices
|
||||
from netbox.models import OrganizationalModel, NetBoxModel
|
||||
|
|
@ -35,6 +36,7 @@ __all__ = (
|
|||
'InventoryItemRole',
|
||||
'ModuleBay',
|
||||
'PathEndpoint',
|
||||
'PortMapping',
|
||||
'PowerOutlet',
|
||||
'PowerPort',
|
||||
'RearPort',
|
||||
|
|
@ -208,10 +210,6 @@ class CabledObjectModel(models.Model):
|
|||
raise ValidationError({
|
||||
"cable_end": _("Must specify cable end (A or B) when attaching a cable.")
|
||||
})
|
||||
if not self.cable_position:
|
||||
raise ValidationError({
|
||||
"cable_position": _("Must specify cable termination position when attaching a cable.")
|
||||
})
|
||||
if self.cable_end and not self.cable:
|
||||
raise ValidationError({
|
||||
"cable_end": _("Cable end must not be set without a cable.")
|
||||
|
|
@ -1069,6 +1067,43 @@ class Interface(
|
|||
# Pass-through ports
|
||||
#
|
||||
|
||||
class PortMapping(PortMappingBase):
|
||||
"""
|
||||
Maps a FrontPort & position to a RearPort & position.
|
||||
"""
|
||||
device = models.ForeignKey(
|
||||
to='dcim.Device',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='port_mappings',
|
||||
)
|
||||
front_port = models.ForeignKey(
|
||||
to='dcim.FrontPort',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='mappings',
|
||||
)
|
||||
rear_port = models.ForeignKey(
|
||||
to='dcim.RearPort',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='mappings',
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Both ports must belong to the same device
|
||||
if self.front_port.device_id != self.rear_port.device_id:
|
||||
raise ValidationError({
|
||||
"rear_port": _("Rear port ({rear_port}) must belong to the same device").format(
|
||||
rear_port=self.rear_port
|
||||
)
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Associate the mapping with the parent Device
|
||||
self.device = self.front_port.device
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
"""
|
||||
A pass-through port on the front of a Device.
|
||||
|
|
@ -1082,22 +1117,16 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
rear_port = models.ForeignKey(
|
||||
to='dcim.RearPort',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='frontports'
|
||||
)
|
||||
rear_port_position = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('rear port position'),
|
||||
positions = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('positions'),
|
||||
default=1,
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
MinValueValidator(PORT_POSITION_MIN),
|
||||
MaxValueValidator(PORT_POSITION_MAX)
|
||||
],
|
||||
help_text=_('Mapped position on corresponding rear port')
|
||||
)
|
||||
|
||||
clone_fields = ('device', 'type', 'color')
|
||||
clone_fields = ('device', 'type', 'color', 'positions')
|
||||
|
||||
class Meta(ModularComponentModel.Meta):
|
||||
constraints = (
|
||||
|
|
@ -1105,10 +1134,6 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||
fields=('device', 'name'),
|
||||
name='%(app_label)s_%(class)s_unique_device_name'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('rear_port', 'rear_port_position'),
|
||||
name='%(app_label)s_%(class)s_unique_rear_port_position'
|
||||
),
|
||||
)
|
||||
verbose_name = _('front port')
|
||||
verbose_name_plural = _('front ports')
|
||||
|
|
@ -1116,27 +1141,14 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if hasattr(self, 'rear_port'):
|
||||
|
||||
# Validate rear port assignment
|
||||
if self.rear_port.device != self.device:
|
||||
# Check that positions is greater than or equal to the number of associated RearPorts
|
||||
if not self._state.adding:
|
||||
mapping_count = self.mappings.count()
|
||||
if self.positions < mapping_count:
|
||||
raise ValidationError({
|
||||
"rear_port": _(
|
||||
"Rear port ({rear_port}) must belong to the same device"
|
||||
).format(rear_port=self.rear_port)
|
||||
})
|
||||
|
||||
# Validate rear port position assignment
|
||||
if self.rear_port_position > self.rear_port.positions:
|
||||
raise ValidationError({
|
||||
"rear_port_position": _(
|
||||
"Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
|
||||
"positions."
|
||||
).format(
|
||||
rear_port_position=self.rear_port_position,
|
||||
name=self.rear_port.name,
|
||||
positions=self.rear_port.positions
|
||||
)
|
||||
"positions": _(
|
||||
"The number of positions cannot be less than the number of mapped rear ports ({count})"
|
||||
).format(count=mapping_count)
|
||||
})
|
||||
|
||||
|
||||
|
|
@ -1157,11 +1169,11 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||
verbose_name=_('positions'),
|
||||
default=1,
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
MinValueValidator(PORT_POSITION_MIN),
|
||||
MaxValueValidator(PORT_POSITION_MAX)
|
||||
],
|
||||
help_text=_('Number of front ports which may be mapped')
|
||||
)
|
||||
|
||||
clone_fields = ('device', 'type', 'color', 'positions')
|
||||
|
||||
class Meta(ModularComponentModel.Meta):
|
||||
|
|
@ -1173,13 +1185,13 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||
|
||||
# Check that positions count is greater than or equal to the number of associated FrontPorts
|
||||
if not self._state.adding:
|
||||
frontport_count = self.frontports.count()
|
||||
if self.positions < frontport_count:
|
||||
mapping_count = self.mappings.count()
|
||||
if self.positions < mapping_count:
|
||||
raise ValidationError({
|
||||
"positions": _(
|
||||
"The number of positions cannot be less than the number of mapped front ports "
|
||||
"({frontport_count})"
|
||||
).format(frontport_count=frontport_count)
|
||||
"({count})"
|
||||
).format(count=mapping_count)
|
||||
})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import decimal
|
||||
import yaml
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
import yaml
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
|
|
@ -19,14 +18,14 @@ from django.utils.translation import gettext_lazy as _
|
|||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.fields import MACAddressField
|
||||
from dcim.utils import update_interface_bridges
|
||||
from dcim.utils import create_port_mappings, update_interface_bridges
|
||||
from extras.models import ConfigContextModel, CustomField
|
||||
from extras.querysets import ConfigContextModelQuerySet
|
||||
from netbox.choices import ColorChoices
|
||||
from netbox.config import ConfigItem
|
||||
from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models.mixins import WeightMixin
|
||||
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
||||
from netbox.models.mixins import WeightMixin
|
||||
from utilities.fields import ColorField, CounterCacheField
|
||||
from utilities.prefetch import get_prefetchable_fields
|
||||
from utilities.tracking import TrackingModelMixin
|
||||
|
|
@ -34,7 +33,6 @@ from .device_components import *
|
|||
from .mixins import RenderConfigMixin
|
||||
from .modules import Module
|
||||
|
||||
|
||||
__all__ = (
|
||||
'Device',
|
||||
'DeviceRole',
|
||||
|
|
@ -650,7 +648,10 @@ class Device(
|
|||
decimal_places=6,
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(-90.0), MaxValueValidator(90.0)],
|
||||
validators=[
|
||||
MinValueValidator(decimal.Decimal('-90.0')),
|
||||
MaxValueValidator(decimal.Decimal('90.0'))
|
||||
],
|
||||
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
|
||||
)
|
||||
longitude = models.DecimalField(
|
||||
|
|
@ -659,7 +660,10 @@ class Device(
|
|||
decimal_places=6,
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(-180.0), MaxValueValidator(180.0)],
|
||||
validators=[
|
||||
MinValueValidator(decimal.Decimal('-180.0')),
|
||||
MaxValueValidator(decimal.Decimal('180.0'))
|
||||
],
|
||||
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
|
||||
)
|
||||
services = GenericRelation(
|
||||
|
|
@ -1003,6 +1007,8 @@ class Device(
|
|||
self._instantiate_components(self.device_type.interfacetemplates.all())
|
||||
self._instantiate_components(self.device_type.rearporttemplates.all())
|
||||
self._instantiate_components(self.device_type.frontporttemplates.all())
|
||||
# Replicate any front/rear port mappings from the DeviceType
|
||||
create_port_mappings(self, self.device_type)
|
||||
# Disable bulk_create to accommodate MPTT
|
||||
self._instantiate_components(self.device_type.modulebaytemplates.all(), bulk_create=False)
|
||||
self._instantiate_components(self.device_type.devicebaytemplates.all())
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from django.utils.translation import gettext_lazy as _
|
|||
from jsonschema.exceptions import ValidationError as JSONValidationError
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import MODULE_TOKEN
|
||||
from dcim.utils import update_interface_bridges
|
||||
from extras.models import ConfigContextModel, CustomField
|
||||
from netbox.models import PrimaryModel
|
||||
|
|
@ -337,7 +336,6 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
|
|||
else:
|
||||
# ModuleBays must be saved individually for MPTT
|
||||
for instance in create_instances:
|
||||
instance.name = instance.name.replace(MODULE_TOKEN, str(self.module_bay.position))
|
||||
instance.save()
|
||||
|
||||
update_fields = ['module']
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import decimal
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
|
|
@ -211,7 +213,10 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
|||
decimal_places=6,
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(-90.0), MaxValueValidator(90.0)],
|
||||
validators=[
|
||||
MinValueValidator(decimal.Decimal('-90.0')),
|
||||
MaxValueValidator(decimal.Decimal('90.0'))
|
||||
],
|
||||
help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
|
||||
)
|
||||
longitude = models.DecimalField(
|
||||
|
|
@ -220,7 +225,10 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
|||
decimal_places=6,
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(-180.0), MaxValueValidator(180.0)],
|
||||
validators=[
|
||||
MinValueValidator(decimal.Decimal('-180.0')),
|
||||
MaxValueValidator(decimal.Decimal('180.0'))
|
||||
],
|
||||
help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
|
||||
from django.db.models import Q
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
|
|
@ -7,7 +8,7 @@ from dcim.choices import CableEndChoices, LinkStatusChoices
|
|||
from virtualization.models import VMInterface
|
||||
from .models import (
|
||||
Cable, CablePath, CableTermination, ConsolePort, ConsoleServerPort, Device, DeviceBay, FrontPort, Interface,
|
||||
InventoryItem, ModuleBay, PathEndpoint, PowerOutlet, PowerPanel, PowerPort, Rack, RearPort, Location,
|
||||
InventoryItem, ModuleBay, PathEndpoint, PortMapping, PowerOutlet, PowerPanel, PowerPort, Rack, RearPort, Location,
|
||||
VirtualChassis,
|
||||
)
|
||||
from .models.cables import trace_paths
|
||||
|
|
@ -135,6 +136,17 @@ def retrace_cable_paths(instance, **kwargs):
|
|||
cablepath.retrace()
|
||||
|
||||
|
||||
@receiver((post_delete, post_save), sender=PortMapping)
|
||||
def update_passthrough_port_paths(instance, **kwargs):
|
||||
"""
|
||||
When a PortMapping is created or deleted, retrace any CablePaths which traverse its front and/or rear ports.
|
||||
"""
|
||||
for cablepath in CablePath.objects.filter(
|
||||
Q(_nodes__contains=instance.front_port) | Q(_nodes__contains=instance.rear_port)
|
||||
):
|
||||
cablepath.retrace()
|
||||
|
||||
|
||||
@receiver(post_delete, sender=CableTermination)
|
||||
def nullify_connected_endpoints(instance, **kwargs):
|
||||
"""
|
||||
|
|
@ -150,17 +162,6 @@ def nullify_connected_endpoints(instance, **kwargs):
|
|||
cablepath.retrace()
|
||||
|
||||
|
||||
@receiver(post_save, sender=FrontPort)
|
||||
def extend_rearport_cable_paths(instance, created, raw, **kwargs):
|
||||
"""
|
||||
When a new FrontPort is created, add it to any CablePaths which end at its corresponding RearPort.
|
||||
"""
|
||||
if created and not raw:
|
||||
rearport = instance.rear_port
|
||||
for cablepath in CablePath.objects.filter(_nodes__contains=rearport):
|
||||
cablepath.retrace()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Interface)
|
||||
@receiver(post_save, sender=VMInterface)
|
||||
def update_mac_address_interface(instance, created, raw, **kwargs):
|
||||
|
|
|
|||
|
|
@ -749,12 +749,9 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
|
|||
color = columns.ColorColumn(
|
||||
verbose_name=_('Color'),
|
||||
)
|
||||
rear_port_position = tables.Column(
|
||||
verbose_name=_('Position')
|
||||
)
|
||||
rear_port = tables.Column(
|
||||
verbose_name=_('Rear Port'),
|
||||
linkify=True
|
||||
mappings = columns.ManyToManyColumn(
|
||||
verbose_name=_('Mappings'),
|
||||
transform=lambda obj: f'{obj.rear_port}:{obj.rear_port_position}'
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:frontport_list'
|
||||
|
|
@ -763,12 +760,12 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
|
|||
class Meta(DeviceComponentTable.Meta):
|
||||
model = models.FrontPort
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'rear_port',
|
||||
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer',
|
||||
'inventory_items', 'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings',
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created',
|
||||
'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||
'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'mappings', 'description',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -786,11 +783,11 @@ class DeviceFrontPortTable(FrontPortTable):
|
|||
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
|
||||
model = models.FrontPort
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'rear_port', 'rear_port_position',
|
||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings',
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'link_peer',
|
||||
'pk', 'name', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'cable', 'link_peer',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -805,6 +802,10 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable):
|
|||
color = columns.ColorColumn(
|
||||
verbose_name=_('Color'),
|
||||
)
|
||||
mappings = columns.ManyToManyColumn(
|
||||
verbose_name=_('Mappings'),
|
||||
transform=lambda obj: f'{obj.front_port}:{obj.front_port_position}'
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:rearport_list'
|
||||
)
|
||||
|
|
@ -812,10 +813,13 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable):
|
|||
class Meta(DeviceComponentTable.Meta):
|
||||
model = models.RearPort
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'description',
|
||||
'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings',
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created',
|
||||
'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'mappings', 'description',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description')
|
||||
|
||||
|
||||
class DeviceRearPortTable(RearPortTable):
|
||||
|
|
@ -832,11 +836,11 @@ class DeviceRearPortTable(RearPortTable):
|
|||
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
|
||||
model = models.RearPort
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'positions', 'description', 'mark_connected',
|
||||
'cable', 'cable_color', 'link_peer', 'tags', 'actions',
|
||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings',
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer',
|
||||
'pk', 'name', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'cable', 'link_peer',
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -250,12 +250,13 @@ class InterfaceTemplateTable(ComponentTemplateTable):
|
|||
|
||||
|
||||
class FrontPortTemplateTable(ComponentTemplateTable):
|
||||
rear_port_position = tables.Column(
|
||||
verbose_name=_('Position')
|
||||
)
|
||||
color = columns.ColorColumn(
|
||||
verbose_name=_('Color'),
|
||||
)
|
||||
mappings = columns.ManyToManyColumn(
|
||||
verbose_name=_('Mappings'),
|
||||
transform=lambda obj: f'{obj.rear_port}:{obj.rear_port_position}'
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
actions=('edit', 'delete'),
|
||||
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||
|
|
@ -263,7 +264,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
|||
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = models.FrontPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
|
|
@ -271,6 +272,10 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
|||
color = columns.ColorColumn(
|
||||
verbose_name=_('Color'),
|
||||
)
|
||||
mappings = columns.ManyToManyColumn(
|
||||
verbose_name=_('Mappings'),
|
||||
transform=lambda obj: f'{obj.front_port}:{obj.front_port_position}'
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
actions=('edit', 'delete'),
|
||||
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||
|
|
@ -278,7 +283,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
|||
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = models.RearPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'description', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -973,72 +973,99 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2', type=PortTypeChoices.TYPE_8P8C),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3', type=PortTypeChoices.TYPE_8P8C),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 4', type=PortTypeChoices.TYPE_8P8C),
|
||||
RearPortTemplate(module_type=moduletype, name='Rear Port Template 5', type=PortTypeChoices.TYPE_8P8C),
|
||||
RearPortTemplate(module_type=moduletype, name='Rear Port Template 6', type=PortTypeChoices.TYPE_8P8C),
|
||||
RearPortTemplate(module_type=moduletype, name='Rear Port Template 7', type=PortTypeChoices.TYPE_8P8C),
|
||||
RearPortTemplate(module_type=moduletype, name='Rear Port Template 8', type=PortTypeChoices.TYPE_8P8C),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 5', type=PortTypeChoices.TYPE_8P8C),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 6', type=PortTypeChoices.TYPE_8P8C),
|
||||
)
|
||||
RearPortTemplate.objects.bulk_create(rear_port_templates)
|
||||
|
||||
front_port_templates = (
|
||||
FrontPortTemplate(
|
||||
device_type=devicetype,
|
||||
name='Front Port Template 1',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
rear_port=rear_port_templates[0]
|
||||
),
|
||||
FrontPortTemplate(
|
||||
device_type=devicetype,
|
||||
name='Front Port Template 2',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
rear_port=rear_port_templates[1]
|
||||
),
|
||||
FrontPortTemplate(
|
||||
module_type=moduletype,
|
||||
name='Front Port Template 5',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
rear_port=rear_port_templates[4]
|
||||
),
|
||||
FrontPortTemplate(
|
||||
module_type=moduletype,
|
||||
name='Front Port Template 6',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
rear_port=rear_port_templates[5]
|
||||
),
|
||||
FrontPortTemplate(device_type=devicetype, name='Front Port Template 1', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPortTemplate(device_type=devicetype, name='Front Port Template 2', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPortTemplate(module_type=moduletype, name='Front Port Template 3', type=PortTypeChoices.TYPE_8P8C),
|
||||
)
|
||||
FrontPortTemplate.objects.bulk_create(front_port_templates)
|
||||
PortTemplateMapping.objects.bulk_create([
|
||||
PortTemplateMapping(
|
||||
device_type=devicetype,
|
||||
front_port=front_port_templates[0],
|
||||
rear_port=rear_port_templates[0],
|
||||
),
|
||||
PortTemplateMapping(
|
||||
device_type=devicetype,
|
||||
front_port=front_port_templates[1],
|
||||
rear_port=rear_port_templates[1],
|
||||
),
|
||||
PortTemplateMapping(
|
||||
module_type=moduletype,
|
||||
front_port=front_port_templates[2],
|
||||
rear_port=rear_port_templates[2],
|
||||
),
|
||||
])
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Front Port Template 3',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': rear_port_templates[2].pk,
|
||||
'rear_port_position': 1,
|
||||
'rear_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'rear_port': rear_port_templates[3].pk,
|
||||
'rear_port_position': 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Front Port Template 4',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': rear_port_templates[3].pk,
|
||||
'rear_port_position': 1,
|
||||
'rear_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'rear_port': rear_port_templates[4].pk,
|
||||
'rear_port_position': 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'module_type': moduletype.pk,
|
||||
'name': 'Front Port Template 7',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': rear_port_templates[6].pk,
|
||||
'rear_port_position': 1,
|
||||
},
|
||||
{
|
||||
'module_type': moduletype.pk,
|
||||
'name': 'Front Port Template 8',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': rear_port_templates[7].pk,
|
||||
'rear_port_position': 1,
|
||||
'rear_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'rear_port': rear_port_templates[5].pk,
|
||||
'rear_port_position': 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
cls.update_data = {
|
||||
'type': PortTypeChoices.TYPE_LC,
|
||||
'rear_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'rear_port': rear_port_templates[3].pk,
|
||||
'rear_port_position': 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def test_update_object(self):
|
||||
super().test_update_object()
|
||||
|
||||
# Check that the port mapping was updated after modifying the front port template
|
||||
front_port_template = FrontPortTemplate.objects.get(name='Front Port Template 1')
|
||||
rear_port_template = RearPortTemplate.objects.get(name='Rear Port Template 4')
|
||||
self.assertTrue(
|
||||
PortTemplateMapping.objects.filter(
|
||||
front_port=front_port_template,
|
||||
front_port_position=1,
|
||||
rear_port=rear_port_template,
|
||||
rear_port_position=1,
|
||||
).exists()
|
||||
)
|
||||
|
||||
|
||||
class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RearPortTemplate
|
||||
|
|
@ -1057,36 +1084,104 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||
manufacturer=manufacturer, model='Module Type 1'
|
||||
)
|
||||
|
||||
front_port_templates = (
|
||||
FrontPortTemplate(device_type=devicetype, name='Front Port Template 1', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPortTemplate(device_type=devicetype, name='Front Port Template 2', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPortTemplate(module_type=moduletype, name='Front Port Template 3', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPortTemplate(module_type=moduletype, name='Front Port Template 4', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPortTemplate(module_type=moduletype, name='Front Port Template 5', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPortTemplate(module_type=moduletype, name='Front Port Template 6', type=PortTypeChoices.TYPE_8P8C),
|
||||
)
|
||||
FrontPortTemplate.objects.bulk_create(front_port_templates)
|
||||
rear_port_templates = (
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1', type=PortTypeChoices.TYPE_8P8C),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2', type=PortTypeChoices.TYPE_8P8C),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3', type=PortTypeChoices.TYPE_8P8C),
|
||||
)
|
||||
RearPortTemplate.objects.bulk_create(rear_port_templates)
|
||||
PortTemplateMapping.objects.bulk_create([
|
||||
PortTemplateMapping(
|
||||
device_type=devicetype,
|
||||
front_port=front_port_templates[0],
|
||||
rear_port=rear_port_templates[0],
|
||||
),
|
||||
PortTemplateMapping(
|
||||
device_type=devicetype,
|
||||
front_port=front_port_templates[1],
|
||||
rear_port=rear_port_templates[1],
|
||||
),
|
||||
PortTemplateMapping(
|
||||
module_type=moduletype,
|
||||
front_port=front_port_templates[2],
|
||||
rear_port=rear_port_templates[2],
|
||||
),
|
||||
])
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Rear Port Template 4',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'front_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'front_port': front_port_templates[3].pk,
|
||||
'front_port_position': 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Rear Port Template 5',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'front_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'front_port': front_port_templates[4].pk,
|
||||
'front_port_position': 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'module_type': moduletype.pk,
|
||||
'name': 'Rear Port Template 6',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
},
|
||||
{
|
||||
'module_type': moduletype.pk,
|
||||
'name': 'Rear Port Template 7',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'front_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'front_port': front_port_templates[5].pk,
|
||||
'front_port_position': 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
cls.update_data = {
|
||||
'type': PortTypeChoices.TYPE_LC,
|
||||
'front_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'front_port': front_port_templates[3].pk,
|
||||
'front_port_position': 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def test_update_object(self):
|
||||
super().test_update_object()
|
||||
|
||||
# Check that the port mapping was updated after modifying the rear port template
|
||||
front_port_template = FrontPortTemplate.objects.get(name='Front Port Template 4')
|
||||
rear_port_template = RearPortTemplate.objects.get(name='Rear Port Template 1')
|
||||
self.assertTrue(
|
||||
PortTemplateMapping.objects.filter(
|
||||
front_port=front_port_template,
|
||||
front_port_position=1,
|
||||
rear_port=rear_port_template,
|
||||
rear_port_position=1,
|
||||
).exists()
|
||||
)
|
||||
|
||||
|
||||
class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ModuleBayTemplate
|
||||
|
|
@ -2015,51 +2110,90 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
|||
RearPort(device=device, name='Rear Port 6', type=PortTypeChoices.TYPE_8P8C),
|
||||
)
|
||||
RearPort.objects.bulk_create(rear_ports)
|
||||
|
||||
front_ports = (
|
||||
FrontPort(device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]),
|
||||
FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]),
|
||||
FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[2]),
|
||||
FrontPort(device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C),
|
||||
)
|
||||
FrontPort.objects.bulk_create(front_ports)
|
||||
PortMapping.objects.bulk_create([
|
||||
PortMapping(device=device, front_port=front_ports[0], rear_port=rear_ports[0]),
|
||||
PortMapping(device=device, front_port=front_ports[1], rear_port=rear_ports[1]),
|
||||
PortMapping(device=device, front_port=front_ports[2], rear_port=rear_ports[2]),
|
||||
])
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Front Port 4',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': rear_ports[3].pk,
|
||||
'rear_port_position': 1,
|
||||
'rear_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'rear_port': rear_ports[3].pk,
|
||||
'rear_port_position': 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Front Port 5',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': rear_ports[4].pk,
|
||||
'rear_port_position': 1,
|
||||
'rear_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'rear_port': rear_ports[4].pk,
|
||||
'rear_port_position': 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Front Port 6',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': rear_ports[5].pk,
|
||||
'rear_port_position': 1,
|
||||
'rear_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'rear_port': rear_ports[5].pk,
|
||||
'rear_port_position': 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
cls.update_data = {
|
||||
'type': PortTypeChoices.TYPE_LC,
|
||||
'rear_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'rear_port': rear_ports[3].pk,
|
||||
'rear_port_position': 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def test_update_object(self):
|
||||
super().test_update_object()
|
||||
|
||||
# Check that the port mapping was updated after modifying the front port
|
||||
front_port = FrontPort.objects.get(name='Front Port 1')
|
||||
rear_port = RearPort.objects.get(name='Rear Port 4')
|
||||
self.assertTrue(
|
||||
PortMapping.objects.filter(
|
||||
front_port=front_port,
|
||||
front_port_position=1,
|
||||
rear_port=rear_port,
|
||||
rear_port_position=1,
|
||||
).exists()
|
||||
)
|
||||
|
||||
@tag('regression') # Issue #18991
|
||||
def test_front_port_paths(self):
|
||||
device = Device.objects.first()
|
||||
rear_port = RearPort.objects.create(
|
||||
device=device, name='Rear Port 10', type=PortTypeChoices.TYPE_8P8C
|
||||
)
|
||||
interface1 = Interface.objects.create(device=device, name='Interface 1')
|
||||
front_port = FrontPort.objects.create(
|
||||
device=device,
|
||||
name='Rear Port 10',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
rear_port=rear_port,
|
||||
)
|
||||
rear_port = RearPort.objects.create(device=device, name='Rear Port 10', type=PortTypeChoices.TYPE_8P8C)
|
||||
front_port = FrontPort.objects.create(device=device, name='Front Port 10', type=PortTypeChoices.TYPE_8P8C)
|
||||
PortMapping.objects.create(device=device, front_port=front_port, rear_port=rear_port)
|
||||
Cable.objects.create(a_terminations=[interface1], b_terminations=[front_port])
|
||||
|
||||
self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
|
||||
|
|
@ -2086,6 +2220,15 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
|
|||
role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
|
||||
device = Device.objects.create(device_type=devicetype, role=role, name='Device 1', site=site)
|
||||
|
||||
front_ports = (
|
||||
FrontPort(device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPort(device=device, name='Front Port 4', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPort(device=device, name='Front Port 5', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPort(device=device, name='Front Port 6', type=PortTypeChoices.TYPE_8P8C),
|
||||
)
|
||||
FrontPort.objects.bulk_create(front_ports)
|
||||
rear_ports = (
|
||||
RearPort(device=device, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C),
|
||||
RearPort(device=device, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||
|
|
@ -2098,19 +2241,66 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
|
|||
'device': device.pk,
|
||||
'name': 'Rear Port 4',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'front_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'front_port': front_ports[3].pk,
|
||||
'front_port_position': 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Rear Port 5',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'front_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'front_port': front_ports[4].pk,
|
||||
'front_port_position': 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Rear Port 6',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'front_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'front_port': front_ports[5].pk,
|
||||
'front_port_position': 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
cls.update_data = {
|
||||
'type': PortTypeChoices.TYPE_LC,
|
||||
'front_ports': [
|
||||
{
|
||||
'position': 1,
|
||||
'front_port': front_ports[3].pk,
|
||||
'front_port_position': 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def test_update_object(self):
|
||||
super().test_update_object()
|
||||
|
||||
# Check that the port mapping was updated after modifying the rear port
|
||||
front_port = FrontPort.objects.get(name='Front Port 4')
|
||||
rear_port = RearPort.objects.get(name='Rear Port 1')
|
||||
self.assertTrue(
|
||||
PortMapping.objects.filter(
|
||||
front_port=front_port,
|
||||
front_port_position=1,
|
||||
rear_port=rear_port,
|
||||
rear_port_position=1,
|
||||
).exists()
|
||||
)
|
||||
|
||||
@tag('regression') # Issue #18991
|
||||
def test_rear_port_paths(self):
|
||||
device = Device.objects.first()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,5 +1,3 @@
|
|||
from unittest import skipIf
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from dcim.choices import CableProfileChoices
|
||||
from dcim.models import *
|
||||
|
|
@ -363,13 +361,17 @@ class CablePathTests(CablePathTestCase):
|
|||
Interface.objects.create(device=self.device, name='Interface 3'),
|
||||
Interface.objects.create(device=self.device, name='Interface 4'),
|
||||
]
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
|
||||
frontport1 = FrontPort.objects.create(
|
||||
device=self.device,
|
||||
name='Front Port 1',
|
||||
rear_port=rearport1,
|
||||
rear_port_position=1
|
||||
)
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
|
||||
frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
|
||||
PortMapping.objects.bulk_create([
|
||||
PortMapping(
|
||||
device=self.device,
|
||||
front_port=frontport1,
|
||||
front_port_position=1,
|
||||
rear_port=rearport1,
|
||||
rear_port_position=1,
|
||||
),
|
||||
])
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
|
|
@ -439,18 +441,40 @@ class CablePathTests(CablePathTestCase):
|
|||
]
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4)
|
||||
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4)
|
||||
frontport1_1 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1
|
||||
)
|
||||
frontport1_2 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2
|
||||
)
|
||||
frontport2_1 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1
|
||||
)
|
||||
frontport2_2 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2
|
||||
)
|
||||
frontport1_1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1')
|
||||
frontport1_2 = FrontPort.objects.create(device=self.device, name='Front Port 1:2')
|
||||
frontport2_1 = FrontPort.objects.create(device=self.device, name='Front Port 2:1')
|
||||
frontport2_2 = FrontPort.objects.create(device=self.device, name='Front Port 2:2')
|
||||
PortMapping.objects.bulk_create([
|
||||
PortMapping(
|
||||
device=self.device,
|
||||
front_port=frontport1_1,
|
||||
front_port_position=1,
|
||||
rear_port=rearport1,
|
||||
rear_port_position=1,
|
||||
),
|
||||
PortMapping(
|
||||
device=self.device,
|
||||
front_port=frontport1_2,
|
||||
front_port_position=1,
|
||||
rear_port=rearport1,
|
||||
rear_port_position=2,
|
||||
),
|
||||
PortMapping(
|
||||
device=self.device,
|
||||
front_port=frontport2_1,
|
||||
front_port_position=1,
|
||||
rear_port=rearport2,
|
||||
rear_port_position=1,
|
||||
),
|
||||
PortMapping(
|
||||
device=self.device,
|
||||
front_port=frontport2_2,
|
||||
front_port_position=1,
|
||||
rear_port=rearport2,
|
||||
rear_port_position=2,
|
||||
),
|
||||
])
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
|
|
@ -654,25 +678,47 @@ class CablePathTests(CablePathTestCase):
|
|||
Interface.objects.create(device=self.device, name='Interface 2'),
|
||||
]
|
||||
rear_ports = [
|
||||
RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1),
|
||||
RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1),
|
||||
RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1),
|
||||
RearPort.objects.create(device=self.device, name='Rear Port 4', positions=1),
|
||||
RearPort.objects.create(device=self.device, name='Rear Port 1'),
|
||||
RearPort.objects.create(device=self.device, name='Rear Port 2'),
|
||||
RearPort.objects.create(device=self.device, name='Rear Port 3'),
|
||||
RearPort.objects.create(device=self.device, name='Rear Port 4'),
|
||||
]
|
||||
front_ports = [
|
||||
FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1
|
||||
),
|
||||
FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1
|
||||
),
|
||||
FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1
|
||||
),
|
||||
FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 4', rear_port=rear_ports[3], rear_port_position=1
|
||||
),
|
||||
FrontPort.objects.create(device=self.device, name='Front Port 1'),
|
||||
FrontPort.objects.create(device=self.device, name='Front Port 2'),
|
||||
FrontPort.objects.create(device=self.device, name='Front Port 3'),
|
||||
FrontPort.objects.create(device=self.device, name='Front Port 4'),
|
||||
]
|
||||
PortMapping.objects.bulk_create([
|
||||
PortMapping(
|
||||
device=self.device,
|
||||
front_port=front_ports[0],
|
||||
front_port_position=1,
|
||||
rear_port=rear_ports[0],
|
||||
rear_port_position=1,
|
||||
),
|
||||
PortMapping(
|
||||
device=self.device,
|
||||
front_port=front_ports[1],
|
||||
front_port_position=1,
|
||||
rear_port=rear_ports[1],
|
||||
rear_port_position=1,
|
||||
),
|
||||
PortMapping(
|
||||
device=self.device,
|
||||
front_port=front_ports[2],
|
||||
front_port_position=1,
|
||||
rear_port=rear_ports[2],
|
||||
rear_port_position=1,
|
||||
),
|
||||
PortMapping(
|
||||
device=self.device,
|
||||
front_port=front_ports[3],
|
||||
front_port_position=1,
|
||||
rear_port=rear_ports[3],
|
||||
rear_port_position=1,
|
||||
),
|
||||
])
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
|
|
@ -723,8 +769,6 @@ class CablePathTests(CablePathTestCase):
|
|||
# Test SVG generation
|
||||
CableTraceSVG(interfaces[0]).render()
|
||||
|
||||
# TODO: Revisit this test under FR #20564
|
||||
@skipIf(True, "Waiting for FR #20564")
|
||||
def test_223_single_path_via_multiple_pass_throughs_with_breakouts(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C2-- [IF3]
|
||||
|
|
@ -736,14 +780,26 @@ class CablePathTests(CablePathTestCase):
|
|||
Interface.objects.create(device=self.device, name='Interface 3'),
|
||||
Interface.objects.create(device=self.device, name='Interface 4'),
|
||||
]
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
|
||||
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1)
|
||||
frontport1 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1
|
||||
)
|
||||
frontport2 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1
|
||||
)
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
|
||||
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2')
|
||||
frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
|
||||
frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2')
|
||||
PortMapping.objects.bulk_create([
|
||||
PortMapping(
|
||||
device=self.device,
|
||||
front_port=frontport1,
|
||||
front_port_position=1,
|
||||
rear_port=rearport1,
|
||||
rear_port_position=1,
|
||||
),
|
||||
PortMapping(
|
||||
device=self.device,
|
||||
front_port=frontport2,
|
||||
front_port_position=1,
|
||||
rear_port=rearport2,
|
||||
rear_port_position=1,
|
||||
),
|
||||
])
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
|
|
@ -761,9 +817,6 @@ class CablePathTests(CablePathTestCase):
|
|||
cable2.clean()
|
||||
cable2.save()
|
||||
|
||||
for path in CablePath.objects.all():
|
||||
print(f'{path}: {path.path_objects}')
|
||||
|
||||
# Validate paths
|
||||
self.assertPathExists(
|
||||
(interfaces[0], cable1, [frontport1, frontport2], [rearport1, rearport2], cable2, interfaces[2]),
|
||||
|
|
@ -786,3 +839,205 @@ class CablePathTests(CablePathTestCase):
|
|||
is_active=True
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 4)
|
||||
|
||||
def test_304_add_port_mapping_between_connected_ports(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C2-- [IF2]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
|
||||
cable1 = Cable(
|
||||
a_terminations=[interface1],
|
||||
b_terminations=[frontport1]
|
||||
)
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
a_terminations=[interface2],
|
||||
b_terminations=[rearport1]
|
||||
)
|
||||
cable2.save()
|
||||
|
||||
# Check for incomplete paths
|
||||
self.assertPathExists(
|
||||
(interface1, cable1, frontport1),
|
||||
is_complete=False,
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
(interface2, cable2, rearport1),
|
||||
is_complete=False,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
# Create a PortMapping between frontport1 and rearport1
|
||||
PortMapping.objects.create(
|
||||
device=self.device,
|
||||
front_port=frontport1,
|
||||
front_port_position=1,
|
||||
rear_port=rearport1,
|
||||
rear_port_position=1,
|
||||
)
|
||||
|
||||
# Check that paths are now complete
|
||||
self.assertPathExists(
|
||||
(interface1, cable1, frontport1, rearport1, cable2, interface2),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
(interface2, cable2, rearport1, frontport1, cable1, interface1),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
def test_305_delete_port_mapping_between_connected_ports(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C2-- [IF2]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
|
||||
cable1 = Cable(
|
||||
a_terminations=[interface1],
|
||||
b_terminations=[frontport1]
|
||||
)
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
a_terminations=[interface2],
|
||||
b_terminations=[rearport1]
|
||||
)
|
||||
cable2.save()
|
||||
portmapping1 = PortMapping.objects.create(
|
||||
device=self.device,
|
||||
front_port=frontport1,
|
||||
front_port_position=1,
|
||||
rear_port=rearport1,
|
||||
rear_port_position=1,
|
||||
)
|
||||
|
||||
# Check for complete paths
|
||||
self.assertPathExists(
|
||||
(interface1, cable1, frontport1, rearport1, cable2, interface2),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
(interface2, cable2, rearport1, frontport1, cable1, interface1),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
# Delete the PortMapping between frontport1 and rearport1
|
||||
portmapping1.delete()
|
||||
|
||||
# Check that paths are no longer complete
|
||||
self.assertPathExists(
|
||||
(interface1, cable1, frontport1),
|
||||
is_complete=False,
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
(interface2, cable2, rearport1),
|
||||
is_complete=False,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
def test_306_change_port_mapping_between_connected_ports(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C3-- [IF3]
|
||||
[IF2] --C2-- [FP2] [RP3] --C4-- [IF4]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
|
||||
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
|
||||
frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
|
||||
frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2')
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
|
||||
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2')
|
||||
cable1 = Cable(
|
||||
a_terminations=[interface1],
|
||||
b_terminations=[frontport1]
|
||||
)
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
a_terminations=[interface2],
|
||||
b_terminations=[frontport2]
|
||||
)
|
||||
cable2.save()
|
||||
cable3 = Cable(
|
||||
a_terminations=[interface3],
|
||||
b_terminations=[rearport1]
|
||||
)
|
||||
cable3.save()
|
||||
cable4 = Cable(
|
||||
a_terminations=[interface4],
|
||||
b_terminations=[rearport2]
|
||||
)
|
||||
cable4.save()
|
||||
portmapping1 = PortMapping.objects.create(
|
||||
device=self.device,
|
||||
front_port=frontport1,
|
||||
front_port_position=1,
|
||||
rear_port=rearport1,
|
||||
rear_port_position=1,
|
||||
)
|
||||
|
||||
# Verify expected initial paths
|
||||
self.assertPathExists(
|
||||
(interface1, cable1, frontport1, rearport1, cable3, interface3),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
(interface3, cable3, rearport1, frontport1, cable1, interface1),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
# Delete and replace the PortMapping to connect interface1 to interface4
|
||||
portmapping1.delete()
|
||||
portmapping2 = PortMapping.objects.create(
|
||||
device=self.device,
|
||||
front_port=frontport1,
|
||||
front_port_position=1,
|
||||
rear_port=rearport2,
|
||||
rear_port_position=1,
|
||||
)
|
||||
|
||||
# Verify expected new paths
|
||||
self.assertPathExists(
|
||||
(interface1, cable1, frontport1, rearport2, cable4, interface4),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
(interface4, cable4, rearport2, frontport1, cable1, interface1),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
# Delete and replace the PortMapping to connect interface2 to interface4
|
||||
portmapping2.delete()
|
||||
PortMapping.objects.create(
|
||||
device=self.device,
|
||||
front_port=frontport2,
|
||||
front_port_position=1,
|
||||
rear_port=rearport2,
|
||||
rear_port_position=1,
|
||||
)
|
||||
|
||||
# Verify expected new paths
|
||||
self.assertPathExists(
|
||||
(interface2, cable2, frontport2, rearport2, cable4, interface4),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
(interface4, cable4, rearport2, frontport2, cable2, interface2),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,13 @@ class DeviceComponentFilterSetTests:
|
|||
params = {'device_status': ['active', 'planned']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_tenant(self):
|
||||
tenants = Tenant.objects.all()[:2]
|
||||
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class DeviceComponentTemplateFilterSetTests:
|
||||
|
||||
|
|
@ -1355,22 +1362,15 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||
)
|
||||
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||
FrontPortTemplate.objects.bulk_create(
|
||||
(
|
||||
FrontPortTemplate(
|
||||
device_type=device_types[0],
|
||||
name='Front Port 1',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
rear_port=rear_ports[0],
|
||||
),
|
||||
FrontPortTemplate(
|
||||
device_type=device_types[1],
|
||||
name='Front Port 2',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
rear_port=rear_ports[1],
|
||||
),
|
||||
)
|
||||
front_ports = (
|
||||
FrontPortTemplate(device_type=device_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPortTemplate(device_type=device_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||
)
|
||||
FrontPortTemplate.objects.bulk_create(front_ports)
|
||||
PortTemplateMapping.objects.bulk_create([
|
||||
PortTemplateMapping(device_type=device_types[0], front_port=front_ports[0], rear_port=rear_ports[0]),
|
||||
PortTemplateMapping(device_type=device_types[1], front_port=front_ports[1], rear_port=rear_ports[1]),
|
||||
])
|
||||
ModuleBayTemplate.objects.bulk_create((
|
||||
ModuleBayTemplate(device_type=device_types[0], name='Module Bay 1'),
|
||||
ModuleBayTemplate(device_type=device_types[1], name='Module Bay 2'),
|
||||
|
|
@ -1626,22 +1626,15 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
RearPortTemplate(module_type=module_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||
)
|
||||
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||
FrontPortTemplate.objects.bulk_create(
|
||||
(
|
||||
FrontPortTemplate(
|
||||
module_type=module_types[0],
|
||||
name='Front Port 1',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
rear_port=rear_ports[0],
|
||||
),
|
||||
FrontPortTemplate(
|
||||
module_type=module_types[1],
|
||||
name='Front Port 2',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
rear_port=rear_ports[1],
|
||||
),
|
||||
)
|
||||
front_ports = (
|
||||
FrontPortTemplate(module_type=module_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPortTemplate(module_type=module_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||
)
|
||||
FrontPortTemplate.objects.bulk_create(front_ports)
|
||||
PortTemplateMapping.objects.bulk_create([
|
||||
PortTemplateMapping(module_type=module_types[0], front_port=front_ports[0], rear_port=rear_ports[0]),
|
||||
PortTemplateMapping(module_type=module_types[1], front_port=front_ports[1], rear_port=rear_ports[1]),
|
||||
])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
|
|
@ -2057,32 +2050,38 @@ class FrontPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests,
|
|||
)
|
||||
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||
|
||||
FrontPortTemplate.objects.bulk_create((
|
||||
front_ports = (
|
||||
FrontPortTemplate(
|
||||
device_type=device_types[0],
|
||||
name='Front Port 1',
|
||||
rear_port=rear_ports[0],
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
positions=1,
|
||||
color=ColorChoices.COLOR_RED,
|
||||
description='foobar1'
|
||||
),
|
||||
FrontPortTemplate(
|
||||
device_type=device_types[1],
|
||||
name='Front Port 2',
|
||||
rear_port=rear_ports[1],
|
||||
type=PortTypeChoices.TYPE_110_PUNCH,
|
||||
positions=2,
|
||||
color=ColorChoices.COLOR_GREEN,
|
||||
description='foobar2'
|
||||
),
|
||||
FrontPortTemplate(
|
||||
device_type=device_types[2],
|
||||
name='Front Port 3',
|
||||
rear_port=rear_ports[2],
|
||||
type=PortTypeChoices.TYPE_BNC,
|
||||
positions=3,
|
||||
color=ColorChoices.COLOR_BLUE,
|
||||
description='foobar3'
|
||||
),
|
||||
))
|
||||
)
|
||||
FrontPortTemplate.objects.bulk_create(front_ports)
|
||||
PortTemplateMapping.objects.bulk_create([
|
||||
PortTemplateMapping(device_type=device_types[0], front_port=front_ports[0], rear_port=rear_ports[0]),
|
||||
PortTemplateMapping(device_type=device_types[1], front_port=front_ports[1], rear_port=rear_ports[1]),
|
||||
PortTemplateMapping(device_type=device_types[2], front_port=front_ports[2], rear_port=rear_ports[2]),
|
||||
])
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Front Port 1', 'Front Port 2']}
|
||||
|
|
@ -2096,6 +2095,10 @@ class FrontPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests,
|
|||
params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_positions(self):
|
||||
params = {'positions': [1, 2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class RearPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
|
||||
queryset = RearPortTemplate.objects.all()
|
||||
|
|
@ -2752,10 +2755,15 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||
)
|
||||
RearPort.objects.bulk_create(rear_ports)
|
||||
FrontPort.objects.bulk_create((
|
||||
FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]),
|
||||
FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]),
|
||||
))
|
||||
front_ports = (
|
||||
FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||
)
|
||||
FrontPort.objects.bulk_create(front_ports)
|
||||
PortMapping.objects.bulk_create([
|
||||
PortMapping(device=devices[0], front_port=front_ports[0], rear_port=rear_ports[0]),
|
||||
PortMapping(device=devices[1], front_port=front_ports[1], rear_port=rear_ports[1]),
|
||||
])
|
||||
ModuleBay.objects.create(device=devices[0], name='Module Bay 1')
|
||||
ModuleBay.objects.create(device=devices[1], name='Module Bay 2')
|
||||
DeviceBay.objects.bulk_create((
|
||||
|
|
@ -3384,9 +3392,17 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
|||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||
Tenant(name='Tenant 3', slug='tenant-3'),
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
devices = (
|
||||
Device(
|
||||
name='Device 1',
|
||||
tenant=tenants[0],
|
||||
device_type=device_types[0],
|
||||
role=roles[0],
|
||||
site=sites[0],
|
||||
|
|
@ -3396,6 +3412,7 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
|||
),
|
||||
Device(
|
||||
name='Device 2',
|
||||
tenant=tenants[1],
|
||||
device_type=device_types[1],
|
||||
role=roles[1],
|
||||
site=sites[1],
|
||||
|
|
@ -3405,6 +3422,7 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
|||
),
|
||||
Device(
|
||||
name='Device 3',
|
||||
tenant=tenants[2],
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
|
|
@ -3624,9 +3642,17 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
|
|||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||
Tenant(name='Tenant 3', slug='tenant-3'),
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
devices = (
|
||||
Device(
|
||||
name='Device 1',
|
||||
tenant=tenants[0],
|
||||
device_type=device_types[0],
|
||||
role=roles[0],
|
||||
site=sites[0],
|
||||
|
|
@ -3636,6 +3662,7 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
|
|||
),
|
||||
Device(
|
||||
name='Device 2',
|
||||
tenant=tenants[1],
|
||||
device_type=device_types[1],
|
||||
role=roles[1],
|
||||
site=sites[1],
|
||||
|
|
@ -3645,6 +3672,7 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
|
|||
),
|
||||
Device(
|
||||
name='Device 3',
|
||||
tenant=tenants[2],
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
|
|
@ -3864,9 +3892,17 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||
Tenant(name='Tenant 3', slug='tenant-3'),
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
devices = (
|
||||
Device(
|
||||
name='Device 1',
|
||||
tenant=tenants[0],
|
||||
device_type=device_types[0],
|
||||
role=roles[0],
|
||||
site=sites[0],
|
||||
|
|
@ -3876,6 +3912,7 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
),
|
||||
Device(
|
||||
name='Device 2',
|
||||
tenant=tenants[1],
|
||||
device_type=device_types[1],
|
||||
role=roles[1],
|
||||
site=sites[1],
|
||||
|
|
@ -3885,6 +3922,7 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
),
|
||||
Device(
|
||||
name='Device 3',
|
||||
tenant=tenants[2],
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
|
|
@ -4118,9 +4156,17 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
|||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||
Tenant(name='Tenant 3', slug='tenant-3'),
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
devices = (
|
||||
Device(
|
||||
name='Device 1',
|
||||
tenant=tenants[0],
|
||||
device_type=device_types[0],
|
||||
role=roles[0],
|
||||
site=sites[0],
|
||||
|
|
@ -4130,6 +4176,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
|||
),
|
||||
Device(
|
||||
name='Device 2',
|
||||
tenant=tenants[1],
|
||||
device_type=device_types[1],
|
||||
role=roles[1],
|
||||
site=sites[1],
|
||||
|
|
@ -4139,6 +4186,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
|||
),
|
||||
Device(
|
||||
name='Device 3',
|
||||
tenant=tenants[2],
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
|
|
@ -4397,9 +4445,17 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
virtual_chassis = VirtualChassis(name='Virtual Chassis')
|
||||
virtual_chassis.save()
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||
Tenant(name='Tenant 3', slug='tenant-3'),
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
devices = (
|
||||
Device(
|
||||
name='Device 1A',
|
||||
tenant=tenants[0],
|
||||
device_type=device_types[0],
|
||||
role=roles[0],
|
||||
site=sites[0],
|
||||
|
|
@ -4412,6 +4468,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
),
|
||||
Device(
|
||||
name='Device 1B',
|
||||
tenant=tenants[1],
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
|
|
@ -4424,6 +4481,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
),
|
||||
Device(
|
||||
name='Device 2',
|
||||
tenant=tenants[2],
|
||||
device_type=device_types[1],
|
||||
role=roles[1],
|
||||
site=sites[1],
|
||||
|
|
@ -4433,6 +4491,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
),
|
||||
Device(
|
||||
name='Device 3',
|
||||
tenant=tenants[2],
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
|
|
@ -5018,9 +5077,17 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||
Tenant(name='Tenant 3', slug='tenant-3'),
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
devices = (
|
||||
Device(
|
||||
name='Device 1',
|
||||
tenant=tenants[0],
|
||||
device_type=device_types[0],
|
||||
role=roles[0],
|
||||
site=sites[0],
|
||||
|
|
@ -5030,6 +5097,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
),
|
||||
Device(
|
||||
name='Device 2',
|
||||
tenant=tenants[1],
|
||||
device_type=device_types[1],
|
||||
role=roles[1],
|
||||
site=sites[1],
|
||||
|
|
@ -5039,6 +5107,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
),
|
||||
Device(
|
||||
name='Device 3',
|
||||
tenant=tenants[2],
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
|
|
@ -5090,8 +5159,6 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
label='A',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
color=ColorChoices.COLOR_RED,
|
||||
rear_port=rear_ports[0],
|
||||
rear_port_position=1,
|
||||
description='First',
|
||||
_site=devices[0].site,
|
||||
_location=devices[0].location,
|
||||
|
|
@ -5104,8 +5171,6 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
label='B',
|
||||
type=PortTypeChoices.TYPE_110_PUNCH,
|
||||
color=ColorChoices.COLOR_GREEN,
|
||||
rear_port=rear_ports[1],
|
||||
rear_port_position=2,
|
||||
description='Second',
|
||||
_site=devices[1].site,
|
||||
_location=devices[1].location,
|
||||
|
|
@ -5118,8 +5183,6 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
label='C',
|
||||
type=PortTypeChoices.TYPE_BNC,
|
||||
color=ColorChoices.COLOR_BLUE,
|
||||
rear_port=rear_ports[2],
|
||||
rear_port_position=3,
|
||||
description='Third',
|
||||
_site=devices[2].site,
|
||||
_location=devices[2].location,
|
||||
|
|
@ -5130,8 +5193,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
name='Front Port 4',
|
||||
label='D',
|
||||
type=PortTypeChoices.TYPE_FC,
|
||||
rear_port=rear_ports[3],
|
||||
rear_port_position=1,
|
||||
positions=2,
|
||||
_site=devices[3].site,
|
||||
_location=devices[3].location,
|
||||
_rack=devices[3].rack,
|
||||
|
|
@ -5141,8 +5203,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
name='Front Port 5',
|
||||
label='E',
|
||||
type=PortTypeChoices.TYPE_FC,
|
||||
rear_port=rear_ports[4],
|
||||
rear_port_position=1,
|
||||
positions=3,
|
||||
_site=devices[3].site,
|
||||
_location=devices[3].location,
|
||||
_rack=devices[3].rack,
|
||||
|
|
@ -5152,14 +5213,21 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
name='Front Port 6',
|
||||
label='F',
|
||||
type=PortTypeChoices.TYPE_FC,
|
||||
rear_port=rear_ports[5],
|
||||
rear_port_position=1,
|
||||
positions=4,
|
||||
_site=devices[3].site,
|
||||
_location=devices[3].location,
|
||||
_rack=devices[3].rack,
|
||||
),
|
||||
)
|
||||
FrontPort.objects.bulk_create(front_ports)
|
||||
PortMapping.objects.bulk_create([
|
||||
PortMapping(device=devices[0], front_port=front_ports[0], rear_port=rear_ports[0]),
|
||||
PortMapping(device=devices[1], front_port=front_ports[1], rear_port=rear_ports[1], rear_port_position=2),
|
||||
PortMapping(device=devices[2], front_port=front_ports[2], rear_port=rear_ports[2], rear_port_position=3),
|
||||
PortMapping(device=devices[3], front_port=front_ports[3], rear_port=rear_ports[3]),
|
||||
PortMapping(device=devices[3], front_port=front_ports[4], rear_port=rear_ports[4]),
|
||||
PortMapping(device=devices[3], front_port=front_ports[5], rear_port=rear_ports[5]),
|
||||
])
|
||||
|
||||
# Cables
|
||||
Cable(a_terminations=[front_ports[0]], b_terminations=[front_ports[3]]).save()
|
||||
|
|
@ -5182,6 +5250,10 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_positions(self):
|
||||
params = {'positions': [2, 3]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['First', 'Second']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
|
@ -5309,9 +5381,17 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
|
|||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||
Tenant(name='Tenant 3', slug='tenant-3'),
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
devices = (
|
||||
Device(
|
||||
name='Device 1',
|
||||
tenant=tenants[0],
|
||||
device_type=device_types[0],
|
||||
role=roles[0],
|
||||
site=sites[0],
|
||||
|
|
@ -5321,6 +5401,7 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
|
|||
),
|
||||
Device(
|
||||
name='Device 2',
|
||||
tenant=tenants[1],
|
||||
device_type=device_types[1],
|
||||
role=roles[1],
|
||||
site=sites[1],
|
||||
|
|
@ -5330,6 +5411,7 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
|
|||
),
|
||||
Device(
|
||||
name='Device 3',
|
||||
tenant=tenants[2],
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
|
|
@ -5586,9 +5668,17 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||
Tenant(name='Tenant 3', slug='tenant-3'),
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
devices = (
|
||||
Device(
|
||||
name='Device 1',
|
||||
tenant=tenants[0],
|
||||
device_type=device_types[0],
|
||||
role=roles[0],
|
||||
site=sites[0],
|
||||
|
|
@ -5598,6 +5688,7 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
),
|
||||
Device(
|
||||
name='Device 2',
|
||||
tenant=tenants[1],
|
||||
device_type=device_types[1],
|
||||
role=roles[1],
|
||||
site=sites[1],
|
||||
|
|
@ -5607,6 +5698,7 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
),
|
||||
Device(
|
||||
name='Device 3',
|
||||
tenant=tenants[2],
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
|
|
@ -5759,9 +5851,17 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||
Tenant(name='Tenant 3', slug='tenant-3'),
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
devices = (
|
||||
Device(
|
||||
name='Device 1',
|
||||
tenant=tenants[0],
|
||||
device_type=device_types[0],
|
||||
role=roles[0],
|
||||
site=sites[0],
|
||||
|
|
@ -5771,6 +5871,7 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
),
|
||||
Device(
|
||||
name='Device 2',
|
||||
tenant=tenants[1],
|
||||
device_type=device_types[1],
|
||||
role=roles[1],
|
||||
site=sites[1],
|
||||
|
|
@ -5780,6 +5881,7 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||
),
|
||||
Device(
|
||||
name='Device 3',
|
||||
tenant=tenants[2],
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
|
|
@ -6420,13 +6522,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1')
|
||||
power_port = PowerPort.objects.create(device=devices[0], name='Power Port 1')
|
||||
power_outlet = PowerOutlet.objects.create(device=devices[0], name='Power Outlet 1')
|
||||
rear_port = RearPort.objects.create(device=devices[0], name='Rear Port 1', positions=1)
|
||||
front_port = FrontPort.objects.create(
|
||||
device=devices[0],
|
||||
name='Front Port 1',
|
||||
rear_port=rear_port,
|
||||
rear_port_position=1
|
||||
)
|
||||
rear_port = RearPort.objects.create(device=devices[0], name='Rear Port 1')
|
||||
front_port = FrontPort.objects.create(device=devices[0], name='Front Port 1')
|
||||
PortMapping.objects.create(device=devices[0], front_port=front_port, rear_port=rear_port)
|
||||
|
||||
power_panel = PowerPanel.objects.create(name='Power Panel 1', site=sites[0])
|
||||
power_feed = PowerFeed.objects.create(name='Power Feed 1', power_panel=power_panel)
|
||||
|
|
|
|||
|
|
@ -193,7 +193,8 @@ class FrontPortTestCase(TestCase):
|
|||
'name': 'FrontPort[1-4]',
|
||||
'label': 'Port[1-4]',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
|
||||
'positions': 1,
|
||||
'rear_ports': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
|
||||
}
|
||||
form = FrontPortCreateForm(front_port_data)
|
||||
|
||||
|
|
@ -208,7 +209,8 @@ class FrontPortTestCase(TestCase):
|
|||
'name': 'FrontPort[1-4]',
|
||||
'label': 'Port[1-2]',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
|
||||
'positions': 1,
|
||||
'rear_ports': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
|
||||
}
|
||||
form = FrontPortCreateForm(bad_front_port_data)
|
||||
|
||||
|
|
|
|||
|
|
@ -444,13 +444,19 @@ class DeviceTestCase(TestCase):
|
|||
)
|
||||
rearport.save()
|
||||
|
||||
FrontPortTemplate(
|
||||
frontport = FrontPortTemplate(
|
||||
device_type=device_type,
|
||||
name='Front Port 1',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
)
|
||||
frontport.save()
|
||||
|
||||
PortTemplateMapping.objects.create(
|
||||
device_type=device_type,
|
||||
front_port=frontport,
|
||||
rear_port=rearport,
|
||||
rear_port_position=2
|
||||
).save()
|
||||
rear_port_position=2,
|
||||
)
|
||||
|
||||
ModuleBayTemplate(
|
||||
device_type=device_type,
|
||||
|
|
@ -528,11 +534,12 @@ class DeviceTestCase(TestCase):
|
|||
device=device,
|
||||
name='Front Port 1',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
rear_port=rearport,
|
||||
rear_port_position=2
|
||||
positions=1
|
||||
)
|
||||
self.assertEqual(frontport.cf['cf1'], 'foo')
|
||||
|
||||
self.assertTrue(PortMapping.objects.filter(front_port=frontport, rear_port=rearport).exists())
|
||||
|
||||
modulebay = ModuleBay.objects.get(
|
||||
device=device,
|
||||
name='Module Bay 1'
|
||||
|
|
@ -792,8 +799,54 @@ class ModuleBayTestCase(TestCase):
|
|||
)
|
||||
device.consoleports.first()
|
||||
|
||||
def test_nested_module_token(self):
|
||||
pass
|
||||
@tag('regression') # #19918
|
||||
def test_nested_module_bay_label_resolution(self):
|
||||
"""Test that nested module bay labels properly resolve {module} placeholders"""
|
||||
manufacturer = Manufacturer.objects.first()
|
||||
site = Site.objects.first()
|
||||
device_role = DeviceRole.objects.first()
|
||||
|
||||
# Create device type with module bay template (position='A')
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Device with Bays',
|
||||
slug='device-with-bays'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='Bay A',
|
||||
position='A'
|
||||
)
|
||||
|
||||
# Create module type with nested bay template using {module} placeholder
|
||||
module_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Module with Nested Bays'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=module_type,
|
||||
name='SFP {module}-21',
|
||||
label='{module}-21',
|
||||
position='21'
|
||||
)
|
||||
|
||||
# Create device and install module
|
||||
device = Device.objects.create(
|
||||
name='Test Device',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
module_bay = device.modulebays.get(name='Bay A')
|
||||
module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=module_bay,
|
||||
module_type=module_type
|
||||
)
|
||||
|
||||
# Verify nested bay label resolves {module} to parent position
|
||||
nested_bay = module.modulebays.get(name='SFP A-21')
|
||||
self.assertEqual(nested_bay.label, 'A-21')
|
||||
|
||||
|
||||
class CableTestCase(TestCase):
|
||||
|
|
@ -835,12 +888,18 @@ class CableTestCase(TestCase):
|
|||
)
|
||||
RearPort.objects.bulk_create(rear_ports)
|
||||
front_ports = (
|
||||
FrontPort(device=patch_panel, name='FP1', type='8p8c', rear_port=rear_ports[0], rear_port_position=1),
|
||||
FrontPort(device=patch_panel, name='FP2', type='8p8c', rear_port=rear_ports[1], rear_port_position=1),
|
||||
FrontPort(device=patch_panel, name='FP3', type='8p8c', rear_port=rear_ports[2], rear_port_position=1),
|
||||
FrontPort(device=patch_panel, name='FP4', type='8p8c', rear_port=rear_ports[3], rear_port_position=1),
|
||||
FrontPort(device=patch_panel, name='FP1', type='8p8c'),
|
||||
FrontPort(device=patch_panel, name='FP2', type='8p8c'),
|
||||
FrontPort(device=patch_panel, name='FP3', type='8p8c'),
|
||||
FrontPort(device=patch_panel, name='FP4', type='8p8c'),
|
||||
)
|
||||
FrontPort.objects.bulk_create(front_ports)
|
||||
PortMapping.objects.bulk_create([
|
||||
PortMapping(device=patch_panel, front_port=front_ports[0], rear_port=rear_ports[0]),
|
||||
PortMapping(device=patch_panel, front_port=front_ports[1], rear_port=rear_ports[1]),
|
||||
PortMapping(device=patch_panel, front_port=front_ports[2], rear_port=rear_ports[2]),
|
||||
PortMapping(device=patch_panel, front_port=front_ports[3], rear_port=rear_ports[3]),
|
||||
])
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=provider)
|
||||
|
|
|
|||
|
|
@ -741,17 +741,16 @@ class DeviceTypeTestCase(
|
|||
)
|
||||
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||
front_ports = (
|
||||
FrontPortTemplate(
|
||||
device_type=devicetype, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1
|
||||
),
|
||||
FrontPortTemplate(
|
||||
device_type=devicetype, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1
|
||||
),
|
||||
FrontPortTemplate(
|
||||
device_type=devicetype, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1
|
||||
),
|
||||
FrontPortTemplate(device_type=devicetype, name='Front Port 1'),
|
||||
FrontPortTemplate(device_type=devicetype, name='Front Port 2'),
|
||||
FrontPortTemplate(device_type=devicetype, name='Front Port 3'),
|
||||
)
|
||||
FrontPortTemplate.objects.bulk_create(front_ports)
|
||||
PortTemplateMapping.objects.bulk_create([
|
||||
PortTemplateMapping(device_type=devicetype, front_port=front_ports[0], rear_port=rear_ports[0]),
|
||||
PortTemplateMapping(device_type=devicetype, front_port=front_ports[1], rear_port=rear_ports[1]),
|
||||
PortTemplateMapping(device_type=devicetype, front_port=front_ports[2], rear_port=rear_ports[2]),
|
||||
])
|
||||
|
||||
url = reverse('dcim:devicetype_frontports', kwargs={'pk': devicetype.pk})
|
||||
self.assertHttpStatus(self.client.get(url), 200)
|
||||
|
|
@ -866,12 +865,16 @@ rear-ports:
|
|||
front-ports:
|
||||
- name: Front Port 1
|
||||
type: 8p8c
|
||||
rear_port: Rear Port 1
|
||||
- name: Front Port 2
|
||||
type: 8p8c
|
||||
rear_port: Rear Port 2
|
||||
- name: Front Port 3
|
||||
type: 8p8c
|
||||
port-mappings:
|
||||
- front_port: Front Port 1
|
||||
rear_port: Rear Port 1
|
||||
- front_port: Front Port 2
|
||||
rear_port: Rear Port 2
|
||||
- front_port: Front Port 3
|
||||
rear_port: Rear Port 3
|
||||
module-bays:
|
||||
- name: Module Bay 1
|
||||
|
|
@ -971,8 +974,12 @@ inventory-items:
|
|||
self.assertEqual(device_type.frontporttemplates.count(), 3)
|
||||
fp1 = FrontPortTemplate.objects.first()
|
||||
self.assertEqual(fp1.name, 'Front Port 1')
|
||||
self.assertEqual(fp1.rear_port, rp1)
|
||||
self.assertEqual(fp1.rear_port_position, 1)
|
||||
|
||||
self.assertEqual(device_type.port_mappings.count(), 3)
|
||||
mapping1 = PortTemplateMapping.objects.first()
|
||||
self.assertEqual(mapping1.device_type, device_type)
|
||||
self.assertEqual(mapping1.front_port, fp1)
|
||||
self.assertEqual(mapping1.rear_port, rp1)
|
||||
|
||||
self.assertEqual(device_type.modulebaytemplates.count(), 3)
|
||||
mb1 = ModuleBayTemplate.objects.first()
|
||||
|
|
@ -1316,17 +1323,16 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
)
|
||||
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||
front_ports = (
|
||||
FrontPortTemplate(
|
||||
module_type=moduletype, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1
|
||||
),
|
||||
FrontPortTemplate(
|
||||
module_type=moduletype, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1
|
||||
),
|
||||
FrontPortTemplate(
|
||||
module_type=moduletype, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1
|
||||
),
|
||||
FrontPortTemplate(module_type=moduletype, name='Front Port 1'),
|
||||
FrontPortTemplate(module_type=moduletype, name='Front Port 2'),
|
||||
FrontPortTemplate(module_type=moduletype, name='Front Port 3'),
|
||||
)
|
||||
FrontPortTemplate.objects.bulk_create(front_ports)
|
||||
PortTemplateMapping.objects.bulk_create([
|
||||
PortTemplateMapping(module_type=moduletype, front_port=front_ports[0], rear_port=rear_ports[0]),
|
||||
PortTemplateMapping(module_type=moduletype, front_port=front_ports[1], rear_port=rear_ports[1]),
|
||||
PortTemplateMapping(module_type=moduletype, front_port=front_ports[2], rear_port=rear_ports[2]),
|
||||
])
|
||||
|
||||
url = reverse('dcim:moduletype_frontports', kwargs={'pk': moduletype.pk})
|
||||
self.assertHttpStatus(self.client.get(url), 200)
|
||||
|
|
@ -1394,12 +1400,16 @@ rear-ports:
|
|||
front-ports:
|
||||
- name: Front Port 1
|
||||
type: 8p8c
|
||||
rear_port: Rear Port 1
|
||||
- name: Front Port 2
|
||||
type: 8p8c
|
||||
rear_port: Rear Port 2
|
||||
- name: Front Port 3
|
||||
type: 8p8c
|
||||
port-mappings:
|
||||
- front_port: Front Port 1
|
||||
rear_port: Rear Port 1
|
||||
- front_port: Front Port 2
|
||||
rear_port: Rear Port 2
|
||||
- front_port: Front Port 3
|
||||
rear_port: Rear Port 3
|
||||
module-bays:
|
||||
- name: Module Bay 1
|
||||
|
|
@ -1477,8 +1487,12 @@ module-bays:
|
|||
self.assertEqual(module_type.frontporttemplates.count(), 3)
|
||||
fp1 = FrontPortTemplate.objects.first()
|
||||
self.assertEqual(fp1.name, 'Front Port 1')
|
||||
self.assertEqual(fp1.rear_port, rp1)
|
||||
self.assertEqual(fp1.rear_port_position, 1)
|
||||
|
||||
self.assertEqual(module_type.port_mappings.count(), 3)
|
||||
mapping1 = PortTemplateMapping.objects.first()
|
||||
self.assertEqual(mapping1.module_type, module_type)
|
||||
self.assertEqual(mapping1.front_port, fp1)
|
||||
self.assertEqual(mapping1.rear_port, rp1)
|
||||
|
||||
self.assertEqual(module_type.modulebaytemplates.count(), 3)
|
||||
mb1 = ModuleBayTemplate.objects.first()
|
||||
|
|
@ -1770,7 +1784,7 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
|
|||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||
|
||||
rearports = (
|
||||
rear_ports = (
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1'),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3'),
|
||||
|
|
@ -1778,35 +1792,33 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
|
|||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 5'),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 6'),
|
||||
)
|
||||
RearPortTemplate.objects.bulk_create(rearports)
|
||||
|
||||
FrontPortTemplate.objects.bulk_create(
|
||||
(
|
||||
FrontPortTemplate(
|
||||
device_type=devicetype, name='Front Port Template 1', rear_port=rearports[0], rear_port_position=1
|
||||
),
|
||||
FrontPortTemplate(
|
||||
device_type=devicetype, name='Front Port Template 2', rear_port=rearports[1], rear_port_position=1
|
||||
),
|
||||
FrontPortTemplate(
|
||||
device_type=devicetype, name='Front Port Template 3', rear_port=rearports[2], rear_port_position=1
|
||||
),
|
||||
)
|
||||
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||
front_ports = (
|
||||
FrontPortTemplate(device_type=devicetype, name='Front Port Template 1'),
|
||||
FrontPortTemplate(device_type=devicetype, name='Front Port Template 2'),
|
||||
FrontPortTemplate(device_type=devicetype, name='Front Port Template 3'),
|
||||
)
|
||||
FrontPortTemplate.objects.bulk_create(front_ports)
|
||||
PortTemplateMapping.objects.bulk_create([
|
||||
PortTemplateMapping(device_type=devicetype, front_port=front_ports[0], rear_port=rear_ports[0]),
|
||||
PortTemplateMapping(device_type=devicetype, front_port=front_ports[1], rear_port=rear_ports[1]),
|
||||
PortTemplateMapping(device_type=devicetype, front_port=front_ports[2], rear_port=rear_ports[2]),
|
||||
])
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Front Port X',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': rearports[3].pk,
|
||||
'rear_port_position': 1,
|
||||
'positions': 1,
|
||||
'rear_ports': [f'{rear_ports[3].pk}:1'],
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Front Port [4-6]',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]],
|
||||
'positions': 1,
|
||||
'rear_ports': [f'{rp.pk}:1' for rp in rear_ports[3:6]],
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
|
|
@ -2276,11 +2288,16 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
)
|
||||
RearPort.objects.bulk_create(rear_ports)
|
||||
front_ports = (
|
||||
FrontPort(device=device, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1),
|
||||
FrontPort(device=device, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1),
|
||||
FrontPort(device=device, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1),
|
||||
FrontPort(device=device, name='Front Port Template 1'),
|
||||
FrontPort(device=device, name='Front Port Template 2'),
|
||||
FrontPort(device=device, name='Front Port Template 3'),
|
||||
)
|
||||
FrontPort.objects.bulk_create(front_ports)
|
||||
PortMapping.objects.bulk_create([
|
||||
PortMapping(device=device, front_port=front_ports[0], rear_port=rear_ports[0]),
|
||||
PortMapping(device=device, front_port=front_ports[1], rear_port=rear_ports[1]),
|
||||
PortMapping(device=device, front_port=front_ports[2], rear_port=rear_ports[2]),
|
||||
])
|
||||
|
||||
url = reverse('dcim:device_frontports', kwargs={'pk': device.pk})
|
||||
self.assertHttpStatus(self.client.get(url), 200)
|
||||
|
|
@ -3065,7 +3082,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||
def setUpTestData(cls):
|
||||
device = create_test_device('Device 1')
|
||||
|
||||
rearports = (
|
||||
rear_ports = (
|
||||
RearPort(device=device, name='Rear Port 1'),
|
||||
RearPort(device=device, name='Rear Port 2'),
|
||||
RearPort(device=device, name='Rear Port 3'),
|
||||
|
|
@ -3073,14 +3090,19 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||
RearPort(device=device, name='Rear Port 5'),
|
||||
RearPort(device=device, name='Rear Port 6'),
|
||||
)
|
||||
RearPort.objects.bulk_create(rearports)
|
||||
RearPort.objects.bulk_create(rear_ports)
|
||||
|
||||
front_ports = (
|
||||
FrontPort(device=device, name='Front Port 1', rear_port=rearports[0]),
|
||||
FrontPort(device=device, name='Front Port 2', rear_port=rearports[1]),
|
||||
FrontPort(device=device, name='Front Port 3', rear_port=rearports[2]),
|
||||
FrontPort(device=device, name='Front Port 1'),
|
||||
FrontPort(device=device, name='Front Port 2'),
|
||||
FrontPort(device=device, name='Front Port 3'),
|
||||
)
|
||||
FrontPort.objects.bulk_create(front_ports)
|
||||
PortMapping.objects.bulk_create([
|
||||
PortMapping(device=device, front_port=front_ports[0], rear_port=rear_ports[0]),
|
||||
PortMapping(device=device, front_port=front_ports[1], rear_port=rear_ports[1]),
|
||||
PortMapping(device=device, front_port=front_ports[2], rear_port=rear_ports[2]),
|
||||
])
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
|
|
@ -3088,8 +3110,8 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||
'device': device.pk,
|
||||
'name': 'Front Port X',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': rearports[3].pk,
|
||||
'rear_port_position': 1,
|
||||
'positions': 1,
|
||||
'rear_ports': [f'{rear_ports[3].pk}:1'],
|
||||
'description': 'New description',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
|
@ -3098,7 +3120,8 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||
'device': device.pk,
|
||||
'name': 'Front Port [4-6]',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]],
|
||||
'positions': 1,
|
||||
'rear_ports': [f'{rp.pk}:1' for rp in rear_ports[3:6]],
|
||||
'description': 'New description',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
|
@ -3109,10 +3132,10 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"device,name,type,rear_port,rear_port_position",
|
||||
"Device 1,Front Port 4,8p8c,Rear Port 4,1",
|
||||
"Device 1,Front Port 5,8p8c,Rear Port 5,1",
|
||||
"Device 1,Front Port 6,8p8c,Rear Port 6,1",
|
||||
"device,name,type,positions",
|
||||
"Device 1,Front Port 4,8p8c,1",
|
||||
"Device 1,Front Port 5,8p8c,1",
|
||||
"Device 1,Front Port 6,8p8c,1",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
|
|
|
|||
|
|
@ -83,3 +83,36 @@ def update_interface_bridges(device, interface_templates, module=None):
|
|||
)
|
||||
interface.full_clean()
|
||||
interface.save()
|
||||
|
||||
|
||||
def create_port_mappings(device, device_type, module=None):
|
||||
"""
|
||||
Replicate all front/rear port mappings from a DeviceType to the given device.
|
||||
"""
|
||||
from dcim.models import FrontPort, PortMapping, RearPort
|
||||
|
||||
templates = device_type.port_mappings.prefetch_related('front_port', 'rear_port')
|
||||
|
||||
# Cache front & rear ports for efficient lookups by name
|
||||
front_ports = {
|
||||
fp.name: fp for fp in FrontPort.objects.filter(device=device)
|
||||
}
|
||||
rear_ports = {
|
||||
rp.name: rp for rp in RearPort.objects.filter(device=device)
|
||||
}
|
||||
|
||||
# Replicate PortMappings
|
||||
mappings = []
|
||||
for template in templates:
|
||||
front_port = front_ports.get(template.front_port.resolve_name(module=module))
|
||||
rear_port = rear_ports.get(template.rear_port.resolve_name(module=module))
|
||||
mappings.append(
|
||||
PortMapping(
|
||||
device_id=front_port.device_id,
|
||||
front_port=front_port,
|
||||
front_port_position=template.front_port_position,
|
||||
rear_port=rear_port,
|
||||
rear_port_position=template.rear_port_position,
|
||||
)
|
||||
)
|
||||
PortMapping.objects.bulk_create(mappings)
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ from wireless.models import WirelessLAN
|
|||
from . import filtersets, forms, tables
|
||||
from .choices import DeviceFaceChoices, InterfaceModeChoices
|
||||
from .models import *
|
||||
from .models.device_components import PortMapping
|
||||
from .object_actions import BulkAddComponents, BulkDisconnect
|
||||
|
||||
CABLE_TERMINATION_TYPES = {
|
||||
|
|
@ -1515,6 +1516,7 @@ class DeviceTypeImportView(generic.BulkImportView):
|
|||
'interfaces': forms.InterfaceTemplateImportForm,
|
||||
'rear-ports': forms.RearPortTemplateImportForm,
|
||||
'front-ports': forms.FrontPortTemplateImportForm,
|
||||
'port-mappings': forms.PortTemplateMappingImportForm,
|
||||
'module-bays': forms.ModuleBayTemplateImportForm,
|
||||
'device-bays': forms.DeviceBayTemplateImportForm,
|
||||
'inventory-items': forms.InventoryItemTemplateImportForm,
|
||||
|
|
@ -1819,6 +1821,7 @@ class ModuleTypeImportView(generic.BulkImportView):
|
|||
'interfaces': forms.InterfaceTemplateImportForm,
|
||||
'rear-ports': forms.RearPortTemplateImportForm,
|
||||
'front-ports': forms.FrontPortTemplateImportForm,
|
||||
'port-mappings': forms.PortTemplateMappingImportForm,
|
||||
'module-bays': forms.ModuleBayTemplateImportForm,
|
||||
}
|
||||
|
||||
|
|
@ -3242,6 +3245,11 @@ class FrontPortListView(generic.ObjectListView):
|
|||
class FrontPortView(generic.ObjectView):
|
||||
queryset = FrontPort.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'rear_port_mappings': PortMapping.objects.filter(front_port=instance).prefetch_related('rear_port'),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(FrontPort, 'add', detail=False)
|
||||
class FrontPortCreateView(generic.ComponentCreateView):
|
||||
|
|
@ -3313,6 +3321,11 @@ class RearPortListView(generic.ObjectListView):
|
|||
class RearPortView(generic.ObjectView):
|
||||
queryset = RearPort.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'front_port_mappings': PortMapping.objects.filter(rear_port=instance).prefetch_related('front_port'),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(RearPort, 'add', detail=False)
|
||||
class RearPortCreateView(generic.ComponentCreateView):
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@ from typing import Annotated, TYPE_CHECKING
|
|||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry_django import FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseFilterMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from netbox.graphql.filter_lookups import JSONFilter
|
||||
|
|
@ -16,37 +13,30 @@ __all__ = (
|
|||
'JournalEntriesFilterMixin',
|
||||
'TagsFilterMixin',
|
||||
'ConfigContextFilterMixin',
|
||||
'TagBaseFilterMixin',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CustomFieldsFilterMixin(BaseFilterMixin):
|
||||
class CustomFieldsFilterMixin:
|
||||
custom_field_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class JournalEntriesFilterMixin(BaseFilterMixin):
|
||||
class JournalEntriesFilterMixin:
|
||||
journal_entries: Annotated['JournalEntryFilter', strawberry.lazy('extras.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TagsFilterMixin(BaseFilterMixin):
|
||||
class TagsFilterMixin:
|
||||
tags: Annotated['TagFilter', strawberry.lazy('extras.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigContextFilterMixin(BaseFilterMixin):
|
||||
class ConfigContextFilterMixin:
|
||||
local_context_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TagBaseFilterMixin(BaseFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import strawberry_django
|
|||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
||||
from extras import models
|
||||
from extras.graphql.filter_mixins import TagBaseFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin
|
||||
from netbox.graphql.filter_mixins import PrimaryModelFilterMixin, SyncedDataFilterMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
|
||||
from netbox.graphql.filter_mixins import SyncedDataFilterMixin
|
||||
from netbox.graphql.filters import ChangeLoggedModelFilter, PrimaryModelFilter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.graphql.filters import ContentTypeFilter
|
||||
|
|
@ -42,7 +42,7 @@ __all__ = (
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ConfigContext, lookups=True)
|
||||
class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
|
||||
class ConfigContextFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
|
@ -99,14 +99,14 @@ class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Chan
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ConfigContextProfile, lookups=True)
|
||||
class ConfigContextProfileFilter(SyncedDataFilterMixin, PrimaryModelFilterMixin):
|
||||
class ConfigContextProfileFilter(SyncedDataFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] = strawberry_django.filter_field()
|
||||
tags: Annotated['TagFilter', strawberry.lazy('extras.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ConfigTemplate, lookups=True)
|
||||
class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
|
||||
class ConfigTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
template_code: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
@ -120,7 +120,7 @@ class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.CustomField, lookups=True)
|
||||
class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class CustomFieldFilter(ChangeLoggedModelFilter):
|
||||
type: BaseFilterLookup[Annotated['CustomFieldTypeEnum', strawberry.lazy('extras.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -179,7 +179,7 @@ class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.CustomFieldChoiceSet, lookups=True)
|
||||
class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class CustomFieldChoiceSetFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
base_choices: (
|
||||
|
|
@ -194,7 +194,7 @@ class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.CustomLink, lookups=True)
|
||||
class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class CustomLinkFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
link_text: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
@ -212,7 +212,7 @@ class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ExportTemplate, lookups=True)
|
||||
class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
|
||||
class ExportTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
template_code: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
@ -226,7 +226,7 @@ class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ImageAttachment, lookups=True)
|
||||
class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class ImageAttachmentFilter(ChangeLoggedModelFilter):
|
||||
object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -241,7 +241,7 @@ class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.JournalEntry, lookups=True)
|
||||
class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
class JournalEntryFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -257,7 +257,7 @@ class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, Tag
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.NotificationGroup, lookups=True)
|
||||
class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class NotificationGroupFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
groups: Annotated['GroupFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
|
|
@ -265,7 +265,7 @@ class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.SavedFilter, lookups=True)
|
||||
class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class SavedFilterFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
@ -282,7 +282,7 @@ class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.TableConfig, lookups=True)
|
||||
class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class TableConfigFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
|
|
@ -295,7 +295,9 @@ class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Tag, lookups=True)
|
||||
class TagFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin, TagBaseFilterMixin):
|
||||
class TagFilter(ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -303,7 +305,7 @@ class TagFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin, TagBaseFilterMi
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Webhook, lookups=True)
|
||||
class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
class WebhookFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
payload_url: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
@ -324,7 +326,7 @@ class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilt
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.EventRule, lookups=True)
|
||||
class EventRuleFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
class EventRuleFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
event_types: Annotated['StringArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ import logging
|
|||
import traceback
|
||||
from contextlib import ExitStack
|
||||
|
||||
from django.db import transaction
|
||||
from django.db import router, transaction
|
||||
from django.db import DEFAULT_DB_ALIAS
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from core.signals import clear_events
|
||||
from dcim.models import Device
|
||||
from extras.models import Script as ScriptModel
|
||||
from netbox.context_managers import event_tracking
|
||||
from netbox.jobs import JobRunner
|
||||
from netbox.registry import registry
|
||||
from utilities.exceptions import AbortScript, AbortTransaction
|
||||
|
|
@ -42,10 +45,21 @@ class ScriptJob(JobRunner):
|
|||
# A script can modify multiple models so need to do an atomic lock on
|
||||
# both the default database (for non ChangeLogged models) and potentially
|
||||
# any other database (for ChangeLogged models)
|
||||
with transaction.atomic():
|
||||
script.output = script.run(data, commit)
|
||||
if not commit:
|
||||
raise AbortTransaction()
|
||||
changeloged_db = router.db_for_write(Device)
|
||||
with transaction.atomic(using=DEFAULT_DB_ALIAS):
|
||||
# If branch database is different from default, wrap in a second atomic transaction
|
||||
# Note: Don't add any extra code between the two atomic transactions,
|
||||
# otherwise the changes might get committed to the default database
|
||||
# if there are any raised exceptions.
|
||||
if changeloged_db != DEFAULT_DB_ALIAS:
|
||||
with transaction.atomic(using=changeloged_db):
|
||||
script.output = script.run(data, commit)
|
||||
if not commit:
|
||||
raise AbortTransaction()
|
||||
else:
|
||||
script.output = script.run(data, commit)
|
||||
if not commit:
|
||||
raise AbortTransaction()
|
||||
except AbortTransaction:
|
||||
script.log_info(message=_("Database changes have been reverted automatically."))
|
||||
if script.failed:
|
||||
|
|
@ -108,14 +122,14 @@ class ScriptJob(JobRunner):
|
|||
script.request = request
|
||||
self.logger.debug(f"Request ID: {request.id if request else None}")
|
||||
|
||||
# Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process
|
||||
# change logging, event rules, etc.
|
||||
if commit:
|
||||
self.logger.info("Executing script (commit enabled)")
|
||||
with ExitStack() as stack:
|
||||
for request_processor in registry['request_processors']:
|
||||
stack.enter_context(request_processor(request))
|
||||
self.run_script(script, request, data, commit)
|
||||
else:
|
||||
self.logger.warning("Executing script (commit disabled)")
|
||||
|
||||
with ExitStack() as stack:
|
||||
for request_processor in registry['request_processors']:
|
||||
if not commit and request_processor is event_tracking:
|
||||
continue
|
||||
stack.enter_context(request_processor(request))
|
||||
self.run_script(script, request, data, commit)
|
||||
|
|
|
|||
|
|
@ -3,21 +3,20 @@ from typing import Annotated, TYPE_CHECKING
|
|||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from core.graphql.filter_mixins import BaseFilterMixin
|
||||
from strawberry_django import BaseFilterLookup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from netbox.graphql.filter_lookups import IntegerLookup
|
||||
from .enums import *
|
||||
|
||||
__all__ = (
|
||||
'ServiceBaseFilterMixin',
|
||||
'ServiceFilterMixin',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceBaseFilterMixin(BaseFilterMixin):
|
||||
protocol: Annotated['ServiceProtocolEnum', strawberry.lazy('ipam.graphql.enums')] | None = (
|
||||
class ServiceFilterMixin:
|
||||
protocol: BaseFilterLookup[Annotated['ServiceProtocolEnum', strawberry.lazy('ipam.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
ports: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
|
|
|
|||
|
|
@ -9,12 +9,13 @@ from netaddr.core import AddrFormatError
|
|||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup, DateFilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
||||
from dcim.graphql.filter_mixins import ScopedFilterMixin
|
||||
from dcim.models import Device
|
||||
from ipam import models
|
||||
from ipam.graphql.filter_mixins import ServiceBaseFilterMixin
|
||||
from netbox.graphql.filter_mixins import NetBoxModelFilterMixin, OrganizationalModelFilterMixin, PrimaryModelFilterMixin
|
||||
from ipam.graphql.filter_mixins import ServiceFilterMixin
|
||||
from netbox.graphql.filters import (
|
||||
ChangeLoggedModelFilter, NetBoxModelFilter, OrganizationalModelFilter, PrimaryModelFilter,
|
||||
)
|
||||
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
|
||||
from virtualization.models import VMInterface
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ __all__ = (
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ASN, lookups=True)
|
||||
class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rir_id: ID | None = strawberry_django.filter_field()
|
||||
asn: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
|
|
@ -64,7 +65,7 @@ class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ASNRange, lookups=True)
|
||||
class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
|
||||
class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
|
|
@ -78,7 +79,7 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Aggregate, lookups=True)
|
||||
class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
prefix: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rir_id: ID | None = strawberry_django.filter_field()
|
||||
|
|
@ -111,7 +112,7 @@ class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.FHRPGroup, lookups=True)
|
||||
class FHRPGroupFilter(PrimaryModelFilterMixin):
|
||||
class FHRPGroupFilter(PrimaryModelFilter):
|
||||
group_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -129,7 +130,7 @@ class FHRPGroupFilter(PrimaryModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.FHRPGroupAssignment, lookups=True)
|
||||
class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
|
||||
interface_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -168,7 +169,7 @@ class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin)
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.IPAddress, lookups=True)
|
||||
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
vrf_id: ID | None = strawberry_django.filter_field()
|
||||
|
|
@ -219,7 +220,7 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.IPRange, lookups=True)
|
||||
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
start_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
end_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
|
|
@ -273,7 +274,7 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMi
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Prefix, lookups=True)
|
||||
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
prefix: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
vrf_id: ID | None = strawberry_django.filter_field()
|
||||
|
|
@ -310,19 +311,19 @@ class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, Pr
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.RIR, lookups=True)
|
||||
class RIRFilter(OrganizationalModelFilterMixin):
|
||||
class RIRFilter(OrganizationalModelFilter):
|
||||
is_private: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Role, lookups=True)
|
||||
class RoleFilter(OrganizationalModelFilterMixin):
|
||||
class RoleFilter(OrganizationalModelFilter):
|
||||
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.RouteTarget, lookups=True)
|
||||
class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
importing_vrfs: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
|
@ -339,7 +340,7 @@ class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Service, lookups=True)
|
||||
class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilterMixin):
|
||||
class ServiceFilter(ContactFilterMixin, ServiceFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
|
@ -351,12 +352,12 @@ class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilt
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ServiceTemplate, lookups=True)
|
||||
class ServiceTemplateFilter(ServiceBaseFilterMixin, PrimaryModelFilterMixin):
|
||||
class ServiceTemplateFilter(ServiceFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VLAN, lookups=True)
|
||||
class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class VLANFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
group: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
|
|
@ -388,19 +389,19 @@ class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.VLANGroup, lookups=True)
|
||||
class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilterMixin):
|
||||
class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilter):
|
||||
vid_ranges: Annotated['IntegerRangeArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VLANTranslationPolicy, lookups=True)
|
||||
class VLANTranslationPolicyFilter(PrimaryModelFilterMixin):
|
||||
class VLANTranslationPolicyFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VLANTranslationRule, lookups=True)
|
||||
class VLANTranslationRuleFilter(NetBoxModelFilterMixin):
|
||||
class VLANTranslationRuleFilter(NetBoxModelFilter):
|
||||
policy: Annotated['VLANTranslationPolicyFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -415,7 +416,7 @@ class VLANTranslationRuleFilter(NetBoxModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.VRF, lookups=True)
|
||||
class VRFFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class VRFFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rd: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
enforce_unique: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
|
|
|||
|
|
@ -4,19 +4,11 @@ from typing import TypeVar, TYPE_CHECKING, Annotated
|
|||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup, DatetimeFilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseFilterMixin, BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, JournalEntriesFilterMixin, TagsFilterMixin
|
||||
|
||||
__all__ = (
|
||||
'DistanceFilterMixin',
|
||||
'ImageAttachmentFilterMixin',
|
||||
'NestedGroupModelFilterMixin',
|
||||
'NetBoxModelFilterMixin',
|
||||
'OrganizationalModelFilterMixin',
|
||||
'PrimaryModelFilterMixin',
|
||||
'SyncedDataFilterMixin',
|
||||
'WeightFilterMixin',
|
||||
)
|
||||
|
|
@ -30,52 +22,15 @@ if TYPE_CHECKING:
|
|||
from extras.graphql.filters import *
|
||||
|
||||
|
||||
class NetBoxModelFilterMixin(
|
||||
ChangeLogFilterMixin,
|
||||
CustomFieldsFilterMixin,
|
||||
JournalEntriesFilterMixin,
|
||||
TagsFilterMixin,
|
||||
BaseObjectTypeFilterMixin,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class NestedGroupModelFilterMixin(NetBoxModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
parent_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrganizationalModelFilterMixin(
|
||||
ChangeLogFilterMixin,
|
||||
CustomFieldsFilterMixin,
|
||||
TagsFilterMixin,
|
||||
BaseObjectTypeFilterMixin,
|
||||
):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrimaryModelFilterMixin(NetBoxModelFilterMixin):
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageAttachmentFilterMixin(BaseFilterMixin):
|
||||
class ImageAttachmentFilterMixin:
|
||||
images: Annotated['ImageAttachmentFilter', strawberry.lazy('extras.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeightFilterMixin(BaseFilterMixin):
|
||||
class WeightFilterMixin:
|
||||
weight: FilterLookup[float] | None = strawberry_django.filter_field()
|
||||
weight_unit: BaseFilterLookup[Annotated['WeightUnitEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
|
@ -83,7 +38,7 @@ class WeightFilterMixin(BaseFilterMixin):
|
|||
|
||||
|
||||
@dataclass
|
||||
class SyncedDataFilterMixin(BaseFilterMixin):
|
||||
class SyncedDataFilterMixin:
|
||||
data_source: Annotated['DataSourceFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -98,7 +53,7 @@ class SyncedDataFilterMixin(BaseFilterMixin):
|
|||
|
||||
|
||||
@dataclass
|
||||
class DistanceFilterMixin(BaseFilterMixin):
|
||||
class DistanceFilterMixin:
|
||||
distance: FilterLookup[float] | None = strawberry_django.filter_field()
|
||||
distance_unit: BaseFilterLookup[Annotated['DistanceUnitEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import strawberry_django
|
||||
from strawberry import ID
|
||||
from strawberry_django import FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import ChangeLoggingMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, JournalEntriesFilterMixin, TagsFilterMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .filters import *
|
||||
|
||||
__all__ = (
|
||||
'BaseModelFilter',
|
||||
'ChangeLoggedModelFilter',
|
||||
'NestedGroupModelFilter',
|
||||
'NetBoxModelFilter',
|
||||
'OrganizationalModelFilter',
|
||||
'PrimaryModelFilter',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseModelFilter:
|
||||
id: FilterLookup[ID] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
class ChangeLoggedModelFilter(ChangeLoggingMixin, BaseModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
class NetBoxModelFilter(
|
||||
CustomFieldsFilterMixin,
|
||||
JournalEntriesFilterMixin,
|
||||
TagsFilterMixin,
|
||||
ChangeLoggingMixin,
|
||||
BaseModelFilter
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class NestedGroupModelFilter(NetBoxModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
parent_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrganizationalModelFilter(NetBoxModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrimaryModelFilter(NetBoxModelFilter):
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import logging
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
|
||||
from django.contrib import messages
|
||||
from django.db import router, transaction
|
||||
|
|
@ -562,8 +561,9 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
|
|||
form.instance._replicated_base = hasattr(self.form, "replication_fields")
|
||||
|
||||
if form.is_valid():
|
||||
changelog_message = form.cleaned_data.pop('changelog_message', '')
|
||||
new_components = []
|
||||
data = deepcopy(request.POST)
|
||||
data = request.POST.copy()
|
||||
pattern_count = len(form.cleaned_data[self.form.replication_fields[0]])
|
||||
|
||||
for i in range(pattern_count):
|
||||
|
|
@ -572,7 +572,8 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
|
|||
data[field_name] = form.cleaned_data[field_name][i]
|
||||
|
||||
if hasattr(form, 'get_iterative_data'):
|
||||
data.update(form.get_iterative_data(i))
|
||||
for k, v in form.get_iterative_data(i).items():
|
||||
data.setlist(k, v)
|
||||
|
||||
component_form = self.model_form(data)
|
||||
|
||||
|
|
@ -588,6 +589,9 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
|
|||
# Create the new components
|
||||
new_objs = []
|
||||
for component_form in new_components:
|
||||
# Record changelog message (if any)
|
||||
if changelog_message:
|
||||
component_form.instance._changelog_message = changelog_message
|
||||
obj = component_form.save()
|
||||
new_objs.append(obj)
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -31,7 +31,7 @@
|
|||
"gridstack": "12.3.3",
|
||||
"htmx.org": "2.0.8",
|
||||
"query-string": "9.3.1",
|
||||
"sass": "1.94.2",
|
||||
"sass": "1.95.0",
|
||||
"tom-select": "2.4.3",
|
||||
"typeface-inter": "3.18.1",
|
||||
"typeface-roboto-mono": "1.1.13"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { getElements } from '../util';
|
||||
|
||||
/**
|
||||
* Move selected options from one select element to another.
|
||||
* Move selected options from one select element to another, preserving optgroup structure.
|
||||
*
|
||||
* @param source Select Element
|
||||
* @param target Select Element
|
||||
|
|
@ -9,14 +9,42 @@ import { getElements } from '../util';
|
|||
function moveOption(source: HTMLSelectElement, target: HTMLSelectElement): void {
|
||||
for (const option of Array.from(source.options)) {
|
||||
if (option.selected) {
|
||||
target.appendChild(option.cloneNode(true));
|
||||
// Check if option is inside an optgroup
|
||||
const parentOptgroup = option.parentElement as HTMLElement;
|
||||
|
||||
if (parentOptgroup.tagName === 'OPTGROUP') {
|
||||
// Find or create matching optgroup in target
|
||||
const groupLabel = parentOptgroup.getAttribute('label');
|
||||
let targetOptgroup = Array.from(target.children).find(
|
||||
child => child.tagName === 'OPTGROUP' && child.getAttribute('label') === groupLabel,
|
||||
) as HTMLOptGroupElement;
|
||||
|
||||
if (!targetOptgroup) {
|
||||
// Create new optgroup in target
|
||||
targetOptgroup = document.createElement('optgroup');
|
||||
targetOptgroup.setAttribute('label', groupLabel!);
|
||||
target.appendChild(targetOptgroup);
|
||||
}
|
||||
|
||||
// Move option to target optgroup
|
||||
targetOptgroup.appendChild(option.cloneNode(true));
|
||||
} else {
|
||||
// Option is not in an optgroup, append directly
|
||||
target.appendChild(option.cloneNode(true));
|
||||
}
|
||||
|
||||
option.remove();
|
||||
|
||||
// Clean up empty optgroups in source
|
||||
if (parentOptgroup.tagName === 'OPTGROUP' && parentOptgroup.children.length === 0) {
|
||||
parentOptgroup.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move selected options of a select element up in order.
|
||||
* Move selected options of a select element up in order, respecting optgroup boundaries.
|
||||
*
|
||||
* Adapted from:
|
||||
* @see https://www.tomred.net/css-html-js/reorder-option-elements-of-an-html-select.html
|
||||
|
|
@ -27,14 +55,21 @@ function moveOptionUp(element: HTMLSelectElement): void {
|
|||
for (let i = 1; i < options.length; i++) {
|
||||
const option = options[i];
|
||||
if (option.selected) {
|
||||
element.removeChild(option);
|
||||
element.insertBefore(option, element.options[i - 1]);
|
||||
const parent = option.parentElement as HTMLElement;
|
||||
const previousOption = element.options[i - 1];
|
||||
const previousParent = previousOption.parentElement as HTMLElement;
|
||||
|
||||
// Only move if previous option is in the same parent (optgroup or select)
|
||||
if (parent === previousParent) {
|
||||
parent.removeChild(option);
|
||||
parent.insertBefore(option, previousOption);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move selected options of a select element down in order.
|
||||
* Move selected options of a select element down in order, respecting optgroup boundaries.
|
||||
*
|
||||
* Adapted from:
|
||||
* @see https://www.tomred.net/css-html-js/reorder-option-elements-of-an-html-select.html
|
||||
|
|
@ -43,12 +78,18 @@ function moveOptionUp(element: HTMLSelectElement): void {
|
|||
function moveOptionDown(element: HTMLSelectElement): void {
|
||||
const options = Array.from(element.options);
|
||||
for (let i = options.length - 2; i >= 0; i--) {
|
||||
let option = options[i];
|
||||
const option = options[i];
|
||||
if (option.selected) {
|
||||
let next = element.options[i + 1];
|
||||
option = element.removeChild(option);
|
||||
next = element.replaceChild(option, next);
|
||||
element.insertBefore(next, option);
|
||||
const parent = option.parentElement as HTMLElement;
|
||||
const nextOption = element.options[i + 1];
|
||||
const nextParent = nextOption.parentElement as HTMLElement;
|
||||
|
||||
// Only move if next option is in the same parent (optgroup or select)
|
||||
if (parent === nextParent) {
|
||||
const optionClone = parent.removeChild(option);
|
||||
const nextClone = parent.replaceChild(optionClone, nextOption);
|
||||
parent.insertBefore(nextClone, optionClone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,20 @@ form.object-edit {
|
|||
}
|
||||
}
|
||||
|
||||
// Make optgroup labels sticky when scrolling through select elements
|
||||
select[multiple] {
|
||||
optgroup {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--bs-body-bg);
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
}
|
||||
option {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter modifier dropdown sizing
|
||||
.modifier-select {
|
||||
min-width: 10rem;
|
||||
|
|
|
|||
|
|
@ -3251,10 +3251,10 @@ safe-regex-test@^1.1.0:
|
|||
es-errors "^1.3.0"
|
||||
is-regex "^1.2.1"
|
||||
|
||||
sass@1.94.2:
|
||||
version "1.94.2"
|
||||
resolved "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz"
|
||||
integrity sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==
|
||||
sass@1.95.0:
|
||||
version "1.95.0"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.95.0.tgz#3a3a4d4d954313ab50eaf16f6e2548a2f6ec0811"
|
||||
integrity sha512-9QMjhLq+UkOg/4bb8Lt8A+hJZvY3t+9xeZMKSBtBEgxrXA3ed5Ts4NDreUkYgJP1BTmrscQE/xYhf7iShow6lw==
|
||||
dependencies:
|
||||
chokidar "^4.0.0"
|
||||
immutable "^5.0.2"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version: "4.4.7"
|
||||
version: "4.4.8"
|
||||
edition: "Community"
|
||||
published: "2025-11-25"
|
||||
published: "2025-12-09"
|
||||
|
|
|
|||
|
|
@ -47,12 +47,8 @@
|
|||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rear Port" %}</th>
|
||||
<td>{{ object.rear_port|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rear Port Position" %}</th>
|
||||
<td>{{ object.rear_port_position }}</td>
|
||||
<th scope="row">{% trans "Positions" %}</th>
|
||||
<td>{{ object.positions }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
|
|
@ -62,6 +58,7 @@
|
|||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
|
|
@ -126,7 +123,29 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Port Mappings" %}</h2>
|
||||
<table class="table table-hover">
|
||||
{% if rear_port_mappings %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Position" %}</th>
|
||||
<th>{% trans "Rear Port" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% endif %}
|
||||
{% for mapping in rear_port_mappings %}
|
||||
<tr>
|
||||
<td>{{ mapping.front_port_position }}</td>
|
||||
<td>
|
||||
<a href="{{ mapping.rear_port.get_absolute_url }}">{{ mapping.rear_port }}:{{ mapping.rear_port_position }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
{% trans "No mappings defined" %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@
|
|||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
|
|
@ -116,7 +117,29 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Port Mappings" %}</h2>
|
||||
<table class="table table-hover">
|
||||
{% if front_port_mappings %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Position" %}</th>
|
||||
<th>{% trans "Front Port" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% endif %}
|
||||
{% for mapping in front_port_mappings %}
|
||||
<tr>
|
||||
<td>{{ mapping.rear_port_position }}</td>
|
||||
<td>
|
||||
<a href="{{ mapping.front_port.get_absolute_url }}">{{ mapping.front_port }}:{{ mapping.front_port_position }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
{% trans "No mappings defined" %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import strawberry
|
|||
import strawberry_django
|
||||
from strawberry import ID
|
||||
|
||||
from core.graphql.filter_mixins import BaseFilterMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from netbox.graphql.filter_lookups import TreeNodeFilter
|
||||
from .filters import ContactAssignmentFilter, TenantFilter, TenantGroupFilter
|
||||
|
|
@ -18,14 +16,14 @@ __all__ = (
|
|||
|
||||
|
||||
@dataclass
|
||||
class ContactFilterMixin(BaseFilterMixin):
|
||||
class ContactFilterMixin:
|
||||
contacts: Annotated['ContactAssignmentFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TenancyFilterMixin(BaseFilterMixin):
|
||||
class TenancyFilterMixin:
|
||||
tenant: Annotated['TenantFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,9 @@ import strawberry_django
|
|||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import ChangeLogFilterMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
|
||||
from netbox.graphql.filter_mixins import (
|
||||
NestedGroupModelFilterMixin,
|
||||
OrganizationalModelFilterMixin,
|
||||
PrimaryModelFilterMixin,
|
||||
from netbox.graphql.filters import (
|
||||
ChangeLoggedModelFilter, NestedGroupModelFilter, OrganizationalModelFilter, PrimaryModelFilter,
|
||||
)
|
||||
from tenancy import models
|
||||
from .filter_mixins import ContactFilterMixin
|
||||
|
|
@ -57,7 +54,7 @@ __all__ = (
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Tenant, lookups=True)
|
||||
class TenantFilter(PrimaryModelFilterMixin, ContactFilterMixin):
|
||||
class TenantFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
|
|
@ -136,7 +133,7 @@ class TenantFilter(PrimaryModelFilterMixin, ContactFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.TenantGroup, lookups=True)
|
||||
class TenantGroupFilter(OrganizationalModelFilterMixin):
|
||||
class TenantGroupFilter(OrganizationalModelFilter):
|
||||
parent: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -150,7 +147,7 @@ class TenantGroupFilter(OrganizationalModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Contact, lookups=True)
|
||||
class ContactFilter(PrimaryModelFilterMixin):
|
||||
class ContactFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
title: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
phone: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
@ -166,19 +163,19 @@ class ContactFilter(PrimaryModelFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.ContactRole, lookups=True)
|
||||
class ContactRoleFilter(OrganizationalModelFilterMixin):
|
||||
class ContactRoleFilter(OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ContactGroup, lookups=True)
|
||||
class ContactGroupFilter(NestedGroupModelFilterMixin):
|
||||
class ContactGroupFilter(NestedGroupModelFilter):
|
||||
parent: Annotated['ContactGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ContactAssignment, lookups=True)
|
||||
class ContactAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
class ContactAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
|
|||
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
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
|
|
@ -1,6 +1,8 @@
|
|||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
from django import forms
|
||||
from django.apps import apps
|
||||
from django.contrib.auth import password_validation
|
||||
from django.contrib.postgres.forms import SimpleArrayField
|
||||
from django.core.exceptions import FieldError
|
||||
|
|
@ -22,6 +24,7 @@ from utilities.forms.fields import (
|
|||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DateTimePicker, SplitMultiSelectWidget
|
||||
from utilities.permissions import qs_filter_from_constraints
|
||||
from utilities.string import title
|
||||
|
||||
__all__ = (
|
||||
'GroupForm',
|
||||
|
|
@ -295,10 +298,24 @@ class GroupForm(forms.ModelForm):
|
|||
|
||||
|
||||
def get_object_types_choices():
|
||||
return [
|
||||
(ot.pk, str(ot))
|
||||
for ot in ObjectType.objects.filter(OBJECTPERMISSION_OBJECT_TYPES).order_by('app_label', 'model')
|
||||
]
|
||||
"""
|
||||
Generate choices for object types grouped by app label using optgroups.
|
||||
Returns nested structure: [(app_label, [(id, model_name), ...]), ...]
|
||||
"""
|
||||
app_label_map = {
|
||||
app_config.label: app_config.verbose_name
|
||||
for app_config in apps.get_app_configs()
|
||||
}
|
||||
choices_by_app = defaultdict(list)
|
||||
|
||||
for ot in ObjectType.objects.filter(OBJECTPERMISSION_OBJECT_TYPES).order_by('app_label', 'model'):
|
||||
app_label = app_label_map.get(ot.app_label, ot.app_label)
|
||||
|
||||
model_class = ot.model_class()
|
||||
model_name = model_class._meta.verbose_name if model_class else ot.model
|
||||
choices_by_app[app_label].append((ot.pk, title(model_name)))
|
||||
|
||||
return list(choices_by_app.items())
|
||||
|
||||
|
||||
class ObjectPermissionForm(forms.ModelForm):
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import strawberry
|
|||
import strawberry_django
|
||||
from strawberry_django import DatetimeFilterLookup, FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin
|
||||
from netbox.graphql.filters import BaseModelFilter
|
||||
from users import models
|
||||
|
||||
__all__ = (
|
||||
|
|
@ -17,13 +17,13 @@ __all__ = (
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Group, lookups=True)
|
||||
class GroupFilter(BaseObjectTypeFilterMixin):
|
||||
class GroupFilter(BaseModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.User, lookups=True)
|
||||
class UserFilter(BaseObjectTypeFilterMixin):
|
||||
class UserFilter(BaseModelFilter):
|
||||
username: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
first_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
last_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
@ -36,7 +36,7 @@ class UserFilter(BaseObjectTypeFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.Owner, lookups=True)
|
||||
class OwnerFilter(BaseObjectTypeFilterMixin):
|
||||
class OwnerFilter(BaseModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group: Annotated['OwnerGroupFilter', strawberry.lazy('users.graphql.filters')] | None = (
|
||||
|
|
@ -49,6 +49,6 @@ class OwnerFilter(BaseObjectTypeFilterMixin):
|
|||
|
||||
|
||||
@strawberry_django.filter_type(models.OwnerGroup, lookups=True)
|
||||
class OwnerGroupFilter(BaseObjectTypeFilterMixin):
|
||||
class OwnerGroupFilter(BaseModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
|
|||
|
|
@ -66,17 +66,45 @@ class SelectWithPK(forms.Select):
|
|||
option_template_name = 'widgets/select_option_with_pk.html'
|
||||
|
||||
|
||||
class AvailableOptions(forms.SelectMultiple):
|
||||
class SelectMultipleBase(forms.SelectMultiple):
|
||||
"""
|
||||
Base class for select widgets that filter choices based on selected values.
|
||||
Subclasses should set `include_selected` to control filtering behavior.
|
||||
"""
|
||||
include_selected = False
|
||||
|
||||
def optgroups(self, name, value, attrs=None):
|
||||
filtered_choices = []
|
||||
include_selected = self.include_selected
|
||||
|
||||
for choice in self.choices:
|
||||
if isinstance(choice[1], (list, tuple)): # optgroup
|
||||
group_label, group_choices = choice
|
||||
filtered_group = [
|
||||
c for c in group_choices if (str(c[0]) in value) == include_selected
|
||||
]
|
||||
|
||||
if filtered_group: # Only include optgroup if it has choices left
|
||||
filtered_choices.append((group_label, filtered_group))
|
||||
else: # option, e.g. flat choice
|
||||
if (str(choice[0]) in value) == include_selected:
|
||||
filtered_choices.append(choice)
|
||||
|
||||
self.choices = filtered_choices
|
||||
value = [] # Clear selected choices
|
||||
return super().optgroups(name, value, attrs)
|
||||
|
||||
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
|
||||
option = super().create_option(name, value, label, selected, index, subindex, attrs)
|
||||
option['attrs']['title'] = label # Add title attribute to show full text on hover
|
||||
return option
|
||||
|
||||
|
||||
class AvailableOptions(SelectMultipleBase):
|
||||
"""
|
||||
Renders a <select multiple=true> including only choices that have been selected. (For unbound fields, this list
|
||||
will be empty.) Employed by SplitMultiSelectWidget.
|
||||
"""
|
||||
def optgroups(self, name, value, attrs=None):
|
||||
self.choices = [
|
||||
choice for choice in self.choices if str(choice[0]) not in value
|
||||
]
|
||||
value = [] # Clear selected choices
|
||||
return super().optgroups(name, value, attrs)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
|
|
@ -87,17 +115,12 @@ class AvailableOptions(forms.SelectMultiple):
|
|||
return context
|
||||
|
||||
|
||||
class SelectedOptions(forms.SelectMultiple):
|
||||
class SelectedOptions(SelectMultipleBase):
|
||||
"""
|
||||
Renders a <select multiple=true> including only choices that have _not_ been selected. (For unbound fields, this
|
||||
will include _all_ choices.) Employed by SplitMultiSelectWidget.
|
||||
"""
|
||||
def optgroups(self, name, value, attrs=None):
|
||||
self.choices = [
|
||||
choice for choice in self.choices if str(choice[0]) in value
|
||||
]
|
||||
value = [] # Clear selected choices
|
||||
return super().optgroups(name, value, attrs)
|
||||
include_selected = True
|
||||
|
||||
|
||||
class SplitMultiSelectWidget(forms.MultiWidget):
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue