Compare commits

...

26 Commits

Author SHA1 Message Date
Jason Novinger
9e13d89baa Fixes #20766: Prevent translation of code/commands in error templates
Use blocktrans 'with' clause to pass literal code/commands as variables,
preventing them from being translated. This fixes issues where commands
like 'manage.py collectstatic' were incorrectly translated to nonsensical
strings in non-English locales.

Updated templates:
- media_failure.html: manage.py collectstatic
- programming_error.html: python3 manage.py migrate, SELECT VERSION()
- import_error.html: requirements.txt, local_requirements.txt, pip freeze
2025-11-14 16:24:17 -06:00
Jeremy Stretch
4961b0d334 Release v4.4.6 2025-11-11 09:58:09 -05:00
github-actions
ab06edd9f5 Update source translation strings 2025-11-11 05:02:13 +00:00
Jeremy Stretch
e787a71c1d Fixes #20660: Optimize loading of custom script modules from remote storage (#20783) 2025-11-10 22:47:02 -06:00
lexapi
cd8878df30 Closes #20774: used gettext_lazy instead gettext (#20782) 2025-11-10 21:54:35 -06:00
Martin Hauser
b5a9cb1762 fix(users): Normalize actions in cloned objects init
Ensure `actions` are consistently normalized to a list of strings during
cloned object initialization. This resolves potential type mismatches
when processing user form data.

