mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-19 01:58:43 -06:00
Compare commits
8 Commits
cf16a29ad3
...
v4.4.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
970f2bd4ed | ||
|
|
a4ee323cb6 | ||
|
|
17e5184a11 | ||
|
|
e1548bb290 | ||
|
|
269112a565 | ||
|
|
c6672538ac | ||
|
|
6efb258b9f | ||
|
|
ebf8f7fa1b |
@@ -15,7 +15,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v4.4.7
|
placeholder: v4.4.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -27,7 +27,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox Version
|
label: NetBox Version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v4.4.7
|
placeholder: v4.4.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.0.3",
|
"openapi": "3.0.3",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "NetBox REST API",
|
"title": "NetBox REST API",
|
||||||
"version": "4.4.7",
|
"version": "4.4.8",
|
||||||
"license": {
|
"license": {
|
||||||
"name": "Apache v2 License"
|
"name": "Apache v2 License"
|
||||||
}
|
}
|
||||||
@@ -27326,6 +27326,58 @@
|
|||||||
"explode": true,
|
"explode": true,
|
||||||
"style": "form"
|
"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",
|
"in": "query",
|
||||||
"name": "type",
|
"name": "type",
|
||||||
@@ -30798,6 +30850,58 @@
|
|||||||
"explode": true,
|
"explode": true,
|
||||||
"style": "form"
|
"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",
|
"in": "query",
|
||||||
"name": "type",
|
"name": "type",
|
||||||
@@ -34158,6 +34262,58 @@
|
|||||||
"explode": true,
|
"explode": true,
|
||||||
"style": "form"
|
"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",
|
"in": "query",
|
||||||
"name": "updated_by_request",
|
"name": "updated_by_request",
|
||||||
@@ -46373,6 +46529,58 @@
|
|||||||
"explode": true,
|
"explode": true,
|
||||||
"style": "form"
|
"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",
|
"in": "query",
|
||||||
"name": "type",
|
"name": "type",
|
||||||
@@ -52303,6 +52511,58 @@
|
|||||||
"explode": true,
|
"explode": true,
|
||||||
"style": "form"
|
"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",
|
"in": "query",
|
||||||
"name": "tx_power",
|
"name": "tx_power",
|
||||||
@@ -58814,6 +59074,58 @@
|
|||||||
"explode": true,
|
"explode": true,
|
||||||
"style": "form"
|
"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",
|
"in": "query",
|
||||||
"name": "updated_by_request",
|
"name": "updated_by_request",
|
||||||
@@ -66953,6 +67265,58 @@
|
|||||||
"explode": true,
|
"explode": true,
|
||||||
"style": "form"
|
"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",
|
"in": "query",
|
||||||
"name": "updated_by_request",
|
"name": "updated_by_request",
|
||||||
@@ -78840,6 +79204,58 @@
|
|||||||
"explode": true,
|
"explode": true,
|
||||||
"style": "form"
|
"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",
|
"in": "query",
|
||||||
"name": "type",
|
"name": "type",
|
||||||
@@ -83976,6 +84392,58 @@
|
|||||||
"explode": true,
|
"explode": true,
|
||||||
"style": "form"
|
"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",
|
"in": "query",
|
||||||
"name": "type",
|
"name": "type",
|
||||||
@@ -96759,6 +97227,58 @@
|
|||||||
"explode": true,
|
"explode": true,
|
||||||
"style": "form"
|
"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",
|
"in": "query",
|
||||||
"name": "type",
|
"name": "type",
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
# NetBox v4.4
|
# 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)
|
## v4.4.7 (2025-11-25)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|||||||
@@ -1626,6 +1626,17 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
|||||||
choices=DeviceStatusChoices,
|
choices=DeviceStatusChoices,
|
||||||
field_name='device__status',
|
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):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
|||||||
@@ -472,14 +472,30 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text=_('Unit for module weight')
|
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:
|
class Meta:
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = [
|
fields = [
|
||||||
'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'profile',
|
'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'profile',
|
||||||
'comments', 'tags'
|
'attribute_data', '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(NetBoxModelImportForm):
|
class DeviceRoleImportForm(NetBoxModelImportForm):
|
||||||
parent = CSVModelChoiceField(
|
parent = CSVModelChoiceField(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from ipam.models import ASN, VRF, VLANTranslationPolicy
|
|||||||
from netbox.choices import *
|
from netbox.choices import *
|
||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||||
|
from tenancy.models import Tenant
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||||
@@ -120,6 +121,11 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Device role')
|
label=_('Device role')
|
||||||
)
|
)
|
||||||
|
tenant_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Tenant')
|
||||||
|
)
|
||||||
device_id = DynamicModelMultipleChoiceField(
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@@ -128,7 +134,8 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
|||||||
'location_id': '$location_id',
|
'location_id': '$location_id',
|
||||||
'virtual_chassis_id': '$virtual_chassis_id',
|
'virtual_chassis_id': '$virtual_chassis_id',
|
||||||
'device_type_id': '$device_type_id',
|
'device_type_id': '$device_type_id',
|
||||||
'role_id': '$role_id'
|
'role_id': '$role_id',
|
||||||
|
'tenant_id': '$tenant_id'
|
||||||
},
|
},
|
||||||
label=_('Device')
|
label=_('Device')
|
||||||
)
|
)
|
||||||
@@ -1317,7 +1324,8 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
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')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
)
|
)
|
||||||
@@ -1341,7 +1349,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
|
|||||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
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')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
@@ -1366,7 +1374,8 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
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')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
)
|
)
|
||||||
@@ -1385,7 +1394,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
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')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
@@ -1418,7 +1427,8 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
FieldSet('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', name=_('Wireless')),
|
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('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
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')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||||
@@ -1539,7 +1549,8 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
|||||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
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')),
|
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||||
)
|
)
|
||||||
@@ -1563,7 +1574,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
|||||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
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')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||||
@@ -1587,7 +1598,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
|
|||||||
FieldSet('name', 'label', 'position', name=_('Attributes')),
|
FieldSet('name', 'label', 'position', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
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')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1605,7 +1616,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
|||||||
FieldSet('name', 'label', name=_('Attributes')),
|
FieldSet('name', 'label', name=_('Attributes')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
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')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1622,7 +1633,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
|||||||
),
|
),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||||
FieldSet(
|
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')
|
name=_('Device')
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -681,8 +681,8 @@ class ModuleBayTemplate(ModularComponentTemplateModel):
|
|||||||
|
|
||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.name,
|
name=self.resolve_name(kwargs.get('module')),
|
||||||
label=self.label,
|
label=self.resolve_label(kwargs.get('module')),
|
||||||
position=self.position,
|
position=self.position,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from jsonschema.exceptions import ValidationError as JSONValidationError
|
from jsonschema.exceptions import ValidationError as JSONValidationError
|
||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import MODULE_TOKEN
|
|
||||||
from dcim.utils import update_interface_bridges
|
from dcim.utils import update_interface_bridges
|
||||||
from extras.models import ConfigContextModel, CustomField
|
from extras.models import ConfigContextModel, CustomField
|
||||||
from netbox.models import PrimaryModel
|
from netbox.models import PrimaryModel
|
||||||
@@ -331,7 +330,6 @@ class Module(PrimaryModel, ConfigContextModel):
|
|||||||
else:
|
else:
|
||||||
# ModuleBays must be saved individually for MPTT
|
# ModuleBays must be saved individually for MPTT
|
||||||
for instance in create_instances:
|
for instance in create_instances:
|
||||||
instance.name = instance.name.replace(MODULE_TOKEN, str(self.module_bay.position))
|
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
update_fields = ['module']
|
update_fields = ['module']
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ class DeviceComponentFilterSetTests:
|
|||||||
params = {'device_status': ['active', 'planned']}
|
params = {'device_status': ['active', 'planned']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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:
|
class DeviceComponentTemplateFilterSetTests:
|
||||||
|
|
||||||
@@ -3377,9 +3384,17 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
|||||||
)
|
)
|
||||||
Rack.objects.bulk_create(racks)
|
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 = (
|
devices = (
|
||||||
Device(
|
Device(
|
||||||
name='Device 1',
|
name='Device 1',
|
||||||
|
tenant=tenants[0],
|
||||||
device_type=device_types[0],
|
device_type=device_types[0],
|
||||||
role=roles[0],
|
role=roles[0],
|
||||||
site=sites[0],
|
site=sites[0],
|
||||||
@@ -3389,6 +3404,7 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 2',
|
name='Device 2',
|
||||||
|
tenant=tenants[1],
|
||||||
device_type=device_types[1],
|
device_type=device_types[1],
|
||||||
role=roles[1],
|
role=roles[1],
|
||||||
site=sites[1],
|
site=sites[1],
|
||||||
@@ -3398,6 +3414,7 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 3',
|
name='Device 3',
|
||||||
|
tenant=tenants[2],
|
||||||
device_type=device_types[2],
|
device_type=device_types[2],
|
||||||
role=roles[2],
|
role=roles[2],
|
||||||
site=sites[2],
|
site=sites[2],
|
||||||
@@ -3617,9 +3634,17 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
|
|||||||
)
|
)
|
||||||
Rack.objects.bulk_create(racks)
|
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 = (
|
devices = (
|
||||||
Device(
|
Device(
|
||||||
name='Device 1',
|
name='Device 1',
|
||||||
|
tenant=tenants[0],
|
||||||
device_type=device_types[0],
|
device_type=device_types[0],
|
||||||
role=roles[0],
|
role=roles[0],
|
||||||
site=sites[0],
|
site=sites[0],
|
||||||
@@ -3629,6 +3654,7 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 2',
|
name='Device 2',
|
||||||
|
tenant=tenants[1],
|
||||||
device_type=device_types[1],
|
device_type=device_types[1],
|
||||||
role=roles[1],
|
role=roles[1],
|
||||||
site=sites[1],
|
site=sites[1],
|
||||||
@@ -3638,6 +3664,7 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 3',
|
name='Device 3',
|
||||||
|
tenant=tenants[2],
|
||||||
device_type=device_types[2],
|
device_type=device_types[2],
|
||||||
role=roles[2],
|
role=roles[2],
|
||||||
site=sites[2],
|
site=sites[2],
|
||||||
@@ -3857,9 +3884,17 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
)
|
)
|
||||||
Rack.objects.bulk_create(racks)
|
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 = (
|
devices = (
|
||||||
Device(
|
Device(
|
||||||
name='Device 1',
|
name='Device 1',
|
||||||
|
tenant=tenants[0],
|
||||||
device_type=device_types[0],
|
device_type=device_types[0],
|
||||||
role=roles[0],
|
role=roles[0],
|
||||||
site=sites[0],
|
site=sites[0],
|
||||||
@@ -3869,6 +3904,7 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 2',
|
name='Device 2',
|
||||||
|
tenant=tenants[1],
|
||||||
device_type=device_types[1],
|
device_type=device_types[1],
|
||||||
role=roles[1],
|
role=roles[1],
|
||||||
site=sites[1],
|
site=sites[1],
|
||||||
@@ -3878,6 +3914,7 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 3',
|
name='Device 3',
|
||||||
|
tenant=tenants[2],
|
||||||
device_type=device_types[2],
|
device_type=device_types[2],
|
||||||
role=roles[2],
|
role=roles[2],
|
||||||
site=sites[2],
|
site=sites[2],
|
||||||
@@ -4111,9 +4148,17 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
|||||||
)
|
)
|
||||||
Rack.objects.bulk_create(racks)
|
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 = (
|
devices = (
|
||||||
Device(
|
Device(
|
||||||
name='Device 1',
|
name='Device 1',
|
||||||
|
tenant=tenants[0],
|
||||||
device_type=device_types[0],
|
device_type=device_types[0],
|
||||||
role=roles[0],
|
role=roles[0],
|
||||||
site=sites[0],
|
site=sites[0],
|
||||||
@@ -4123,6 +4168,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 2',
|
name='Device 2',
|
||||||
|
tenant=tenants[1],
|
||||||
device_type=device_types[1],
|
device_type=device_types[1],
|
||||||
role=roles[1],
|
role=roles[1],
|
||||||
site=sites[1],
|
site=sites[1],
|
||||||
@@ -4132,6 +4178,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 3',
|
name='Device 3',
|
||||||
|
tenant=tenants[2],
|
||||||
device_type=device_types[2],
|
device_type=device_types[2],
|
||||||
role=roles[2],
|
role=roles[2],
|
||||||
site=sites[2],
|
site=sites[2],
|
||||||
@@ -4390,9 +4437,17 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
virtual_chassis = VirtualChassis(name='Virtual Chassis')
|
virtual_chassis = VirtualChassis(name='Virtual Chassis')
|
||||||
virtual_chassis.save()
|
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 = (
|
devices = (
|
||||||
Device(
|
Device(
|
||||||
name='Device 1A',
|
name='Device 1A',
|
||||||
|
tenant=tenants[0],
|
||||||
device_type=device_types[0],
|
device_type=device_types[0],
|
||||||
role=roles[0],
|
role=roles[0],
|
||||||
site=sites[0],
|
site=sites[0],
|
||||||
@@ -4405,6 +4460,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 1B',
|
name='Device 1B',
|
||||||
|
tenant=tenants[1],
|
||||||
device_type=device_types[2],
|
device_type=device_types[2],
|
||||||
role=roles[2],
|
role=roles[2],
|
||||||
site=sites[2],
|
site=sites[2],
|
||||||
@@ -4417,6 +4473,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 2',
|
name='Device 2',
|
||||||
|
tenant=tenants[2],
|
||||||
device_type=device_types[1],
|
device_type=device_types[1],
|
||||||
role=roles[1],
|
role=roles[1],
|
||||||
site=sites[1],
|
site=sites[1],
|
||||||
@@ -4426,6 +4483,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 3',
|
name='Device 3',
|
||||||
|
tenant=tenants[2],
|
||||||
device_type=device_types[2],
|
device_type=device_types[2],
|
||||||
role=roles[2],
|
role=roles[2],
|
||||||
site=sites[2],
|
site=sites[2],
|
||||||
@@ -5011,9 +5069,17 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
)
|
)
|
||||||
Rack.objects.bulk_create(racks)
|
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 = (
|
devices = (
|
||||||
Device(
|
Device(
|
||||||
name='Device 1',
|
name='Device 1',
|
||||||
|
tenant=tenants[0],
|
||||||
device_type=device_types[0],
|
device_type=device_types[0],
|
||||||
role=roles[0],
|
role=roles[0],
|
||||||
site=sites[0],
|
site=sites[0],
|
||||||
@@ -5023,6 +5089,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 2',
|
name='Device 2',
|
||||||
|
tenant=tenants[1],
|
||||||
device_type=device_types[1],
|
device_type=device_types[1],
|
||||||
role=roles[1],
|
role=roles[1],
|
||||||
site=sites[1],
|
site=sites[1],
|
||||||
@@ -5032,6 +5099,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 3',
|
name='Device 3',
|
||||||
|
tenant=tenants[2],
|
||||||
device_type=device_types[2],
|
device_type=device_types[2],
|
||||||
role=roles[2],
|
role=roles[2],
|
||||||
site=sites[2],
|
site=sites[2],
|
||||||
@@ -5302,9 +5370,17 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
|
|||||||
)
|
)
|
||||||
Rack.objects.bulk_create(racks)
|
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 = (
|
devices = (
|
||||||
Device(
|
Device(
|
||||||
name='Device 1',
|
name='Device 1',
|
||||||
|
tenant=tenants[0],
|
||||||
device_type=device_types[0],
|
device_type=device_types[0],
|
||||||
role=roles[0],
|
role=roles[0],
|
||||||
site=sites[0],
|
site=sites[0],
|
||||||
@@ -5314,6 +5390,7 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 2',
|
name='Device 2',
|
||||||
|
tenant=tenants[1],
|
||||||
device_type=device_types[1],
|
device_type=device_types[1],
|
||||||
role=roles[1],
|
role=roles[1],
|
||||||
site=sites[1],
|
site=sites[1],
|
||||||
@@ -5323,6 +5400,7 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 3',
|
name='Device 3',
|
||||||
|
tenant=tenants[2],
|
||||||
device_type=device_types[2],
|
device_type=device_types[2],
|
||||||
role=roles[2],
|
role=roles[2],
|
||||||
site=sites[2],
|
site=sites[2],
|
||||||
@@ -5579,9 +5657,17 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
)
|
)
|
||||||
Rack.objects.bulk_create(racks)
|
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 = (
|
devices = (
|
||||||
Device(
|
Device(
|
||||||
name='Device 1',
|
name='Device 1',
|
||||||
|
tenant=tenants[0],
|
||||||
device_type=device_types[0],
|
device_type=device_types[0],
|
||||||
role=roles[0],
|
role=roles[0],
|
||||||
site=sites[0],
|
site=sites[0],
|
||||||
@@ -5591,6 +5677,7 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 2',
|
name='Device 2',
|
||||||
|
tenant=tenants[1],
|
||||||
device_type=device_types[1],
|
device_type=device_types[1],
|
||||||
role=roles[1],
|
role=roles[1],
|
||||||
site=sites[1],
|
site=sites[1],
|
||||||
@@ -5600,6 +5687,7 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 3',
|
name='Device 3',
|
||||||
|
tenant=tenants[2],
|
||||||
device_type=device_types[2],
|
device_type=device_types[2],
|
||||||
role=roles[2],
|
role=roles[2],
|
||||||
site=sites[2],
|
site=sites[2],
|
||||||
@@ -5752,9 +5840,17 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
)
|
)
|
||||||
Rack.objects.bulk_create(racks)
|
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 = (
|
devices = (
|
||||||
Device(
|
Device(
|
||||||
name='Device 1',
|
name='Device 1',
|
||||||
|
tenant=tenants[0],
|
||||||
device_type=device_types[0],
|
device_type=device_types[0],
|
||||||
role=roles[0],
|
role=roles[0],
|
||||||
site=sites[0],
|
site=sites[0],
|
||||||
@@ -5764,6 +5860,7 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 2',
|
name='Device 2',
|
||||||
|
tenant=tenants[1],
|
||||||
device_type=device_types[1],
|
device_type=device_types[1],
|
||||||
role=roles[1],
|
role=roles[1],
|
||||||
site=sites[1],
|
site=sites[1],
|
||||||
@@ -5773,6 +5870,7 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
name='Device 3',
|
name='Device 3',
|
||||||
|
tenant=tenants[2],
|
||||||
device_type=device_types[2],
|
device_type=device_types[2],
|
||||||
role=roles[2],
|
role=roles[2],
|
||||||
site=sites[2],
|
site=sites[2],
|
||||||
|
|||||||
@@ -792,8 +792,54 @@ class ModuleBayTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
device.consoleports.first()
|
device.consoleports.first()
|
||||||
|
|
||||||
def test_nested_module_token(self):
|
@tag('regression') # #19918
|
||||||
pass
|
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):
|
class CableTestCase(TestCase):
|
||||||
|
|||||||
2
netbox/project-static/dist/netbox.css
vendored
2
netbox/project-static/dist/netbox.css
vendored
File diff suppressed because one or more lines are too long
8
netbox/project-static/dist/netbox.js
vendored
8
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/netbox.js.map
vendored
6
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -30,7 +30,7 @@
|
|||||||
"gridstack": "12.3.3",
|
"gridstack": "12.3.3",
|
||||||
"htmx.org": "2.0.8",
|
"htmx.org": "2.0.8",
|
||||||
"query-string": "9.3.1",
|
"query-string": "9.3.1",
|
||||||
"sass": "1.94.2",
|
"sass": "1.95.0",
|
||||||
"tom-select": "2.4.3",
|
"tom-select": "2.4.3",
|
||||||
"typeface-inter": "3.18.1",
|
"typeface-inter": "3.18.1",
|
||||||
"typeface-roboto-mono": "1.1.13"
|
"typeface-roboto-mono": "1.1.13"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { getElements } from '../util';
|
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 source Select Element
|
||||||
* @param target Select Element
|
* @param target Select Element
|
||||||
@@ -9,14 +9,42 @@ import { getElements } from '../util';
|
|||||||
function moveOption(source: HTMLSelectElement, target: HTMLSelectElement): void {
|
function moveOption(source: HTMLSelectElement, target: HTMLSelectElement): void {
|
||||||
for (const option of Array.from(source.options)) {
|
for (const option of Array.from(source.options)) {
|
||||||
if (option.selected) {
|
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();
|
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:
|
* Adapted from:
|
||||||
* @see https://www.tomred.net/css-html-js/reorder-option-elements-of-an-html-select.html
|
* @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++) {
|
for (let i = 1; i < options.length; i++) {
|
||||||
const option = options[i];
|
const option = options[i];
|
||||||
if (option.selected) {
|
if (option.selected) {
|
||||||
element.removeChild(option);
|
const parent = option.parentElement as HTMLElement;
|
||||||
element.insertBefore(option, element.options[i - 1]);
|
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:
|
* Adapted from:
|
||||||
* @see https://www.tomred.net/css-html-js/reorder-option-elements-of-an-html-select.html
|
* @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 {
|
function moveOptionDown(element: HTMLSelectElement): void {
|
||||||
const options = Array.from(element.options);
|
const options = Array.from(element.options);
|
||||||
for (let i = options.length - 2; i >= 0; i--) {
|
for (let i = options.length - 2; i >= 0; i--) {
|
||||||
let option = options[i];
|
const option = options[i];
|
||||||
if (option.selected) {
|
if (option.selected) {
|
||||||
let next = element.options[i + 1];
|
const parent = option.parentElement as HTMLElement;
|
||||||
option = element.removeChild(option);
|
const nextOption = element.options[i + 1];
|
||||||
next = element.replaceChild(option, next);
|
const nextParent = nextOption.parentElement as HTMLElement;
|
||||||
element.insertBefore(next, option);
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,3 +32,17 @@ form.object-edit {
|
|||||||
border: 1px solid $red;
|
border: 1px solid $red;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3190,10 +3190,10 @@ safe-regex-test@^1.1.0:
|
|||||||
es-errors "^1.3.0"
|
es-errors "^1.3.0"
|
||||||
is-regex "^1.2.1"
|
is-regex "^1.2.1"
|
||||||
|
|
||||||
sass@1.94.2:
|
sass@1.95.0:
|
||||||
version "1.94.2"
|
version "1.95.0"
|
||||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.94.2.tgz#198511fc6fdd2fc0a71b8d1261735c12608d4ef3"
|
resolved "https://registry.yarnpkg.com/sass/-/sass-1.95.0.tgz#3a3a4d4d954313ab50eaf16f6e2548a2f6ec0811"
|
||||||
integrity sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==
|
integrity sha512-9QMjhLq+UkOg/4bb8Lt8A+hJZvY3t+9xeZMKSBtBEgxrXA3ed5Ts4NDreUkYgJP1BTmrscQE/xYhf7iShow6lw==
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar "^4.0.0"
|
chokidar "^4.0.0"
|
||||||
immutable "^5.0.2"
|
immutable "^5.0.2"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version: "4.4.7"
|
version: "4.4.8"
|
||||||
edition: "Community"
|
edition: "Community"
|
||||||
published: "2025-11-25"
|
published: "2025-12-09"
|
||||||
|
|||||||
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
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import password_validation
|
from django.contrib.auth import password_validation
|
||||||
from django.contrib.postgres.forms import SimpleArrayField
|
from django.contrib.postgres.forms import SimpleArrayField
|
||||||
@@ -21,6 +23,7 @@ from utilities.forms.fields import (
|
|||||||
DynamicModelMultipleChoiceField,
|
DynamicModelMultipleChoiceField,
|
||||||
JSONField,
|
JSONField,
|
||||||
)
|
)
|
||||||
|
from utilities.string import title
|
||||||
from utilities.forms.rendering import FieldSet
|
from utilities.forms.rendering import FieldSet
|
||||||
from utilities.forms.widgets import DateTimePicker, SplitMultiSelectWidget
|
from utilities.forms.widgets import DateTimePicker, SplitMultiSelectWidget
|
||||||
from utilities.permissions import qs_filter_from_constraints
|
from utilities.permissions import qs_filter_from_constraints
|
||||||
@@ -283,10 +286,24 @@ class GroupForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
def get_object_types_choices():
|
def get_object_types_choices():
|
||||||
return [
|
"""
|
||||||
(ot.pk, str(ot))
|
Generate choices for object types grouped by app label using optgroups.
|
||||||
for ot in ObjectType.objects.filter(OBJECTPERMISSION_OBJECT_TYPES).order_by('app_label', 'model')
|
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):
|
class ObjectPermissionForm(forms.ModelForm):
|
||||||
|
|||||||
@@ -66,17 +66,45 @@ class SelectWithPK(forms.Select):
|
|||||||
option_template_name = 'widgets/select_option_with_pk.html'
|
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
|
Renders a <select multiple=true> including only choices that have been selected. (For unbound fields, this list
|
||||||
will be empty.) Employed by SplitMultiSelectWidget.
|
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):
|
def get_context(self, name, value, attrs):
|
||||||
context = super().get_context(name, value, attrs)
|
context = super().get_context(name, value, attrs)
|
||||||
@@ -87,17 +115,12 @@ class AvailableOptions(forms.SelectMultiple):
|
|||||||
return context
|
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
|
Renders a <select multiple=true> including only choices that have _not_ been selected. (For unbound fields, this
|
||||||
will include _all_ choices.) Employed by SplitMultiSelectWidget.
|
will include _all_ choices.) Employed by SplitMultiSelectWidget.
|
||||||
"""
|
"""
|
||||||
def optgroups(self, name, value, attrs=None):
|
include_selected = True
|
||||||
self.choices = [
|
|
||||||
choice for choice in self.choices if str(choice[0]) in value
|
|
||||||
]
|
|
||||||
value = [] # Clear selected choices
|
|
||||||
return super().optgroups(name, value, attrs)
|
|
||||||
|
|
||||||
|
|
||||||
class SplitMultiSelectWidget(forms.MultiWidget):
|
class SplitMultiSelectWidget(forms.MultiWidget):
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from utilities.forms.bulk_import import BulkImportForm
|
|||||||
from utilities.forms.fields.csv import CSVSelectWidget
|
from utilities.forms.fields.csv import CSVSelectWidget
|
||||||
from utilities.forms.forms import BulkRenameForm
|
from utilities.forms.forms import BulkRenameForm
|
||||||
from utilities.forms.utils import get_field_value, expand_alphanumeric_pattern, expand_ipaddress_pattern
|
from utilities.forms.utils import get_field_value, expand_alphanumeric_pattern, expand_ipaddress_pattern
|
||||||
|
from utilities.forms.widgets.select import AvailableOptions, SelectedOptions
|
||||||
|
|
||||||
|
|
||||||
class ExpandIPAddress(TestCase):
|
class ExpandIPAddress(TestCase):
|
||||||
@@ -481,3 +482,71 @@ class CSVSelectWidgetTest(TestCase):
|
|||||||
widget = CSVSelectWidget()
|
widget = CSVSelectWidget()
|
||||||
data = {'test_field': 'valid_value'}
|
data = {'test_field': 'valid_value'}
|
||||||
self.assertFalse(widget.value_omitted_from_data(data, {}, 'test_field'))
|
self.assertFalse(widget.value_omitted_from_data(data, {}, 'test_field'))
|
||||||
|
|
||||||
|
|
||||||
|
class SelectMultipleWidgetTest(TestCase):
|
||||||
|
"""
|
||||||
|
Validate filtering behavior of AvailableOptions and SelectedOptions widgets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_available_options_flat_choices(self):
|
||||||
|
"""AvailableOptions should exclude selected values from flat choices"""
|
||||||
|
widget = AvailableOptions(choices=[
|
||||||
|
(1, 'Option 1'),
|
||||||
|
(2, 'Option 2'),
|
||||||
|
(3, 'Option 3'),
|
||||||
|
])
|
||||||
|
widget.optgroups('test', ['2'], None)
|
||||||
|
|
||||||
|
self.assertEqual(len(widget.choices), 2)
|
||||||
|
self.assertEqual(widget.choices[0], (1, 'Option 1'))
|
||||||
|
self.assertEqual(widget.choices[1], (3, 'Option 3'))
|
||||||
|
|
||||||
|
def test_available_options_optgroups(self):
|
||||||
|
"""AvailableOptions should exclude selected values from optgroups"""
|
||||||
|
widget = AvailableOptions(choices=[
|
||||||
|
('Group A', [(1, 'Option 1'), (2, 'Option 2')]),
|
||||||
|
('Group B', [(3, 'Option 3'), (4, 'Option 4')]),
|
||||||
|
])
|
||||||
|
|
||||||
|
# Select options 2 and 3
|
||||||
|
widget.optgroups('test', ['2', '3'], None)
|
||||||
|
|
||||||
|
# Should have 2 groups with filtered choices
|
||||||
|
self.assertEqual(len(widget.choices), 2)
|
||||||
|
self.assertEqual(widget.choices[0][0], 'Group A')
|
||||||
|
self.assertEqual(widget.choices[0][1], [(1, 'Option 1')])
|
||||||
|
self.assertEqual(widget.choices[1][0], 'Group B')
|
||||||
|
self.assertEqual(widget.choices[1][1], [(4, 'Option 4')])
|
||||||
|
|
||||||
|
def test_selected_options_flat_choices(self):
|
||||||
|
"""SelectedOptions should include only selected values from flat choices"""
|
||||||
|
widget = SelectedOptions(choices=[
|
||||||
|
(1, 'Option 1'),
|
||||||
|
(2, 'Option 2'),
|
||||||
|
(3, 'Option 3'),
|
||||||
|
])
|
||||||
|
|
||||||
|
# Select option 2
|
||||||
|
widget.optgroups('test', ['2'], None)
|
||||||
|
|
||||||
|
# Should only have option 2
|
||||||
|
self.assertEqual(len(widget.choices), 1)
|
||||||
|
self.assertEqual(widget.choices[0], (2, 'Option 2'))
|
||||||
|
|
||||||
|
def test_selected_options_optgroups(self):
|
||||||
|
"""SelectedOptions should include only selected values from optgroups"""
|
||||||
|
widget = SelectedOptions(choices=[
|
||||||
|
('Group A', [(1, 'Option 1'), (2, 'Option 2')]),
|
||||||
|
('Group B', [(3, 'Option 3'), (4, 'Option 4')]),
|
||||||
|
])
|
||||||
|
|
||||||
|
# Select options 2 and 3
|
||||||
|
widget.optgroups('test', ['2', '3'], None)
|
||||||
|
|
||||||
|
# Should have 2 groups with only selected choices
|
||||||
|
self.assertEqual(len(widget.choices), 2)
|
||||||
|
self.assertEqual(widget.choices[0][0], 'Group A')
|
||||||
|
self.assertEqual(widget.choices[0][1], [(2, 'Option 2')])
|
||||||
|
self.assertEqual(widget.choices[1][0], 'Group B')
|
||||||
|
self.assertEqual(widget.choices[1][1], [(3, 'Option 3')])
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
Django==5.2.8
|
Django==5.2.9
|
||||||
django-cors-headers==4.9.0
|
django-cors-headers==4.9.0
|
||||||
django-debug-toolbar==6.1.0
|
django-debug-toolbar==6.1.0
|
||||||
django-filter==25.2
|
django-filter==25.2
|
||||||
django-graphiql-debug-toolbar==0.2.0
|
django-graphiql-debug-toolbar==0.2.0
|
||||||
django-htmx==1.26.0
|
django-htmx==1.27.0
|
||||||
django-mptt==0.17.0
|
django-mptt==0.17.0
|
||||||
django-pglocks==1.0.4
|
django-pglocks==1.0.4
|
||||||
django-prometheus==2.4.1
|
django-prometheus==2.4.1
|
||||||
@@ -14,30 +14,30 @@ django-rq==3.2.1
|
|||||||
django-storages==1.14.6
|
django-storages==1.14.6
|
||||||
django-tables2==2.8.0
|
django-tables2==2.8.0
|
||||||
django-taggit==6.1.0
|
django-taggit==6.1.0
|
||||||
django-timezone-field==7.1
|
django-timezone-field==7.2.1
|
||||||
djangorestframework==3.16.1
|
djangorestframework==3.16.1
|
||||||
drf-spectacular==0.29.0
|
drf-spectacular==0.29.0
|
||||||
drf-spectacular-sidecar==2025.10.1
|
drf-spectacular-sidecar==2025.12.1
|
||||||
feedparser==6.0.12
|
feedparser==6.0.12
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
Jinja2==3.1.6
|
Jinja2==3.1.6
|
||||||
jsonschema==4.25.1
|
jsonschema==4.25.1
|
||||||
Markdown==3.10
|
Markdown==3.10
|
||||||
mkdocs-material==9.7.0
|
mkdocs-material==9.7.0
|
||||||
mkdocstrings==0.30.1
|
mkdocstrings==1.0.0
|
||||||
mkdocstrings-python==1.19.0
|
mkdocstrings-python==2.0.1
|
||||||
netaddr==1.3.0
|
netaddr==1.3.0
|
||||||
nh3==0.3.2
|
nh3==0.3.2
|
||||||
Pillow==12.0.0
|
Pillow==12.0.0
|
||||||
psycopg[c,pool]==3.2.13
|
psycopg[c,pool]==3.3.2
|
||||||
PyYAML==6.0.3
|
PyYAML==6.0.3
|
||||||
requests==2.32.5
|
requests==2.32.5
|
||||||
rq==2.6.1
|
rq==2.6.1
|
||||||
social-auth-app-django==5.6.0
|
social-auth-app-django==5.6.0
|
||||||
social-auth-core==4.8.1
|
social-auth-core==4.8.1
|
||||||
sorl-thumbnail==12.11.0
|
sorl-thumbnail==12.11.0
|
||||||
strawberry-graphql==0.287.0
|
strawberry-graphql==0.287.2
|
||||||
strawberry-graphql-django==0.67.2
|
strawberry-graphql-django==0.70.1
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
tablib==3.9.0
|
tablib==3.9.0
|
||||||
tzdata==2025.2
|
tzdata==2025.2
|
||||||
|
|||||||
Reference in New Issue
Block a user