Compare commits

...

101 Commits

Author SHA1 Message Date
Jeremy Stretch
03f3f5c957 Release v4.2.7 2025-04-10 16:07:24 -04:00
Jeremy Stretch
fe7fb94e44 Revert "Fixes: #18783 Add a tag_id filter for all models which support taggin…"
This reverts commit 9a1d9365cd.
2025-04-10 15:42:26 -04:00
github-actions
82b9e4ca26 Update source translation strings 2025-04-10 05:02:11 +00:00
Jeremy Stretch
457fb977a7 Fixes #19122: Fix styling of the server error (500) page (#19126) 2025-04-09 14:57:25 -07:00
Jeremy Stretch
13c20957a6 Closes #18652: Run housekeeping GitHub actions only on the main repository (#19125) 2025-04-09 16:28:00 -05:00
Jason Novinger
30208549ba Fixes #19092: scope type selection lost when editing multiple/all objects (#19102) 2025-04-09 14:55:41 -04:00
atownson
bf286df670 Fixes #19030 - Update z-index of floating buttons (#19118)
* Increase z-index of form floating buttons

* Update netbox.css
2025-04-08 16:02:08 -05:00
Jeremy Stretch
2be257db48 Closes #19112: Configure ruff to target Python 3.10 (#19113) 2025-04-08 09:46:31 -05:00
bctiemann
2207ea1a32 Merge pull request #19046 from pheus/docs/18733-add-version-requirements-matrix
Fixes #18733: Add Dependency Version Matrix for NetBox Versions to the Upgrade Documentation
2025-04-07 14:01:54 -04:00
Martin Hauser
10e1ae8292 docs(release): Update Dependency Requirements Matrix header
Renames the "Adopt the Dependency Requirements Matrix" section to
"Update the Dependency Requirements Matrix" for clarity.
2025-04-07 17:17:26 +02:00
Martin Hauser
f8f5ab8d61 docs(release): Correct formatting in release checklist 2025-04-07 16:55:23 +02:00
github-actions
92317248a3 Update source translation strings 2025-04-05 05:02:12 +00:00
Jeremy Stretch
426e6439e3 Fixes #18553: Update site for VMs only if cluster has a site assigned (#19086) 2025-04-04 10:58:06 -07:00
Jeremy Stretch
621b29cd71 Closes #19035: Move the registration of core event types to the app config (#19088) 2025-04-04 11:18:42 -05:00
github-actions
8f5d273f08 Update source translation strings 2025-04-03 05:02:01 +00:00
Martin Hauser
45779a24a4 docs(release): Update checklist with dependency requirements
Add steps to update the dependency requirements matrix for each minor
release in the release checklist. Clarify how to document changes for
system requirements and linked installation guides.

Fixes #18733
2025-04-02 21:41:36 +02:00
Martin Hauser
f17bbe610e Fixes #19041: Call super().clean() in FrontPortCreateForm (#19051)
* fix(forms): Call super().clean() in clean methods

Adds a call to super().clean() in the clean methods of object creation
forms. This ensures base class validation logic is executed properly
before custom logic is applied.

Fixes #19041

* test(forms): Add tests for front port form validation

Introduces unit tests for validating FrontPortCreateForm behavior.
Tests include scenarios for matching and mismatched name-label pairs
to ensure proper form validation logic.

Fixes #19041

* Omit errant print statement

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-04-02 08:29:21 -04:00
bctiemann
bad820001d Merge pull request #19015 from netbox-community/18738-script-list-ignoring-script-order
Fixes #18738: Ensure ScriptList respects script_order option
2025-04-01 10:09:39 -04:00
Martin Hauser
a5106b858d docs(upgrading): Add dependency matrix for NetBox versions
Adds a dependency matrix to the upgrade guide, detailing supported
Python, PostgreSQL, and Redis versions for each NetBox release. This
helps users verify compatibility before upgrading.

Fixes #18733
2025-04-01 07:23:42 +02:00
github-actions
bbd5e9cab9 Update source translation strings 2025-04-01 05:02:06 +00:00
bctiemann
12231ad71a Merge pull request #18997 from antoinekh/18964_fix_bulk_edit_all
Fixes #18964 Select all with bulk edit only changes the currently visible objects
2025-03-31 18:36:04 -04:00
Jeremy Stretch
88ef9ecfa3 Fixes #19023: get_field_value() should respect null values in bound forms (#19024) 2025-03-31 16:34:46 -05:00
github-actions
6f78b3d0cd Update source translation strings 2025-03-29 05:02:03 +00:00
Jason Novinger
d3f42deb32 Fixes #18895: Allows VirtualCircuitTerminations as Interface connected_endpoints (#19027) 2025-03-28 08:58:09 -07:00
Jason Novinger
db4fb8f406 Fixes #18999: Allow GraphQL to represent inventory items with no set manufacturar (#19016) 2025-03-28 07:57:56 -07:00
ZPrimed
5b8eaced1a Update choices.py - add MoCA (Coaxial) 2025-03-28 08:45:04 -04:00
github-actions
ada0c7f687 Update source translation strings 2025-03-28 05:02:14 +00:00
Jeremy Stretch
b750d0dff2 Fixes #19021: Ensure consistent styling for JSON form fields (#19022) 2025-03-27 08:58:37 -05:00
Jason Novinger
e1e514251e Fixes #18965: Ensure script list run buttons respect scripts' commit_default option (#19013)
* Fixes #18965: Script list run buttons respect scripts' commit_default

* Cleanup script .Meta access in template
2025-03-27 08:39:50 -04:00
Renato Almeida de Oliveira
7d80a45bf8 Fixes: #16144 GetReturnURLMixin Support for Plugin Views (#18996)
* Add plugin support to GetReturnURLMixin