Fixes #20750
2025-11-10 09:50:41 -05:00
github-actions
9723a2f0ad Update source translation strings 2025-11-08 05:02:14 +00:00
Arthur Hanson
327d08f4c2 Fixes #20771: make comments for JournalEntryies required (#20773) 2025-11-07 17:27:41 -06:00
Martin Hauser
4be476eb49 fix(config): Change log level for missing config revision (#20762)
Update the log level from `warning` to `debug` when no active
configuration revision is found. This prevents unnecessary warnings in
normal operation scenarios, improving log clarity and relevance.

Fixes #20688
2025-11-07 10:38:55 -08:00
Martin Hauser
8005b56ab4 Fixes #20755: Limit Provider search scope (#20763) 2025-11-07 08:27:54 -06:00
github-actions
3f1654c9ba Update source translation strings 2025-11-07 05:02:15 +00:00
bctiemann
95f8fe788d Merge pull request #20764 from netbox-community/20378-del-script
#20378 fix delete of DataSource
2025-11-06 20:14:29 -05:00
bctiemann
5b3ff3c0e9 Merge pull request #20739 from netbox-community/20738-vc-delete
20738 update vc_position in delete not signal handler
2025-11-06 15:37:21 -05:00
bctiemann
730d73042d Merge pull request #20717 from m-hau/bugfix/related-object-validation
Fixes: #20670: Related Object Validation
2025-11-06 13:49:19 -05:00
bctiemann
6c2a6d0e90 Merge pull request #20725 from netbox-community/20645-bulk-upload
20645 CSVChoiceField use default if blank
2025-11-06 13:42:52 -05:00
github-actions
e6a6ff7aec Update source translation strings 2025-11-05 05:02:10 +00:00
Martin Hauser
87ff83ef1f feat(filtersets): Add object_type_id filter for Jobs (#20674)
Introduce a new `object_type_id` filter to enhance filtering by object
type for Jobs. Update related forms and fieldsets to incorporate the
new filter for better usability and consistency.

Fixes #20653
2025-11-04 13:58:54 -08:00
Arthur
8522c03b71 20738 add tests 2025-11-03 14:22:27 -08:00
Arthur
20af97ce24 20738 update vc_position in delete not signal handler 2025-11-03 14:06:02 -08:00
Arthur
264b40a269 20738 update vc_position in delete not signal handler 2025-11-03 13:48:50 -08:00
Arthur
90712fa865 20645 CSVChoiceField use default if blank 2025-10-30 15:34:27 -07:00
Marko Hauptvogel
fbe76ac98a Fix non-existent-id error message
Change this one special case to also use the same communication channel
(toast notification) and message format as all other validation errors.

The error message is kept mostly the same, just the index prefix is
removed. This allowed keeping and easily adjusting the existing
localizations of it.
2025-10-30 14:08:15 +01:00
Marko Hauptvogel
1245a9f99d Validate related object is dictionary
Elements of the "related objects list" are passed to the
`prep_related_object_data` function before any validation takes place,
with the potential of failing with a hard error. Similar to the "related
objects not list" case explicitly validate the elements general type,
and raise a normal validation error if it isn't a dictionary.

The word "dictionary" is used here, since it is python terminology, and
is close enough to yaml's "mapping". While json calls them "objects",
their key-value syntax should make it obvious what "dictionary" means
here.
2025-10-30 13:33:34 +01:00
Marko Hauptvogel
78223cea03 Validate related object field is list
The related object fields are not covered by the form, so don't pass
any validation before trying to iterate over them and accessing their
elements. Instead of allowing a hard technical error to be raised,
explicitly check that it is indeed a list, and raise a normal validation
error if not.

The error message is chosen to be similar in format and wording to the
other existing validation errors. The used word "list" is quite
universal, and conveys the wanted meaning in the context of python,
json and yaml.
2025-10-30 13:33:34 +01:00
Marko Hauptvogel
8452222761 Fix record index for related objects
Use the parent object index as record index, and its own index only on
the field name.
2025-10-30 13:33:34 +01:00
Marko Hauptvogel
8a59fc733c Fix related object index
Index related objects from 1 and not from 0, just like top-level objects.
2025-10-30 13:33:34 +01:00
62 changed files with 8035 additions and 6888 deletions

View File

@@ -15,7 +15,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v4.4.5
placeholder: v4.4.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.4.5
placeholder: v4.4.6
validations:
required: true
- type: dropdown

View File

@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "NetBox REST API",
"version": "4.4.5",
"version": "4.4.6",
"license": {
"name": "Apache v2 License"
}
@@ -19688,6 +19688,32 @@
"type": "string"
}
},
{
"in": "query",
"name": "object_type_id",
"schema": {
"type": "array",
"items": {
"type": "integer",
"nullable": true
}
},
"explode": true,
"style": "form"
},
{
"in": "query",
"name": "object_type_id__n",
"schema": {
"type": "array",
"items": {
"type": "integer",
"nullable": true
}
},
"explode": true,
"style": "form"
},
{
"name": "offset",
"required": false,
@@ -23626,7 +23652,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "c731f2793fceac04",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
}
},
@@ -23647,7 +23673,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "c731f2793fceac04",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
}
},
@@ -23661,7 +23687,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "c731f2793fceac04",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
}
},
@@ -23675,7 +23701,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "c731f2793fceac04",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
}
},
@@ -23689,7 +23715,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "c731f2793fceac04",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
}
},
@@ -23703,7 +23729,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "c731f2793fceac04",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
}
},
@@ -23717,7 +23743,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "c731f2793fceac04",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
}
},
@@ -23731,7 +23757,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "c731f2793fceac04",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
}
},
@@ -23745,7 +23771,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "c731f2793fceac04",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
}
},
@@ -23759,7 +23785,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "c731f2793fceac04",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
}
},
@@ -23773,7 +23799,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "c731f2793fceac04",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
}
},
@@ -23787,7 +23813,7 @@
"type": "array",
"items": {
"type": "string",
"x-spec-enum-id": "c731f2793fceac04",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
}
},
@@ -205023,6 +205049,15 @@
"dac-active",
"dac-passive",
"coaxial",
"rg-6",
"rg-8",
"rg-11",
"rg-59",
"rg-62",
"rg-213",
"lmr-100",
"lmr-200",
"lmr-400",
"mmf",
"mmf-om1",
"mmf-om2",
@@ -205039,8 +205074,8 @@
null
],
"type": "string",
"description": "* `cat3` - CAT3\n* `cat5` - CAT5\n* `cat5e` - CAT5e\n* `cat6` - CAT6\n* `cat6a` - CAT6a\n* `cat7` - CAT7\n* `cat7a` - CAT7a\n* `cat8` - CAT8\n* `mrj21-trunk` - MRJ21 Trunk\n* `dac-active` - Direct Attach Copper (Active)\n* `dac-passive` - Direct Attach Copper (Passive)\n* `coaxial` - Coaxial\n* `mmf` - Multimode Fiber\n* `mmf-om1` - Multimode Fiber (OM1)\n* `mmf-om2` - Multimode Fiber (OM2)\n* `mmf-om3` - Multimode Fiber (OM3)\n* `mmf-om4` - Multimode Fiber (OM4)\n* `mmf-om5` - Multimode Fiber (OM5)\n* `smf` - Single-mode Fiber\n* `smf-os1` - Single-mode Fiber (OS1)\n* `smf-os2` - Single-mode Fiber (OS2)\n* `aoc` - Active Optical Cabling (AOC)\n* `power` - Power\n* `usb` - USB",
"x-spec-enum-id": "c731f2793fceac04",
"description": "* `cat3` - CAT3\n* `cat5` - CAT5\n* `cat5e` - CAT5e\n* `cat6` - CAT6\n* `cat6a` - CAT6a\n* `cat7` - CAT7\n* `cat7a` - CAT7a\n* `cat8` - CAT8\n* `mrj21-trunk` - MRJ21 Trunk\n* `dac-active` - Direct Attach Copper (Active)\n* `dac-passive` - Direct Attach Copper (Passive)\n* `coaxial` - Coaxial\n* `rg-6` - RG-6\n* `rg-8` - RG-8\n* `rg-11` - RG-11\n* `rg-59` - RG-59\n* `rg-62` - RG-62\n* `rg-213` - RG-213\n* `lmr-100` - LMR-100\n* `lmr-200` - LMR-200\n* `lmr-400` - LMR-400\n* `mmf` - Multimode Fiber\n* `mmf-om1` - Multimode Fiber (OM1)\n* `mmf-om2` - Multimode Fiber (OM2)\n* `mmf-om3` - Multimode Fiber (OM3)\n* `mmf-om4` - Multimode Fiber (OM4)\n* `mmf-om5` - Multimode Fiber (OM5)\n* `smf` - Single-mode Fiber\n* `smf-os1` - Single-mode Fiber (OS1)\n* `smf-os2` - Single-mode Fiber (OS2)\n* `aoc` - Active Optical Cabling (AOC)\n* `power` - Power\n* `usb` - USB",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
},
"a_terminations": {
@@ -205193,6 +205228,15 @@
"dac-active",
"dac-passive",
"coaxial",
"rg-6",
"rg-8",
"rg-11",
"rg-59",
"rg-62",
"rg-213",
"lmr-100",
"lmr-200",
"lmr-400",
"mmf",
"mmf-om1",
"mmf-om2",
@@ -205209,8 +205253,8 @@
null
],
"type": "string",
"description": "* `cat3` - CAT3\n* `cat5` - CAT5\n* `cat5e` - CAT5e\n* `cat6` - CAT6\n* `cat6a` - CAT6a\n* `cat7` - CAT7\n* `cat7a` - CAT7a\n* `cat8` - CAT8\n* `mrj21-trunk` - MRJ21 Trunk\n* `dac-active` - Direct Attach Copper (Active)\n* `dac-passive` - Direct Attach Copper (Passive)\n* `coaxial` - Coaxial\n* `mmf` - Multimode Fiber\n* `mmf-om1` - Multimode Fiber (OM1)\n* `mmf-om2` - Multimode Fiber (OM2)\n* `mmf-om3` - Multimode Fiber (OM3)\n* `mmf-om4` - Multimode Fiber (OM4)\n* `mmf-om5` - Multimode Fiber (OM5)\n* `smf` - Single-mode Fiber\n* `smf-os1` - Single-mode Fiber (OS1)\n* `smf-os2` - Single-mode Fiber (OS2)\n* `aoc` - Active Optical Cabling (AOC)\n* `power` - Power\n* `usb` - USB",
"x-spec-enum-id": "c731f2793fceac04",
"description": "* `cat3` - CAT3\n* `cat5` - CAT5\n* `cat5e` - CAT5e\n* `cat6` - CAT6\n* `cat6a` - CAT6a\n* `cat7` - CAT7\n* `cat7a` - CAT7a\n* `cat8` - CAT8\n* `mrj21-trunk` - MRJ21 Trunk\n* `dac-active` - Direct Attach Copper (Active)\n* `dac-passive` - Direct Attach Copper (Passive)\n* `coaxial` - Coaxial\n* `rg-6` - RG-6\n* `rg-8` - RG-8\n* `rg-11` - RG-11\n* `rg-59` - RG-59\n* `rg-62` - RG-62\n* `rg-213` - RG-213\n* `lmr-100` - LMR-100\n* `lmr-200` - LMR-200\n* `lmr-400` - LMR-400\n* `mmf` - Multimode Fiber\n* `mmf-om1` - Multimode Fiber (OM1)\n* `mmf-om2` - Multimode Fiber (OM2)\n* `mmf-om3` - Multimode Fiber (OM3)\n* `mmf-om4` - Multimode Fiber (OM4)\n* `mmf-om5` - Multimode Fiber (OM5)\n* `smf` - Single-mode Fiber\n* `smf-os1` - Single-mode Fiber (OS1)\n* `smf-os2` - Single-mode Fiber (OS2)\n* `aoc` - Active Optical Cabling (AOC)\n* `power` - Power\n* `usb` - USB",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
},
"a_terminations": {
@@ -219171,6 +219215,10 @@
"format": "int64",
"nullable": true
},
"object": {
"nullable": true,
"readOnly": true
},
"name": {
"type": "string",
"maxLength": 200
@@ -219264,6 +219312,7 @@
"id",
"job_id",
"name",
"object",
"object_type",
"status",
"url",
@@ -229509,6 +229558,15 @@
"dac-active",
"dac-passive",
"coaxial",
"rg-6",
"rg-8",
"rg-11",
"rg-59",
"rg-62",
"rg-213",
"lmr-100",
"lmr-200",
"lmr-400",
"mmf",
"mmf-om1",
"mmf-om2",
@@ -229525,8 +229583,8 @@
null
],
"type": "string",
"description": "* `cat3` - CAT3\n* `cat5` - CAT5\n* `cat5e` - CAT5e\n* `cat6` - CAT6\n* `cat6a` - CAT6a\n* `cat7` - CAT7\n* `cat7a` - CAT7a\n* `cat8` - CAT8\n* `mrj21-trunk` - MRJ21 Trunk\n* `dac-active` - Direct Attach Copper (Active)\n* `dac-passive` - Direct Attach Copper (Passive)\n* `coaxial` - Coaxial\n* `mmf` - Multimode Fiber\n* `mmf-om1` - Multimode Fiber (OM1)\n* `mmf-om2` - Multimode Fiber (OM2)\n* `mmf-om3` - Multimode Fiber (OM3)\n* `mmf-om4` - Multimode Fiber (OM4)\n* `mmf-om5` - Multimode Fiber (OM5)\n* `smf` - Single-mode Fiber\n* `smf-os1` - Single-mode Fiber (OS1)\n* `smf-os2` - Single-mode Fiber (OS2)\n* `aoc` - Active Optical Cabling (AOC)\n* `power` - Power\n* `usb` - USB",
"x-spec-enum-id": "c731f2793fceac04",
"description": "* `cat3` - CAT3\n* `cat5` - CAT5\n* `cat5e` - CAT5e\n* `cat6` - CAT6\n* `cat6a` - CAT6a\n* `cat7` - CAT7\n* `cat7a` - CAT7a\n* `cat8` - CAT8\n* `mrj21-trunk` - MRJ21 Trunk\n* `dac-active` - Direct Attach Copper (Active)\n* `dac-passive` - Direct Attach Copper (Passive)\n* `coaxial` - Coaxial\n* `rg-6` - RG-6\n* `rg-8` - RG-8\n* `rg-11` - RG-11\n* `rg-59` - RG-59\n* `rg-62` - RG-62\n* `rg-213` - RG-213\n* `lmr-100` - LMR-100\n* `lmr-200` - LMR-200\n* `lmr-400` - LMR-400\n* `mmf` - Multimode Fiber\n* `mmf-om1` - Multimode Fiber (OM1)\n* `mmf-om2` - Multimode Fiber (OM2)\n* `mmf-om3` - Multimode Fiber (OM3)\n* `mmf-om4` - Multimode Fiber (OM4)\n* `mmf-om5` - Multimode Fiber (OM5)\n* `smf` - Single-mode Fiber\n* `smf-os1` - Single-mode Fiber (OS1)\n* `smf-os2` - Single-mode Fiber (OS2)\n* `aoc` - Active Optical Cabling (AOC)\n* `power` - Power\n* `usb` - USB",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
},
"a_terminations": {
@@ -249743,6 +249801,15 @@
"dac-active",
"dac-passive",
"coaxial",
"rg-6",
"rg-8",
"rg-11",
"rg-59",
"rg-62",
"rg-213",
"lmr-100",
"lmr-200",
"lmr-400",
"mmf",
"mmf-om1",
"mmf-om2",
@@ -249759,8 +249826,8 @@
null
],
"type": "string",
"description": "* `cat3` - CAT3\n* `cat5` - CAT5\n* `cat5e` - CAT5e\n* `cat6` - CAT6\n* `cat6a` - CAT6a\n* `cat7` - CAT7\n* `cat7a` - CAT7a\n* `cat8` - CAT8\n* `mrj21-trunk` - MRJ21 Trunk\n* `dac-active` - Direct Attach Copper (Active)\n* `dac-passive` - Direct Attach Copper (Passive)\n* `coaxial` - Coaxial\n* `mmf` - Multimode Fiber\n* `mmf-om1` - Multimode Fiber (OM1)\n* `mmf-om2` - Multimode Fiber (OM2)\n* `mmf-om3` - Multimode Fiber (OM3)\n* `mmf-om4` - Multimode Fiber (OM4)\n* `mmf-om5` - Multimode Fiber (OM5)\n* `smf` - Single-mode Fiber\n* `smf-os1` - Single-mode Fiber (OS1)\n* `smf-os2` - Single-mode Fiber (OS2)\n* `aoc` - Active Optical Cabling (AOC)\n* `power` - Power\n* `usb` - USB",
"x-spec-enum-id": "c731f2793fceac04",
"description": "* `cat3` - CAT3\n* `cat5` - CAT5\n* `cat5e` - CAT5e\n* `cat6` - CAT6\n* `cat6a` - CAT6a\n* `cat7` - CAT7\n* `cat7a` - CAT7a\n* `cat8` - CAT8\n* `mrj21-trunk` - MRJ21 Trunk\n* `dac-active` - Direct Attach Copper (Active)\n* `dac-passive` - Direct Attach Copper (Passive)\n* `coaxial` - Coaxial\n* `rg-6` - RG-6\n* `rg-8` - RG-8\n* `rg-11` - RG-11\n* `rg-59` - RG-59\n* `rg-62` - RG-62\n* `rg-213` - RG-213\n* `lmr-100` - LMR-100\n* `lmr-200` - LMR-200\n* `lmr-400` - LMR-400\n* `mmf` - Multimode Fiber\n* `mmf-om1` - Multimode Fiber (OM1)\n* `mmf-om2` - Multimode Fiber (OM2)\n* `mmf-om3` - Multimode Fiber (OM3)\n* `mmf-om4` - Multimode Fiber (OM4)\n* `mmf-om5` - Multimode Fiber (OM5)\n* `smf` - Single-mode Fiber\n* `smf-os1` - Single-mode Fiber (OS1)\n* `smf-os2` - Single-mode Fiber (OS2)\n* `aoc` - Active Optical Cabling (AOC)\n* `power` - Power\n* `usb` - USB",
"x-spec-enum-id": "8d6d8ba53d82f066",
"nullable": true
},
"a_terminations": {

View File

@@ -1,5 +1,33 @@
# NetBox v4.4
## v4.4.6 (2025-11-11)
### Enhancements
* [#14171](https://github.com/netbox-community/netbox/issues/14171) - Support VLAN assignment for device & VM interfaces being bulk imported
* [#20297](https://github.com/netbox-community/netbox/issues/20297) - Introduce additional coaxial cable types
### Bug Fixes
* [#20378](https://github.com/netbox-community/netbox/issues/20378) - Prevent exception when attempting to delete a data source utilized by a custom script
* [#20645](https://github.com/netbox-community/netbox/issues/20645) - CSVChoiceField should defer to model field's default value when CSV field is empty
* [#20647](https://github.com/netbox-community/netbox/issues/20647) - Improve handling of empty strings during bulk imports
* [#20653](https://github.com/netbox-community/netbox/issues/20653) - Fix filtering of jobs by object type ID
* [#20660](https://github.com/netbox-community/netbox/issues/20660) - Optimize loading of custom script modules from remote storage
* [#20670](https://github.com/netbox-community/netbox/issues/20670) - Improve validation of related objects during bulk import
* [#20688](https://github.com/netbox-community/netbox/issues/20688) - Suppress non-harmful "No active configuration revision found" warning message
* [#20697](https://github.com/netbox-community/netbox/issues/20697) - Prevent duplication of signals which increment/decrement related object counts
* [#20699](https://github.com/netbox-community/netbox/issues/20699) - Ensure proper ordering of changelog entries resulting from cascading deletions
* [#20713](https://github.com/netbox-community/netbox/issues/20713) - Ensure a pre-change snapshot is recorded on virtual chassis members being added/removed
* [#20721](https://github.com/netbox-community/netbox/issues/20721) - Fix breadcrumb navigation links in UI for background tasks
* [#20738](https://github.com/netbox-community/netbox/issues/20738) - Deleting a virtual chassis should nullify the `vc_position` of all former members
* [#20750](https://github.com/netbox-community/netbox/issues/20750) - Fix cloning of permissions when only one action is enabled
* [#20755](https://github.com/netbox-community/netbox/issues/20755) - Prevent duplicate results under certain conditions when filtering providers
* [#20771](https://github.com/netbox-community/netbox/issues/20771) - Comments are required when creating a new journal entry
* [#20774](https://github.com/netbox-community/netbox/issues/20774) - Bulk action button labels should be translated
---
## v4.4.5 (2025-10-28)
### Enhancements

View File

@@ -89,8 +89,6 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value) |
Q(accounts__account__icontains=value) |
Q(accounts__name__icontains=value) |
Q(comments__icontains=value)
)

View File

@@ -1,8 +1,13 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from core.choices import *
from core.models import Job
from netbox.api.exceptions import SerializerNotFound
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import BaseModelSerializer
from users.api.serializers_.users import UserSerializer
from utilities.api import get_serializer_for_model
__all__ = (
'JobSerializer',
@@ -18,11 +23,28 @@ class JobSerializer(BaseModelSerializer):
object_type = ContentTypeField(
read_only=True
)
object = serializers.SerializerMethodField(
read_only=True
)
class Meta:
model = Job
fields = [
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled',
'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries',
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'object', 'name', 'status', 'created',
'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries',
]
brief_fields = ('url', 'created', 'completed', 'user', 'status')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_object(self, obj):
"""
Serialize a nested representation of the object.
"""
if obj.object is None:
return None
try:
serializer = get_serializer_for_model(obj.object)
except SerializerNotFound:
return obj.object_repr
context = {'request': self.context['request']}
return serializer(obj.object, nested=True, context=context).data

View File

@@ -80,6 +80,10 @@ class JobFilterSet(BaseFilterSet):
method='search',
label=_('Search'),
)
object_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ObjectType.objects.with_feature('jobs'),
field_name='object_type_id',
)
object_type = ContentTypeFilter()
created = django_filters.DateTimeFilter()
created__before = django_filters.DateTimeFilter(
@@ -124,7 +128,7 @@ class JobFilterSet(BaseFilterSet):
class Meta:
model = Job
fields = ('id', 'object_type', 'object_id', 'name', 'interval', 'status', 'user', 'job_id')
fields = ('id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id')
def search(self, queryset, name, value):
if not value.strip():

View File

@@ -70,13 +70,13 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
model = Job
fieldsets = (
FieldSet('q', 'filter_id'),
FieldSet('object_type', 'status', name=_('Attributes')),
FieldSet('object_type_id', 'status', name=_('Attributes')),
FieldSet(
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation')
),
)
object_type = ContentTypeChoiceField(
object_type_id = ContentTypeChoiceField(
label=_('Object Type'),
queryset=ObjectType.objects.with_feature('jobs'),
required=False,

View File

@@ -1,4 +1,4 @@
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from netbox.object_actions import ObjectAction

View File

@@ -1154,7 +1154,6 @@ class VirtualChassis(PrimaryModel):
})
def delete(self, *args, **kwargs):
# Check for LAG interfaces split across member chassis
interfaces = Interface.objects.filter(
device__in=self.members.all(),
@@ -1168,6 +1167,13 @@ class VirtualChassis(PrimaryModel):
"interfaces."
).format(self=self, interfaces=InterfaceSpeedChoices))
# Clear vc_position and vc_priority on member devices BEFORE calling super().delete()
# This must be done here because on_delete=SET_NULL executes before pre_delete signal
for device in self.members.all():
device.vc_position = None
device.vc_priority = None
device.save()
return super().delete(*args, **kwargs)

View File

@@ -1,4 +1,4 @@
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from netbox.object_actions import ObjectAction

View File

@@ -1,6 +1,6 @@
import logging
from django.db.models.signals import post_save, post_delete, pre_delete
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from dcim.choices import CableEndChoices, LinkStatusChoices
@@ -85,18 +85,6 @@ def assign_virtualchassis_master(instance, created, **kwargs):
master.save()
@receiver(pre_delete, sender=VirtualChassis)
def clear_virtualchassis_members(instance, **kwargs):
"""
When a VirtualChassis is deleted, nullify the vc_position and vc_priority fields of its prior members.
"""
devices = Device.objects.filter(virtual_chassis=instance.pk)
for device in devices:
device.vc_position = None
device.vc_priority = None
device.save()
#
# Cables
#

View File

@@ -1031,3 +1031,92 @@ class VirtualDeviceContextTestCase(TestCase):
vdc2 = VirtualDeviceContext(device=device, name="VDC 2", identifier=1, status='active')
with self.assertRaises(ValidationError):
vdc2.full_clean()
class VirtualChassisTestCase(TestCase):
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
)
role = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
)
Device.objects.create(
device_type=devicetype, role=role, name='TestDevice1', site=site
)
Device.objects.create(
device_type=devicetype, role=role, name='TestDevice2', site=site
)
def test_virtualchassis_deletion_clears_vc_position(self):
"""
Test that when a VirtualChassis is deleted, member devices have their
vc_position and vc_priority fields set to None.
"""
devices = Device.objects.all()
device1 = devices[0]
device2 = devices[1]
# Create a VirtualChassis with two member devices
vc = VirtualChassis.objects.create(name='Test VC', master=device1)
device1.virtual_chassis = vc
device1.vc_position = 1
device1.vc_priority = 10
device1.save()
device2.virtual_chassis = vc
device2.vc_position = 2
device2.vc_priority = 20
device2.save()
# Verify devices are members of the VC with positions set
device1.refresh_from_db()
device2.refresh_from_db()
self.assertEqual(device1.virtual_chassis, vc)
self.assertEqual(device1.vc_position, 1)
self.assertEqual(device1.vc_priority, 10)
self.assertEqual(device2.virtual_chassis, vc)
self.assertEqual(device2.vc_position, 2)
self.assertEqual(device2.vc_priority, 20)
# Delete the VirtualChassis
vc.delete()
# Verify devices have vc_position and vc_priority set to None
device1.refresh_from_db()
device2.refresh_from_db()
self.assertIsNone(device1.virtual_chassis)
self.assertIsNone(device1.vc_position)
self.assertIsNone(device1.vc_priority)
self.assertIsNone(device2.virtual_chassis)
self.assertIsNone(device2.vc_position)
self.assertIsNone(device2.vc_priority)
def test_virtualchassis_duplicate_vc_position(self):
"""
Test that two devices cannot be assigned to the same vc_position
within the same VirtualChassis.
"""
devices = Device.objects.all()
device1 = devices[0]
device2 = devices[1]
# Create a VirtualChassis
vc = VirtualChassis.objects.create(name='Test VC')
# Assign first device to vc_position 1
device1.virtual_chassis = vc
device1.vc_position = 1
device1.full_clean()
device1.save()
# Try to assign second device to the same vc_position
device2.virtual_chassis = vc
device2.vc_position = 1
with self.assertRaises(ValidationError):
device2.full_clean()

View File

@@ -986,6 +986,131 @@ inventory-items:
ii1 = InventoryItemTemplate.objects.first()
self.assertEqual(ii1.name, 'Inventory Item 1')
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_import_error_numbering(self):
# Add all required permissions to the test user
self.add_permissions(
'dcim.view_devicetype',
'dcim.add_devicetype',
'dcim.add_consoleporttemplate',
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
'dcim.add_modulebaytemplate',
'dcim.add_devicebaytemplate',
'dcim.add_inventoryitemtemplate',
)
import_data = '''
---
manufacturer: Manufacturer 1
model: TEST-2001
slug: test-2001
u_height: 1
module-bays:
- name: Module Bay 1-1
- name: Module Bay 1-2
---
- manufacturer: Manufacturer 1
model: TEST-2002
slug: test-2002
u_height: 1
module-bays:
- name: Module Bay 2-1
- name: Module Bay 2-2
- not_name: Module Bay 2-3
- manufacturer: Manufacturer 1
model: TEST-2003
slug: test-2003
u_height: 1
module-bays:
- name: Module Bay 3-1
'''
form_data = {
'data': import_data,
'format': 'yaml'
}
response = self.client.post(reverse('dcim:devicetype_bulk_import'), data=form_data, follow=True)
self.assertHttpStatus(response, 200)
self.assertContains(response, "Record 2 module-bays[3].name: This field is required.")
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_import_nolist(self):
# Add all required permissions to the test user
self.add_permissions(
'dcim.view_devicetype',
'dcim.add_devicetype',
'dcim.add_consoleporttemplate',
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
'dcim.add_modulebaytemplate',
'dcim.add_devicebaytemplate',
'dcim.add_inventoryitemtemplate',
)
for value in ('', 'null', '3', '"My console port"', '{name: "My other console port"}'):
with self.subTest(value=value):
import_data = f'''
manufacturer: Manufacturer 1
model: TEST-3000
slug: test-3000
u_height: 1
console-ports: {value}
'''
form_data = {
'data': import_data,
'format': 'yaml'
}
response = self.client.post(reverse('dcim:devicetype_bulk_import'), data=form_data, follow=True)
self.assertHttpStatus(response, 200)
self.assertContains(response, "Record 1 console-ports: Must be a list.")
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_import_nodict(self):
# Add all required permissions to the test user
self.add_permissions(
'dcim.view_devicetype',
'dcim.add_devicetype',
'dcim.add_consoleporttemplate',
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
'dcim.add_modulebaytemplate',
'dcim.add_devicebaytemplate',
'dcim.add_inventoryitemtemplate',
)
for value in ('', 'null', '3', '"My console port"', '["My other console port"]'):
with self.subTest(value=value):
import_data = f'''
manufacturer: Manufacturer 1
model: TEST-4000
slug: test-4000
u_height: 1
console-ports:
- {value}
'''
form_data = {
'data': import_data,
'format': 'yaml'
}
response = self.client.post(reverse('dcim:devicetype_bulk_import'), data=form_data, follow=True)
self.assertHttpStatus(response, 200)
self.assertContains(response, "Record 1 console-ports[1]: Must be a dictionary.")
def test_export_objects(self):
url = reverse('dcim:devicetype_list')
self.add_permissions('dcim.view_devicetype')

View File

@@ -272,6 +272,10 @@ class JournalEntryImportForm(NetBoxModelImportForm):
choices=JournalEntryKindChoices,
help_text=_('The classification of entry')
)
comments = forms.CharField(
label=_('Comments'),
required=True
)
class Meta:
model = JournalEntry

View File

@@ -793,7 +793,7 @@ class JournalEntryForm(NetBoxModelForm):
label=_('Kind'),
choices=JournalEntryKindChoices
)
comments = CommentField()
comments = CommentField(required=True)
class Meta:
model = JournalEntry

View File

@@ -30,8 +30,7 @@ class CustomStoragesLoader(importlib.abc.Loader):
return None # Use default module creation
def exec_module(self, module):
storage = storages.create_storage(storages.backends["scripts"])
with storage.open(self.filename, 'rb') as f:
with storages["scripts"].open(self.filename, 'rb') as f:
code = f.read()
exec(code, module.__dict__)

View File

@@ -126,7 +126,7 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
ordered.extend(script_objects.values())
return ordered
@property
@cached_property
def module_scripts(self):
def _get_name(cls):

View File

@@ -82,7 +82,7 @@ class Config:
revision = ConfigRevision.objects.get(active=True)
logger.debug(f"Loaded active configuration revision #{revision.pk}")
except (ConfigRevision.DoesNotExist, ConfigRevision.MultipleObjectsReturned):
logger.warning("No active configuration revision found - falling back to most recent")
logger.debug("No active configuration revision found - falling back to most recent")
revision = ConfigRevision.objects.order_by('-created').first()
if revision is None:
logger.debug("No previous configuration found in database; proceeding with default values")

View File

@@ -1,6 +1,6 @@
from django.template import loader
from django.urls.exceptions import NoReverseMatch
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from core.models import ObjectType
from extras.models import ExportTemplate

View File

@@ -323,7 +323,7 @@ class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView):
class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
"""
Import objects in bulk (CSV format).
Import objects in bulk (CSV/JSON/YAML format).
Attributes:
model_form: The form used to create each imported object
@@ -368,7 +368,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
error_messages.append(f"Record {index} {prefix}{field_name}: {err}")
return error_messages
def _save_object(self, model_form, request):
def _save_object(self, model_form, request, parent_idx):
_action = 'Updated' if model_form.instance.pk else 'Created'
# Save the primary object
@@ -381,8 +381,25 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
# Iterate through the related object forms (if any), validating and saving each instance.
for field_name, related_object_form in self.related_object_forms.items():
related_objects = model_form.data.get(field_name, list())
if not isinstance(related_objects, list):
raise ValidationError(
self._compile_form_errors(
{field_name: [_("Must be a list.")]},
index=parent_idx
)
)
related_obj_pks = []
for i, rel_obj_data in enumerate(model_form.data.get(field_name, list())):
for i, rel_obj_data in enumerate(related_objects, start=1):
if not isinstance(rel_obj_data, dict):
raise ValidationError(
self._compile_form_errors(
{f'{field_name}[{i}]': [_("Must be a dictionary.")]},
index=parent_idx,
)
)
rel_obj_data = self.prep_related_object_data(obj, rel_obj_data)
f = related_object_form(rel_obj_data)
@@ -396,7 +413,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
else:
# Replicate errors on the related object form to the import form for display and abort
raise ValidationError(
self._compile_form_errors(f.errors, index=i, prefix=f'{field_name}[{i}]')
self._compile_form_errors(f.errors, index=parent_idx, prefix=f'{field_name}[{i}]')
)
# Enforce object-level permissions on related objects
@@ -439,8 +456,12 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
try:
instance = prefetched_objects[object_id]
except KeyError:
form.add_error('data', _("Row {i}: Object with ID {id} does not exist").format(i=i, id=object_id))
raise ValidationError('')
raise ValidationError(
self._compile_form_errors(
{'id': [_("Object with ID {id} does not exist").format(id=object_id)]},
index=i
)
)
# Take a snapshot for change logging
if instance.pk and hasattr(instance, 'snapshot'):
@@ -481,7 +502,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
restrict_form_fields(model_form, request.user)
if model_form.is_valid():
obj = self._save_object(model_form, request)
obj = self._save_object(model_form, request, i)
saved_objects.append(obj)
else:
# Raise model form errors

View File

@@ -30,7 +30,7 @@
"gridstack": "12.3.3",
"htmx.org": "2.0.8",
"query-string": "9.3.1",
"sass": "1.93.2",
"sass": "1.94.0",
"tom-select": "2.4.3",
"typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13"

View File

@@ -3190,10 +3190,10 @@ safe-regex-test@^1.1.0:
es-errors "^1.3.0"
is-regex "^1.2.1"
sass@1.93.2:
version "1.93.2"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.93.2.tgz#e97d225d60f59a3b3dbb6d2ae3c1b955fd1f2cd1"
integrity sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==
sass@1.94.0:
version "1.94.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.94.0.tgz#a04198d8940358ca6ad537d2074051edbbe7c1a7"
integrity sha512-Dqh7SiYcaFtdv5Wvku6QgS5IGPm281L+ZtVD1U2FJa7Q0EFRlq8Z3sjYtz6gYObsYThUOz9ArwFqPZx+1azILQ==
dependencies:
chokidar "^4.0.0"
immutable "^5.0.2"

View File

@@ -1,3 +1,3 @@
version: "4.4.5"
version: "4.4.6"
edition: "Community"
published: "2025-10-28"
published: "2025-11-11"

View File

@@ -8,10 +8,10 @@
<p>
<i class="mdi mdi-alert"></i>
<strong>{% trans "Missing required packages" %}.</strong>
{% blocktrans trimmed %}
{% blocktrans trimmed with req_file="requirements.txt" local_req_file="local_requirements.txt" pip_cmd="pip freeze" %}
This installation of NetBox might be missing one or more required Python packages. These packages are listed in
<code>requirements.txt</code> and <code>local_requirements.txt</code>, and are normally installed as part of the
installation or upgrade process. To verify installed packages, run <code>pip freeze</code> from the console and
<code>{{ req_file }}</code> and <code>{{ local_req_file }}</code>, and are normally installed as part of the
installation or upgrade process. To verify installed packages, run <code>{{ pip_cmd }}</code> from the console and
compare the output to the list of required packages.
{% endblocktrans %}
</p>

View File

@@ -8,17 +8,17 @@
<p>
<i class="mdi mdi-alert"></i>
<strong>{% trans "Database migrations missing" %}.</strong>
{% blocktrans trimmed %}
{% blocktrans trimmed with command="python3 manage.py migrate" %}
When upgrading to a new NetBox release, the upgrade script must be run to apply any new database migrations. You
can run migrations manually by executing <code>python3 manage.py migrate</code> from the command line.
can run migrations manually by executing <code>{{ command }}</code> from the command line.
{% endblocktrans %}
</p>
<p>
<i class="mdi mdi-alert"></i>
<strong>{% trans "Unsupported PostgreSQL version" %}.</strong>
{% blocktrans trimmed %}
{% blocktrans trimmed with sql_query="SELECT VERSION()" %}
Ensure that PostgreSQL version 14 or later is in use. You can check this by connecting to the database using
NetBox's credentials and issuing a query for <code>SELECT VERSION()</code>.
NetBox's credentials and issuing a query for <code>{{ sql_query }}</code>.
{% endblocktrans %}
</p>
{% endblock message %}

View File

@@ -26,8 +26,8 @@
<p>{% trans "Check the following" %}:</p>
<ul>
<li class="tip">
{% blocktrans trimmed %}
<code>manage.py collectstatic</code> was run during the most recent upgrade. This installs the most
{% blocktrans trimmed with command="manage.py collectstatic" %}
<code>{{ command }}</code> was run during the most recent upgrade. This installs the most
recent iteration of each static file into the static root path.
{% endblocktrans %}
</li>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-01 05:01+0000\n"
"POT-Creation-Date: 2025-11-11 05:01+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -84,9 +84,9 @@ msgstr ""
#: netbox/circuits/choices.py:21 netbox/dcim/choices.py:20
#: netbox/dcim/choices.py:102 netbox/dcim/choices.py:204
#: netbox/dcim/choices.py:257 netbox/dcim/choices.py:1836
#: netbox/dcim/choices.py:1894 netbox/dcim/choices.py:1961
#: netbox/dcim/choices.py:1983 netbox/virtualization/choices.py:20
#: netbox/dcim/choices.py:257 netbox/dcim/choices.py:1854
#: netbox/dcim/choices.py:1912 netbox/dcim/choices.py:1979
#: netbox/dcim/choices.py:2001 netbox/virtualization/choices.py:20
#: netbox/virtualization/choices.py:46 netbox/vpn/choices.py:18
#: netbox/vpn/choices.py:281
msgid "Planned"
@@ -100,8 +100,8 @@ msgstr ""
#: netbox/core/tables/tasks.py:23 netbox/dcim/choices.py:22
#: netbox/dcim/choices.py:103 netbox/dcim/choices.py:155
#: netbox/dcim/choices.py:203 netbox/dcim/choices.py:256
#: netbox/dcim/choices.py:1893 netbox/dcim/choices.py:1960
#: netbox/dcim/choices.py:1982 netbox/extras/tables/tables.py:598
#: netbox/dcim/choices.py:1911 netbox/dcim/choices.py:1978
#: netbox/dcim/choices.py:2000 netbox/extras/tables/tables.py:598
#: netbox/ipam/choices.py:31 netbox/ipam/choices.py:49
#: netbox/ipam/choices.py:69 netbox/ipam/choices.py:154
#: netbox/templates/extras/configcontext.html:29
@@ -113,8 +113,8 @@ msgid "Active"
msgstr ""
#: netbox/circuits/choices.py:24 netbox/dcim/choices.py:202
#: netbox/dcim/choices.py:255 netbox/dcim/choices.py:1892
#: netbox/dcim/choices.py:1962 netbox/dcim/choices.py:1981
#: netbox/dcim/choices.py:255 netbox/dcim/choices.py:1910
#: netbox/dcim/choices.py:1980 netbox/dcim/choices.py:1999
#: netbox/virtualization/choices.py:24 netbox/virtualization/choices.py:44
msgid "Offline"
msgstr ""
@@ -127,7 +127,7 @@ msgstr ""
msgid "Decommissioned"
msgstr ""
#: netbox/circuits/choices.py:90 netbox/dcim/choices.py:1905
#: netbox/circuits/choices.py:90 netbox/dcim/choices.py:1923
#: netbox/dcim/tables/devices.py:1178 netbox/templates/dcim/interface.html:135
#: netbox/templates/virtualization/vminterface.html:83
#: netbox/tenancy/choices.py:17
@@ -160,8 +160,8 @@ msgstr ""
msgid "Spoke"
msgstr ""
#: netbox/circuits/filtersets.py:37 netbox/circuits/filtersets.py:204
#: netbox/circuits/filtersets.py:284 netbox/dcim/base_filtersets.py:22
#: netbox/circuits/filtersets.py:37 netbox/circuits/filtersets.py:202
#: netbox/circuits/filtersets.py:282 netbox/dcim/base_filtersets.py:22
#: netbox/dcim/filtersets.py:101 netbox/dcim/filtersets.py:155
#: netbox/dcim/filtersets.py:215 netbox/dcim/filtersets.py:336
#: netbox/dcim/filtersets.py:467 netbox/dcim/filtersets.py:1108
@@ -172,8 +172,8 @@ msgstr ""
msgid "Region (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:44 netbox/circuits/filtersets.py:211
#: netbox/circuits/filtersets.py:291 netbox/dcim/base_filtersets.py:29
#: netbox/circuits/filtersets.py:44 netbox/circuits/filtersets.py:209
#: netbox/circuits/filtersets.py:289 netbox/dcim/base_filtersets.py:29
#: netbox/dcim/filtersets.py:108 netbox/dcim/filtersets.py:161
#: netbox/dcim/filtersets.py:222 netbox/dcim/filtersets.py:343
#: netbox/dcim/filtersets.py:474 netbox/dcim/filtersets.py:1115
@@ -185,8 +185,8 @@ msgstr ""
msgid "Region (slug)"
msgstr ""
#: netbox/circuits/filtersets.py:50 netbox/circuits/filtersets.py:217
#: netbox/circuits/filtersets.py:297 netbox/dcim/base_filtersets.py:35
#: netbox/circuits/filtersets.py:50 netbox/circuits/filtersets.py:215
#: netbox/circuits/filtersets.py:295 netbox/dcim/base_filtersets.py:35
#: netbox/dcim/filtersets.py:131 netbox/dcim/filtersets.py:228
#: netbox/dcim/filtersets.py:349 netbox/dcim/filtersets.py:480
#: netbox/dcim/filtersets.py:1121 netbox/dcim/filtersets.py:1442
@@ -197,8 +197,8 @@ msgstr ""
msgid "Site group (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:57 netbox/circuits/filtersets.py:224
#: netbox/circuits/filtersets.py:304 netbox/dcim/base_filtersets.py:42
#: netbox/circuits/filtersets.py:57 netbox/circuits/filtersets.py:222
#: netbox/circuits/filtersets.py:302 netbox/dcim/base_filtersets.py:42
#: netbox/dcim/filtersets.py:138 netbox/dcim/filtersets.py:235
#: netbox/dcim/filtersets.py:356 netbox/dcim/filtersets.py:487
#: netbox/dcim/filtersets.py:1128 netbox/dcim/filtersets.py:1449
@@ -258,8 +258,8 @@ msgstr ""
msgid "Site"
msgstr ""
#: netbox/circuits/filtersets.py:68 netbox/circuits/filtersets.py:235
#: netbox/circuits/filtersets.py:315 netbox/dcim/base_filtersets.py:53
#: netbox/circuits/filtersets.py:68 netbox/circuits/filtersets.py:233
#: netbox/circuits/filtersets.py:313 netbox/dcim/base_filtersets.py:53
#: netbox/dcim/filtersets.py:245 netbox/dcim/filtersets.py:366
#: netbox/dcim/filtersets.py:461 netbox/extras/filtersets.py:668
#: netbox/ipam/filtersets.py:257 netbox/ipam/filtersets.py:972
@@ -278,44 +278,44 @@ msgstr ""
msgid "ASN"
msgstr ""
#: netbox/circuits/filtersets.py:101 netbox/circuits/filtersets.py:128
#: netbox/circuits/filtersets.py:162 netbox/circuits/filtersets.py:338
#: netbox/circuits/filtersets.py:406 netbox/circuits/filtersets.py:482
#: netbox/circuits/filtersets.py:550 netbox/ipam/filtersets.py:262
#: netbox/circuits/filtersets.py:99 netbox/circuits/filtersets.py:126
#: netbox/circuits/filtersets.py:160 netbox/circuits/filtersets.py:336
#: netbox/circuits/filtersets.py:404 netbox/circuits/filtersets.py:480
#: netbox/circuits/filtersets.py:548 netbox/ipam/filtersets.py:262
msgid "Provider (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:107 netbox/circuits/filtersets.py:134
#: netbox/circuits/filtersets.py:168 netbox/circuits/filtersets.py:344
#: netbox/circuits/filtersets.py:488 netbox/circuits/filtersets.py:556
#: netbox/circuits/filtersets.py:105 netbox/circuits/filtersets.py:132
#: netbox/circuits/filtersets.py:166 netbox/circuits/filtersets.py:342
#: netbox/circuits/filtersets.py:486 netbox/circuits/filtersets.py:554
#: netbox/ipam/filtersets.py:268
msgid "Provider (slug)"
msgstr ""
#: netbox/circuits/filtersets.py:173 netbox/circuits/filtersets.py:493
#: netbox/circuits/filtersets.py:561
#: netbox/circuits/filtersets.py:171 netbox/circuits/filtersets.py:491
#: netbox/circuits/filtersets.py:559
msgid "Provider account (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:179 netbox/circuits/filtersets.py:499
#: netbox/circuits/filtersets.py:567
#: netbox/circuits/filtersets.py:177 netbox/circuits/filtersets.py:497
#: netbox/circuits/filtersets.py:565
msgid "Provider account (account)"
msgstr ""
#: netbox/circuits/filtersets.py:184 netbox/circuits/filtersets.py:503
#: netbox/circuits/filtersets.py:572
#: netbox/circuits/filtersets.py:182 netbox/circuits/filtersets.py:501
#: netbox/circuits/filtersets.py:570
msgid "Provider network (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:188
#: netbox/circuits/filtersets.py:186
msgid "Circuit type (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:194
#: netbox/circuits/filtersets.py:192
msgid "Circuit type (slug)"
msgstr ""
#: netbox/circuits/filtersets.py:229 netbox/circuits/filtersets.py:309
#: netbox/circuits/filtersets.py:227 netbox/circuits/filtersets.py:307
#: netbox/dcim/base_filtersets.py:47 netbox/dcim/filtersets.py:239
#: netbox/dcim/filtersets.py:360 netbox/dcim/filtersets.py:455
#: netbox/dcim/filtersets.py:1132 netbox/dcim/filtersets.py:1454
@@ -326,7 +326,7 @@ msgstr ""
msgid "Site (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:239 netbox/circuits/filtersets.py:321
#: netbox/circuits/filtersets.py:237 netbox/circuits/filtersets.py:319
#: netbox/dcim/base_filtersets.py:59 netbox/dcim/filtersets.py:261
#: netbox/dcim/filtersets.py:372 netbox/dcim/filtersets.py:493
#: netbox/dcim/filtersets.py:1144 netbox/dcim/filtersets.py:1465
@@ -334,14 +334,14 @@ msgstr ""
msgid "Location (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:244 netbox/circuits/filtersets.py:248
#: netbox/circuits/filtersets.py:242 netbox/circuits/filtersets.py:246
msgid "Termination A (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:273 netbox/circuits/filtersets.py:375
#: netbox/circuits/filtersets.py:537 netbox/core/filtersets.py:81
#: netbox/core/filtersets.py:141 netbox/core/filtersets.py:166
#: netbox/core/filtersets.py:205 netbox/dcim/filtersets.py:787
#: netbox/circuits/filtersets.py:271 netbox/circuits/filtersets.py:373
#: netbox/circuits/filtersets.py:535 netbox/core/filtersets.py:81
#: netbox/core/filtersets.py:145 netbox/core/filtersets.py:170
#: netbox/core/filtersets.py:209 netbox/dcim/filtersets.py:787
#: netbox/dcim/filtersets.py:1521 netbox/dcim/filtersets.py:2626
#: netbox/extras/filtersets.py:45 netbox/extras/filtersets.py:67
#: netbox/extras/filtersets.py:96 netbox/extras/filtersets.py:136
@@ -364,7 +364,7 @@ msgstr ""
msgid "Search"
msgstr ""
#: netbox/circuits/filtersets.py:277 netbox/circuits/forms/bulk_edit.py:195
#: netbox/circuits/filtersets.py:275 netbox/circuits/forms/bulk_edit.py:195
#: netbox/circuits/forms/bulk_edit.py:284
#: netbox/circuits/forms/bulk_import.py:128
#: netbox/circuits/forms/filtersets.py:224
@@ -383,7 +383,7 @@ msgstr ""
msgid "Circuit"
msgstr ""
#: netbox/circuits/filtersets.py:328 netbox/dcim/base_filtersets.py:66
#: netbox/circuits/filtersets.py:326 netbox/dcim/base_filtersets.py:66
#: netbox/dcim/filtersets.py:268 netbox/dcim/filtersets.py:379
#: netbox/dcim/filtersets.py:500 netbox/dcim/filtersets.py:1151
#: netbox/dcim/filtersets.py:1471 netbox/dcim/filtersets.py:1569
@@ -391,47 +391,47 @@ msgstr ""
msgid "Location (slug)"
msgstr ""
#: netbox/circuits/filtersets.py:333
#: netbox/circuits/filtersets.py:331
msgid "ProviderNetwork (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:381
#: netbox/circuits/filtersets.py:379
msgid "Circuit (CID)"
msgstr ""
#: netbox/circuits/filtersets.py:386
#: netbox/circuits/filtersets.py:384
msgid "Circuit (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:391
#: netbox/circuits/filtersets.py:389
msgid "Virtual circuit (CID)"
msgstr ""
#: netbox/circuits/filtersets.py:396 netbox/dcim/filtersets.py:2056
#: netbox/circuits/filtersets.py:394 netbox/dcim/filtersets.py:2056
msgid "Virtual circuit (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:401
#: netbox/circuits/filtersets.py:399
msgid "Provider (name)"
msgstr ""
#: netbox/circuits/filtersets.py:410
#: netbox/circuits/filtersets.py:408
msgid "Circuit group (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:416
#: netbox/circuits/filtersets.py:414
msgid "Circuit group (slug)"
msgstr ""
#: netbox/circuits/filtersets.py:507
#: netbox/circuits/filtersets.py:505
msgid "Virtual circuit type (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:513
#: netbox/circuits/filtersets.py:511
msgid "Virtual circuit type (slug)"
msgstr ""
#: netbox/circuits/filtersets.py:541 netbox/circuits/forms/bulk_edit.py:355
#: netbox/circuits/filtersets.py:539 netbox/circuits/forms/bulk_edit.py:355
#: netbox/circuits/forms/bulk_import.py:249
#: netbox/circuits/forms/filtersets.py:373
#: netbox/circuits/forms/filtersets.py:379
@@ -443,7 +443,7 @@ msgstr ""
msgid "Virtual circuit"
msgstr ""
#: netbox/circuits/filtersets.py:577 netbox/dcim/filtersets.py:1361
#: netbox/circuits/filtersets.py:575 netbox/dcim/filtersets.py:1361
#: netbox/dcim/filtersets.py:1796 netbox/ipam/filtersets.py:628
#: netbox/vpn/filtersets.py:102 netbox/vpn/filtersets.py:404
msgid "Interface (ID)"
@@ -1469,7 +1469,7 @@ msgstr ""
#: netbox/core/models/jobs.py:95 netbox/dcim/models/cables.py:52
#: netbox/dcim/models/device_components.py:488
#: netbox/dcim/models/device_components.py:1319
#: netbox/dcim/models/devices.py:580 netbox/dcim/models/devices.py:1188
#: netbox/dcim/models/devices.py:580 netbox/dcim/models/devices.py:1194
#: netbox/dcim/models/modules.py:210 netbox/dcim/models/power.py:94
#: netbox/dcim/models/racks.py:294 netbox/dcim/models/racks.py:677
#: netbox/dcim/models/sites.py:154 netbox/dcim/models/sites.py:270
@@ -1603,7 +1603,7 @@ msgstr ""
#: netbox/core/models/jobs.py:56
#: netbox/dcim/models/device_component_templates.py:44
#: netbox/dcim/models/device_components.py:53 netbox/dcim/models/devices.py:524
#: netbox/dcim/models/devices.py:1120 netbox/dcim/models/devices.py:1183
#: netbox/dcim/models/devices.py:1120 netbox/dcim/models/devices.py:1189
#: netbox/dcim/models/modules.py:32 netbox/dcim/models/power.py:38
#: netbox/dcim/models/power.py:89 netbox/dcim/models/racks.py:263
#: netbox/dcim/models/sites.py:142 netbox/extras/models/configs.py:36
@@ -1880,13 +1880,13 @@ msgstr ""
#: netbox/dcim/tables/racks.py:148 netbox/dcim/tables/racks.py:236
#: netbox/dcim/tables/sites.py:40 netbox/dcim/tables/sites.py:74
#: netbox/dcim/tables/sites.py:121 netbox/dcim/tables/sites.py:179
#: netbox/extras/tables/tables.py:702 netbox/ipam/tables/asn.py:69
#: netbox/ipam/tables/fhrp.py:34 netbox/ipam/tables/ip.py:83
#: netbox/ipam/tables/ip.py:227 netbox/ipam/tables/ip.py:286
#: netbox/ipam/tables/ip.py:355 netbox/ipam/tables/services.py:25
#: netbox/ipam/tables/services.py:55 netbox/ipam/tables/vlans.py:124
#: netbox/ipam/tables/vrfs.py:47 netbox/ipam/tables/vrfs.py:72
#: netbox/templates/dcim/htmx/cable_edit.html:90
#: netbox/extras/forms/bulk_import.py:276 netbox/extras/tables/tables.py:702
#: netbox/ipam/tables/asn.py:69 netbox/ipam/tables/fhrp.py:34
#: netbox/ipam/tables/ip.py:83 netbox/ipam/tables/ip.py:227
#: netbox/ipam/tables/ip.py:286 netbox/ipam/tables/ip.py:355
#: netbox/ipam/tables/services.py:25 netbox/ipam/tables/services.py:55
#: netbox/ipam/tables/vlans.py:124 netbox/ipam/tables/vrfs.py:47
#: netbox/ipam/tables/vrfs.py:72 netbox/templates/dcim/htmx/cable_edit.html:90
#: netbox/templates/generic/bulk_edit.html:86
#: netbox/templates/inc/panels/comments.html:5
#: netbox/tenancy/tables/contacts.py:35 netbox/tenancy/tables/contacts.py:76
@@ -2084,7 +2084,7 @@ msgstr ""
#: netbox/core/choices.py:22 netbox/core/choices.py:59
#: netbox/core/constants.py:21 netbox/core/tables/tasks.py:35
#: netbox/dcim/choices.py:206 netbox/dcim/choices.py:259
#: netbox/dcim/choices.py:1895 netbox/dcim/choices.py:1985
#: netbox/dcim/choices.py:1913 netbox/dcim/choices.py:2003
#: netbox/virtualization/choices.py:48
msgid "Failed"
msgstr ""
@@ -2243,19 +2243,19 @@ msgstr ""
msgid "Data source (name)"
msgstr ""
#: netbox/core/filtersets.py:176 netbox/dcim/filtersets.py:508
#: netbox/core/filtersets.py:180 netbox/dcim/filtersets.py:508
#: netbox/extras/filtersets.py:292 netbox/extras/filtersets.py:344
#: netbox/extras/filtersets.py:389 netbox/extras/filtersets.py:411
#: netbox/extras/filtersets.py:475 netbox/users/filtersets.py:28
msgid "User (ID)"
msgstr ""
#: netbox/core/filtersets.py:182
#: netbox/core/filtersets.py:186
msgid "User name"
msgstr ""
#: netbox/core/forms/bulk_edit.py:26 netbox/core/forms/filtersets.py:43
#: netbox/core/tables/data.py:27 netbox/dcim/choices.py:1943
#: netbox/core/tables/data.py:27 netbox/dcim/choices.py:1961
#: netbox/dcim/forms/bulk_edit.py:1211 netbox/dcim/forms/bulk_edit.py:1492
#: netbox/dcim/forms/filtersets.py:1458 netbox/dcim/tables/devices.py:596
#: netbox/dcim/tables/devicetypes.py:231 netbox/extras/forms/bulk_edit.py:127
@@ -2444,7 +2444,7 @@ msgstr ""
msgid "Rack Elevations"
msgstr ""
#: netbox/core/forms/model_forms.py:160 netbox/dcim/choices.py:1814
#: netbox/core/forms/model_forms.py:160 netbox/dcim/choices.py:1832
#: netbox/dcim/forms/bulk_edit.py:1054 netbox/dcim/forms/bulk_edit.py:1446
#: netbox/dcim/forms/bulk_edit.py:1467 netbox/dcim/tables/racks.py:161
#: netbox/netbox/navigation/menu.py:313 netbox/netbox/navigation/menu.py:317
@@ -2550,7 +2550,7 @@ msgid "Change logging is not supported for this object type ({type})."
msgstr ""
#: netbox/core/models/config.py:21 netbox/core/models/data.py:269
#: netbox/core/models/files.py:30 netbox/core/models/jobs.py:60
#: netbox/core/models/files.py:29 netbox/core/models/jobs.py:60
#: netbox/extras/models/models.py:839 netbox/extras/models/notifications.py:39
#: netbox/extras/models/notifications.py:195
#: netbox/netbox/models/features.py:61 netbox/users/models/tokens.py:32
@@ -2665,7 +2665,7 @@ msgid ""
"installed: "
msgstr ""
#: netbox/core/models/data.py:273 netbox/core/models/files.py:34
#: netbox/core/models/data.py:273 netbox/core/models/files.py:33
#: netbox/netbox/models/features.py:67
msgid "last updated"
msgstr ""
@@ -2710,27 +2710,27 @@ msgstr ""
msgid "auto sync records"
msgstr ""
#: netbox/core/models/files.py:40
#: netbox/core/models/files.py:39
msgid "file root"
msgstr ""
#: netbox/core/models/files.py:45
#: netbox/core/models/files.py:44
msgid "file path"
msgstr ""
#: netbox/core/models/files.py:47
#: netbox/core/models/files.py:46
msgid "File path relative to the designated root path"
msgstr ""
#: netbox/core/models/files.py:61
#: netbox/core/models/files.py:60
msgid "managed file"
msgstr ""
#: netbox/core/models/files.py:62
#: netbox/core/models/files.py:61
msgid "managed files"
msgstr ""
#: netbox/core/models/files.py:112
#: netbox/core/models/files.py:108
#, python-brace-format
msgid "A {model} with this file path already exists ({path})."
msgstr ""
@@ -3082,8 +3082,8 @@ msgid "Staging"
msgstr ""
#: netbox/dcim/choices.py:23 netbox/dcim/choices.py:208
#: netbox/dcim/choices.py:260 netbox/dcim/choices.py:1837
#: netbox/dcim/choices.py:1986 netbox/virtualization/choices.py:23
#: netbox/dcim/choices.py:260 netbox/dcim/choices.py:1855
#: netbox/dcim/choices.py:2004 netbox/virtualization/choices.py:23
#: netbox/virtualization/choices.py:49 netbox/vpn/choices.py:282
msgid "Decommissioning"
msgstr ""
@@ -3148,7 +3148,7 @@ msgstr ""
msgid "Millimeters"
msgstr ""
#: netbox/dcim/choices.py:115 netbox/dcim/choices.py:1859
#: netbox/dcim/choices.py:115 netbox/dcim/choices.py:1877
msgid "Inches"
msgstr ""
@@ -3232,7 +3232,7 @@ msgid "Rear"
msgstr ""
#: netbox/dcim/choices.py:205 netbox/dcim/choices.py:258
#: netbox/dcim/choices.py:1984 netbox/virtualization/choices.py:47
#: netbox/dcim/choices.py:2002 netbox/virtualization/choices.py:47
msgid "Staged"
msgstr ""
@@ -3469,80 +3469,80 @@ msgstr ""
msgid "Fiber Optic"
msgstr ""
#: netbox/dcim/choices.py:1695 netbox/dcim/choices.py:1820
#: netbox/dcim/choices.py:1695 netbox/dcim/choices.py:1838
msgid "USB"
msgstr ""
#: netbox/dcim/choices.py:1764
#: netbox/dcim/choices.py:1773
msgid "Copper - Twisted Pair (UTP/STP)"
msgstr ""
#: netbox/dcim/choices.py:1778
#: netbox/dcim/choices.py:1787
msgid "Copper - Twinax (DAC)"
msgstr ""
#: netbox/dcim/choices.py:1785
#: netbox/dcim/choices.py:1794
msgid "Copper - Coaxial"
msgstr ""
#: netbox/dcim/choices.py:1791
#: netbox/dcim/choices.py:1809
msgid "Fiber - Multimode"
msgstr ""
#: netbox/dcim/choices.py:1802
#: netbox/dcim/choices.py:1820
msgid "Fiber - Single-mode"
msgstr ""
#: netbox/dcim/choices.py:1810
#: netbox/dcim/choices.py:1828
msgid "Fiber - Other"
msgstr ""
#: netbox/dcim/choices.py:1835 netbox/dcim/forms/filtersets.py:1305
#: netbox/dcim/choices.py:1853 netbox/dcim/forms/filtersets.py:1305
msgid "Connected"
msgstr ""
#: netbox/dcim/choices.py:1854 netbox/netbox/choices.py:177
#: netbox/dcim/choices.py:1872 netbox/netbox/choices.py:177
msgid "Kilometers"
msgstr ""
#: netbox/dcim/choices.py:1855 netbox/netbox/choices.py:178
#: netbox/dcim/choices.py:1873 netbox/netbox/choices.py:178
#: netbox/templates/dcim/cable_trace.html:65
msgid "Meters"
msgstr ""
#: netbox/dcim/choices.py:1856
#: netbox/dcim/choices.py:1874
msgid "Centimeters"
msgstr ""
#: netbox/dcim/choices.py:1857 netbox/netbox/choices.py:179
#: netbox/dcim/choices.py:1875 netbox/netbox/choices.py:179
msgid "Miles"
msgstr ""
#: netbox/dcim/choices.py:1858 netbox/netbox/choices.py:180
#: netbox/dcim/choices.py:1876 netbox/netbox/choices.py:180
#: netbox/templates/dcim/cable_trace.html:66
msgid "Feet"
msgstr ""
#: netbox/dcim/choices.py:1906
#: netbox/dcim/choices.py:1924
msgid "Redundant"
msgstr ""
#: netbox/dcim/choices.py:1927
#: netbox/dcim/choices.py:1945
msgid "Single phase"
msgstr ""
#: netbox/dcim/choices.py:1928
#: netbox/dcim/choices.py:1946
msgid "Three-phase"
msgstr ""
#: netbox/dcim/choices.py:1944 netbox/extras/choices.py:53
#: netbox/dcim/choices.py:1962 netbox/extras/choices.py:53
#: netbox/netbox/preferences.py:32 netbox/netbox/preferences.py:55
#: netbox/netbox/preferences.py:80 netbox/templates/extras/customfield.html:78
#: netbox/vpn/choices.py:20 netbox/wireless/choices.py:27
msgid "Disabled"
msgstr ""
#: netbox/dcim/choices.py:1945
#: netbox/dcim/choices.py:1963
msgid "Faulty"
msgstr ""
@@ -3815,8 +3815,8 @@ msgstr ""
#: netbox/dcim/filtersets.py:1197 netbox/dcim/forms/filtersets.py:848
#: netbox/dcim/forms/filtersets.py:1473 netbox/dcim/forms/filtersets.py:1688
#: netbox/dcim/forms/model_forms.py:1899 netbox/dcim/models/devices.py:1284
#: netbox/dcim/models/devices.py:1304 netbox/virtualization/filtersets.py:201
#: netbox/dcim/forms/model_forms.py:1899 netbox/dcim/models/devices.py:1290
#: netbox/dcim/models/devices.py:1310 netbox/virtualization/filtersets.py:201
#: netbox/virtualization/filtersets.py:273
#: netbox/virtualization/forms/filtersets.py:178
#: netbox/virtualization/forms/filtersets.py:231
@@ -6857,12 +6857,12 @@ msgstr ""
msgid "rack face"
msgstr ""
#: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1204
#: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1210
#: netbox/virtualization/models/virtualmachines.py:94
msgid "primary IPv4"
msgstr ""
#: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1212
#: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1218
#: netbox/virtualization/models/virtualmachines.py:102
msgid "primary IPv6"
msgstr ""
@@ -7023,55 +7023,55 @@ msgstr ""
msgid "The selected master ({master}) is not assigned to this virtual chassis."
msgstr ""
#: netbox/dcim/models/devices.py:1167
#: netbox/dcim/models/devices.py:1166
#, python-brace-format
msgid ""
"Unable to delete virtual chassis {self}. There are member interfaces which "
"form a cross-chassis LAG interfaces."
msgstr ""
#: netbox/dcim/models/devices.py:1193 netbox/vpn/models/l2vpn.py:42
#: netbox/dcim/models/devices.py:1199 netbox/vpn/models/l2vpn.py:42
msgid "identifier"
msgstr ""
#: netbox/dcim/models/devices.py:1194
#: netbox/dcim/models/devices.py:1200
msgid "Numeric identifier unique to the parent device"
msgstr ""
#: netbox/dcim/models/devices.py:1222 netbox/extras/models/customfields.py:231
#: netbox/dcim/models/devices.py:1228 netbox/extras/models/customfields.py:231
#: netbox/extras/models/models.py:111 netbox/extras/models/models.py:800
#: netbox/netbox/models/__init__.py:120 netbox/netbox/models/__init__.py:155
msgid "comments"
msgstr ""
#: netbox/dcim/models/devices.py:1238
#: netbox/dcim/models/devices.py:1244
msgid "virtual device context"
msgstr ""
#: netbox/dcim/models/devices.py:1239
#: netbox/dcim/models/devices.py:1245
msgid "virtual device contexts"
msgstr ""
#: netbox/dcim/models/devices.py:1268
#: netbox/dcim/models/devices.py:1274
#, python-brace-format
msgid "{ip} is not an IPv{family} address."
msgstr ""
#: netbox/dcim/models/devices.py:1274
#: netbox/dcim/models/devices.py:1280
msgid "Primary IP address must belong to an interface on the assigned device."
msgstr ""
#: netbox/dcim/models/devices.py:1305
#: netbox/dcim/models/devices.py:1311
msgid "MAC addresses"
msgstr ""
#: netbox/dcim/models/devices.py:1337
#: netbox/dcim/models/devices.py:1343
msgid ""
"Cannot unassign MAC Address while it is designated as the primary MAC for an "
"object"
msgstr ""
#: netbox/dcim/models/devices.py:1341
#: netbox/dcim/models/devices.py:1347
msgid ""
"Cannot reassign MAC Address while it is designated as the primary MAC for an "
"object"
@@ -8454,7 +8454,7 @@ msgstr ""
#: netbox/extras/forms/bulk_edit.py:158 netbox/extras/forms/bulk_edit.py:383
#: netbox/extras/forms/filtersets.py:193 netbox/extras/forms/filtersets.py:498
#: netbox/extras/models/mixins.py:101
#: netbox/extras/models/mixins.py:100
msgid "MIME type"
msgstr ""
@@ -8615,7 +8615,7 @@ msgstr ""
msgid "The classification of entry"
msgstr ""
#: netbox/extras/forms/bulk_import.py:285
#: netbox/extras/forms/bulk_import.py:289
#: netbox/extras/forms/model_forms.py:400 netbox/netbox/navigation/menu.py:414
#: netbox/templates/extras/notificationgroup.html:41
#: netbox/templates/users/group.html:29 netbox/users/forms/model_forms.py:247
@@ -8624,11 +8624,11 @@ msgstr ""
msgid "Users"
msgstr ""
#: netbox/extras/forms/bulk_import.py:289
#: netbox/extras/forms/bulk_import.py:293
msgid "User names separated by commas, encased with double quotes"
msgstr ""
#: netbox/extras/forms/bulk_import.py:292
#: netbox/extras/forms/bulk_import.py:296
#: netbox/extras/forms/model_forms.py:395 netbox/netbox/navigation/menu.py:295
#: netbox/netbox/navigation/menu.py:434
#: netbox/templates/extras/notificationgroup.html:31
@@ -8641,7 +8641,7 @@ msgstr ""
msgid "Groups"
msgstr ""
#: netbox/extras/forms/bulk_import.py:296
#: netbox/extras/forms/bulk_import.py:300
msgid "Group names separated by commas, encased with double quotes"
msgstr ""
@@ -9360,51 +9360,51 @@ msgstr ""
msgid "dashboards"
msgstr ""
#: netbox/extras/models/mixins.py:86
#: netbox/extras/models/mixins.py:85
msgid "template code"
msgstr ""
#: netbox/extras/models/mixins.py:87
#: netbox/extras/models/mixins.py:86
msgid "Jinja template code."
msgstr ""
#: netbox/extras/models/mixins.py:90
#: netbox/extras/models/mixins.py:89
msgid "environment parameters"
msgstr ""
#: netbox/extras/models/mixins.py:95
#: netbox/extras/models/mixins.py:94
#, python-brace-format
msgid ""
"Any <a href=\"{url}\">additional parameters</a> to pass when constructing "
"the Jinja environment"
msgstr ""
#: netbox/extras/models/mixins.py:102
#: netbox/extras/models/mixins.py:101
#, python-brace-format
msgid "Defaults to <code>{default}</code>"
msgstr ""
#: netbox/extras/models/mixins.py:107
#: netbox/extras/models/mixins.py:106
msgid "Filename to give to the rendered export file"
msgstr ""
#: netbox/extras/models/mixins.py:110
#: netbox/extras/models/mixins.py:109
msgid "file extension"
msgstr ""
#: netbox/extras/models/mixins.py:113
#: netbox/extras/models/mixins.py:112
msgid "Extension to append to the rendered filename"
msgstr ""
#: netbox/extras/models/mixins.py:116
#: netbox/extras/models/mixins.py:115
msgid "as attachment"
msgstr ""
#: netbox/extras/models/mixins.py:118
#: netbox/extras/models/mixins.py:117
msgid "Download file as attachment"
msgstr ""
#: netbox/extras/models/mixins.py:125
#: netbox/extras/models/mixins.py:124
#, python-brace-format
msgid "{class_name} must implement a get_context() method."
msgstr ""
@@ -12539,54 +12539,62 @@ msgid ""
"{error}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:442
#, python-brace-format
msgid "Row {i}: Object with ID {id} does not exist"
#: netbox/netbox/views/generic/bulk_views.py:388
msgid "Must be a list."
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:525
#: netbox/netbox/views/generic/bulk_views.py:398
msgid "Must be a dictionary."
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:461
#, python-brace-format
msgid "Object with ID {id} does not exist"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:546
#, python-brace-format
msgid "Bulk import {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:541
#: netbox/netbox/views/generic/bulk_views.py:562
#, python-brace-format
msgid "Imported {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:731
#: netbox/netbox/views/generic/bulk_views.py:752
#, python-brace-format
msgid "Bulk edit {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:747
#: netbox/netbox/views/generic/bulk_views.py:768
#, python-brace-format
msgid "Updated {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:780
#: netbox/netbox/views/generic/bulk_views.py:1015
#: netbox/netbox/views/generic/bulk_views.py:1063
#: netbox/netbox/views/generic/bulk_views.py:801
#: netbox/netbox/views/generic/bulk_views.py:1036
#: netbox/netbox/views/generic/bulk_views.py:1084
#, python-brace-format
msgid "No {object_type} were selected."
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:873
#: netbox/netbox/views/generic/bulk_views.py:894
#, python-brace-format
msgid "Renamed {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:943
#: netbox/netbox/views/generic/bulk_views.py:964
#, python-brace-format
msgid "Bulk delete {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:970
#: netbox/netbox/views/generic/bulk_views.py:991
#, python-brace-format
msgid "Deleted {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:987
#: netbox/netbox/views/generic/bulk_views.py:1008
msgid "Deletion failed due to the presence of one or more dependent objects."
msgstr ""
@@ -15817,11 +15825,11 @@ msgstr ""
msgid "Objects"
msgstr ""
#: netbox/users/forms/model_forms.py:400
#: netbox/users/forms/model_forms.py:403
msgid "At least one action must be selected."
msgstr ""
#: netbox/users/forms/model_forms.py:418
#: netbox/users/forms/model_forms.py:421
#, python-brace-format
msgid "Invalid filter for {model}: {error}"
msgstr ""
@@ -16049,33 +16057,33 @@ msgid ""
"Invalid ranges ({value}). Must be a range of integers in ascending order."
msgstr ""
#: netbox/utilities/forms/fields/csv.py:44
#: netbox/utilities/forms/fields/csv.py:59
#, python-brace-format
msgid "Invalid value for a multiple choice field: {value}"
msgstr ""
#: netbox/utilities/forms/fields/csv.py:57
#: netbox/utilities/forms/fields/csv.py:78
#: netbox/utilities/forms/fields/csv.py:77
#: netbox/utilities/forms/fields/csv.py:98
#, python-format
msgid "Object not found: %(value)s"
msgstr ""
#: netbox/utilities/forms/fields/csv.py:65
#: netbox/utilities/forms/fields/csv.py:85
#, python-brace-format
msgid ""
"\"{value}\" is not a unique value for this field; multiple objects were found"
msgstr ""
#: netbox/utilities/forms/fields/csv.py:69
#: netbox/utilities/forms/fields/csv.py:89
#, python-brace-format
msgid "\"{field_name}\" is an invalid accessor field name."
msgstr ""
#: netbox/utilities/forms/fields/csv.py:102
#: netbox/utilities/forms/fields/csv.py:122
msgid "Object type must be specified as \"<app>.<model>\""
msgstr ""
#: netbox/utilities/forms/fields/csv.py:106
#: netbox/utilities/forms/fields/csv.py:126
msgid "Invalid object type"
msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -372,6 +372,9 @@ class ObjectPermissionForm(forms.ModelForm):
elif self.initial:
# Handle cloned objects - actions come from initial data (URL parameters)
if 'actions' in self.initial:
# Normalize actions to a list of strings
if isinstance(self.initial['actions'], str):
self.initial['actions'] = [self.initial['actions']]
if cloned_actions := self.initial['actions']:
for action in ['view', 'add', 'change', 'delete']:
if action in cloned_actions:

View File

@@ -18,6 +18,20 @@ __all__ = (
)
class CSVSelectWidget(forms.Select):
"""
Custom Select widget for CSV imports that treats blank values as omitted.
This allows model defaults to be applied when a CSV field is present but empty.
"""
def value_omitted_from_data(self, data, files, name):
# Check if value is omitted using parent behavior
if super().value_omitted_from_data(data, files, name):
return True
# Treat blank/empty strings as omitted to allow model defaults
value = data.get(name)
return value == '' or value is None
class CSVChoicesMixin:
STATIC_CHOICES = True
@@ -29,8 +43,9 @@ class CSVChoicesMixin:
class CSVChoiceField(CSVChoicesMixin, forms.ChoiceField):
"""
A CSV field which accepts a single selection value.
Treats blank CSV values as omitted to allow model defaults.
"""
pass
widget = CSVSelectWidget
class CSVMultipleChoiceField(CSVChoicesMixin, forms.MultipleChoiceField):
@@ -46,7 +61,12 @@ class CSVMultipleChoiceField(CSVChoicesMixin, forms.MultipleChoiceField):
class CSVTypedChoiceField(forms.TypedChoiceField):
"""
A CSV field for typed choice values.
Treats blank CSV values as omitted to allow model defaults.
"""
STATIC_CHOICES = True
widget = CSVSelectWidget
class CSVModelChoiceField(forms.ModelChoiceField):

View File

@@ -4,6 +4,7 @@ from django.test import TestCase
from dcim.models import Site
from netbox.choices import ImportFormatChoices
from utilities.forms.bulk_import import BulkImportForm
from utilities.forms.fields.csv import CSVSelectWidget
from utilities.forms.forms import BulkRenameForm
from utilities.forms.utils import get_field_value, expand_alphanumeric_pattern, expand_ipaddress_pattern
@@ -448,3 +449,35 @@ class GetFieldValueTest(TestCase):
get_field_value(form, 'site'),
None
)
class CSVSelectWidgetTest(TestCase):
"""
Validate that CSVSelectWidget treats blank values as omitted.
This allows model defaults to be applied when CSV fields are present but empty.
Related to issue #20645.
"""
def test_blank_value_treated_as_omitted(self):
"""Test that blank string values are treated as omitted"""
widget = CSVSelectWidget()
data = {'test_field': ''}
self.assertTrue(widget.value_omitted_from_data(data, {}, 'test_field'))
def test_none_value_treated_as_omitted(self):
"""Test that None values are treated as omitted"""
widget = CSVSelectWidget()
data = {'test_field': None}
self.assertTrue(widget.value_omitted_from_data(data, {}, 'test_field'))
def test_missing_field_treated_as_omitted(self):
"""Test that missing fields are treated as omitted"""
widget = CSVSelectWidget()
data = {}
self.assertTrue(widget.value_omitted_from_data(data, {}, 'test_field'))
def test_valid_value_not_omitted(self):
"""Test that valid values are not treated as omitted"""
widget = CSVSelectWidget()
data = {'test_field': 'valid_value'}
self.assertFalse(widget.value_omitted_from_data(data, {}, 'test_field'))

View File

@@ -1,4 +1,4 @@
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from netbox.object_actions import ObjectAction

View File

@@ -3,7 +3,7 @@
[project]
name = "netbox"
version = "4.4.5"
version = "4.4.6"
requires-python = ">=3.10"
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.7
Django==5.2.8
django-cors-headers==4.9.0
django-debug-toolbar==6.0.0
django-debug-toolbar==6.1.0
django-filter==25.2
django-graphiql-debug-toolbar==0.2.0
django-htmx==1.26.0
@@ -16,18 +16,18 @@ django-tables2==2.7.5
django-taggit==6.1.0
django-timezone-field==7.1
djangorestframework==3.16.1
drf-spectacular==0.28.0
drf-spectacular==0.29.0
drf-spectacular-sidecar==2025.10.1
feedparser==6.0.12
gunicorn==23.0.0
Jinja2==3.1.6
jsonschema==4.25.1
Markdown==3.9
Markdown==3.10
mkdocs-material==9.6.22
mkdocstrings==0.30.1
mkdocstrings-python==1.18.2
mkdocstrings-python==1.19.0
netaddr==1.3.0
nh3==0.3.1
nh3==0.3.2
Pillow==12.0.0
psycopg[c,pool]==3.2.12
PyYAML==6.0.3
@@ -36,7 +36,7 @@ rq==2.6.0
social-auth-app-django==5.6.0
social-auth-core==4.8.1
sorl-thumbnail==12.11.0
strawberry-graphql==0.284.1
strawberry-graphql==0.285.0
strawberry-graphql-django==0.67.0
svgwrite==1.4.3
tablib==3.9.0