Compare commits

..

1 Commits

Author SHA1 Message Date
Martin Hauser
209c60ea6e test(tables): Add reusable OrderableColumnsTestCase
Introduce `TableTestCases.OrderableColumnsTestCase`, a shared base class
that automatically discovers sortable columns from list-view querysets
and verifies each renders without exceptions in both ascending and
descending order.

Add per-table smoke tests across circuits, core, dcim, extras, ipam,
tenancy, users, virtualization, vpn, and wireless apps.

Fixes #21766
2026-04-03 15:01:57 +02:00
52 changed files with 15797 additions and 13685 deletions

View File

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

View File

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

View File

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

View File

@@ -47,7 +47,8 @@ django-rich
# Django integration for RQ (Reqis queuing)
# https://github.com/rq/django-rq/blob/master/CHANGELOG.md
django-rq
# See https://github.com/netbox-community/netbox/issues/21696
django-rq<4.0
# Provides a variety of storage backends
# https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst

View File

@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "NetBox REST API",
"version": "4.5.7",
"version": "4.5.6",
"license": {
"name": "Apache v2 License"
}
@@ -25468,7 +25468,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "f566e6df6572f5d0"
"x-spec-enum-id": "5e0f85310f0184ea"
}
},
"explode": true,
@@ -25488,7 +25488,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "f566e6df6572f5d0"
"x-spec-enum-id": "5e0f85310f0184ea"
}
},
"explode": true,
@@ -25501,7 +25501,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "f566e6df6572f5d0"
"x-spec-enum-id": "5e0f85310f0184ea"
}
},
"explode": true,
@@ -25514,7 +25514,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "f566e6df6572f5d0"
"x-spec-enum-id": "5e0f85310f0184ea"
}
},
"explode": true,
@@ -25527,7 +25527,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "f566e6df6572f5d0"
"x-spec-enum-id": "5e0f85310f0184ea"
}
},
"explode": true,
@@ -25540,7 +25540,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "f566e6df6572f5d0"
"x-spec-enum-id": "5e0f85310f0184ea"
}
},
"explode": true,
@@ -25553,7 +25553,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "f566e6df6572f5d0"
"x-spec-enum-id": "5e0f85310f0184ea"
}
},
"explode": true,
@@ -25566,7 +25566,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "f566e6df6572f5d0"
"x-spec-enum-id": "5e0f85310f0184ea"
}
},
"explode": true,
@@ -25579,7 +25579,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "f566e6df6572f5d0"
"x-spec-enum-id": "5e0f85310f0184ea"
}
},
"explode": true,
@@ -25592,7 +25592,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "f566e6df6572f5d0"
"x-spec-enum-id": "5e0f85310f0184ea"
}
},
"explode": true,
@@ -25605,7 +25605,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "f566e6df6572f5d0"
"x-spec-enum-id": "5e0f85310f0184ea"
}
},
"explode": true,
@@ -25618,7 +25618,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "f566e6df6572f5d0"
"x-spec-enum-id": "5e0f85310f0184ea"
}
},
"explode": true,
@@ -138591,50 +138591,6 @@
}
}
},
"/api/extras/scripts/upload/": {
"post": {
"operationId": "extras_scripts_upload_create",
"description": "Post a list of script module objects.",
"tags": [
"extras"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScriptModuleRequest"
}
},
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/ScriptModuleRequest"
}
}
},
"required": true
},
"security": [
{
"cookieAuth": []
},
{
"tokenAuth": []
}
],
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScriptModule"
}
}
},
"description": ""
}
}
}
},
"/api/extras/subscriptions/": {
"get": {
"operationId": "extras_subscriptions_list",
@@ -228090,14 +228046,13 @@
"trunk-4c6p",
"trunk-4c8p",
"trunk-8c4p",
"breakout-1c2p-2c1p",
"breakout-1c4p-4c1p",
"breakout-1c6p-6c1p",
"breakout-2c4p-8c1p-shuffle"
],
"type": "string",
"description": "* `single-1c1p` - 1C1P\n* `single-1c2p` - 1C2P\n* `single-1c4p` - 1C4P\n* `single-1c6p` - 1C6P\n* `single-1c8p` - 1C8P\n* `single-1c12p` - 1C12P\n* `single-1c16p` - 1C16P\n* `trunk-2c1p` - 2C1P trunk\n* `trunk-2c2p` - 2C2P trunk\n* `trunk-2c4p` - 2C4P trunk\n* `trunk-2c4p-shuffle` - 2C4P trunk (shuffle)\n* `trunk-2c6p` - 2C6P trunk\n* `trunk-2c8p` - 2C8P trunk\n* `trunk-2c12p` - 2C12P trunk\n* `trunk-4c1p` - 4C1P trunk\n* `trunk-4c2p` - 4C2P trunk\n* `trunk-4c4p` - 4C4P trunk\n* `trunk-4c4p-shuffle` - 4C4P trunk (shuffle)\n* `trunk-4c6p` - 4C6P trunk\n* `trunk-4c8p` - 4C8P trunk\n* `trunk-8c4p` - 8C4P trunk\n* `breakout-1c2p-2c1p` - 1C2P:2C1P breakout\n* `breakout-1c4p-4c1p` - 1C4P:4C1P breakout\n* `breakout-1c6p-6c1p` - 1C6P:6C1P breakout\n* `breakout-2c4p-8c1p-shuffle` - 2C4P:8C1P breakout (shuffle)",
"x-spec-enum-id": "f566e6df6572f5d0"
"description": "* `single-1c1p` - 1C1P\n* `single-1c2p` - 1C2P\n* `single-1c4p` - 1C4P\n* `single-1c6p` - 1C6P\n* `single-1c8p` - 1C8P\n* `single-1c12p` - 1C12P\n* `single-1c16p` - 1C16P\n* `trunk-2c1p` - 2C1P trunk\n* `trunk-2c2p` - 2C2P trunk\n* `trunk-2c4p` - 2C4P trunk\n* `trunk-2c4p-shuffle` - 2C4P trunk (shuffle)\n* `trunk-2c6p` - 2C6P trunk\n* `trunk-2c8p` - 2C8P trunk\n* `trunk-2c12p` - 2C12P trunk\n* `trunk-4c1p` - 4C1P trunk\n* `trunk-4c2p` - 4C2P trunk\n* `trunk-4c4p` - 4C4P trunk\n* `trunk-4c4p-shuffle` - 4C4P trunk (shuffle)\n* `trunk-4c6p` - 4C6P trunk\n* `trunk-4c8p` - 4C8P trunk\n* `trunk-8c4p` - 8C4P trunk\n* `breakout-1c4p-4c1p` - 1C4P:4C1P breakout\n* `breakout-1c6p-6c1p` - 1C6P:6C1P breakout\n* `breakout-2c4p-8c1p-shuffle` - 2C4P:8C1P breakout (shuffle)",
"x-spec-enum-id": "5e0f85310f0184ea"
},
"label": {
"type": "string",
@@ -228123,7 +228078,6 @@
"4C6P trunk",
"4C8P trunk",
"8C4P trunk",
"1C2P:2C1P breakout",
"1C4P:4C1P breakout",
"1C6P:6C1P breakout",
"2C4P:8C1P breakout (shuffle)"
@@ -228328,14 +228282,13 @@
"trunk-4c6p",
"trunk-4c8p",
"trunk-8c4p",
"breakout-1c2p-2c1p",
"breakout-1c4p-4c1p",
"breakout-1c6p-6c1p",
"breakout-2c4p-8c1p-shuffle"
],
"type": "string",
"description": "* `single-1c1p` - 1C1P\n* `single-1c2p` - 1C2P\n* `single-1c4p` - 1C4P\n* `single-1c6p` - 1C6P\n* `single-1c8p` - 1C8P\n* `single-1c12p` - 1C12P\n* `single-1c16p` - 1C16P\n* `trunk-2c1p` - 2C1P trunk\n* `trunk-2c2p` - 2C2P trunk\n* `trunk-2c4p` - 2C4P trunk\n* `trunk-2c4p-shuffle` - 2C4P trunk (shuffle)\n* `trunk-2c6p` - 2C6P trunk\n* `trunk-2c8p` - 2C8P trunk\n* `trunk-2c12p` - 2C12P trunk\n* `trunk-4c1p` - 4C1P trunk\n* `trunk-4c2p` - 4C2P trunk\n* `trunk-4c4p` - 4C4P trunk\n* `trunk-4c4p-shuffle` - 4C4P trunk (shuffle)\n* `trunk-4c6p` - 4C6P trunk\n* `trunk-4c8p` - 4C8P trunk\n* `trunk-8c4p` - 8C4P trunk\n* `breakout-1c2p-2c1p` - 1C2P:2C1P breakout\n* `breakout-1c4p-4c1p` - 1C4P:4C1P breakout\n* `breakout-1c6p-6c1p` - 1C6P:6C1P breakout\n* `breakout-2c4p-8c1p-shuffle` - 2C4P:8C1P breakout (shuffle)",
"x-spec-enum-id": "f566e6df6572f5d0"
"description": "* `single-1c1p` - 1C1P\n* `single-1c2p` - 1C2P\n* `single-1c4p` - 1C4P\n* `single-1c6p` - 1C6P\n* `single-1c8p` - 1C8P\n* `single-1c12p` - 1C12P\n* `single-1c16p` - 1C16P\n* `trunk-2c1p` - 2C1P trunk\n* `trunk-2c2p` - 2C2P trunk\n* `trunk-2c4p` - 2C4P trunk\n* `trunk-2c4p-shuffle` - 2C4P trunk (shuffle)\n* `trunk-2c6p` - 2C6P trunk\n* `trunk-2c8p` - 2C8P trunk\n* `trunk-2c12p` - 2C12P trunk\n* `trunk-4c1p` - 4C1P trunk\n* `trunk-4c2p` - 4C2P trunk\n* `trunk-4c4p` - 4C4P trunk\n* `trunk-4c4p-shuffle` - 4C4P trunk (shuffle)\n* `trunk-4c6p` - 4C6P trunk\n* `trunk-4c8p` - 4C8P trunk\n* `trunk-8c4p` - 8C4P trunk\n* `breakout-1c4p-4c1p` - 1C4P:4C1P breakout\n* `breakout-1c6p-6c1p` - 1C6P:6C1P breakout\n* `breakout-2c4p-8c1p-shuffle` - 2C4P:8C1P breakout (shuffle)",
"x-spec-enum-id": "5e0f85310f0184ea"
},
"tenant": {
"oneOf": [
@@ -254535,7 +254488,8 @@
"size": {
"type": "integer",
"maximum": 2147483647,
"minimum": 0
"minimum": 0,
"title": "Size (MB)"
},
"owner": {
"oneOf": [
@@ -254820,15 +254774,14 @@
"trunk-4c6p",
"trunk-4c8p",
"trunk-8c4p",
"breakout-1c2p-2c1p",
"breakout-1c4p-4c1p",
"breakout-1c6p-6c1p",
"breakout-2c4p-8c1p-shuffle",
""
],
"type": "string",
"description": "* `single-1c1p` - 1C1P\n* `single-1c2p` - 1C2P\n* `single-1c4p` - 1C4P\n* `single-1c6p` - 1C6P\n* `single-1c8p` - 1C8P\n* `single-1c12p` - 1C12P\n* `single-1c16p` - 1C16P\n* `trunk-2c1p` - 2C1P trunk\n* `trunk-2c2p` - 2C2P trunk\n* `trunk-2c4p` - 2C4P trunk\n* `trunk-2c4p-shuffle` - 2C4P trunk (shuffle)\n* `trunk-2c6p` - 2C6P trunk\n* `trunk-2c8p` - 2C8P trunk\n* `trunk-2c12p` - 2C12P trunk\n* `trunk-4c1p` - 4C1P trunk\n* `trunk-4c2p` - 4C2P trunk\n* `trunk-4c4p` - 4C4P trunk\n* `trunk-4c4p-shuffle` - 4C4P trunk (shuffle)\n* `trunk-4c6p` - 4C6P trunk\n* `trunk-4c8p` - 4C8P trunk\n* `trunk-8c4p` - 8C4P trunk\n* `breakout-1c2p-2c1p` - 1C2P:2C1P breakout\n* `breakout-1c4p-4c1p` - 1C4P:4C1P breakout\n* `breakout-1c6p-6c1p` - 1C6P:6C1P breakout\n* `breakout-2c4p-8c1p-shuffle` - 2C4P:8C1P breakout (shuffle)",
"x-spec-enum-id": "f566e6df6572f5d0"
"description": "* `single-1c1p` - 1C1P\n* `single-1c2p` - 1C2P\n* `single-1c4p` - 1C4P\n* `single-1c6p` - 1C6P\n* `single-1c8p` - 1C8P\n* `single-1c12p` - 1C12P\n* `single-1c16p` - 1C16P\n* `trunk-2c1p` - 2C1P trunk\n* `trunk-2c2p` - 2C2P trunk\n* `trunk-2c4p` - 2C4P trunk\n* `trunk-2c4p-shuffle` - 2C4P trunk (shuffle)\n* `trunk-2c6p` - 2C6P trunk\n* `trunk-2c8p` - 2C8P trunk\n* `trunk-2c12p` - 2C12P trunk\n* `trunk-4c1p` - 4C1P trunk\n* `trunk-4c2p` - 4C2P trunk\n* `trunk-4c4p` - 4C4P trunk\n* `trunk-4c4p-shuffle` - 4C4P trunk (shuffle)\n* `trunk-4c6p` - 4C6P trunk\n* `trunk-4c8p` - 4C8P trunk\n* `trunk-8c4p` - 8C4P trunk\n* `breakout-1c4p-4c1p` - 1C4P:4C1P breakout\n* `breakout-1c6p-6c1p` - 1C6P:6C1P breakout\n* `breakout-2c4p-8c1p-shuffle` - 2C4P:8C1P breakout (shuffle)",
"x-spec-enum-id": "5e0f85310f0184ea"
},
"tenant": {
"oneOf": [
@@ -262866,13 +262819,15 @@
"type": "integer",
"maximum": 2147483647,
"minimum": 0,
"nullable": true
"nullable": true,
"title": "Memory (MB)"
},
"disk": {
"type": "integer",
"maximum": 2147483647,
"minimum": 0,
"nullable": true
"nullable": true,
"title": "Disk (MB)"
},
"description": {
"type": "string",
@@ -270385,56 +270340,6 @@
"data"
]
},
"ScriptModule": {
"type": "object",
"description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)",
"properties": {
"id": {
"type": "integer",
"readOnly": true
},
"display": {
"type": "string",
"readOnly": true
},
"file_path": {
"type": "string",
"readOnly": true
},
"created": {
"type": "string",
"format": "date-time",
"readOnly": true
},
"last_updated": {
"type": "string",
"format": "date-time",
"readOnly": true,
"nullable": true
}
},
"required": [
"created",
"display",
"file_path",
"id",
"last_updated"
]
},
"ScriptModuleRequest": {
"type": "object",
"description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)",
"properties": {
"file": {
"type": "string",
"format": "binary",
"writeOnly": true
}
},
"required": [
"file"
]
},
"Service": {
"type": "object",
"description": "Base serializer class for models inheriting from PrimaryModel.",
@@ -275479,7 +275384,8 @@
"size": {
"type": "integer",
"maximum": 2147483647,
"minimum": 0
"minimum": 0,
"title": "Size (MB)"
},
"owner": {
"allOf": [
@@ -275550,7 +275456,8 @@
"size": {
"type": "integer",
"maximum": 2147483647,
"minimum": 0
"minimum": 0,
"title": "Size (MB)"
},
"owner": {
"oneOf": [
@@ -275755,13 +275662,15 @@
"type": "integer",
"maximum": 2147483647,
"minimum": 0,
"nullable": true
"nullable": true,
"title": "Memory (MB)"
},
"disk": {
"type": "integer",
"maximum": 2147483647,
"minimum": 0,
"nullable": true
"nullable": true,
"title": "Disk (MB)"
},
"description": {
"type": "string",
@@ -276017,13 +275926,15 @@
"type": "integer",
"maximum": 2147483647,
"minimum": 0,
"nullable": true
"nullable": true,
"title": "Memory (MB)"
},
"disk": {
"type": "integer",
"maximum": 2147483647,
"minimum": 0,
"nullable": true
"nullable": true,
"title": "Disk (MB)"
},
"description": {
"type": "string",
@@ -277309,15 +277220,14 @@
"trunk-4c6p",
"trunk-4c8p",
"trunk-8c4p",
"breakout-1c2p-2c1p",
"breakout-1c4p-4c1p",
"breakout-1c6p-6c1p",
"breakout-2c4p-8c1p-shuffle",
""
],
"type": "string",
"description": "* `single-1c1p` - 1C1P\n* `single-1c2p` - 1C2P\n* `single-1c4p` - 1C4P\n* `single-1c6p` - 1C6P\n* `single-1c8p` - 1C8P\n* `single-1c12p` - 1C12P\n* `single-1c16p` - 1C16P\n* `trunk-2c1p` - 2C1P trunk\n* `trunk-2c2p` - 2C2P trunk\n* `trunk-2c4p` - 2C4P trunk\n* `trunk-2c4p-shuffle` - 2C4P trunk (shuffle)\n* `trunk-2c6p` - 2C6P trunk\n* `trunk-2c8p` - 2C8P trunk\n* `trunk-2c12p` - 2C12P trunk\n* `trunk-4c1p` - 4C1P trunk\n* `trunk-4c2p` - 4C2P trunk\n* `trunk-4c4p` - 4C4P trunk\n* `trunk-4c4p-shuffle` - 4C4P trunk (shuffle)\n* `trunk-4c6p` - 4C6P trunk\n* `trunk-4c8p` - 4C8P trunk\n* `trunk-8c4p` - 8C4P trunk\n* `breakout-1c2p-2c1p` - 1C2P:2C1P breakout\n* `breakout-1c4p-4c1p` - 1C4P:4C1P breakout\n* `breakout-1c6p-6c1p` - 1C6P:6C1P breakout\n* `breakout-2c4p-8c1p-shuffle` - 2C4P:8C1P breakout (shuffle)",
"x-spec-enum-id": "f566e6df6572f5d0"
"description": "* `single-1c1p` - 1C1P\n* `single-1c2p` - 1C2P\n* `single-1c4p` - 1C4P\n* `single-1c6p` - 1C6P\n* `single-1c8p` - 1C8P\n* `single-1c12p` - 1C12P\n* `single-1c16p` - 1C16P\n* `trunk-2c1p` - 2C1P trunk\n* `trunk-2c2p` - 2C2P trunk\n* `trunk-2c4p` - 2C4P trunk\n* `trunk-2c4p-shuffle` - 2C4P trunk (shuffle)\n* `trunk-2c6p` - 2C6P trunk\n* `trunk-2c8p` - 2C8P trunk\n* `trunk-2c12p` - 2C12P trunk\n* `trunk-4c1p` - 4C1P trunk\n* `trunk-4c2p` - 4C2P trunk\n* `trunk-4c4p` - 4C4P trunk\n* `trunk-4c4p-shuffle` - 4C4P trunk (shuffle)\n* `trunk-4c6p` - 4C6P trunk\n* `trunk-4c8p` - 4C8P trunk\n* `trunk-8c4p` - 8C4P trunk\n* `breakout-1c4p-4c1p` - 1C4P:4C1P breakout\n* `breakout-1c6p-6c1p` - 1C6P:6C1P breakout\n* `breakout-2c4p-8c1p-shuffle` - 2C4P:8C1P breakout (shuffle)",
"x-spec-enum-id": "5e0f85310f0184ea"
},
"tenant": {
"oneOf": [
@@ -285610,13 +285520,15 @@
"type": "integer",
"maximum": 2147483647,
"minimum": 0,
"nullable": true
"nullable": true,
"title": "Memory (MB)"
},
"disk": {
"type": "integer",
"maximum": 2147483647,
"minimum": 0,
"nullable": true
"nullable": true,
"title": "Disk (MB)"
},
"description": {
"type": "string",

View File

@@ -220,14 +220,6 @@ This parameter defines the URL of the repository that will be checked for new Ne
---
## RQ
Default: `{}` (Empty)
This is a wrapper for passing global configuration parameters to [Django RQ](https://github.com/rq/django-rq) to customize its behavior. It is employed within NetBox primarily to alter conditions during testing.
---
## RQ_DEFAULT_TIMEOUT
Default: `300`

View File

@@ -1,31 +1,5 @@
# NetBox v4.5
## v4.5.7 (2026-04-03)
### Enhancements
* [#21095](https://github.com/netbox-community/netbox/issues/21095) - Adopt IEC unit labels (e.g. GiB) for virtual machine resources
* [#21696](https://github.com/netbox-community/netbox/issues/21696) - Add support for django-rq 4.0 and introduce `RQ` configuration parameter
* [#21701](https://github.com/netbox-community/netbox/issues/21701) - Support uploading custom scripts via the REST API (`/api/extras/scripts/upload/`)
* [#21760](https://github.com/netbox-community/netbox/issues/21760) - Add a 1C2P:2C1P breakout cable profile
### Performance Improvements
* [#21655](https://github.com/netbox-community/netbox/issues/21655) - Optimize queries for object and multi-object type custom fields
### Bug Fixes
* [#20474](https://github.com/netbox-community/netbox/issues/20474) - Fix installation of modules with placeholder values in component names
* [#21498](https://github.com/netbox-community/netbox/issues/21498) - Fix server error triggered by event rules referencing deleted objects
* [#21533](https://github.com/netbox-community/netbox/issues/21533) - Ensure read-only fields are included in REST API responses upon object creation
* [#21535](https://github.com/netbox-community/netbox/issues/21535) - Fix filtering of object-type custom fields when "is empty" is selected
* [#21784](https://github.com/netbox-community/netbox/issues/21784) - Fix `AttributeError` exception when sorting a table as an anonymous user
* [#21808](https://github.com/netbox-community/netbox/issues/21808) - Fix `RelatedObjectDoesNotExist` exception when viewing an interface with a virtual circuit termination
* [#21810](https://github.com/netbox-community/netbox/issues/21810) - Fix `AttributeError` exception when viewing virtual chassis member
* [#21825](https://github.com/netbox-community/netbox/issues/21825) - Fix sorting by broken columns in several object lists
---
## v4.5.6 (2026-03-31)
### Enhancements

View File

@@ -1,48 +1,46 @@
from django.test import RequestFactory, TestCase, tag
from circuits.models import CircuitGroupAssignment, CircuitTermination
from circuits.tables import CircuitGroupAssignmentTable, CircuitTerminationTable
from circuits.tables import *
from utilities.testing import TableTestCases
@tag('regression')
class CircuitTerminationTableTest(TestCase):
def test_every_orderable_field_does_not_throw_exception(self):
terminations = CircuitTermination.objects.all()
disallowed = {
'actions',
}
orderable_columns = [
column.name
for column in CircuitTerminationTable(terminations).columns
if column.orderable and column.name not in disallowed
]
fake_request = RequestFactory().get('/')
for col in orderable_columns:
for direction in ('-', ''):
table = CircuitTerminationTable(terminations)
table.order_by = f'{direction}{col}'
table.as_html(fake_request)
class CircuitTypeTableTest(TableTestCases.OrderableColumnsTestCase):
table = CircuitTypeTable
@tag('regression')
class CircuitGroupAssignmentTableTest(TestCase):
def test_every_orderable_field_does_not_throw_exception(self):
assignment = CircuitGroupAssignment.objects.all()
disallowed = {
'actions',
}
class CircuitTableTest(TableTestCases.OrderableColumnsTestCase):
table = CircuitTable
orderable_columns = [
column.name
for column in CircuitGroupAssignmentTable(assignment).columns
if column.orderable and column.name not in disallowed
]
fake_request = RequestFactory().get('/')
for col in orderable_columns:
for direction in ('-', ''):
table = CircuitGroupAssignmentTable(assignment)
table.order_by = f'{direction}{col}'
table.as_html(fake_request)
class CircuitTerminationTableTest(TableTestCases.OrderableColumnsTestCase):
table = CircuitTerminationTable
class CircuitGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = CircuitGroupTable
class CircuitGroupAssignmentTableTest(TableTestCases.OrderableColumnsTestCase):
table = CircuitGroupAssignmentTable
class ProviderTableTest(TableTestCases.OrderableColumnsTestCase):
table = ProviderTable
class ProviderAccountTableTest(TableTestCases.OrderableColumnsTestCase):
table = ProviderAccountTable
class ProviderNetworkTableTest(TableTestCases.OrderableColumnsTestCase):
table = ProviderNetworkTable
class VirtualCircuitTypeTableTest(TableTestCases.OrderableColumnsTestCase):
table = VirtualCircuitTypeTable
class VirtualCircuitTableTest(TableTestCases.OrderableColumnsTestCase):
table = VirtualCircuitTable
class VirtualCircuitTerminationTableTest(TableTestCases.OrderableColumnsTestCase):
table = VirtualCircuitTerminationTable

View File

@@ -0,0 +1,26 @@
from core.models import ObjectChange
from core.tables import *
from utilities.testing import TableTestCases
class DataSourceTableTest(TableTestCases.OrderableColumnsTestCase):
table = DataSourceTable
class DataFileTableTest(TableTestCases.OrderableColumnsTestCase):
table = DataFileTable
class JobTableTest(TableTestCases.OrderableColumnsTestCase):
table = JobTable
class ObjectChangeTableTest(TableTestCases.OrderableColumnsTestCase):
table = ObjectChangeTable
queryset_sources = [
('ObjectChangeListView', ObjectChange.objects.valid_models()),
]
class ConfigRevisionTableTest(TableTestCases.OrderableColumnsTestCase):
table = ConfigRevisionTable

View File

@@ -0,0 +1,204 @@
from dcim.models import ConsolePort, Interface, PowerPort
from dcim.tables import *
from utilities.testing import TableTestCases
#
# Sites
#
class RegionTableTest(TableTestCases.OrderableColumnsTestCase):
table = RegionTable
class SiteGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = SiteGroupTable
class SiteTableTest(TableTestCases.OrderableColumnsTestCase):
table = SiteTable
class LocationTableTest(TableTestCases.OrderableColumnsTestCase):
table = LocationTable
#
# Racks
#
class RackRoleTableTest(TableTestCases.OrderableColumnsTestCase):
table = RackRoleTable
class RackTypeTableTest(TableTestCases.OrderableColumnsTestCase):
table = RackTypeTable
class RackTableTest(TableTestCases.OrderableColumnsTestCase):
table = RackTable
class RackReservationTableTest(TableTestCases.OrderableColumnsTestCase):
table = RackReservationTable
#
# Device types
#
class ManufacturerTableTest(TableTestCases.OrderableColumnsTestCase):
table = ManufacturerTable
class DeviceTypeTableTest(TableTestCases.OrderableColumnsTestCase):
table = DeviceTypeTable
#
# Module types
#
class ModuleTypeProfileTableTest(TableTestCases.OrderableColumnsTestCase):
table = ModuleTypeProfileTable
class ModuleTypeTableTest(TableTestCases.OrderableColumnsTestCase):
table = ModuleTypeTable
class ModuleTableTest(TableTestCases.OrderableColumnsTestCase):
table = ModuleTable
#
# Devices
#
class DeviceRoleTableTest(TableTestCases.OrderableColumnsTestCase):
table = DeviceRoleTable
class PlatformTableTest(TableTestCases.OrderableColumnsTestCase):
table = PlatformTable
class DeviceTableTest(TableTestCases.OrderableColumnsTestCase):
table = DeviceTable
#
# Device components
#
class ConsolePortTableTest(TableTestCases.OrderableColumnsTestCase):
table = ConsolePortTable
class ConsoleServerPortTableTest(TableTestCases.OrderableColumnsTestCase):
table = ConsoleServerPortTable
class PowerPortTableTest(TableTestCases.OrderableColumnsTestCase):
table = PowerPortTable
class PowerOutletTableTest(TableTestCases.OrderableColumnsTestCase):
table = PowerOutletTable
class InterfaceTableTest(TableTestCases.OrderableColumnsTestCase):
table = InterfaceTable
class FrontPortTableTest(TableTestCases.OrderableColumnsTestCase):
table = FrontPortTable
class RearPortTableTest(TableTestCases.OrderableColumnsTestCase):
table = RearPortTable
class ModuleBayTableTest(TableTestCases.OrderableColumnsTestCase):
table = ModuleBayTable
class DeviceBayTableTest(TableTestCases.OrderableColumnsTestCase):
table = DeviceBayTable
class InventoryItemTableTest(TableTestCases.OrderableColumnsTestCase):
table = InventoryItemTable
class InventoryItemRoleTableTest(TableTestCases.OrderableColumnsTestCase):
table = InventoryItemRoleTable
#
# Connections
#
class ConsoleConnectionTableTest(TableTestCases.OrderableColumnsTestCase):
table = ConsoleConnectionTable
queryset_sources = [
('ConsoleConnectionsListView', ConsolePort.objects.filter(_path__is_complete=True)),
]
class PowerConnectionTableTest(TableTestCases.OrderableColumnsTestCase):
table = PowerConnectionTable
queryset_sources = [
('PowerConnectionsListView', PowerPort.objects.filter(_path__is_complete=True)),
]
class InterfaceConnectionTableTest(TableTestCases.OrderableColumnsTestCase):
table = InterfaceConnectionTable
queryset_sources = [
('InterfaceConnectionsListView', Interface.objects.filter(_path__is_complete=True)),
]
#
# Cables
#
class CableTableTest(TableTestCases.OrderableColumnsTestCase):
table = CableTable
#
# Power
#
class PowerPanelTableTest(TableTestCases.OrderableColumnsTestCase):
table = PowerPanelTable
class PowerFeedTableTest(TableTestCases.OrderableColumnsTestCase):
table = PowerFeedTable
#
# Virtual chassis
#
class VirtualChassisTableTest(TableTestCases.OrderableColumnsTestCase):
table = VirtualChassisTable
#
# Virtual device contexts
#
class VirtualDeviceContextTableTest(TableTestCases.OrderableColumnsTestCase):
table = VirtualDeviceContextTable
#
# MAC addresses
#
class MACAddressTableTest(TableTestCases.OrderableColumnsTestCase):
table = MACAddressTable

View File

@@ -1,24 +1,84 @@
from django.test import RequestFactory, TestCase, tag
from extras.models import EventRule
from extras.tables import EventRuleTable
from extras.models import Bookmark, Notification, Subscription
from extras.tables import *
from utilities.testing import TableTestCases
@tag('regression')
class EventRuleTableTest(TestCase):
def test_every_orderable_field_does_not_throw_exception(self):
rule = EventRule.objects.all()
disallowed = {
'actions',
}
class CustomFieldTableTest(TableTestCases.OrderableColumnsTestCase):
table = CustomFieldTable
orderable_columns = [
column.name for column in EventRuleTable(rule).columns if column.orderable and column.name not in disallowed
]
fake_request = RequestFactory().get('/')
for col in orderable_columns:
for direction in ('-', ''):
table = EventRuleTable(rule)
table.order_by = f'{direction}{col}'
table.as_html(fake_request)
class CustomFieldChoiceSetTableTest(TableTestCases.OrderableColumnsTestCase):
table = CustomFieldChoiceSetTable
class CustomLinkTableTest(TableTestCases.OrderableColumnsTestCase):
table = CustomLinkTable
class ExportTemplateTableTest(TableTestCases.OrderableColumnsTestCase):
table = ExportTemplateTable
class SavedFilterTableTest(TableTestCases.OrderableColumnsTestCase):
table = SavedFilterTable
class TableConfigTableTest(TableTestCases.OrderableColumnsTestCase):
table = TableConfigTable
class BookmarkTableTest(TableTestCases.OrderableColumnsTestCase):
table = BookmarkTable
queryset_sources = [
('BookmarkListView', Bookmark.objects.all()),
]
class NotificationGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = NotificationGroupTable
class NotificationTableTest(TableTestCases.OrderableColumnsTestCase):
table = NotificationTable
queryset_sources = [
('NotificationListView', Notification.objects.all()),
]
class SubscriptionTableTest(TableTestCases.OrderableColumnsTestCase):
table = SubscriptionTable
queryset_sources = [
('SubscriptionListView', Subscription.objects.all()),
]
class WebhookTableTest(TableTestCases.OrderableColumnsTestCase):
table = WebhookTable
class EventRuleTableTest(TableTestCases.OrderableColumnsTestCase):
table = EventRuleTable
class TagTableTest(TableTestCases.OrderableColumnsTestCase):
table = TagTable
class ConfigContextProfileTableTest(TableTestCases.OrderableColumnsTestCase):
table = ConfigContextProfileTable
class ConfigContextTableTest(TableTestCases.OrderableColumnsTestCase):
table = ConfigContextTable
class ConfigTemplateTableTest(TableTestCases.OrderableColumnsTestCase):
table = ConfigTemplateTable
class ImageAttachmentTableTest(TableTestCases.OrderableColumnsTestCase):
table = ImageAttachmentTable
class JournalEntryTableTest(TableTestCases.OrderableColumnsTestCase):
table = JournalEntryTable

View File

@@ -1,9 +1,10 @@
from django.test import RequestFactory, TestCase
from netaddr import IPNetwork
from ipam.models import IPAddress, IPRange, Prefix
from ipam.tables import AnnotatedIPAddressTable
from ipam.models import FHRPGroupAssignment, IPAddress, IPRange, Prefix
from ipam.tables import *
from ipam.utils import annotate_ip_space
from utilities.testing import TableTestCases
class AnnotatedIPAddressTableTest(TestCase):
@@ -168,3 +169,82 @@ class AnnotatedIPAddressTableTest(TestCase):
# Pools are fully usable
self.assertEqual(available.first_ip, '2001:db8:1::/126')
self.assertEqual(available.size, 4)
#
# Table ordering tests
#
class VRFTableTest(TableTestCases.OrderableColumnsTestCase):
table = VRFTable
class RouteTargetTableTest(TableTestCases.OrderableColumnsTestCase):
table = RouteTargetTable
class RIRTableTest(TableTestCases.OrderableColumnsTestCase):
table = RIRTable
class AggregateTableTest(TableTestCases.OrderableColumnsTestCase):
table = AggregateTable
class RoleTableTest(TableTestCases.OrderableColumnsTestCase):
table = RoleTable
class PrefixTableTest(TableTestCases.OrderableColumnsTestCase):
table = PrefixTable
class IPRangeTableTest(TableTestCases.OrderableColumnsTestCase):
table = IPRangeTable
class IPAddressTableTest(TableTestCases.OrderableColumnsTestCase):
table = IPAddressTable
class FHRPGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = FHRPGroupTable
class FHRPGroupAssignmentTableTest(TableTestCases.OrderableColumnsTestCase):
table = FHRPGroupAssignmentTable
queryset_sources = [
('FHRPGroupAssignmentTable', FHRPGroupAssignment.objects.all()),
]
class VLANGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = VLANGroupTable
class VLANTableTest(TableTestCases.OrderableColumnsTestCase):
table = VLANTable
class VLANTranslationPolicyTableTest(TableTestCases.OrderableColumnsTestCase):
table = VLANTranslationPolicyTable
class VLANTranslationRuleTableTest(TableTestCases.OrderableColumnsTestCase):
table = VLANTranslationRuleTable
class ASNRangeTableTest(TableTestCases.OrderableColumnsTestCase):
table = ASNRangeTable
class ASNTableTest(TableTestCases.OrderableColumnsTestCase):
table = ASNTable
class ServiceTemplateTableTest(TableTestCases.OrderableColumnsTestCase):
table = ServiceTemplateTable
class ServiceTableTest(TableTestCases.OrderableColumnsTestCase):
table = ServiceTable

View File

@@ -1,3 +1,3 @@
version: "4.5.7"
version: "4.5.6"
edition: "Community"
published: "2026-04-03"
published: "2026-03-31"

View File

@@ -0,0 +1,26 @@
from tenancy.tables import *
from utilities.testing import TableTestCases
class TenantGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = TenantGroupTable
class TenantTableTest(TableTestCases.OrderableColumnsTestCase):
table = TenantTable
class ContactGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = ContactGroupTable
class ContactRoleTableTest(TableTestCases.OrderableColumnsTestCase):
table = ContactRoleTable
class ContactTableTest(TableTestCases.OrderableColumnsTestCase):
table = ContactTable
class ContactAssignmentTableTest(TableTestCases.OrderableColumnsTestCase):
table = ContactAssignmentTable

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,26 @@
from django.test import RequestFactory, TestCase, tag
from users.models import Token
from users.tables import TokenTable
from users.tables import *
from utilities.testing import TableTestCases
class TokenTableTest(TestCase):
@tag('regression')
def test_every_orderable_field_does_not_throw_exception(self):
tokens = Token.objects.all()
disallowed = {'actions'}
class TokenTableTest(TableTestCases.OrderableColumnsTestCase):
table = TokenTable
orderable_columns = [
column.name for column in TokenTable(tokens).columns
if column.orderable and column.name not in disallowed
]
fake_request = RequestFactory().get("/")
for col in orderable_columns:
for direction in ('-', ''):
with self.subTest(col=col, direction=direction):
table = TokenTable(tokens)
table.order_by = f'{direction}{col}'
table.as_html(fake_request)
class UserTableTest(TableTestCases.OrderableColumnsTestCase):
table = UserTable
class GroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = GroupTable
class ObjectPermissionTableTest(TableTestCases.OrderableColumnsTestCase):
table = ObjectPermissionTable
class OwnerGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = OwnerGroupTable
class OwnerTableTest(TableTestCases.OrderableColumnsTestCase):
table = OwnerTable

View File

@@ -1,5 +1,6 @@
from .api import *
from .base import *
from .filtersets import *
from .tables import *
from .utils import *
from .views import *

View File

@@ -0,0 +1,130 @@
import inspect
from importlib import import_module
from django.test import RequestFactory
from netbox.views import generic
from .base import TestCase
__all__ = (
"ModelTableTestCase",
"TableTestCases",
)
class ModelTableTestCase(TestCase):
"""
Shared helpers for model-backed table tests.
Concrete subclasses should set `table` and may override `get_queryset()`
or `excluded_orderable_columns` as needed.
"""
table = None
excluded_orderable_columns = frozenset({"actions"})
# Optional explicit override for odd cases
queryset_sources = None
# Only these view types are considered sortable queryset sources by default
queryset_source_view_classes = (generic.ObjectListView,)
@classmethod
def validate_table_test_case(cls):
if cls.table is None:
raise AssertionError(f"{cls.__name__} must define `table`")
if getattr(cls.table._meta, "model", None) is None:
raise AssertionError(f"{cls.__name__}.table must be model-backed")
def get_request(self):
request = RequestFactory().get("/")
request.user = self.user
return request
def get_table(self, queryset):
return self.table(queryset)
@classmethod
def is_queryset_source_view(cls, view):
model = cls.table._meta.model
app_label = model._meta.app_label
return (
inspect.isclass(view)
and view.__module__.startswith(f"{app_label}.views")
and getattr(view, "table", None) is cls.table
and getattr(view, "queryset", None) is not None
and issubclass(view, cls.queryset_source_view_classes)
)
@classmethod
def get_queryset_sources(cls):
"""
Return iterable of (label, queryset) pairs to test.
By default, only discover list-style views that declare this table.
That keeps bulk edit/delete confirmation tables out of the ordering
smoke test.
"""
if cls.queryset_sources is not None:
return tuple(cls.queryset_sources)
model = cls.table._meta.model
app_label = model._meta.app_label
module = import_module(f"{app_label}.views")
sources = []
for _, view in inspect.getmembers(module, inspect.isclass):
if not cls.is_queryset_source_view(view):
continue
queryset = view.queryset
if hasattr(queryset, "all"):
queryset = queryset.all()
sources.append((view.__name__, queryset))
if not sources:
raise AssertionError(
f"{cls.__name__} could not find any list-style queryset source for "
f"{cls.table.__module__}.{cls.table.__name__}; "
"set `queryset_sources` explicitly if needed."
)
return tuple(sources)
def iter_orderable_columns(self, queryset):
for column in self.get_table(queryset).columns:
if not column.orderable:
continue
if column.name in self.excluded_orderable_columns:
continue
yield column.name
class TableTestCases:
"""
Keep test_* methods nested to avoid unittest auto-discovering the reusable
base classes directly.
"""
class OrderableColumnsTestCase(ModelTableTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.validate_table_test_case()
def test_every_orderable_column_renders(self):
request = self.get_request()
for source_name, queryset in self.get_queryset_sources():
for column_name in self.iter_orderable_columns(queryset):
for direction, prefix in (("asc", ""), ("desc", "-")):
with self.cleanupSubTest(
source=source_name,
column=column_name,
direction=direction,
):
table = self.get_table(queryset)
table.order_by = f"{prefix}{column_name}"
table.as_html(request)

View File

@@ -0,0 +1,26 @@
from utilities.testing import TableTestCases
from virtualization.tables import *
class ClusterTypeTableTest(TableTestCases.OrderableColumnsTestCase):
table = ClusterTypeTable
class ClusterGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = ClusterGroupTable
class ClusterTableTest(TableTestCases.OrderableColumnsTestCase):
table = ClusterTable
class VirtualMachineTableTest(TableTestCases.OrderableColumnsTestCase):
table = VirtualMachineTable
class VMInterfaceTableTest(TableTestCases.OrderableColumnsTestCase):
table = VMInterfaceTable
class VirtualDiskTableTest(TableTestCases.OrderableColumnsTestCase):
table = VirtualDiskTable

View File

@@ -1,23 +1,42 @@
from django.test import RequestFactory, TestCase, tag
from vpn.models import TunnelTermination
from vpn.tables import TunnelTerminationTable
from utilities.testing import TableTestCases
from vpn.tables import *
@tag('regression')
class TunnelTerminationTableTest(TestCase):
def test_every_orderable_field_does_not_throw_exception(self):
terminations = TunnelTermination.objects.all()
fake_request = RequestFactory().get("/")
disallowed = {'actions'}
class TunnelGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = TunnelGroupTable
orderable_columns = [
column.name for column in TunnelTerminationTable(terminations).columns
if column.orderable and column.name not in disallowed
]
for col in orderable_columns:
for dir in ('-', ''):
table = TunnelTerminationTable(terminations)
table.order_by = f'{dir}{col}'
table.as_html(fake_request)
class TunnelTableTest(TableTestCases.OrderableColumnsTestCase):
table = TunnelTable
class TunnelTerminationTableTest(TableTestCases.OrderableColumnsTestCase):
table = TunnelTerminationTable
class IKEProposalTableTest(TableTestCases.OrderableColumnsTestCase):
table = IKEProposalTable
class IKEPolicyTableTest(TableTestCases.OrderableColumnsTestCase):
table = IKEPolicyTable
class IPSecProposalTableTest(TableTestCases.OrderableColumnsTestCase):
table = IPSecProposalTable
class IPSecPolicyTableTest(TableTestCases.OrderableColumnsTestCase):
table = IPSecPolicyTable
class IPSecProfileTableTest(TableTestCases.OrderableColumnsTestCase):
table = IPSecProfileTable
class L2VPNTableTest(TableTestCases.OrderableColumnsTestCase):
table = L2VPNTable
class L2VPNTerminationTableTest(TableTestCases.OrderableColumnsTestCase):
table = L2VPNTerminationTable

View File

@@ -0,0 +1,14 @@
from utilities.testing import TableTestCases
from wireless.tables import *
class WirelessLANGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = WirelessLANGroupTable
class WirelessLANTableTest(TableTestCases.OrderableColumnsTestCase):
table = WirelessLANTable
class WirelessLinkTableTest(TableTestCases.OrderableColumnsTestCase):
table = WirelessLinkTable

View File

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

View File

@@ -1,7 +1,7 @@
colorama==0.4.6
Django==5.2.12
django-cors-headers==4.9.0
django-debug-toolbar==6.3.0
django-debug-toolbar==6.2.0
django-filter==25.2
django-graphiql-debug-toolbar==0.2.0
django-htmx==1.27.0
@@ -17,7 +17,7 @@ django-taggit==6.1.0
django-timezone-field==7.2.1
djangorestframework==3.16.1
drf-spectacular==0.29.0
drf-spectacular-sidecar==2026.4.1
drf-spectacular-sidecar==2026.3.1
feedparser==6.0.12
gunicorn==25.3.0
Jinja2==3.1.6
@@ -29,7 +29,7 @@ mkdocstrings==1.0.3
mkdocstrings-python==2.0.3
netaddr==1.3.0
nh3==0.3.4
Pillow==12.2.0
Pillow==12.1.1
psycopg[c,pool]==3.3.3
PyYAML==6.0.3
requests==2.33.1
@@ -41,4 +41,4 @@ strawberry-graphql==0.312.2
strawberry-graphql-django==0.82.1
svgwrite==1.4.3
tablib==3.9.0
tzdata==2026.1
tzdata==2025.3