* use get_viewname instead of resolving the name
2025-03-27 08:33:09 -04:00
github-actions
09854a3d54 Update source translation strings 2025-03-27 05:02:15 +00:00
Jason Novinger
39a96ddf3a Fixes #18738: Ensure ScriptList respects script_order option 2025-03-26 15:35:06 -05:00
atownson
be26f86b62 Added advanced object selector to custom field object and multi-object inputs (#18830) 2025-03-26 10:42:45 -07:00
Jason Novinger
fd2bcda8b8 Fixes #18991: AttributeError: NoneType object has not attribute model (#19006) 2025-03-26 10:12:45 -07:00
github-actions
817d7efee3 Update source translation strings 2025-03-26 05:03:20 +00:00
Renato Almeida de Oliveira
9a1d9365cd Fixes: #18783 Add a tag_id filter for all models which support tagging (#18889) 2025-03-25 08:06:22 -07:00
Antoine Keranflec'h
ada4a4b93c fix #18964 reinsert else condition 2025-03-25 08:22:59 +00:00
github-actions
64a98fd87f Update source translation strings 2025-03-25 05:02:04 +00:00
Arthur Hanson
bd8e00a935 18904 add tags to config context table (#18938)
* 18904 add tags to config context table

* 18904 tag to correct table
2025-03-24 17:02:36 -04:00
Jeremy Stretch
af5a600583 Closes #18980: Optimize update of object data when adding/removing custom fields (#18983)
* Employ native PostgreSQL functions for updating object JSON data when adding/removing custom fields

* Optimize rename_object_data()

* remove_stale_data() should validate model class
2025-03-24 12:02:54 -05:00
github-actions
8ab73501d1 Update source translation strings 2025-03-22 05:02:10 +00:00
Renato Almeida de Oliveira
447e108d97 Fixes: #18656 Unable to import IP Address and assign to FHRP Group (#18950)
* Add fhrpgroup to IPAddressImportForm

* Change fhrpgroup accessor to name

* rename fhrpgroup to fhrp_group

* Add fhrp_group to  IPAddressTestCase csv_data
2025-03-21 16:44:10 -05:00
bctiemann
e186113cb3 Merge pull request #18986 from netbox-community/release-v4.2.6
Release v4.2.6
2025-03-21 17:17:50 -04:00
Jason Novinger
40452ead62 Cleanup release notes 2025-03-21 16:03:34 -05:00
Jason Novinger
34d80beaa2 Release v4.2.6 2025-03-21 15:49:20 -05:00
Yi
b1d014b520 Fixes #18949: Add missing GraphQL ContactsMixin in types with ContactAssignments 2025-03-21 13:00:50 -05:00
github-actions
7db0765ed2 Update source translation strings 2025-03-21 05:02:02 +00:00
Jason Novinger
b8cc2d7116 Fixes #18887: Allows VMInterface object custom field on Prefix (#18945) 2025-03-20 13:55:12 -07:00
Renato Almeida de Oliveira
d332a0c0d7 Fix sitegroup typo, VLANGroupFilterForm field didn't match between form and filterset (#18947) 2025-03-20 11:30:39 -05:00
Jason Novinger
f07e2dd4e2 Fixes #18944: Clearing widget type field no longer causes 500 error (#18946)
* Fixes #18944: Clearing widget type field no longer causes 500 error

* Address PR feedback, cleanup implementation
2025-03-20 09:12:05 -04:00
Renato Almeida de Oliveira
d7b9b09d56 Fixes: #18939 Allow ASN search by site-group (#18948)
* Add site_group and site_group_id to ASNFilterSet and ASNFilterForm

* Implement ASNTestCase site_group test case
2025-03-20 09:07:48 -04:00
github-actions
9da4cf31ab Update source translation strings 2025-03-20 05:02:02 +00:00
Renato Almeida de Oliveira
bf1a9a6e2d Fixes: #18833 Inventory Item Bulk Import - 'InventoryItemImportForm' has no field named 'component_id'. (#18874)
* Refactor InventoryItemImportForm clean method

* Add super().clean(); renamed content_type; simplified component creation

* Fix missing component_name issue

* Update netbox/dcim/forms/bulk_import.py

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-03-19 09:56:42 -04:00
bctiemann
c50b1c989d Merge pull request #18929 from netbox-community/18928-housekeeping-changelog-cleanup
Fixes #18928: Fix support for cascading deletions when cleaning up expired changelog records
2025-03-19 09:51:11 -04:00
bctiemann
79b0c0f5d6 Merge pull request #18901 from netbox-community/18872-JournalEntry-kind-required
Fixes #18872: JournalEntry `kind` is a required field
2025-03-19 09:40:55 -04:00
github-actions
43840e6a72 Update source translation strings 2025-03-19 05:02:13 +00:00
bctiemann
7c152e9234 Merge pull request #18854 from netbox-community/18782-dashboard-broken-on-notification-list-widget
Fixes #18782: Misconfigured `ObjectListWidget`s now degrade gracefully
2025-03-18 14:45:23 -04:00
Jason Novinger
d7709a2a55 Fixes #18926: Uses correct icon for base GitHub auth 2025-03-18 08:48:27 -04:00
github-actions
dce694afa9 Update source translation strings 2025-03-18 05:02:07 +00:00
Jeremy Stretch
c5801f9881 Fixes #18928: Fix support for cascading deletions when cleaning up expired changelog records 2025-03-17 14:16:55 -04:00
Jeremy Stretch
f86647dc28 Fixes #18884: Handle tag assignments manually within deserialize_object (#18908) 2025-03-17 08:43:08 -07:00
Benjamin Dale
0094703609 Closes #18816:Add FC/UPC, FC/APC and FC/PC port types (#18913)
* Closes #18816:Add FC/UPC, FC/APC and FC/PC port types

* Remove release note

---------

Co-authored-by: Ben Dale <7621213-ben_dale@users.noreply.gitlab.com>
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-03-17 10:39:28 -04:00
github-actions
f286449284 Update source translation strings 2025-03-14 05:02:07 +00:00
bctiemann
4f45328c77 Fixes: #18863 - Exempt MPTT-based models from centrally applying ordering on querysets (#18867) 2025-03-13 13:38:33 -07:00
Jeremy Stretch
994e7eb9f4 Fixes #18872: JournalEntry kind is a required field 2025-03-13 16:23:53 -04:00
Jeremy Stretch
ed135102be Fixes #18880: Delay enqueuing of background tasks until the DB transaction has been committed (#18899) 2025-03-13 11:34:12 -05:00
Jason Novinger
78332d44c7 Fixes #18845: restores sort behavior for DeviceTable.name column (#18861)
* Fixes #18845: restores sort behavior for DeviceTable.name column

* Remove accessor/order_by and modify DEVICE_LINK template

Thanks to @alehaa for the suggestion.

This also includes an additional `.select_related()` operation on
`DeviceListView.queryset` to avoid extra queries. Thanks to
@renatoalmeidaoliveira and @jeremystretch for pointing out the need for
this.
2025-03-13 09:22:49 -04:00
github-actions
80926cda8f Update source translation strings 2025-03-13 05:02:07 +00:00
bctiemann
d924d4eb33 Merge pull request #18886 from ITJamie/add-rackname-to-device-rack-view
Add rack title above rack view on device page
2025-03-12 19:11:22 -04:00
bctiemann
b1e7d7c76b Merge pull request #18826 from Tishka17/fix/generic_prefetch_4.2
Prefetch interface data for REST API on netbox 4.2
2025-03-12 18:55:58 -04:00
Jeremy Stretch
092f7549ca Fixes #18869: Ensure to_meters() always returns a clean decimal value (#18883)
* Fixes #18869: Ensure to_meters() always returns a clean decimal value

* Handle float values
2025-03-12 13:42:38 -05:00
Andrey Tikhonov
2f51dfc07a Merge branch 'fix/generic_prefetch_4.2' of github.com:Tishka17/netbox into fix/generic_prefetch_4.2 2025-03-12 19:13:34 +01:00
Andrey Tikhonov
906654d807 Fix empty line, comments 2025-03-12 19:09:46 +01:00
Renato Almeida de Oliveira
749a83d742 Change JSONField, prepare_value to output str with enclosing " (#18846) 2025-03-12 10:00:40 -07:00
Andrey Tikhonov
cdd25368e7 Merge branch 'main' into fix/generic_prefetch_4.2 2025-03-12 16:27:12 +01:00
Andrey Tikhonov
7d64d3b5ed Move GenericArrayForeignKey to fields module 2025-03-12 16:22:11 +01:00
github-actions
5e22ef59c5 Update source translation strings 2025-03-12 05:02:02 +00:00
Fabi
19d1282683 Fixes #18838: Correctly reject invalid falsy local context data (#18860)
* Correctly reject invalid falsy local context data.

* move tests
2025-03-11 14:18:42 -05:00
bctiemann
2266a8af67 Merge pull request #18804 from renatoalmeidaoliveira/18742-Location-List-and-Locations-not-Showing-Associated-VLAN-Groups
Fixes: #18742 Location List and Locations not Showing Associated VLAN Groups
2025-03-11 11:13:24 -04:00
Renato Almeida de Oliveira
5d81f911d6 Fixes: #18582 Bulk import prefixes with associated VLAN not working when multiple VLANs with the same vid exist. (#18844)
* Add site CSVModelChoiceField

* Change site field to vlan_site
2025-03-11 08:46:46 -04:00
github-actions
89e3f3d3e9 Update source translation strings 2025-03-11 05:02:10 +00:00
Jeremy Stretch
292463c0de Closes #18857: Fix deprecation warnings (#18858) 2025-03-10 14:40:08 -07:00
Antoine Keranflec'h
a9fd191086 Fixes: #18744 Add Contact link as hyperlink (#18847)
* contact link as hyperlink

* fix(lint) add missing blank lines

* simplify by using lambda function
2025-03-10 12:49:24 -05:00
Tobias Genannt
1a60cb9884 Fixes: #18568 Update mkdocstrings and adapt config (#18841)
* Fixes: #18568 Update mkdocstrings and adapt config

* Fixed some output formatting errors

When trying to compare the output from "mkdocstrings[python-legacy]==0.27.0" and
"mkdocstrings[python]==0.28.2" I encountered some HTML errors:
- <div> inside a <p>
- unescaped non-tags <pk>
2025-03-10 13:11:35 -04:00
Jason Novinger
76c3c613a9 Adds validation for ObjectListWidget.ConfigForm.model field 2025-03-10 09:57:45 -05:00
Jason Novinger
528248b560 Fixes #18782: properly check if htmx_url is None
If this is done incorrently, then the string formatting operation turns
`htmx_url` into a string and the test in the template fails.
2025-03-10 09:52:59 -05:00
github-actions
8823b07745 Update source translation strings 2025-03-08 05:02:21 +00:00
Mika Busch
29c25e39fc 17686 config option for disk divider (#18011) 2025-03-07 10:47:27 -08:00
Andrey Tikhonov
d103e13732 Removed automatic cache invalidation 2025-03-07 17:29:04 +01:00
github-actions
6d69c76b83 Update source translation strings 2025-03-07 05:02:48 +00:00
bctiemann
f9c8d12a51 Add docstrings for get_queryset base class methods (#18832) 2025-03-06 11:11:55 -06:00
Renato Almeida de Oliveira
3ef7ab4416 Fixes: #18579 Add contact filters to services (#18818)
* Add Contact filter to Services

* Add ContactModelFilterForm to ProviderAccountFilterForm

* Add Contact filter support for Aggregate

* Add Contact filter support for Prefix

* Add Contact filter to IPRange

* Add Contact filter to IPAddress

* Add Contact filter to L2VPN

* Add Contact filter to TunnelGroup

* Add Contact filter to Tunnel

* Add ContactModelFilterSet to ProviderAccountFilterSet

* Fixes classes inheritance order

Setup NetBoxModelFilterSetForm as the last inherited class

Co-authored-by: Jason Novinger <jnovinger@gmail.com>

---------

Co-authored-by: Jason Novinger <jnovinger@gmail.com>
2025-03-06 09:41:41 -06:00
Andrey Tikhonov
741645c9f7 Merge branch 'develop' into fix/generic_prefetch_4.2 2025-03-06 16:05:25 +01:00
Andrey Tikhonov
d226af420b Merge branch 'develop' into fix/generic_prefetch_4.2 2025-03-06 16:02:32 +01:00
Andrey Tikhonov
5c88317745 cleanup unused imports and extra line 2025-03-06 16:01:59 +01:00
Renato Almeida de Oliveira Zaroubin
d83c2f45bc Add vlangroup into LocationTable, LocationListView queryset and related_models in LocationView 2025-03-04 18:34:23 +00:00
Andrey Tikhonov
83ca0ef955 Add missed init 2025-02-24 15:43:07 +01:00
Andrey Tikhonov
9c3e7f2c5d Cleanup 2025-02-24 15:38:19 +01:00
Andrey Tikhonov
7794c6cfcb Fix link peers 2025-02-24 15:28:45 +01:00
Andrey Tikhonov
8dc1d68aee port generic field to 4.2 2025-02-24 15:20:18 +01:00
Jamie Murphy
d9066d6cff add device rack title 2025-02-19 22:02:59 +00:00
128 changed files with 19157 additions and 17131 deletions

View File

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

View File

@@ -12,6 +12,7 @@ permissions:
jobs:
stale:
if: github.repository == 'netbox-community/netbox'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9

View File

@@ -13,6 +13,7 @@ permissions:
jobs:
stale:
if: github.repository == 'netbox-community/netbox'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9

View File

@@ -13,6 +13,7 @@ permissions:
jobs:
lock:
if: github.repository == 'netbox-community/netbox'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5

View File

@@ -13,6 +13,7 @@ env:
jobs:
makemessages:
if: github.repository == 'netbox-community/netbox'
runs-on: ubuntu-latest
env:
NETBOX_CONFIGURATION: netbox.configuration_testing

View File

@@ -8,7 +8,10 @@ django-cors-headers
# Runtime UI tool for debugging Django
# https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst
django-debug-toolbar
# See: https://django-debug-toolbar.readthedocs.io/en/latest/changes.html#id1
# "Wrap SHOW_TOOLBAR_CALLBACK function with sync_to_async or async_to_sync to allow sync/async
# compatibility." breaks stawberry-graphql-django at version 0.52.0 (current)
django-debug-toolbar==5.0.1
# Library for writing reusable URL query filters
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
@@ -88,8 +91,7 @@ mkdocs-material
# Introspection for embedded code
# https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md
# See #18568
mkdocstrings[python-legacy]==0.27.0
mkdocstrings[python]
# Library for manipulating IP prefixes and addresses
# https://github.com/netaddr/netaddr/blob/master/CHANGELOG.rst

View File

@@ -427,6 +427,7 @@
"e3",
"xdsl",
"docsis",
"moca",
"bpon",
"epon",
"10g-epon",
@@ -500,6 +501,9 @@
"n",
"mrj21",
"fc",
"fc-pc",
"fc-upc",
"fc-apc",
"lc",
"lc-pc",
"lc-upc",
@@ -565,6 +569,9 @@
"n",
"mrj21",
"fc",
"fc-pc",
"fc-upc",
"fc-apc",
"lc",
"lc-pc",
"lc-upc",

View File

@@ -54,6 +54,7 @@ Icons](https://github.com/google/material-design-icons) icon's name; or be
`None` for no icon.
For instance, the OIDC backend may be customized with
```python
SOCIAL_AUTH_BACKEND_ATTRS = {
'oidc': ("My awesome SSO", "login"),

View File

@@ -233,3 +233,15 @@ This parameter controls how frequently a failed job is retried, up to the maximu
Default: `0` (retries disabled)
The maximum number of times a background task will be retried before being marked as failed.
## DISK_BASE_UNIT
Default: `1000`
The base unit for disk sizes. Set this to `1024` to use binary prefixes (MiB, GiB, etc.) instead of decimal prefixes (MB, GB, etc.).
## RAM_BASE_UNIT
Default: `1000`
The base unit for RAM sizes. Set this to `1024` to use binary prefixes (MiB, GiB, etc.) instead of decimal prefixes (MB, GB, etc.).

View File

@@ -25,6 +25,7 @@ Height: {{ rack.u_height }}U
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example:
```
{% for server in queryset %}
{% set data = server.get_config_context() %}

View File

@@ -1,12 +1,12 @@
# Release Checklist
This documentation describes the process of packaging and publishing a new NetBox release. There are three types of release:
This documentation describes the process of packaging and publishing a new NetBox release. There are three types of releases:
* Major release (e.g. v3.7.8 to v4.0.0)
* Minor release (e.g. v4.0.10 to v4.1.0)
* Patch release (e.g. v4.1.0 to v4.1.1)
While major releases generally introduce some very substantial change to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
While major releases generally introduce some very substantial changes to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
For patch releases (e.g. upgrading from v4.2.2 to v4.2.3), begin at the [patch releases](#patch-releases) heading below. For minor or major releases, complete the entire checklist.
@@ -31,6 +31,29 @@ Close the [release milestone](https://github.com/netbox-community/netbox/milesto
Check that a link to the release notes for the new version is present in the navigation menu (defined in `mkdocs.yml`), and that a summary of all major new features has been added to `docs/index.md`.
### Update the Dependency Requirements Matrix
For every minor release, update the dependency requirements matrix in `docs/installation/upgrading.md` ("All versions") to reflect the supported versions of Python, PostgreSQL, and Redis:
1. Add a new row with the supported dependency versions.
2. Include a documentation link using the release tag format: `https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md`
3. Bold any version changes for clarity.
**Example Update:**
```markdown
| NetBox Version | Python min | Python max | PostgreSQL min | Redis min | Documentation |
|:--------------:|:----------:|:----------:|:--------------:|:---------:|:-------------------------------------------------------------------------------------------------:|
| 4.2 | 3.10 | 3.12 | **13** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md) |
```
### Update System Requirements
If a new Django release is adopted or other major dependencies (Python, PostgreSQL, Redis) change:
* Update the installation guide (`docs/installation/index.md`) with the new minimum versions.
* Update the upgrade guide (`docs/installation/upgrading.md`) for the current version accordingly.
### Manually Perform a New Install
Start the documentation server and navigate to the current version of the installation docs:
@@ -39,15 +62,25 @@ Start the documentation server and navigate to the current version of the instal
mkdocs serve
```
Follow these instructions to perform a new installation of NetBox in a temporary environment. This process must not be automated: The goal of this step is to catch any errors or omissions in the documentation, and ensure that it is kept up-to-date for each release. Make any necessary changes to the documentation before proceeding with the release.
Follow these instructions to perform a new installation of NetBox in a temporary environment. This process must not be automated: The goal of this step is to catch any errors or omissions in the documentation and ensure that it is kept up to date for each release. Make any necessary changes to the documentation before proceeding with the release.
### Test Upgrade Paths
Upgrading from a previous version typically involves database migrations, which must work without errors. Supported upgrade paths include from one minor version to another within the same major version (i.e. 4.0 to 4.1), as well as from the latest patch version of the previous minor version (i.e. 3.7 to 4.0 or to 4.1). Prior to release, test all these supported paths by loading demo data from the source version and performing a `./manage.py migrate`.
Upgrading from a previous version typically involves database migrations, which must work without errors.
Test the following supported upgrade paths:
- From one minor version to another within the same major version (e.g. 4.0 to 4.1).
- From the latest patch version of the previous minor version (e.g. 3.7 to 4.0 or 4.1).
Prior to release, test all these supported paths by loading demo data from the source version and performing:
```no-highlight
./manage.py migrate
```
### Merge the `feature` Branch
Submit a pull request to merge the `feature` branch into the `main` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
Submit a pull request to merge the `feature` branch into the `main` branch in preparation for its release. Once it has been merged, continue with the section for the patch releases below.
### Rebuild Demo Data (After Release)
@@ -59,7 +92,7 @@ After the release of a new minor version, generate a new demo data snapshot comp
### Create a Release Branch
Begin by creating a new branch (based off of `main`) to effect the release. This will comprise the changes listed below.
Begin by creating a new branch (based on `main`) to effect the release. This will comprise the changes listed below.
```
git checkout main
@@ -136,7 +169,7 @@ Then, compile these portable (`.po`) files for use in the application:
* Add a section for this release at the top of the changelog page for the minor version (e.g. `docs/release-notes/version-4.2.md`) listing all relevant changes made in this release.
!!! tip
Put yourself in the shoes of the user when recording change notes. Focus on the effect that each change has for the end user, rather than the specific bits of code that were modified in a PR. Ensure that each message conveys meaning absent context of the initial feature request or bug report. Remember to include key words or phrases (such as exception names) that can be easily searched.
Put yourself in the shoes of the user when recording change notes. Focus on the effect that each change has for the end user, rather than the specific bits of code that were modified in a PR. Ensure that each message conveys meaning absent context of the initial feature request or bug report. Remember to include keywords or phrases (such as exception names) that can be easily searched.
### Submit a Pull Request

View File

@@ -17,11 +17,52 @@ Prior to upgrading your NetBox instance, be sure to carefully review all [releas
NetBox requires the following dependencies:
| Dependency | Supported Versions |
|------------|--------------------|
| Python | 3.10, 3.11, 3.12 |
| PostgreSQL | 13+ |
| Redis | 4.0+ |
=== "Current Version"
| Dependency | Supported Versions |
|------------|--------------------|
| Python | 3.10, 3.11, 3.12 |
| PostgreSQL | 13+ |
| Redis | 4.0+ |
=== "All Versions"
| NetBox Version | Python min | Python max | PostgreSQL min | Redis min | Documentation |
|:--------------:|:----------:|:----------:|:--------------:|:---------:|:-------------------------------------------------------------------------------------------------:|
| 4.2 | 3.10 | 3.12 | **13** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md) |
| 4.1 | 3.10 | 3.12 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.1.0/docs/installation/index.md) |
| 4.0 | **3.10** | **3.12** | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.0.0/docs/installation/index.md) |
| 3.7 | 3.8 | 3.11 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.7.0/docs/installation/index.md) |
| 3.6 | 3.8 | **3.11** | **12** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.6.0/docs/installation/index.md) |
| 3.5 | 3.8 | 3.10 | 11 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.5.0/docs/installation/index.md) |
| 3.4 | 3.8 | 3.10 | **11** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.4.0/docs/installation/index.md) |
| 3.3 | 3.8 | 3.10 | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.3.0/docs/installation/index.md) |
| 3.2 | **3.8** | **3.10** | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.2.0/docs/installation/index.md) |
| 3.1 | 3.7 | 3.9 | **10** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.1.0/docs/installation/index.md) |
| 3.0 | **3.7** | 3.9 | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.0.0/docs/installation/index.md) |
| 2.11 | 3.6 | **3.9** | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.11.0/docs/installation/index.md) |
| 2.10 | 3.6 | 3.8 | **9.6** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.10.0/docs/installation/index.md) |
| 2.9 | 3.6 | 3.8 | 9.5 | **4.0** | [Link](https://github.com/netbox-community/netbox/blob/v2.9.0/docs/installation/index.md) |
| 2.8 | **3.6** | **3.8** | **9.5** | **3.4** | [Link](https://github.com/netbox-community/netbox/blob/v2.8.0/docs/installation/index.md) |
| 2.7 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.7.0/docs/installation/index.md) |
| 2.6 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.6.0/docs/installation/index.md) |
| 2.5 | **3.5** | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.5.0/docs/installation/index.md) |
| 2.4 | **3.4** | **3.7** | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.4.0/docs/installation/index.md) |
| 2.3 | 2.7 | 3.6 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.3.0/docs/installation/postgresql.md) |
| 2.2 | 2.7 | 3.6 | **9.4** | - | [Link](https://github.com/netbox-community/netbox/blob/v2.2.0/docs/installation/postgresql.md) |
| 2.1 | 2.7 | 3.6 | 9.3 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.1.0/docs/installation/postgresql.md) |
| 2.0 | 2.7 | **3.6** | **9.3** | - | [Link](https://github.com/netbox-community/netbox/blob/v2.0.0/docs/installation/postgresql.md) |
| 1.9 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.9.0-r1/docs/installation/postgresql.md) |
| 1.8 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.8.0/docs/installation/postgresql.md) |
| 1.7 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.7.0/docs/installation/postgresql.md) |
| 1.6 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.6.0/docs/installation/postgresql.md) |
| 1.5 | 2.7 | 3.5 | **9.2** | - | [Link](https://github.com/netbox-community/netbox/blob/v1.5.0/docs/installation/postgresql.md) |
| 1.4 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.4.0/docs/installation/postgresql.md) |
| 1.3 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.3.0/docs/installation/postgresql.md) |
| 1.2 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.2.0/docs/installation/postgresql.md) |
| 1.1 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.1.0/docs/getting-started.md) |
| 1.0 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/1.0.0/docs/getting-started.md) |
## 3. Install the Latest Release

View File

@@ -60,6 +60,7 @@ query {
}
}
```
In addition, filtering can be done on list of related objects as shown in the following query:
```
@@ -98,8 +99,8 @@ Certain queries can return multiple types of objects, for example cable terminat
}
}
}
```
The field "class_type" is an easy way to distinguish what type of object it is when viewing the returned data, or when filtering. It contains the class name, for example "CircuitTermination" or "ConsoleServerPort".
## Authentication

View File

@@ -204,6 +204,7 @@ To ease development, it is recommended to go ahead and install the plugin at thi
```no-highlight
$ pip install -e .
```
More information on editable builds can be found at [Editable installs for pyproject.toml ](https://peps.python.org/pep-0660/).
## Configure NetBox

View File

@@ -150,5 +150,5 @@ The [NAPALM automation](https://github.com/napalm-automation/napalm) library pro
* Modified the interface serializer to include three discrete fields relating to connections: `is_connected` (boolean), `interface_connection`, and `circuit_termination`
* Added two new fields to the inventory item serializer: `asset_tag` and `description`
* Added "wireless" to interface type filter (in addition to physical, virtual, and LAG)
* Added a new endpoint at /api/ipam/prefixes/<pk>/available-ips/ to retrieve or create available IPs within a prefix
* Added a new endpoint at /api/ipam/prefixes/<pk\>/available-ips/ to retrieve or create available IPs within a prefix
* Extended `parent_device` on DeviceSerializer to include the `url` and `display_name` of the parent Device, and the `url` of the DeviceBay

View File

@@ -1,5 +1,69 @@
# NetBox v4.2
## v4.2.7 (2025-04-10)
### Enhancements
* [#16144](https://github.com/netbox-community/netbox/issues/16144) - Add support for plugin models to GetReturnURLMixin
* [#18138](https://github.com/netbox-community/netbox/issues/18138) - Enable filtering of ObjectVar and MultiObjectVar input selections for custom fields
* [#18656](https://github.com/netbox-community/netbox/issues/18656) - Enable FHRP group assignment when bulk importing IP addresses
* [#18980](https://github.com/netbox-community/netbox/issues/18980) - Optimize bulk updates of custom field values when custom fields are added/removed
* [#19018](https://github.com/netbox-community/netbox/issues/19018) - Add MoCA interface type
### Bug Fixes
* [#18553](https://github.com/netbox-community/netbox/issues/18553) - Avoid clearing site of assigned virtual machines when editing a cluster
* [#18738](https://github.com/netbox-community/netbox/issues/18738) - Respect declared ordering of custom scripts within a module
* [#18895](https://github.com/netbox-community/netbox/issues/18895) - Fix GraphQL support for interfaces which terminate virtual circuits
* [#18904](https://github.com/netbox-community/netbox/issues/18904) - Add missing tags column to config contexts table
* [#18964](https://github.com/netbox-community/netbox/issues/18964) - Fix "select all" behavior on object lists
* [#18965](https://github.com/netbox-community/netbox/issues/18965) - "Run script" button should respect default commit toggle for custom scripts
* [#18991](https://github.com/netbox-community/netbox/issues/18991) - Fix cable path tracing for pass-through ports in REST API
* [#18999](https://github.com/netbox-community/netbox/issues/18999) - Fix filtering of inventory items with no manufacturer in GraphQL API
* [#19021](https://github.com/netbox-community/netbox/issues/19021) - Preserve JSONField stylign when `help_text` is passed
* [#19023](https://github.com/netbox-community/netbox/issues/19023) - `get_field_value()` should honor null values on bound form fields
* [#19030](https://github.com/netbox-community/netbox/issues/19030) - Prevent pagination buttons from overlapping bulk action buttons on object lists
* [#19041](https://github.com/netbox-community/netbox/issues/19041) - Fix `IndexError` exception when creating multiple front ports with a label
* [#19092](https://github.com/netbox-community/netbox/issues/19092) - Fix clearing of scope field when bulk editing prefixes
* [#19122](https://github.com/netbox-community/netbox/issues/19122) - Fix styling of server error page
---
## v4.2.6 (2025-03-21)
### Enhancements
* [#17503](https://github.com/netbox-community/netbox/issues/17503) - Add rack title above rack on rack detail view
* [#17686](https://github.com/netbox-community/netbox/issues/17686) - Add config option for disk space divisor
* [#18579](https://github.com/netbox-community/netbox/issues/18579) - Update filtersets and filter forms to include contact filters where missing
* [#18744](https://github.com/netbox-community/netbox/issues/18744) - Ensure contact link in tables is hyperlinked
* [#18816](https://github.com/netbox-community/netbox/issues/18816) - Add FC/UPC, FC/APC and FC/PC port types
* [#18880](https://github.com/netbox-community/netbox/issues/18880) - Delay enqueuing background tasks until DB transaction is committed to avoid race condition
* [#18939](https://github.com/netbox-community/netbox/issues/18939) - Support site group search for ASNs
### Bug Fixes
* [#18409](https://github.com/netbox-community/netbox/issues/18409) - Eliminate N+1 issue by adding generic prefetch operation to Interface API endpoint
* [#18557](https://github.com/netbox-community/netbox/issues/18557) - Update JSONField to enclose bare string values in quotes
* [#18582](https://github.com/netbox-community/netbox/issues/18582) - Fix prefix bulk import with associated VLAN and conflicting VLAN IDs
* [#18742](https://github.com/netbox-community/netbox/issues/18742) - Ensure location list and detail views show related VLAN group information
* [#18782](https://github.com/netbox-community/netbox/issues/18782) - Ensure misconfigured object list widgets on the dashboard now degrade gracefully
* [#18833](https://github.com/netbox-community/netbox/issues/18833) - Fix inventory item bulk edit to ensure that component name and type are both validated Ensure
* [#18838](https://github.com/netbox-community/netbox/issues/18838) - Ensure that local context data correctly rejects falsy values
* [#18845](https://github.com/netbox-community/netbox/issues/18845) - Restore default sort behavior of name column on devices list view
* [#18863](https://github.com/netbox-community/netbox/issues/18863) - Exempt MPTT-based models from ordering fix introduced in #18279
* [#18869](https://github.com/netbox-community/netbox/issues/18869) - Ensure numeric conversion helper always return a clean decimal value
* [#18872](https://github.com/netbox-community/netbox/issues/18872) - Ensure that `kind` is a required field when making journal entries
* [#18884](https://github.com/netbox-community/netbox/issues/18884) - Ensure tag deserialization is handled correctly
* [#18887](https://github.com/netbox-community/netbox/issues/18887) - Allow VM interface objects to be set on prefix object-type custom field
* [#18926](https://github.com/netbox-community/netbox/issues/18926) - Fix icon displayed for GitHub authentication on login page
* [#18928](https://github.com/netbox-community/netbox/issues/18928) - Support cascading deletions when cleaning up expired changelog records
* [#18933](https://github.com/netbox-community/netbox/issues/18933) - Allow filtering VLAN groups by associated site groups
* [#18944](https://github.com/netbox-community/netbox/issues/18944) - Ensure clearing "Widget type" field when adding widgets to dashboard does not cause a "ValueError: Unregistered widget class" error
* [#18949](https://github.com/netbox-community/netbox/issues/18949) - Add missing contacts property to GraphQL types where the associated model has a connection to a contact
---
## v4.2.5 (2025-03-06)
### Enhancements

View File

@@ -28,12 +28,7 @@ plugins:
- mkdocstrings:
handlers:
python:
setup_commands:
- import os
- import django
- os.chdir('netbox/')
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
- django.setup()
paths: ["netbox"]
options:
heading_level: 3
members_order: source

View File

@@ -95,7 +95,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
)
class ProviderAccountFilterSet(NetBoxModelFilterSet):
class ProviderAccountFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
label=_('Provider (ID)'),

View File

@@ -66,11 +66,12 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model)
class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
class ProviderAccountFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = ProviderAccount
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('provider_id', 'account', name=_('Attributes')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),

View File

@@ -43,7 +43,7 @@ class ProviderType(NetBoxObjectType, ContactsMixin):
fields='__all__',
filters=ProviderAccountFilter
)
class ProviderAccountType(NetBoxObjectType):
class ProviderAccountType(ContactsMixin, NetBoxObjectType):
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]

View File

@@ -111,7 +111,7 @@ class CircuitTerminationTable(NetBoxTable):
provider = tables.Column(
verbose_name=_('Provider'),
linkify=True,
accessor='circuit.provider'
accessor='circuit__provider'
)
term_side = tables.Column(
verbose_name=_('Side')

View File

@@ -3,7 +3,10 @@ from django.conf import settings
from django.core.cache import cache
from django.db import models
from django.db.migrations.operations import AlterModelOptions
from django.utils.translation import gettext as _
from core.events import *
from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING
from utilities.migration import custom_deconstruct
# Ignore verbose_name & verbose_name_plural Meta options when calculating model migrations
@@ -26,6 +29,15 @@ class CoreConfig(AppConfig):
# Register models
register_models(*self.get_models())
# Register core events
EventType(OBJECT_CREATED, _('Object created')).register()
EventType(OBJECT_UPDATED, _('Object updated')).register()
EventType(OBJECT_DELETED, _('Object deleted'), destructive=True).register()
EventType(JOB_STARTED, _('Job started')).register()
EventType(JOB_COMPLETED, _('Job completed'), kind=EVENT_TYPE_KIND_SUCCESS).register()
EventType(JOB_FAILED, _('Job failed'), kind=EVENT_TYPE_KIND_WARNING).register()
EventType(JOB_ERRORED, _('Job errored'), kind=EVENT_TYPE_KIND_DANGER).register()
# Clear Redis cache on startup in development mode
if settings.DEBUG:
try:

View File

@@ -1,7 +1,3 @@
from django.utils.translation import gettext as _
from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING
__all__ = (
'JOB_COMPLETED',
'JOB_ERRORED',
@@ -22,12 +18,3 @@ JOB_STARTED = 'job_started'
JOB_COMPLETED = 'job_completed'
JOB_FAILED = 'job_failed'
JOB_ERRORED = 'job_errored'
# Register core events
EventType(OBJECT_CREATED, _('Object created')).register()
EventType(OBJECT_UPDATED, _('Object updated')).register()
EventType(OBJECT_DELETED, _('Object deleted'), destructive=True).register()
EventType(JOB_STARTED, _('Job started')).register()
EventType(JOB_COMPLETED, _('Job completed'), kind=EVENT_TYPE_KIND_SUCCESS).register()
EventType(JOB_FAILED, _('Job failed'), kind=EVENT_TYPE_KIND_WARNING).register()
EventType(JOB_ERRORED, _('Job errored'), kind=EVENT_TYPE_KIND_DANGER).register()

View File

@@ -1,11 +1,12 @@
import uuid
from functools import partial
import django_rq
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.db import models, transaction
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext as _
@@ -258,10 +259,12 @@ class Job(models.Model):
# Schedule the job to run at a specific date & time.
elif schedule_at:
queue.enqueue_at(schedule_at, func, job_id=str(job.job_id), job=job, **kwargs)
callback = partial(queue.enqueue_at, schedule_at, func, job_id=str(job.job_id), job=job, **kwargs)
transaction.on_commit(callback)
# Schedule the job to run asynchronously at this first available opportunity.
else:
queue.enqueue(func, job_id=str(job.job_id), job=job, **kwargs)
callback = partial(queue.enqueue, func, job_id=str(job.job_id), job=job, **kwargs)
transaction.on_commit(callback)
return job

View File

@@ -1,3 +1,4 @@
from django.contrib.contenttypes.prefetch import GenericPrefetch
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from drf_spectacular.types import OpenApiTypes
@@ -442,7 +443,18 @@ class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = Interface.objects.prefetch_related(
'_path', 'cable__terminations',
GenericPrefetch(
"cable__terminations__termination",
[
Interface.objects.select_related("device", "cable"),
],
),
GenericPrefetch(
"_path__path_objects",
[
Interface.objects.select_related("device", "cable"),
],
),
'l2vpn_terminations', # Referenced by InterfaceSerializer.l2vpn_termination
'ip_addresses', # Referenced by Interface.count_ipaddresses()
'fhrp_group_assignments', # Referenced by Interface.count_fhrp_groups()

View File

@@ -986,6 +986,7 @@ class InterfaceTypeChoices(ChoiceSet):
# Coaxial
TYPE_DOCSIS = 'docsis'
TYPE_MOCA = 'moca'
# PON
TYPE_BPON = 'bpon'
@@ -1182,6 +1183,7 @@ class InterfaceTypeChoices(ChoiceSet):
_('Coaxial'),
(
(TYPE_DOCSIS, 'DOCSIS'),
(TYPE_MOCA, 'MoCA'),
)
),
(
@@ -1345,6 +1347,9 @@ class PortTypeChoices(ChoiceSet):
TYPE_SC_UPC = 'sc-upc'
TYPE_SC_APC = 'sc-apc'
TYPE_FC = 'fc'
TYPE_FC_PC = 'fc-pc'
TYPE_FC_UPC = 'fc-upc'
TYPE_FC_APC = 'fc-apc'
TYPE_LC = 'lc'
TYPE_LC_PC = 'lc-pc'
TYPE_LC_UPC = 'lc-upc'
@@ -1405,6 +1410,9 @@ class PortTypeChoices(ChoiceSet):
_('Fiber Optic'),
(
(TYPE_FC, 'FC'),
(TYPE_FC_PC, 'FC/PC'),
(TYPE_FC_UPC, 'FC/UPC'),
(TYPE_FC_APC, 'FC/APC'),
(TYPE_LC, 'LC'),
(TYPE_LC_PC, 'LC/PC'),
(TYPE_LC_UPC, 'LC/UPC'),

View File

@@ -1161,27 +1161,45 @@ class InventoryItemImportForm(NetBoxModelImportForm):
else:
self.fields['parent'].queryset = InventoryItem.objects.none()
def clean_component_name(self):
content_type = self.cleaned_data.get('component_type')
component_name = self.cleaned_data.get('component_name')
def clean(self):
super().clean()
cleaned_data = self.cleaned_data
component_type = cleaned_data.get('component_type')
component_name = cleaned_data.get('component_name')
device = self.cleaned_data.get("device")
if not device and hasattr(self, 'instance') and hasattr(self.instance, 'device'):
device = self.instance.device
if not all([device, content_type, component_name]):
return None
model = content_type.model_class()
try:
component = model.objects.get(device=device, name=component_name)
self.instance.component = component
except ObjectDoesNotExist:
raise forms.ValidationError(
_("Component not found: {device} - {component_name}").format(
device=device, component_name=component_name
if component_type:
if device is None:
cleaned_data.pop('component_type', None)
if component_name is None:
cleaned_data.pop('component_type', None)
raise forms.ValidationError(
_("Component name must be specified when component type is specified")
)
)
if all([device, component_name]):
try:
model = component_type.model_class()
self.instance.component = model.objects.get(device=device, name=component_name)
except ObjectDoesNotExist:
cleaned_data.pop('component_type', None)
cleaned_data.pop('component_name', None)
raise forms.ValidationError(
_("Component not found: {device} - {component_name}").format(
device=device, component_name=component_name
)
)
else:
cleaned_data.pop('component_type', None)
if not component_name:
raise forms.ValidationError(
_("Component name must be specified when component type is specified")
)
else:
if component_name:
raise forms.ValidationError(
_("Component type must be specified when component name is specified")
)
return cleaned_data
#

View File

@@ -153,6 +153,7 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
self.fields['rear_port'].choices = choices
def clean(self):
super().clean()
# Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
# positions
@@ -302,6 +303,7 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
self.fields['rear_port'].choices = choices
def clean(self):
super().clean()
# Check that the number of FrontPorts to be created matches the selected number of RearPort positions
frontport_count = len(self.cleaned_data['name'])

View File

@@ -30,6 +30,7 @@ class PathEndpointMixin:
connected_endpoints: List[Annotated[Union[
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
Annotated["VirtualCircuitTerminationType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821

View File

@@ -429,7 +429,7 @@ class InterfaceTemplateType(ModularComponentTemplateType):
)
class InventoryItemType(ComponentType):
role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] | None
child_items: List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]

View File

@@ -1,5 +1,4 @@
import itertools
from collections import defaultdict
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
@@ -16,7 +15,7 @@ from dcim.utils import decompile_path_node, object_to_path_node
from netbox.models import ChangeLoggedModel, PrimaryModel
from utilities.conversion import to_meters
from utilities.exceptions import AbortRequest
from utilities.fields import ColorField
from utilities.fields import ColorField, GenericArrayForeignKey
from utilities.querysets import RestrictedQuerySet
from wireless.models import WirelessLink
from .device_components import FrontPort, RearPort, PathEndpoint
@@ -494,13 +493,16 @@ class CablePath(models.Model):
return ObjectType.objects.get_for_id(ct_id)
@property
def path_objects(self):
"""
Cache and return the complete path as lists of objects, derived from their annotation within the path.
"""
if not hasattr(self, '_path_objects'):
self._path_objects = self._get_path()
return self._path_objects
def _path_decompiled(self):
res = []
for step in self.path:
nodes = []
for node in step:
nodes.append(decompile_path_node(node))
res.append(nodes)
return res
path_objects = GenericArrayForeignKey("_path_decompiled")
@property
def origins(self):
@@ -757,42 +759,6 @@ class CablePath(models.Model):
self.delete()
retrace.alters_data = True
def _get_path(self):
"""
Return the path as a list of prefetched objects.
"""
# Compile a list of IDs to prefetch for each type of model in the path
to_prefetch = defaultdict(list)
for node in self._nodes:
ct_id, object_id = decompile_path_node(node)
to_prefetch[ct_id].append(object_id)
# Prefetch path objects using one query per model type. Prefetch related devices where appropriate.
prefetched = {}
for ct_id, object_ids in to_prefetch.items():
model_class = ObjectType.objects.get_for_id(ct_id).model_class()
queryset = model_class.objects.filter(pk__in=object_ids)
if hasattr(model_class, 'device'):
queryset = queryset.prefetch_related('device')
prefetched[ct_id] = {
obj.id: obj for obj in queryset
}
# Replicate the path using the prefetched objects.
path = []
for step in self.path:
nodes = []
for node in step:
ct_id, object_id = decompile_path_node(node)
try:
nodes.append(prefetched[ct_id][object_id])
except KeyError:
# Ignore stale (deleted) object IDs
pass
path.append(nodes)
return path
def get_cable_ids(self):
"""
Return all Cable IDs within the path.

View File

@@ -184,8 +184,11 @@ class CabledObjectModel(models.Model):
@cached_property
def link_peers(self):
if self.cable:
peers = self.cable.terminations.exclude(cable_end=self.cable_end).prefetch_related('termination')
return [peer.termination for peer in peers]
return [
peer.termination
for peer in self.cable.terminations.all()
if peer.cable_end != self.cable_end
]
return []
@property

View File

@@ -143,9 +143,8 @@ class PlatformTable(NetBoxTable):
class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
accessor=Accessor('label'),
template_code=DEVICE_LINK,
linkify=True
linkify=True,
)
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),

View File

@@ -146,6 +146,11 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
url_params={'location_id': 'pk'},
verbose_name=_('Devices')
)
vlangroup_count = columns.LinkedCountColumn(
viewname='ipam:vlangroup_list',
url_params={'location': 'pk'},
verbose_name=_('VLAN Groups')
)
tags = columns.TagColumn(
url_name='dcim:location_list'
)
@@ -157,8 +162,9 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
model = Location
fields = (
'pk', 'id', 'name', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count', 'device_count',
'description', 'slug', 'contacts', 'tags', 'actions', 'created', 'last_updated',
'description', 'slug', 'contacts', 'tags', 'actions', 'created', 'last_updated', 'vlangroup_count',
)
default_columns = (
'pk', 'name', 'site', 'status', 'facility', 'tenant', 'rack_count', 'device_count', 'description'
'pk', 'name', 'site', 'status', 'facility', 'tenant', 'rack_count', 'device_count', 'vlangroup_count',
'description'
)

View File

@@ -35,7 +35,7 @@ WEIGHT = """
"""
DEVICE_LINK = """
{{ value|default:'<span class="badge text-bg-info">Unnamed device</span>' }}
{{ record.label|default:'<span class="badge text-bg-info">Unnamed device</span>' }}
"""
DEVICEBAY_STATUS = """

View File

@@ -1,6 +1,6 @@
import json
from django.test import override_settings
from django.test import override_settings, tag
from django.urls import reverse
from django.utils.translation import gettext as _
from rest_framework import status
@@ -1904,6 +1904,27 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
},
]
@tag('regression') # Issue #18991
def test_front_port_paths(self):
device = Device.objects.first()
rear_port = RearPort.objects.create(
device=device, name='Rear Port 10', type=PortTypeChoices.TYPE_8P8C
)
interface1 = Interface.objects.create(device=device, name='Interface 1')
front_port = FrontPort.objects.create(
device=device,
name='Rear Port 10',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port,
)
Cable.objects.create(a_terminations=[interface1], b_terminations=[front_port])
self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
url = reverse(f'dcim-api:{self.model._meta.model_name}-paths', kwargs={'pk': front_port.pk})
response = self.client.get(url, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
class RearPortTest(APIViewTestCases.APIViewTestCase):
model = RearPort
@@ -1947,6 +1968,23 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
},
]
@tag('regression') # Issue #18991
def test_rear_port_paths(self):
device = Device.objects.first()
interface1 = Interface.objects.create(device=device, name='Interface 1')
rear_port = RearPort.objects.create(
device=device,
name='Rear Port 10',
type=PortTypeChoices.TYPE_8P8C,
)
Cable.objects.create(a_terminations=[interface1], b_terminations=[rear_port])
self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
url = reverse(f'dcim-api:{self.model._meta.model_name}-paths', kwargs={'pk': rear_port.pk})
response = self.client.get(url, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
class ModuleBayTest(APIViewTestCases.APIViewTestCase):
model = ModuleBay

View File

@@ -1,6 +1,12 @@
from django.test import TestCase
from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices, InterfaceModeChoices
from dcim.choices import (
DeviceFaceChoices,
DeviceStatusChoices,
InterfaceModeChoices,
InterfaceTypeChoices,
PortTypeChoices,
)
from dcim.forms import *
from dcim.models import *
from ipam.models import VLAN
@@ -118,6 +124,51 @@ class DeviceTestCase(TestCase):
self.assertIn('position', form.errors)
class FrontPortTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.device = create_test_device('Panel Device 1')
cls.rear_ports = (
RearPort(name='RearPort1', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
RearPort(name='RearPort2', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
RearPort(name='RearPort3', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
RearPort(name='RearPort4', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
)
RearPort.objects.bulk_create(cls.rear_ports)
def test_front_port_label_count_valid(self):
"""
Test that generating an equal number of names and labels passes form validation.
"""
front_port_data = {
'device': self.device.pk,
'name': 'FrontPort[1-4]',
'label': 'Port[1-4]',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
}
form = FrontPortCreateForm(front_port_data)
self.assertTrue(form.is_valid())
def test_front_port_label_count_mismatch(self):
"""
Check that attempting to generate a differing number of names and labels results in a validation error.
"""
bad_front_port_data = {
'device': self.device.pk,
'name': 'FrontPort[1-4]',
'label': 'Port[1-2]',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
}
form = FrontPortCreateForm(bad_front_port_data)
self.assertFalse(form.is_valid())
self.assertIn('label', form.errors)
class InterfaceTestCase(TestCase):
@classmethod

View File

@@ -505,18 +505,24 @@ class SiteContactsView(ObjectContactsView):
@register_model_view(Location, 'list', path='', detail=False)
class LocationListView(generic.ObjectListView):
queryset = Location.objects.add_related_count(
Location.objects.add_related_count(
Location.objects.all(),
Device,
'location',
'device_count',
cumulative=True
),
Rack,
'location',
'rack_count',
cumulative=True
)
Location.objects.add_related_count(
Location.objects.add_related_count(
Location.objects.all(),
Device,
'location',
'device_count',
cumulative=True
),
Rack,
'location',
'rack_count',
cumulative=True
),
VLANGroup,
'location',
'vlangroup_count',
cumulative=True
)
filterset = filtersets.LocationFilterSet
filterset_form = forms.LocationFilterForm
table = tables.LocationTable
@@ -528,6 +534,7 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView):
def get_extra_context(self, request, instance):
locations = instance.get_descendants(include_self=True)
location_content_type = ContentType.objects.get_for_model(instance)
return {
'related_models': self.get_related_models(
request,
@@ -545,6 +552,8 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView):
(Cluster.objects.restrict(request.user, 'view').filter(_location=instance), 'location_id'),
(Prefix.objects.restrict(request.user, 'view').filter(_location=instance), 'location_id'),
(WirelessLAN.objects.restrict(request.user, 'view').filter(_location=instance), 'location_id'),
(VLANGroup.objects.restrict(request.user, 'view').filter(
scope_type_id=location_content_type.id, scope_id=instance.id), 'location'),
),
),
}
@@ -2025,7 +2034,7 @@ class PlatformBulkDeleteView(generic.BulkDeleteView):
@register_model_view(Device, 'list', path='', detail=False)
class DeviceListView(generic.ObjectListView):
queryset = Device.objects.all()
queryset = Device.objects.select_related('virtual_chassis')
filterset = filtersets.DeviceFilterSet
filterset_form = forms.DeviceFilterForm
table = tables.DeviceTable

View File

@@ -9,6 +9,7 @@ import requests
from django import forms
from django.conf import settings
from django.core.cache import cache
from django.db.models import Model
from django.template.loader import render_to_string
from django.urls import NoReverseMatch, resolve, reverse
from django.utils.translation import gettext as _
@@ -42,6 +43,27 @@ def get_object_type_choices():
]
def object_list_widget_supports_model(model: Model) -> bool:
"""Test whether a model is supported by the ObjectListWidget
In theory there could be more than one reason why a model isn't supported by the
ObjectListWidget, although we've only identified one so far--there's no resolve-able 'list' URL
for the model. Add more tests if more conditions arise.
"""
def can_resolve_model_list_view(model: Model) -> bool:
try:
reverse(get_viewname(model, action='list'))
return True
except Exception:
return False
tests = [
can_resolve_model_list_view,
]
return all(test(model) for test in tests)
def get_bookmarks_object_type_choices():
return [
(object_type_identifier(ot), object_type_name(ot))
@@ -234,6 +256,17 @@ class ObjectListWidget(DashboardWidget):
raise forms.ValidationError(_("Invalid format. URL parameters must be passed as a dictionary."))
return data
def clean_model(self):
if model_info := self.cleaned_data['model']:
app_label, model_name = model_info.split('.')
model = ObjectType.objects.get_by_natural_key(app_label, model_name).model_class()
if not object_list_widget_supports_model(model):
raise forms.ValidationError(
_(f"Invalid model selection: {self['model'].data} is not supported.")
)
return model_info
def render(self, request):
app_label, model_name = self.config['model'].split('.')
model = ObjectType.objects.get_by_natural_key(app_label, model_name).model_class()
@@ -257,7 +290,7 @@ class ObjectListWidget(DashboardWidget):
parameters['per_page'] = page_size
parameters['embedded'] = True
if parameters:
if parameters and htmx_url is not None:
try:
htmx_url = f'{htmx_url}?{urlencode(parameters, doseq=True)}'
except ValueError:
@@ -284,7 +317,8 @@ class RSSFeedWidget(DashboardWidget):
class ConfigForm(WidgetConfigForm):
feed_url = forms.URLField(
label=_('Feed URL')
label=_('Feed URL'),
assume_scheme='https'
)
requires_internet = forms.BooleanField(
label=_('Requires external connection'),

View File

@@ -14,7 +14,7 @@ from netbox.events import get_event_type_choices
from netbox.forms import NetBoxModelForm
from tenancy.models import Tenant, TenantGroup
from users.models import Group, User
from utilities.forms import add_blank_choice, get_field_value
from utilities.forms import get_field_value
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, JSONField, SlugField,
@@ -687,8 +687,7 @@ class ImageAttachmentForm(forms.ModelForm):
class JournalEntryForm(NetBoxModelForm):
kind = forms.ChoiceField(
label=_('Kind'),
choices=add_blank_choice(JournalEntryKindChoices),
required=False
choices=JournalEntryKindChoices
)
comments = CommentField()

View File

@@ -5,7 +5,6 @@ import requests
from django.conf import settings
from django.core.cache import cache
from django.core.management.base import BaseCommand
from django.db import DEFAULT_DB_ALIAS
from django.utils import timezone
from packaging import version
@@ -53,7 +52,7 @@ class Command(BaseCommand):
ending=""
)
self.stdout.flush()
ObjectChange.objects.filter(time__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS)
ObjectChange.objects.filter(time__lt=cutoff).delete()
if options['verbosity']:
self.stdout.write("Done.", self.style.SUCCESS)
elif options['verbosity']:

View File

@@ -0,0 +1,25 @@
from django.db import migrations
from extras.choices import JournalEntryKindChoices
def set_kind_default(apps, schema_editor):
"""
Set kind to "info" on any entries with no kind assigned.
"""
JournalEntry = apps.get_model('extras', 'JournalEntry')
JournalEntry.objects.filter(kind='').update(kind=JournalEntryKindChoices.KIND_INFO)
class Migration(migrations.Migration):
dependencies = [
('extras', '0122_charfield_null_choices'),
]
operations = [
migrations.RunPython(
code=set_kind_default,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -200,7 +200,7 @@ class ConfigContextModel(models.Model):
super().clean()
# Verify that JSON data is provided as an object
if self.local_context_data and type(self.local_context_data) is not dict:
if self.local_context_data is not None and type(self.local_context_data) is not dict:
raise ValidationError(
{'local_context_data': _('JSON data must be in object form. Example:') + ' {"foo": 123}'}
)

View File

@@ -9,6 +9,8 @@ from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.core.validators import RegexValidator, ValidationError
from django.db import models
from django.db.models import F, Func, Value
from django.db.models.expressions import RawSQL
from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe
@@ -281,12 +283,20 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
Populate initial custom field data upon either a) the creation of a new CustomField, or
b) the assignment of an existing CustomField to new object types.
"""
if self.default is None:
# We have to convert None to a JSON null for jsonb_set()
value = RawSQL("'null'::jsonb", [])
else:
value = Value(self.default, models.JSONField())
for ct in content_types:
model = ct.model_class()
instances = model.objects.exclude(**{'custom_field_data__contains': self.name})
for instance in instances:
instance.custom_field_data[self.name] = self.default
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
ct.model_class().objects.update(
custom_field_data=Func(
F('custom_field_data'),
Value([self.name]),
value,
function='jsonb_set'
)
)
def remove_stale_data(self, content_types):
"""
@@ -295,22 +305,27 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
"""
for ct in content_types:
if model := ct.model_class():
instances = model.objects.filter(custom_field_data__has_key=self.name)
for instance in instances:
del instance.custom_field_data[self.name]
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
model.objects.update(
custom_field_data=F('custom_field_data') - self.name
)
def rename_object_data(self, old_name, new_name):
"""
Called when a CustomField has been renamed. Updates all assigned object data.
Called when a CustomField has been renamed. Removes the original key and inserts the new
one, copying the value of the old key.
"""
for ct in self.object_types.all():
model = ct.model_class()
params = {f'custom_field_data__{old_name}__isnull': False}
instances = model.objects.filter(**params)
for instance in instances:
instance.custom_field_data[new_name] = instance.custom_field_data.pop(old_name)
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
ct.model_class().objects.update(
custom_field_data=Func(
F('custom_field_data') - old_name,
Value([new_name]),
Func(
F('custom_field_data'),
function='jsonb_extract_path_text',
template=f"to_jsonb(%(expressions)s -> '{old_name}')"
),
function='jsonb_set')
)
def clean(self):
super().clean()
@@ -515,7 +530,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# URL
elif self.type == CustomFieldTypeChoices.TYPE_URL:
field = LaxURLField(required=required, initial=initial)
field = LaxURLField(assume_scheme='https', required=required, initial=initial)
# JSON
elif self.type == CustomFieldTypeChoices.TYPE_JSON:
@@ -532,6 +547,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
}
if not for_csv_import:
kwargs['query_params'] = self.related_object_filter
kwargs['selector'] = True
field = field_class(**kwargs)
@@ -546,6 +562,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
}
if not for_csv_import:
kwargs['query_params'] = self.related_object_filter
kwargs['selector'] = True
field = field_class(**kwargs)

View File

@@ -117,6 +117,15 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
def __str__(self):
return self.python_name
@property
def ordered_scripts(self):
script_objects = {s.name: s for s in self.scripts.all()}
ordered = [
script_objects.pop(sc) for sc in self.module_scripts.keys() if sc in script_objects
]
ordered.extend(script_objects.items())
return ordered
@property
def module_scripts(self):

View File

@@ -498,13 +498,16 @@ class ConfigContextTable(NetBoxTable):
orderable=False,
verbose_name=_('Synced')
)
tags = columns.TagColumn(
url_name='extras:configcontext_list'
)
class Meta(NetBoxTable.Meta):
model = ConfigContext
fields = (
'pk', 'id', 'name', 'weight', 'is_active', 'is_synced', 'description', 'regions', 'sites', 'locations',
'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants',
'data_source', 'data_file', 'data_synced', 'created', 'last_updated',
'data_source', 'data_file', 'data_synced', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'weight', 'is_active', 'is_synced', 'description')

View File

@@ -0,0 +1,48 @@
from django.test import tag, TestCase
from extras.dashboard.widgets import ObjectListWidget
class ObjectListWidgetTests(TestCase):
def test_widget_config_form_validates_model(self):
model_info = 'extras.notification'
form = ObjectListWidget.ConfigForm({'model': model_info})
self.assertFalse(form.is_valid())
@tag('regression')
def test_widget_fails_gracefully(self):
"""
Example:
'2829fd9b-5dee-4c9a-81f2-5bd84c350a27': {
'class': 'extras.ObjectListWidget',
'color': 'indigo',
'title': 'Object List',
'config': {
'model': 'extras.notification',
'page_size': None,
'url_params': None
}
}
"""
config = {
# 'class': 'extras.ObjectListWidget', # normally popped off, left for clarity
'color': 'yellow',
'title': 'this should fail',
'config': {
'model': 'extras.notification',
'page_size': None,
'url_params': None,
},
}
class Request:
class User:
def has_perm(self, *args, **kwargs):
return True
user = User()
mock_request = Request()
widget = ObjectListWidget(id='2829fd9b-5dee-4c9a-81f2-5bd84c350a27', **config)
rendered = widget.render(mock_request)
self.assertTrue('Unable to load content. Invalid view name:' in rendered)

View File

@@ -1,3 +1,4 @@
from django.forms import ValidationError
from django.test import TestCase
from core.models import ObjectType
@@ -478,3 +479,30 @@ class ConfigContextTest(TestCase):
annotated_queryset = Device.objects.filter(name=device.name).annotate_config_context_data()
self.assertEqual(ConfigContext.objects.get_for_object(device).count(), 2)
self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context())
def test_valid_local_context_data(self):
device = Device.objects.first()
device.local_context_data = None
device.clean()
device.local_context_data = {"foo": "bar"}
device.clean()
def test_invalid_local_context_data(self):
device = Device.objects.first()
device.local_context_data = ""
with self.assertRaises(ValidationError):
device.clean()
device.local_context_data = 0
with self.assertRaises(ValidationError):
device.clean()
device.local_context_data = False
with self.assertRaises(ValidationError):
device.clean()
device.local_context_data = 'foo'
with self.assertRaises(ValidationError):
device.clean()

View File

@@ -1098,8 +1098,8 @@ class DashboardWidgetAddView(LoginRequiredMixin, View):
if not request.htmx:
return redirect('home')
initial = request.GET or {
'widget_class': 'extras.NoteWidget',
initial = {
'widget_class': request.GET.get('widget_class') or 'extras.NoteWidget',
}
widget_form = DashboardWidgetAddForm(initial=initial)
widget_name = get_field_value(widget_form, 'widget_class')

View File

@@ -12,7 +12,8 @@ from netaddr.core import AddrFormatError
from circuits.models import Provider
from dcim.models import Device, Interface, Region, Site, SiteGroup
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
)
@@ -148,7 +149,7 @@ class RIRFilterSet(OrganizationalModelFilterSet):
fields = ('id', 'name', 'slug', 'is_private', 'description')
class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
family = django_filters.NumberFilter(
field_name='prefix',
lookup_expr='family'
@@ -231,6 +232,19 @@ class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
to_field_name='slug',
label=_('RIR (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='sites__group',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='sites__group',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='sites',
queryset=Site.objects.all(),
@@ -276,7 +290,7 @@ class RoleFilterSet(OrganizationalModelFilterSet):
fields = ('id', 'name', 'slug', 'description', 'weight')
class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet):
class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet, ContactModelFilterSet):
family = django_filters.NumberFilter(
field_name='prefix',
lookup_expr='family'
@@ -430,7 +444,7 @@ class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet):
).distinct()
class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet, ContactModelFilterSet):
family = django_filters.NumberFilter(
field_name='start_address',
lookup_expr='family'
@@ -522,7 +536,7 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
return queryset.filter(q)
class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
family = django_filters.NumberFilter(
field_name='address',
lookup_expr='family'
@@ -1136,7 +1150,7 @@ class ServiceTemplateFilterSet(NetBoxModelFilterSet):
return queryset.filter(qs_filter)
class ServiceFilterSet(NetBoxModelFilterSet):
class ServiceFilterSet(ContactModelFilterSet, NetBoxModelFilterSet):
device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
label=_('Device (ID)'),

View File

@@ -177,6 +177,13 @@ class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm):
to_field_name='name',
help_text=_("VLAN's group (if any)")
)
vlan_site = CSVModelChoiceField(
label=_('VLAN Site'),
queryset=Site.objects.all(),
required=False,
to_field_name='name',
help_text=_("VLAN's site (if any)")
)
vlan = CSVModelChoiceField(
label=_('VLAN'),
queryset=VLAN.objects.all(),
@@ -200,8 +207,8 @@ class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm):
class Meta:
model = Prefix
fields = (
'prefix', 'vrf', 'tenant', 'vlan_group', 'vlan', 'status', 'role', 'scope_type', 'scope_id', 'is_pool',
'mark_utilized', 'description', 'comments', 'tags',
'prefix', 'vrf', 'tenant', 'vlan_group', 'vlan_site', 'vlan', 'status', 'role', 'scope_type', 'scope_id',
'is_pool', 'mark_utilized', 'description', 'comments', 'tags',
)
labels = {
'scope_id': _('Scope ID'),
@@ -213,19 +220,19 @@ class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm):
if not data:
return
site = data.get('site')
vlan_site = data.get('vlan_site')
vlan_group = data.get('vlan_group')
# Limit VLAN queryset by assigned site and/or group (if specified)
query = Q()
if site:
if vlan_site:
query |= Q(**{
f"site__{self.fields['site'].to_field_name}": site
f"site__{self.fields['vlan_site'].to_field_name}": vlan_site
})
# Don't Forget to include VLANs without a site in the filter
query |= Q(**{
f"site__{self.fields['site'].to_field_name}__isnull": True
f"site__{self.fields['vlan_site'].to_field_name}__isnull": True
})
if vlan_group:
@@ -320,6 +327,13 @@ class IPAddressImportForm(NetBoxModelImportForm):
to_field_name='name',
help_text=_('Assigned interface')
)
fhrp_group = CSVModelChoiceField(
label=_('FHRP Group'),
queryset=FHRPGroup.objects.all(),
required=False,
to_field_name='name',
help_text=_('Assigned FHRP Group name')
)
is_primary = forms.BooleanField(
label=_('Is primary'),
help_text=_('Make this the primary IP for the assigned device'),
@@ -334,8 +348,8 @@ class IPAddressImportForm(NetBoxModelImportForm):
class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary',
'is_oob', 'dns_name', 'description', 'comments', 'tags',
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'fhrp_group',
'is_primary', 'is_oob', 'dns_name', 'description', 'comments', 'tags',
]
def __init__(self, data=None, *args, **kwargs):
@@ -391,6 +405,8 @@ class IPAddressImportForm(NetBoxModelImportForm):
# Set interface assignment
if self.cleaned_data.get('interface'):
self.instance.assigned_object = self.cleaned_data['interface']
if self.cleaned_data.get('fhrp_group'):
self.instance.assigned_object = self.cleaned_data['fhrp_group']
ipaddress = super().save(*args, **kwargs)

View File

@@ -6,7 +6,7 @@ from ipam.choices import *
from ipam.constants import *
from ipam.models import *
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.rendering import FieldSet
@@ -94,12 +94,13 @@ class RIRFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model)
class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
model = Aggregate
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('family', 'rir_id', name=_('Attributes')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
)
family = forms.ChoiceField(
required=False,
@@ -141,7 +142,7 @@ class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = ASN
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('rir_id', 'site_id', name=_('Assignment')),
FieldSet('rir_id', 'site_group_id', 'site_id', name=_('Assignment')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
)
rir_id = DynamicModelMultipleChoiceField(
@@ -149,6 +150,11 @@ class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
required=False,
label=_('RIR')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
@@ -162,7 +168,7 @@ class RoleFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model)
class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm, ):
model = Prefix
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
@@ -174,6 +180,7 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
)
mask_length__lte = forms.IntegerField(
widget=forms.HiddenInput()
@@ -262,12 +269,13 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model)
class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
model = IPRange
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('family', 'vrf_id', 'status', 'role_id', 'mark_utilized', name=_('Attributes')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
)
family = forms.ChoiceField(
required=False,
@@ -301,7 +309,7 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model)
class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
model = IPAddress
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
@@ -312,6 +320,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('device_id', 'virtual_machine_id', name=_('Device/VM')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
)
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
parent = forms.CharField(
@@ -414,7 +423,7 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
class VLANGroupFilterForm(NetBoxModelFilterSetForm):
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('region', 'sitegroup', 'site', 'location', 'rack', name=_('Location')),
FieldSet('region', 'site_group', 'site', 'location', 'rack', name=_('Location')),
FieldSet('cluster_group', 'cluster', name=_('Cluster')),
FieldSet('contains_vid', name=_('VLANs')),
)
@@ -424,7 +433,7 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
required=False,
label=_('Region')
)
sitegroup = DynamicModelMultipleChoiceField(
site_group = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
@@ -590,12 +599,13 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model)
class ServiceFilterForm(ServiceTemplateFilterForm):
class ServiceFilterForm(ContactModelFilterForm, ServiceTemplateFilterForm):
model = Service
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('protocol', 'port', name=_('Attributes')),
FieldSet('device_id', 'virtual_machine_id', name=_('Assignment')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),

View File

@@ -5,6 +5,7 @@ import strawberry_django
from circuits.graphql.types import ProviderType
from dcim.graphql.types import SiteType
from extras.graphql.mixins import ContactsMixin
from ipam import models
from netbox.graphql.scalars import BigInt
from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType
@@ -83,7 +84,7 @@ class ASNRangeType(NetBoxObjectType):
fields='__all__',
filters=AggregateFilter
)
class AggregateType(NetBoxObjectType, BaseIPAddressFamilyType):
class AggregateType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType):
prefix: str
rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@@ -120,7 +121,7 @@ class FHRPGroupAssignmentType(BaseObjectType):
exclude=('assigned_object_type', 'assigned_object_id', 'address'),
filters=IPAddressFilter
)
class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType):
class IPAddressType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType):
address: str
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@@ -144,7 +145,7 @@ class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType):
fields='__all__',
filters=IPRangeFilter
)
class IPRangeType(NetBoxObjectType):
class IPRangeType(NetBoxObjectType, ContactsMixin):
start_address: str
end_address: str
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
@@ -157,7 +158,7 @@ class IPRangeType(NetBoxObjectType):
exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'),
filters=PrefixFilter
)
class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType):
class PrefixType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType):
prefix: str
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@@ -217,7 +218,7 @@ class RouteTargetType(NetBoxObjectType):
fields='__all__',
filters=ServiceFilter
)
class ServiceType(NetBoxObjectType):
class ServiceType(NetBoxObjectType, ContactsMixin):
ports: List[int]
device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None
virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')] | None

View File

@@ -133,10 +133,18 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
)
ASN.objects.bulk_create(asns)
site_groups = (
SiteGroup(name='Site Group 1', slug='site-group-1'),
SiteGroup(name='Site Group 2', slug='site-group-2'),
SiteGroup(name='Site Group 3', slug='site-group-3'),
)
for site_group in site_groups:
site_group.save()
sites = [
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
Site(name='Site 3', slug='site-3')
Site(name='Site 1', slug='site-1', group=site_groups[0]),
Site(name='Site 2', slug='site-2', group=site_groups[1]),
Site(name='Site 3', slug='site-3', group=site_groups[2]),
]
Site.objects.bulk_create(sites)
asns[0].sites.set([sites[0]])
@@ -178,6 +186,13 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'rir': [rirs[0].slug, rirs[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_site_group(self):
site_groups = SiteGroup.objects.all()[:2]
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}

View File

@@ -666,6 +666,24 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
tags = create_tags('Alpha', 'Bravo', 'Charlie')
fhrp_groups = (
FHRPGroup(
name='FHRP Group 1',
protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP,
group_id=10
),
FHRPGroup(
name='FHRP Group 2',
protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP,
group_id=20
),
FHRPGroup(
name='FHRP Group 3',
protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP,
group_id=30
),
)
FHRPGroup.objects.bulk_create(fhrp_groups)
cls.form_data = {
'vrf': vrfs[1].pk,
'address': IPNetwork('192.0.2.99/24'),
@@ -679,10 +697,10 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
cls.csv_data = (
"vrf,address,status",
"VRF 1,192.0.2.4/24,active",
"VRF 1,192.0.2.5/24,active",
"VRF 1,192.0.2.6/24,active",
"vrf,address,status,fhrp_group",
"VRF 1,192.0.2.4/24,active,FHRP Group 1",
"VRF 1,192.0.2.5/24,active,FHRP Group 2",
"VRF 1,192.0.2.6/24,active,FHRP Group 3",
)
cls.csv_update_data = (

View File

@@ -12,6 +12,7 @@ from rest_framework.viewsets import GenericViewSet
from utilities.api import get_annotations_for_serializer, get_prefetches_for_serializer
from utilities.exceptions import AbortRequest
from utilities.query import reapply_model_ordering
from . import mixins
__all__ = (
@@ -123,8 +124,7 @@ class NetBoxModelViewSet(
def get_queryset(self):
qs = super().get_queryset()
ordering = qs.model._meta.ordering
return qs.order_by(*ordering)
return reapply_model_ordering(qs)
def get_serializer(self, *args, **kwargs):
# If a list of objects has been provided, initialize the serializer with many=True

View File

@@ -28,7 +28,7 @@ AUTH_BACKEND_ATTRS = {
'bitbucket-oauth2': ('BitBucket', 'bitbucket'),
'digitalocean': ('DigitalOcean', 'digital-ocean'),
'docker': ('Docker', 'docker'),
'github': ('GitHub', 'docker'),
'github': ('GitHub', 'github'),
'github-app': ('GitHub', 'github'),
'github-org': ('GitHub', 'github'),
'github-team': ('GitHub', 'github'),

View File

@@ -221,6 +221,11 @@ SESSION_COOKIE_NAME = 'sessionid'
# database access.) Note that the user as which NetBox runs must have read and write permissions to this path.
SESSION_FILE_PATH = None
# By default the memory and disk sizes are displayed using base 10 (e.g. 1000 MB = 1 GB).
# If you would like to use base 2 (e.g. 1024 MB = 1 GB) set this to 1024.
# DISK_BASE_UNIT = 1024
# RAM_BASE_UNIT = 1024
# By default, uploaded media is stored on the local filesystem. Using Django-storages is also supported. Provide the
# class path of the storage driver in STORAGE_BACKEND and any configuration options in STORAGE_CONFIG. For example:
# STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage'

View File

@@ -176,6 +176,12 @@ STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None)
STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {})
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
TRANSLATION_ENABLED = getattr(configuration, 'TRANSLATION_ENABLED', True)
DISK_BASE_UNIT = getattr(configuration, 'DISK_BASE_UNIT', 1000)
if DISK_BASE_UNIT not in [1000, 1024]:
raise ImproperlyConfigured(f"DISK_BASE_UNIT must be 1000 or 1024 (found {DISK_BASE_UNIT})")
RAM_BASE_UNIT = getattr(configuration, 'RAM_BASE_UNIT', 1000)
if RAM_BASE_UNIT not in [1000, 1024]:
raise ImproperlyConfigured(f"RAM_BASE_UNIT must be 1000 or 1024 (found {RAM_BASE_UNIT})")
# Load any dynamic configuration parameters which have been hard-coded in the configuration file
for param in CONFIG_PARAMS:

View File

@@ -28,6 +28,7 @@ from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fiel
from utilities.forms.bulk_import import BulkImportForm
from utilities.htmx import htmx_partial
from utilities.permissions import get_permission_for_model
from utilities.query import reapply_model_ordering
from utilities.views import GetReturnURLMixin, get_viewname
from .base import BaseMultiObjectView
from .mixins import ActionsMixin, TableMixin
@@ -127,8 +128,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
def get_queryset(self, request):
qs = super().get_queryset(request)
ordering = qs.model._meta.ordering
return qs.order_by(*ordering)
return reapply_model_ordering(qs)
def get(self, request):
"""
@@ -666,7 +666,9 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
elif 'virtual_machine' in request.GET:
initial_data['virtual_machine'] = request.GET.get('virtual_machine')
form = self.form(request.POST, initial=initial_data)
post_data = request.POST.copy()
post_data.setlist('pk', pk_list)
form = self.form(post_data, initial=initial_data)
restrict_form_fields(form, request.user)
if '_apply' in request.POST:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -27,10 +27,10 @@
"bootstrap": "5.3.3",
"clipboard": "2.0.11",
"flatpickr": "4.6.13",
"gridstack": "11.3.0",
"gridstack": "11.5.0",
"htmx.org": "1.9.12",
"query-string": "9.1.1",
"sass": "1.85.0",
"sass": "1.86.0",
"tom-select": "2.4.3",
"typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13"

View File

@@ -38,7 +38,7 @@ span.color-label {
.btn-float-group {
position: sticky;
bottom: 10px;
z-index: 2;
z-index: 4;
}
.btn-float-group-left {

View File

@@ -769,9 +769,9 @@
bootstrap "5.3.3"
"@tabler/icons@^3.14.0":
version "3.16.0"
resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-3.16.0.tgz#d618670b80163925a31a6c2290e8775f6058d81a"
integrity sha512-GU7MSx4uQEr55BmyON6hD/QYTl6k1v0YlRhM91gBWDoKAbyCt6QIYw7rpJ/ecdh5zrHaTOJKPenZ4+luoutwFA==
version "3.31.0"
resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-3.31.0.tgz#92d39dc336f2e3e312170420b00ffe9ca474925e"
integrity sha512-dblAdeKY3+GA1U+Q9eziZ0ooVlZMHsE8dqP0RkwvRtEsAULoKOYaCUOcJ4oW1DjWegdxk++UAt2SlQVnmeHv+g==
"@tanstack/react-virtual@^3.0.0-beta.60":
version "3.5.0"
@@ -1911,10 +1911,10 @@ graphql@16.10.0:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c"
integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==
gridstack@11.3.0:
version "11.3.0"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-11.3.0.tgz#b110c66bafc64c920fc54933e2c9df4f7b2cfffe"
integrity sha512-Z0eRovKcZTRTs3zetJwjO6CNwrgIy845WfOeZGk8ybpeMCE8fMA8tScyKU72Y2M6uGHkjgwnjflglvPiv+RcBQ==
gridstack@11.5.0:
version "11.5.0"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-11.5.0.tgz#ecd507776db857f3308d37a8fd67d6a24c7fdd74"
integrity sha512-SE1a/aC2K8VKQr5cqV7gSJ+r/xIYghijIjHzkZ3Xo3aS1/4dvwIgPYT7QqgV1z+d7XjKYUPEizcgVQ5HhdFTng==
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
@@ -2673,10 +2673,10 @@ safe-regex-test@^1.0.3:
es-errors "^1.3.0"
is-regex "^1.1.4"
sass@1.85.0:
version "1.85.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.85.0.tgz#0127ef697d83144496401553f0a0e87be83df45d"
integrity sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==
sass@1.86.0:
version "1.86.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.86.0.tgz#f49464fb6237a903a93f4e8760ef6e37a5030114"
integrity sha512-zV8vGUld/+mP4KbMLJMX7TyGCuUp7hnkOScgCMsWuHtns8CWBoz+vmEhoGMXsaJrbUP8gj+F1dLvVe79sK8UdA==
dependencies:
chokidar "^4.0.0"
immutable "^5.0.2"

View File

@@ -1,3 +1,3 @@
version: "4.2.5"
version: "4.2.7"
edition: "Community"
published: "2025-03-06"
published: "2025-04-10"

View File

@@ -4,30 +4,30 @@
<html lang="en">
<head>
<title>{% trans "Server Error" %}</title>
<link rel="stylesheet" href="{% static 'netbox-light.css'%}" />
<meta charset="UTF-8">
<title>{% trans "Server Error" %}</title>
<link rel="stylesheet" href="{% static 'netbox.css'%}" />
<meta charset="UTF-8">
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col col-md-6 offset-md-3">
<div class="card border-danger mt-5">
<h2 class="card-header">
<i class="mdi mdi-alert"></i> {% trans "Server Error" %}
</h2>
<div class="card-body">
{% block message %}
<p>
{% trans "There was a problem with your request. Please contact an administrator" %}.
</p>
{% endblock %}
<hr />
<p>
{% trans "The complete exception is provided below" %}:
</p>
<pre class="block"><strong>{{ exception }}</strong><br />
<div class="container-fluid">
<div class="row">
<div class="col col-md-6 offset-md-3">
<div class="card border-danger mt-5">
<h2 class="card-header text-bg-danger">
<i class="mdi mdi-alert"></i> {% trans "Server Error" %}
</h2>
<div class="card-body">
{% block message %}
<p>
{% trans "There was a problem with your request. Please contact an administrator" %}.
</p>
{% endblock %}
<hr />
<p>
{% trans "The complete exception is provided below" %}:
</p>
<pre class="block"><strong>{{ exception }}</strong><br />
{{ error }}
{% trans "Python version" %}: {{ python_version }}
@@ -35,17 +35,17 @@
{% trans "Plugins" %}: {% for plugin, version in plugins.items %}
{{ plugin }}: {{ version }}{% empty %}{% trans "None installed" %}{% endfor %}
</pre>
<p>
{% trans "If further assistance is required, please post to the" %} <a href="https://github.com/netbox-community/netbox/discussions">{% trans "NetBox discussion forum" %}</a> {% trans "on GitHub" %}.
</p>
<div class="text-end">
<a href="{% url 'home' %}" class="btn btn-primary">{% trans "Home Page" %}</a>
</div>
</div>
</div>
<p>
{% trans "If further assistance is required, please post to the" %} <a href="https://github.com/netbox-community/netbox/discussions">{% trans "NetBox discussion forum" %}</a> {% trans "on GitHub" %}.
</p>
<div class="text-end">
<a href="{% url 'home' %}" class="btn btn-primary">{% trans "Home Page" %}</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -335,6 +335,15 @@
</div>
{% if object.rack and object.position %}
<div class="row" style="margin-bottom: 20px">
<div class="text-center">
<strong><a href="{% url 'dcim:rack' pk=object.rack.pk %}">{{ object.rack.name }}</a></strong>
{% if object.rack.role %}
<br /><span class="badge my-3" style="color: {{ object.rack.role.color|fgcolor }}; background-color: #{{ object.rack.role.color }}">{{ object.rack.role }}</span>
{% endif %}
{% if object.rack.facility_id %}
<br /><small class="text-muted">{{ object.rack.facility_id }}</small>
{% endif %}
</div>
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
<div style="margin-left: 30px">
<h2 class="h4">{% trans "Front" %}</h2>

View File

@@ -37,7 +37,7 @@
{% endif %}
</div>
</h2>
{% with scripts=module.scripts.all %}
{% with scripts=module.ordered_scripts %}
{% if scripts %}
<table class="table table-hover scripts">
<thead>
@@ -63,7 +63,7 @@
</span>
{% endif %}
</td>
<td>{{ script.python_class.Meta.description|markdown|placeholder }}</td>
<td>{{ script.python_class.description|markdown|placeholder }}</td>
{% if last_job %}
<td>
<a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a>
@@ -79,6 +79,9 @@
{% if request.user|can_run:script and script.is_executable %}
<div class="float-end d-print-none">
<form action="{% url 'extras:script' script.pk %}" method="post">
{% if script.python_class.commit_default %}
<input type="checkbox" name="_commit" hidden checked>
{% endif %}
{% csrf_token %}
<button type="submit" name="_run" class="btn btn-primary btn-sm">
{% if last_job %}

View File

@@ -63,7 +63,7 @@
<th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
<td>
{% if memory_sum %}
<span title={{ memory_sum }}>{{ memory_sum|humanize_megabytes }}</span>
<span title={{ memory_sum }}>{{ memory_sum|humanize_ram_megabytes }}</span>
{% else %}
{{ ''|placeholder }}
{% endif %}
@@ -73,7 +73,7 @@
<th scope="row"><i class="mdi mdi-harddisk"></i> {% trans "Disk Space" %}</th>
<td>
{% if disk_sum %}
{{ disk_sum|humanize_megabytes }}
{{ disk_sum|humanize_disk_megabytes }}
{% else %}
{{ ''|placeholder }}
{% endif %}

View File

@@ -29,7 +29,7 @@
<th scope="row"><i class="mdi mdi-harddisk"></i> {% trans "Size" %}</th>
<td>
{% if object.size %}
{{ object.size|humanize_megabytes }}
{{ object.size|humanize_disk_megabytes }}
{% else %}
{{ ''|placeholder }}
{% endif %}

View File

@@ -129,7 +129,7 @@
<th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
<td>
{% if object.memory %}
<span title={{ object.memory }}>{{ object.memory|humanize_megabytes }}</span>
<span title={{ object.memory }}>{{ object.memory|humanize_ram_megabytes }}</span>
{% else %}
{{ ''|placeholder }}
{% endif %}
@@ -141,7 +141,7 @@
</th>
<td>
{% if object.disk %}
{{ object.disk|humanize_megabytes }}
{{ object.disk|humanize_disk_megabytes }}
{% else %}
{{ ''|placeholder }}
{% endif %}

View File

@@ -116,6 +116,7 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm):
)
link = forms.URLField(
label=_('Link'),
assume_scheme='https',
required=False
)
description = forms.CharField(

View File

@@ -3,7 +3,7 @@ from typing import Annotated, List
import strawberry
import strawberry_django
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin, ContactsMixin
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
from tenancy import models
from .mixins import ContactAssignmentsMixin
@@ -28,7 +28,7 @@ __all__ = (
fields='__all__',
filters=TenantFilter
)
class TenantType(NetBoxObjectType):
class TenantType(ContactsMixin, NetBoxObjectType):
group: Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')] | None
asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]

View File

@@ -126,7 +126,8 @@ class ContactAssignmentTable(NetBoxTable):
)
contact_link = tables.Column(
accessor=Accessor('contact__link'),
verbose_name=_('Contact Link')
verbose_name=_('Contact Link'),
linkify=lambda value: value,
)
contact_description = tables.Column(
accessor=Accessor('contact__description'),

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

Some files were not shown because too many files have changed in this diff Show More