Compare commits

...

58 Commits

Author SHA1 Message Date
Jeremy Stretch
08ae139161 Release v4.3.7 (#20182)
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
* Release v4.3.7

* Revert to django-mptt v0.17.0 due to migrations check failure
2025-08-26 13:52:42 -04:00
bctiemann
1c1073e160 Merge pull request #20177 from netbox-community/18916-TomSelect-invalid-styling
Fixes #18916: Fix styling of dynamic dropdowns with invalid selection
2025-08-26 11:47:21 -04:00
github-actions
0870ec6eb8 Update source translation strings
Some checks are pending
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-26 05:02:12 +00:00
Jeremy Stretch
81579b6739 Fixes #18916: Fix styling of dynamic dropdowns with invalid selection 2025-08-25 15:58:21 -04:00
Martin Hauser
b334931513 fix(dcim): Add status field to PowerOutlet bulk create form
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
Includes the `status` field in the PowerOutlet bulk create form to allow
configuration during bulk creation.
2025-08-25 14:38:31 -04:00
bctiemann
704f0507e7 Merge pull request #20141 from netbox-community/19970-devicerole-child-counts
Fixes #19970: Report device & VM counts for child device roles on parents
2025-08-25 14:33:42 -04:00
mr1716
122e2d13dd #20175 Fix Grammatical Error Related To Capitalization 2025-08-25 14:28:35 -04:00
Jeremy Stretch
0c3beec3a2 Fixes #20120: Cast RQ task args & kwargs to strings for serialization (#20166)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
2025-08-25 07:57:35 -05:00
mr1716
758be46a6f Fixes #20168: Update error-reporting.md To Make Configuration Consistent (#20169) 2025-08-25 07:48:56 -05:00
github-actions
5ac3e79e7b Update source translation strings
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
2025-08-23 05:02:16 +00:00
Jeremy Stretch
7033230388 Fixes #20157: Overwrite existing user notifications to avoid duplications (#20167) 2025-08-22 16:13:24 -07:00
Jeremy Stretch
66140fc017 Closes #18147: Include device & VM interfaces in VRF related objects (#20158) 2025-08-22 16:01:34 -07:00
Jeremy Stretch
d5e49c8cb0 Closes #20122: Improve text contrast on highlighted changes (#20161)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
* Closes #20122: Improve text contrast on highlighted changes

* Fix indentation
2025-08-22 15:34:22 -04:00
github-actions
6b3b4b3193 Update source translation strings
Some checks are pending
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
2025-08-22 05:02:31 +00:00
Jeremy Stretch
2e809904fa Fixes #20043: Prevent rack elevation SVG styling from overflowing to parent page (#20139)
Some checks failed
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
2025-08-21 09:09:44 -07:00
Jeremy Stretch
8b397f3b42 Fixes #20012: Fix support for empty filter for custom fields (#20072) 2025-08-21 09:27:01 -05:00
github-actions
f2b29273d0 Update source translation strings
Some checks are pending
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-21 05:03:51 +00:00
Jeremy Stretch
92fba0bed4 Fixes #19970: Report device & VM counts for child device roles on parents 2025-08-20 16:13:33 -04:00
Daniel Sheppard
53c890c081 Closes #20131: Add selector to the MACAddress model_form for interface and vminterface (#20132)
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-08-20 11:09:21 -05:00
Tomas Neuner
db1786c385 Fixes #19990: add optional return_url parameter to "Add" button for missing prerequisites (#20128) 2025-08-20 11:04:00 -05:00
github-actions
9580ac2946 Update source translation strings
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
2025-08-16 05:02:12 +00:00
Daniel Sheppard
a9ada4457b Fixes: #19669 & #18396 - Allow Token Authentication against Media view (#20046)
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-08-15 13:22:03 -07:00
Daniel Sheppard
9f605a2db1 Fixes #19645: Correct Interface selection for Cable add when VC master is the selected device (#20041)
* Fixes: #19645 - Correct Interface selection for Cable add when VC master is the selected device

* Clarify label

* Add test
2025-08-15 13:54:18 -05:00
bctiemann
44f173f01d Fixes: #20098 - Handle empty object_types field in Tag bulk import (#20099) 2025-08-15 11:34:21 -07:00
Jason Novinger
290e4afaa0 Release v4.3.6 (#20081)
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
2025-08-12 12:15:08 -05:00
github-actions
ca95050b7d Update source translation strings
Some checks are pending
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-12 05:02:37 +00:00
m-hau
34e4ccb212 Fixes #20056: Add rf_role to generate_schema.json (#20071)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-11 10:40:01 -07:00
Jonathan Ramstedt
fcb49f9881 Closes #19728: add c18 power port type (#20064)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-11 08:41:14 -05:00
Jason Novinger
7e40f40248 Closes #20060: Update v4.3.5 release docs to note impact of fixing #18900 2025-08-11 08:31:03 -04:00
github-actions
8e08524fed Update source translation strings
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
2025-08-09 05:03:34 +00:00
Jason Novinger
8bb47dad0f Fixes #20023: Add GiST index on Prefix.prefix for net contains ops (#20059)
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
Resolves performance issue where prefix deletion with 2000+ children
took 5-10 minutes due to sequential scans in hierarchy depth/children
calculations. Adding PostgreSQL GiST index with inet_ops enables efficient
network containment operators (>>, <<, <<=) in annotate_hierarchy() queries.

Performance impact:
- 30-60x speedup: 5-10 minutes → 10 seconds for large prefix deletions
- Real-world validation: 4s migration time on 1.24M prefix dataset
- Storage cost: 47MB index (11% of table storage, 38 bytes per prefix)

Works in conjunction with existing B-tree indexes on vrf_id for optimal
query performance. Benefits all network containment operations including
hierarchy navigation, aggregate views, and available IP/prefix calculations.
2025-08-08 14:14:55 -05:00
Daniel Sheppard
5d7c8318aa Fixes: #19996 - Correct dynamic query parameters for IP Address field in Add/Edit Service form (#20040)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
* Fixes: #19996 - Correct dynamic query parameters for IP Address field in Add/Edit Service form

* Remove debug and do some cleanup
2025-08-08 09:52:03 -04:00
bctiemann
2d495d4f32 Merge pull request #20026 from netbox-community/19998-fixes-missing-changelog-cleared-tags
Fixes: #19998 - Add changelog entry when clearing M2M fields
2025-08-08 06:17:30 -04:00
github-actions
cea83f31b8 Update source translation strings
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-08 05:09:12 +00:00
Daniel Sheppard
6c0dc8b630 Correct mistake made on determination of whether it is a tag or not
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-08-07 21:12:25 -05:00
bctiemann
1c86f81298 Merge pull request #20034 from netbox-community/20033-bookmark-bulk-deletion
Fixes #20033: Fix exception when bulk deleting bookmarks
2025-08-07 20:29:10 -04:00
Daniel Sheppard
630d7aa4c2 Clarify additional branch functionality
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
2025-08-07 08:27:13 -05:00
Daniel Sheppard
043275df19 Clarify label 2025-08-07 08:24:54 -05:00
Jonathan Ramstedt
122f612750 Fixes #19379: allow standalone id in vlan-ids range list (#20024)
Some checks failed
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
* Fixes #19379: allow standalone id in vlan-ids range list

* Misc cleanup

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-08-07 08:56:07 -04:00
github-actions
65b36fd594 Update source translation strings
Some checks are pending
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-07 05:08:34 +00:00
Jeremy Stretch
e828ca5cb4 Fixes #20030: Fix height of object list action buttons & others (#20036)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-06 13:49:52 -07:00
Jeremy Stretch
fce10c73b7 Closes #17222: Improve visibility of notifications icon (#20035) 2025-08-06 14:28:01 -05:00
bctiemann
0cf76bc5c7 Merge pull request #20021 from netbox-community/19999-script-list-widget-misformatted
Fixes #19999: Script list dashboard widget now displays correctly
2025-08-06 15:08:56 -04:00
Jeremy Stretch
11f228cae9 Fixes #20033: Fix exception when bulk deleting bookmarks 2025-08-06 10:29:17 -04:00
Daniel Sheppard
a86cd9dfc6 Clarify comment 2025-08-05 15:49:01 -05:00
Daniel Sheppard
15541c6440 Fixes: #19998 - Add changelog entry when clearing M2M fields 2025-08-05 15:28:41 -05:00
github-actions
6ce3012f93 Update source translation strings
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
2025-08-05 05:08:54 +00:00
bctiemann
fec6cf705f Merge pull request #20015 from mraerino/graphql-contains-prefix
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
Implement `contains` filter for IPAM prefixes and IP ranges
2025-08-04 18:16:19 -04:00
Jason Novinger
9c6d0d1ddc Fixes #19999: Script list widget now displays correctly
- Extract script list content into reusable partial template
- Add object-list CSS class for proper embedded table styling
- Hide module headers and management actions in widget context
- Use compact buttons with icon-only labels for widgets
- Add test coverage for embedded parameter handling

The embedded version now renders cleanly in dashboard widgets while
preserving full functionality in the main script list page.
2025-08-04 16:52:11 -05:00
Jason Novinger
47359d9284 Fixes #20017: ensures full ChangeLog change is highlighted (#20018) 2025-08-04 14:27:39 -07:00
Jeremy Stretch
669df62cde Closes #18873: Add a request timeout parameter to the RSS feed dashboard widget (#20004) 2025-08-04 14:23:33 -07:00
Marcus Weiner
9df0bdcfaf Fixes #19622: Allow loading graphql query from URL (#20014)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-04 09:28:59 -05:00
Jad Seifeddine
d222913716 Fixes: #19917 - Fix MAC address pagination duplicates by adding 'pk' to model ordering (#19961)
* Fix MAC address pagination duplicates by adding 'pk' to model ordering

Add 'pk' to MACAddress model ordering to ensure deterministic results
when multiple MAC addresses have the same value. This prevents the same
MAC address from appearing on multiple pages during pagination.

The issue occurred because Django's default ordering by 'mac_address'
alone is non-deterministic when multiple records share the same MAC
address value, causing inconsistent pagination results when the same
MAC address is assigned to multiple interfaces on a device.

Added regression test that verifies MAC addresses with identical values
are properly ordered by their primary key, ensuring consistent pagination
behavior across the application.

Fixes netbox-community#19917

* Remove test

* Resolve migration conflict

---------

Co-authored-by: Jad Seifeddine <jseifeddine@macquarietelecom.com>
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-08-04 10:15:05 -04:00
Jason Novinger
2c09973e01 Fixes #20009: Fix DOM-based XSS vulnerability in search export functionality
Replace direct string concatenation with URLSearchParams to properly
encode user input in export link URLs, preventing injection of malicious
parameters or scripts through the search functionality.

Resolves CodeQL Alert #63 (js/xss-through-dom)
2025-08-04 09:12:49 -04:00
Marcus Weiner
4506c809d8 Fix formatting 2025-08-03 15:32:55 +02:00
Marcus Weiner
5d194214aa Implement contains filter for IPAM IP ranges 2025-08-03 15:32:14 +02:00
Marcus Weiner
0827198cad Implement contains filter for IPAM prefixes 2025-08-03 15:19:58 +02:00
github-actions
bb83187505 Update source translation strings
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
2025-08-02 05:05:04 +00:00
107 changed files with 11173 additions and 10257 deletions

View File

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

View File

@@ -95,6 +95,7 @@
"iec-60320-c8",
"iec-60320-c14",
"iec-60320-c16",
"iec-60320-c18",
"iec-60320-c20",
"iec-60320-c22",
"iec-60309-p-n-e-4h",
@@ -209,6 +210,7 @@
"iec-60320-c7",
"iec-60320-c13",
"iec-60320-c15",
"iec-60320-c17",
"iec-60320-c19",
"iec-60320-c21",
"iec-60309-p-n-e-4h",
@@ -474,6 +476,13 @@
"passive-48v-2pair",
"passive-48v-4pair"
]
},
"rf_role": {
"type": "string",
"enum": [
"ap",
"station"
]
}
}
},

View File

@@ -4,7 +4,7 @@
### Enabling Error Reporting
NetBox supports native integration with [Sentry](https://sentry.io/) for automatic error reporting. To enable this functionality, set `SENTRY_ENABLED` to True and define your unique [data source name (DSN)](https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/) in `configuration.py`.
NetBox supports native integration with [Sentry](https://sentry.io/) for automatic error reporting. To enable this functionality, set `SENTRY_ENABLED` to `True` and define your unique [data source name (DSN)](https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/) in `configuration.py`.
```python
SENTRY_ENABLED = True

View File

@@ -89,7 +89,7 @@ The following condition will evaluate as true:
```
!!! note "Evaluating static choice fields"
Pay close attention when evaluating static choice fields, such as the `status` field above. These fields typically render as a dictionary specifying both the field's raw value (`value`) and its human-friendly label (`label`). be sure to specify on which of these you want to match.
Pay close attention when evaluating static choice fields, such as the `status` field above. These fields typically render as a dictionary specifying both the field's raw value (`value`) and its human-friendly label (`label`). Be sure to specify on which of these you want to match.
## Condition Sets

View File

@@ -1,5 +1,60 @@
# NetBox v4.3
## v4.3.7 (2025-08-26)
### Enhancements
* [#18147](https://github.com/netbox-community/netbox/issues/18147) - Add device & VM interface counts under related objects for VRFs
* [#19990](https://github.com/netbox-community/netbox/issues/19990) - Button to add a missing prerequisite now includes a return URL
* [#20122](https://github.com/netbox-community/netbox/issues/20122) - Improve color contrast of highlighted data under changelog diff view
* [#20131](https://github.com/netbox-community/netbox/issues/20131) - Add object selector for interface to the MAC address edit form
### Bug Fixes
* [#18916](https://github.com/netbox-community/netbox/issues/18916) - Fix dynamic dropdown selection styling for required fields when no selection is made
* [#19645](https://github.com/netbox-community/netbox/issues/19645) - Fix interface selection when adding a cable for a virtual chassis master
* [#19669](https://github.com/netbox-community/netbox/issues/19669) - Restore token authentication support for fetching media assets
* [#19970](https://github.com/netbox-community/netbox/issues/19970) - Device role child device counts should be cumulative
* [#20012](https://github.com/netbox-community/netbox/issues/20012) - Fix support for `empty` filter lookup on custom fields
* [#20043](https://github.com/netbox-community/netbox/issues/20043) - Fix page styling when rack elevations are embedded
* [#20098](https://github.com/netbox-community/netbox/issues/20098) - Fix `AttributeError` exception when assigning tags during bulk import
* [#20120](https://github.com/netbox-community/netbox/issues/20120) - Fix REST API serialization of jobs under `/api/core/background-tasks/`
* [#20157](https://github.com/netbox-community/netbox/issues/20157) - Fix `IntegrityError` exception when a duplicate notification is triggered
* [#20164](https://github.com/netbox-community/netbox/issues/20164) - Fix `ValueError` exception when attempting to add power outlets to devices in bulk
---
## v4.3.6 (2025-08-12)
### Enhancements
* [#17222](https://github.com/netbox-community/netbox/issues/17222) - Made unread notifications more visible with improved styling and positioning
* [#18843](https://github.com/netbox-community/netbox/issues/18843) - Include color name when exporting cables
* [#18873](https://github.com/netbox-community/netbox/issues/18873) - Add a request timeout parameter to the RSS feed dashboard widget
* [#19622](https://github.com/netbox-community/netbox/issues/19622) - Allow sharing GraphQL queries as links
* [#19728](https://github.com/netbox-community/netbox/issues/19728) - Added C18 power port type for audio devices
* [#19968](https://github.com/netbox-community/netbox/issues/19968) - Improve object type selection form field when editing permissions
* [#19977](https://github.com/netbox-community/netbox/issues/19977) - Improve performance when filtering device components by site, location, or rack
### Bug Fixes
* [#19321](https://github.com/netbox-community/netbox/issues/19321) - Reduce redundant database queries when bulk importing devices
* [#19379](https://github.com/netbox-community/netbox/issues/19379) - Support singular VLAN IDs in list when editing a VLAN group
* [#19812](https://github.com/netbox-community/netbox/issues/19812) - Implement `contains` GraphQL filter for IPAM prefixes and IP ranges
* [#19917](https://github.com/netbox-community/netbox/issues/19917) - Ensure deterministic ordering of duplicate MAC addresses
* [#19996](https://github.com/netbox-community/netbox/issues/19996) - Correct dynamic query parameters for IP Address field in Add/Edit Service form
* [#19998](https://github.com/netbox-community/netbox/issues/19998) - Fix missing changelog records for deleted tags
* [#19999](https://github.com/netbox-community/netbox/issues/19999) - Corrected excessive whitespace in script list dashboard widget
* [#20001](https://github.com/netbox-community/netbox/issues/20001) - `is_api_request()` should not evaluate a request's content type
* [#20009](https://github.com/netbox-community/netbox/issues/20009) - Ensure search parameter is escaped for export links under object list views
* [#20017](https://github.com/netbox-community/netbox/issues/20017) - Fix highlighting of changed lines in changelog data
* [#20023](https://github.com/netbox-community/netbox/issues/20023) - Add GiST index on prefixes table to vastly improve bulk deletion time
* [#20030](https://github.com/netbox-community/netbox/issues/20030) - Fix height of object list action buttons & others
* [#20033](https://github.com/netbox-community/netbox/issues/20033) - Fix `TypeError` exception when bulk deleting bookmarks
* [#20056](https://github.com/netbox-community/netbox/issues/20056) - Fixed missing RF role options in device type schema validation
---
## v4.3.5 (2025-07-29)
### Enhancements
@@ -16,6 +71,11 @@
* [#19934](https://github.com/netbox-community/netbox/issues/19934) - Added missing description field to tenant bulk edit form
* [#19956](https://github.com/netbox-community/netbox/issues/19956) - Prevent duplicate deletion records in changelog from cascading deletions
!!! note "Plugin Developer Advisory"
The fix for bug [#18900](https://github.com/netbox-community/netbox/issues/18900) now raises explicit exceptions when API endpoints attempt to paginate unordered querysets. Plugin maintainers should review their API viewsets to ensure proper queryset ordering is applied before pagination, either by using `.order_by()` on querysets or by setting `ordering` in model Meta classes. Previously silent pagination issues in plugin code will now raise `QuerySetNotOrdered` exceptions and may require updates to maintain compatibility.
---
## v4.3.4 (2025-07-15)
### Enhancements

View File

@@ -18,8 +18,8 @@ class BackgroundTaskSerializer(serializers.Serializer):
description = serializers.CharField()
origin = serializers.CharField()
func_name = serializers.CharField()
args = serializers.ListField(child=serializers.CharField())
kwargs = serializers.DictField()
args = serializers.SerializerMethodField()
kwargs = serializers.SerializerMethodField()
result = serializers.CharField()
timeout = serializers.IntegerField()
result_ttl = serializers.IntegerField()
@@ -42,6 +42,16 @@ class BackgroundTaskSerializer(serializers.Serializer):
is_scheduled = serializers.BooleanField()
is_stopped = serializers.BooleanField()
def get_args(self, obj) -> list:
return [
str(arg) for arg in obj.args
]
def get_kwargs(self, obj) -> dict:
return {
key: str(value) for key, value in obj.kwargs.items()
}
def get_position(self, obj) -> int:
return obj.get_position()

View File

@@ -13,6 +13,7 @@ from django_prometheus.models import model_deletes, model_inserts, model_updates
from core.choices import JobStatusChoices, ObjectChangeActionChoices
from core.events import *
from extras.events import enqueue_event
from extras.models import Tag
from extras.utils import run_validators
from netbox.config import get_config
from netbox.context import current_request, events_queue
@@ -72,6 +73,17 @@ def handle_changed_object(sender, instance, **kwargs):
# m2m_changed with objects added or removed
m2m_changed = True
event_type = OBJECT_UPDATED
elif kwargs.get('action') == 'post_clear':
# Handle clearing of an M2M field
if kwargs.get('model') == Tag and getattr(instance, '_prechange_snapshot', {}).get('tags'):
# Handle generation of M2M changes for Tags which have a previous value (ignoring changes where the
# prechange snapshot is empty)
m2m_changed = True
event_type = OBJECT_UPDATED
else:
# Other endpoints are unimpacted as they send post_add and post_remove
# This will impact changes that utilize clear() however so we may want to give consideration for this branch
return
else:
return

View File

@@ -1,3 +1,5 @@
from rest_framework import serializers
from dcim.models import DeviceRole, InventoryItemRole
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
from netbox.api.fields import RelatedObjectCountField
@@ -13,10 +15,8 @@ __all__ = (
class DeviceRoleSerializer(NestedGroupModelSerializer):
parent = NestedDeviceRoleSerializer(required=False, allow_null=True, default=None)
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
# Related object counts
device_count = RelatedObjectCountField('devices')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
device_count = serializers.IntegerField(read_only=True, default=0)
virtualmachine_count = serializers.IntegerField(read_only=True, default=0)
class Meta:
model = DeviceRole

View File

@@ -20,6 +20,7 @@ from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
from utilities.api import get_serializer_for_model
from utilities.query_functions import CollateAsChar
from virtualization.models import VirtualMachine
from . import serializers
from .exceptions import MissingFilterException
@@ -351,7 +352,19 @@ class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
#
class DeviceRoleViewSet(NetBoxModelViewSet):
queryset = DeviceRole.objects.all()
queryset = DeviceRole.objects.add_related_count(
DeviceRole.objects.add_related_count(
DeviceRole.objects.all(),
VirtualMachine,
'role',
'virtualmachine_count',
cumulative=True
),
Device,
'role',
'device_count',
cumulative=True
)
serializer_class = serializers.DeviceRoleSerializer
filterset_class = filtersets.DeviceRoleFilterSet

View File

@@ -344,6 +344,7 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_IEC_C8 = 'iec-60320-c8'
TYPE_IEC_C14 = 'iec-60320-c14'
TYPE_IEC_C16 = 'iec-60320-c16'
TYPE_IEC_C18 = 'iec-60320-c18'
TYPE_IEC_C20 = 'iec-60320-c20'
TYPE_IEC_C22 = 'iec-60320-c22'
# IEC 60309
@@ -462,6 +463,7 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_IEC_C8, 'C8'),
(TYPE_IEC_C14, 'C14'),
(TYPE_IEC_C16, 'C16'),
(TYPE_IEC_C18, 'C18'),
(TYPE_IEC_C20, 'C20'),
(TYPE_IEC_C22, 'C22'),
)),
@@ -599,6 +601,7 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_IEC_C7 = 'iec-60320-c7'
TYPE_IEC_C13 = 'iec-60320-c13'
TYPE_IEC_C15 = 'iec-60320-c15'
TYPE_IEC_C17 = 'iec-60320-c17'
TYPE_IEC_C19 = 'iec-60320-c19'
TYPE_IEC_C21 = 'iec-60320-c21'
# IEC 60309
@@ -711,6 +714,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_IEC_C7, 'C7'),
(TYPE_IEC_C13, 'C13'),
(TYPE_IEC_C15, 'C15'),
(TYPE_IEC_C17, 'C17'),
(TYPE_IEC_C19, 'C19'),
(TYPE_IEC_C21, 'C21'),
)),

View File

@@ -1885,6 +1885,16 @@ class InterfaceFilterSet(
PathEndpointFilterSet,
CommonInterfaceFilterSet
):
virtual_chassis_member_or_master = MultiValueCharFilter(
method='filter_virtual_chassis_member_or_master',
field_name='name',
label=_('Virtual Chassis Interfaces for Device when device is master')
)
virtual_chassis_member_or_master_id = MultiValueNumberFilter(
method='filter_virtual_chassis_member_or_master',
field_name='pk',
label=_('Virtual Chassis Interfaces for Device when device is master (ID)')
)
virtual_chassis_member = MultiValueCharFilter(
method='filter_virtual_chassis_member',
field_name='name',
@@ -1995,11 +2005,14 @@ class InterfaceFilterSet(
'cable_id', 'cable_end',
)
def filter_virtual_chassis_member(self, queryset, name, value):
def filter_virtual_chassis_member_or_master(self, queryset, name, value):
return self.filter_virtual_chassis_member(queryset, name, value, if_master=True)
def filter_virtual_chassis_member(self, queryset, name, value, if_master=False):
try:
vc_interface_ids = []
for device in Device.objects.filter(**{f'{name}__in': value}):
vc_interface_ids.extend(device.vc_interfaces(if_master=False).values_list('id', flat=True))
vc_interface_ids.extend(device.vc_interfaces(if_master=if_master).values_list('id', flat=True))
return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist:
return queryset.none()

View File

@@ -69,11 +69,14 @@ class PowerPortBulkCreateForm(
class PowerOutletBulkCreateForm(
form_from_model(PowerOutlet, ['type', 'color', 'feed_leg', 'mark_connected']),
form_from_model(PowerOutlet, ['type', 'status', 'color', 'feed_leg', 'mark_connected']),
DeviceBulkAddComponentForm
):
model = PowerOutlet
field_order = ('name', 'label', 'type', 'feed_leg', 'description', 'tags')
field_order = (
'name', 'label', 'type', 'status', 'color', 'feed_leg', 'mark_connected',
'description', 'tags',
)
class InterfaceBulkCreateForm(

View File

@@ -19,6 +19,11 @@ def get_cable_form(a_type, b_type):
# Device component
if hasattr(term_cls, 'device'):
# Dynamically change the param field for interfaces to use virtual_chassis filter
query_param_device_field = 'device_id'
if term_cls == Interface:
query_param_device_field = 'virtual_chassis_member_or_master_id'
attrs[f'termination_{cable_end}_device'] = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
label=_('Device'),
@@ -36,7 +41,7 @@ def get_cable_form(a_type, b_type):
'parent': 'device',
},
query_params={
'device_id': f'$termination_{cable_end}_device',
query_param_device_field: f'$termination_{cable_end}_device',
'kind': 'physical', # Exclude virtual interfaces
}
)

View File

@@ -1890,6 +1890,7 @@ class MACAddressForm(NetBoxModelForm):
label=_('Interface'),
queryset=Interface.objects.all(),
required=False,
selector=True,
context={
'parent': 'device',
},
@@ -1898,6 +1899,7 @@ class MACAddressForm(NetBoxModelForm):
label=_('VM Interface'),
queryset=VMInterface.objects.all(),
required=False,
selector=True,
context={
'parent': 'virtual_machine',
},

View File

@@ -7,6 +7,7 @@ from jinja2 import FileSystemLoader, Environment
from dcim.choices import *
from netbox.choices import WeightUnitChoices
from wireless.choices import WirelessRoleChoices
TEMPLATE_FILENAME = 'devicetype_schema.jinja2'
OUTPUT_FILENAME = 'contrib/generated_schema.json'
@@ -23,6 +24,7 @@ CHOICES_MAP = {
'interface_type_choices': InterfaceTypeChoices,
'interface_poe_mode_choices': InterfacePoEModeChoices,
'interface_poe_type_choices': InterfacePoETypeChoices,
'interface_rf_role_choices': WirelessRoleChoices,
'front_port_type_choices': PortTypeChoices,
'rear_port_type_choices': PortTypeChoices,
}

View File

@@ -0,0 +1,19 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dcim', '0209_device_component_denorm_site_location'),
]
operations = [
migrations.AlterModelOptions(
name='macaddress',
options={
'ordering': ('mac_address', 'pk'),
'verbose_name': 'MAC address',
'verbose_name_plural': 'MAC addresses'
},
),
]

View File

@@ -1276,7 +1276,7 @@ class MACAddress(PrimaryModel):
)
class Meta:
ordering = ('mac_address',)
ordering = ('mac_address', 'pk',)
verbose_name = _('MAC address')
verbose_name_plural = _('MAC addresses')

View File

@@ -4373,6 +4373,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
)
Device.objects.bulk_create(devices)
virtual_chassis.master = devices[0]
virtual_chassis.save()
module_bays = (
ModuleBay(device=devices[0], name='Module Bay 1'),
ModuleBay(device=devices[1], name='Module Bay 2'),
@@ -4759,6 +4762,19 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
params = {'device': [devices[0].name, devices[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_virtual_chassis_member_or_master(self):
vc = VirtualChassis.objects.first()
master = vc.master
member = vc.members.exclude(pk=master.pk).first()
params = {'virtual_chassis_member_or_master_id': [master.pk,]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'virtual_chassis_member_or_master_id': [member.pk,]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'virtual_chassis_member_or_master': [master.name,]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'virtual_chassis_member_or_master': [member.name,]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_virtual_chassis_member(self):
# Device 1A & 3 have 1 management interface, Device 1B has 1 interfaces
devices = Device.objects.filter(name__in=['Device 1A', 'Device 3'])

View File

@@ -1990,9 +1990,18 @@ class InventoryItemTemplateBulkDeleteView(generic.BulkDeleteView):
@register_model_view(DeviceRole, 'list', path='', detail=False)
class DeviceRoleListView(generic.ObjectListView):
queryset = DeviceRole.objects.annotate(
device_count=count_related(Device, 'role'),
vm_count=count_related(VirtualMachine, 'role')
queryset = DeviceRole.objects.add_related_count(
DeviceRole.objects.add_related_count(
DeviceRole.objects.all(),
VirtualMachine,
'role',
'vm_count',
cumulative=True
),
Device,
'role',
'device_count',
cumulative=True
)
filterset = filtersets.DeviceRoleFilterSet
filterset_form = forms.DeviceRoleFilterForm

View File

@@ -309,6 +309,7 @@ class RSSFeedWidget(DashboardWidget):
default_config = {
'max_entries': 10,
'cache_timeout': 3600, # seconds
'request_timeout': 3, # seconds
'requires_internet': True,
}
description = _('Embed an RSS feed from an external website.')
@@ -335,6 +336,12 @@ class RSSFeedWidget(DashboardWidget):
max_value=86400, # 24 hours
help_text=_('How long to stored the cached content (in seconds)')
)
request_timeout = forms.IntegerField(
min_value=1,
max_value=60,
required=False,
help_text=_('Timeout value for fetching the feed (in seconds)')
)
def render(self, request):
return render_to_string(self.template_name, {
@@ -366,7 +373,7 @@ class RSSFeedWidget(DashboardWidget):
url=self.config['feed_url'],
headers={'User-Agent': f'NetBox/{settings.RELEASE.version}'},
proxies=resolve_proxies(url=self.config['feed_url'], context={'client': self}),
timeout=3
timeout=self.config.get('request_timeout', 3),
)
response.raise_for_status()
except requests.exceptions.RequestException as e:

View File

@@ -1,4 +1,5 @@
from django.db.models import CharField, Lookup
from django.db.models import CharField, JSONField, Lookup
from django.db.models.fields.json import KeyTextTransform
from .fields import CachedValueField
@@ -18,6 +19,30 @@ class Empty(Lookup):
return f"CAST(LENGTH({sql}) AS BOOLEAN) IS TRUE", params
class JSONEmpty(Lookup):
"""
Support "empty" lookups for JSONField keys.
A key is considered empty if it is "", null, or does not exist.
"""
lookup_name = "empty"
def as_sql(self, compiler, connection):
# self.lhs.lhs is the parent expression (could be a JSONField or another KeyTransform)
# Rebuild the expression using KeyTextTransform to guarantee ->> (text)
text_expr = KeyTextTransform(self.lhs.key_name, self.lhs.lhs)
lhs_sql, lhs_params = compiler.compile(text_expr)
value = self.rhs
if value not in (True, False):
raise ValueError("The 'empty' lookup only accepts True or False.")
condition = '' if value else 'NOT '
sql = f"(NULLIF({lhs_sql}, '') IS {condition}NULL)"
return sql, lhs_params
class NetHost(Lookup):
"""
Similar to ipam.lookups.NetHost, but casts the field to INET.
@@ -45,5 +70,6 @@ class NetContainsOrEquals(Lookup):
CharField.register_lookup(Empty)
JSONField.register_lookup(JSONEmpty)
CachedValueField.register_lookup(NetHost)
CachedValueField.register_lookup(NetContainsOrEquals)

View File

@@ -600,11 +600,19 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
kwargs = {
'field_name': f'custom_field_data__{self.name}'
}
# Native numeric filters will use `isnull` by default for empty lookups, but
# JSON fields require `empty` (see bug #20012).
if lookup_expr == 'isnull':
lookup_expr = 'empty'
if lookup_expr is not None:
kwargs['lookup_expr'] = lookup_expr
# 'Empty' lookup is always a boolean
if lookup_expr == 'empty':
filter_class = django_filters.BooleanFilter
# Text/URL
if self.type in (
elif self.type in (
CustomFieldTypeChoices.TYPE_TEXT,
CustomFieldTypeChoices.TYPE_LONGTEXT,
CustomFieldTypeChoices.TYPE_URL,

View File

@@ -849,6 +849,9 @@ class Bookmark(models.Model):
return str(self.object)
return super().__str__()
def get_absolute_url(self):
return reverse('account:bookmarks')
def clean(self):
super().clean()

View File

@@ -173,14 +173,17 @@ class NotificationGroup(ChangeLoggedModel):
User.objects.filter(groups__in=self.groups.all())
).order_by('username')
def notify(self, **kwargs):
def notify(self, object_type, object_id, **kwargs):
"""
Bulk-create Notifications for all members of this group.
"""
Notification.objects.bulk_create([
Notification(user=member, **kwargs)
for member in self.members
])
for user in self.members:
Notification.objects.update_or_create(
object_type=object_type,
object_id=object_id,
user=user,
defaults=kwargs
)
notify.alters_data = True

View File

@@ -317,11 +317,12 @@ class TableConfigTable(NetBoxTable):
class BookmarkTable(NetBoxTable):
object_type = columns.ContentTypeColumn(
verbose_name=_('Object Types'),
verbose_name=_('Object Type'),
)
object = tables.Column(
verbose_name=_('Object'),
linkify=True
linkify=True,
orderable=False
)
actions = columns.ActionsColumn(
actions=('delete',)

View File

@@ -1615,6 +1615,7 @@ class CustomFieldModelFilterTest(TestCase):
'cf11': manufacturers[2].pk,
'cf12': [manufacturers[2].pk, manufacturers[3].pk],
}),
Site(name='Site 4', slug='site-4'),
])
def test_filter_integer(self):
@@ -1624,6 +1625,7 @@ class CustomFieldModelFilterTest(TestCase):
self.assertEqual(self.filterset({'cf_cf1__gte': [200]}, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset({'cf_cf1__lt': [200]}, self.queryset).qs.count(), 1)
self.assertEqual(self.filterset({'cf_cf1__lte': [200]}, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset({'cf_cf1__empty': True}, self.queryset).qs.count(), 1)
def test_filter_decimal(self):
self.assertEqual(self.filterset({'cf_cf2': [100.1, 200.2]}, self.queryset).qs.count(), 2)
@@ -1632,6 +1634,7 @@ class CustomFieldModelFilterTest(TestCase):
self.assertEqual(self.filterset({'cf_cf2__gte': [200.2]}, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset({'cf_cf2__lt': [200.2]}, self.queryset).qs.count(), 1)
self.assertEqual(self.filterset({'cf_cf2__lte': [200.2]}, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset({'cf_cf2__empty': True}, self.queryset).qs.count(), 1)
def test_filter_boolean(self):
self.assertEqual(self.filterset({'cf_cf3': True}, self.queryset).qs.count(), 2)
@@ -1648,6 +1651,7 @@ class CustomFieldModelFilterTest(TestCase):
self.assertEqual(self.filterset({'cf_cf4__niew': ['bar']}, self.queryset).qs.count(), 1)
self.assertEqual(self.filterset({'cf_cf4__ie': ['FOO']}, self.queryset).qs.count(), 1)
self.assertEqual(self.filterset({'cf_cf4__nie': ['FOO']}, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset({'cf_cf4__empty': True}, self.queryset).qs.count(), 1)
def test_filter_text_loose(self):
self.assertEqual(self.filterset({'cf_cf5': ['foo']}, self.queryset).qs.count(), 2)
@@ -1659,6 +1663,7 @@ class CustomFieldModelFilterTest(TestCase):
self.assertEqual(self.filterset({'cf_cf6__gte': ['2016-06-27']}, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset({'cf_cf6__lt': ['2016-06-27']}, self.queryset).qs.count(), 1)
self.assertEqual(self.filterset({'cf_cf6__lte': ['2016-06-27']}, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset({'cf_cf6__empty': True}, self.queryset).qs.count(), 1)
def test_filter_url_strict(self):
self.assertEqual(
@@ -1674,17 +1679,20 @@ class CustomFieldModelFilterTest(TestCase):
self.assertEqual(self.filterset({'cf_cf7__niew': ['.com']}, self.queryset).qs.count(), 0)
self.assertEqual(self.filterset({'cf_cf7__ie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 1)
self.assertEqual(self.filterset({'cf_cf7__nie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset({'cf_cf7__empty': True}, self.queryset).qs.count(), 1)
def test_filter_url_loose(self):
self.assertEqual(self.filterset({'cf_cf8': ['example.com']}, self.queryset).qs.count(), 3)
def test_filter_select(self):
self.assertEqual(self.filterset({'cf_cf9': ['A', 'B']}, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset({'cf_cf9__empty': True}, self.queryset).qs.count(), 1)
def test_filter_multiselect(self):
self.assertEqual(self.filterset({'cf_cf10': ['A']}, self.queryset).qs.count(), 1)
self.assertEqual(self.filterset({'cf_cf10': ['A', 'C']}, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset({'cf_cf10': ['null']}, self.queryset).qs.count(), 1)
self.assertEqual(self.filterset({'cf_cf10': ['null']}, self.queryset).qs.count(), 1) # Contains a literal null
self.assertEqual(self.filterset({'cf_cf10__empty': True}, self.queryset).qs.count(), 2)
def test_filter_object(self):
manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
@@ -1692,6 +1700,7 @@ class CustomFieldModelFilterTest(TestCase):
self.filterset({'cf_cf11': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(),
2
)
self.assertEqual(self.filterset({'cf_cf11__empty': True}, self.queryset).qs.count(), 1)
def test_filter_multiobject(self):
manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
@@ -1703,3 +1712,4 @@ class CustomFieldModelFilterTest(TestCase):
self.filterset({'cf_cf12': [manufacturer_ids[3]]}, self.queryset).qs.count(),
3
)
self.assertEqual(self.filterset({'cf_cf12__empty': True}, self.queryset).qs.count(), 1)

View File

@@ -807,3 +807,21 @@ class NotificationTestCase(
def test_list_objects_with_constrained_permission(self):
return
class ScriptListViewTest(TestCase):
user_permissions = ['extras.view_script']
def test_script_list_embedded_parameter(self):
"""Test that ScriptListView accepts embedded parameter without error"""
url = reverse('extras:script_list')
# Test normal request
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'extras/script_list.html')
# Test embedded request
response = self.client.get(url, {'embedded': 'true'})
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'extras/inc/script_list_content.html')

View File

@@ -1282,11 +1282,18 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View):
script_modules = ScriptModule.objects.restrict(request.user).prefetch_related(
'data_source', 'data_file', 'jobs'
)
return render(request, 'extras/script_list.html', {
context = {
'model': ScriptModule,
'script_modules': script_modules,
})
}
# Use partial template for dashboard widgets
template_name = 'extras/script_list.html'
if request.GET.get('embedded'):
template_name = 'extras/inc/script_list_content.html'
context['embedded'] = True
return render(request, template_name, context)
class BaseScriptView(generic.ObjectView):

View File

@@ -21,7 +21,7 @@ from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, T
from utilities.forms.utils import get_field_value
from utilities.forms.widgets import DatePicker, HTMXSelect
from utilities.templatetags.builtins.filters import bettertitle
from virtualization.models import VMInterface
from virtualization.models import VMInterface, VirtualMachine
__all__ = (
'AggregateForm',
@@ -783,10 +783,6 @@ class ServiceForm(NetBoxModelForm):
queryset=IPAddress.objects.all(),
required=False,
label=_('IP Addresses'),
query_params={
'device_id': '$device',
'virtual_machine_id': '$virtual_machine',
}
)
comments = CommentField()
@@ -815,10 +811,22 @@ class ServiceForm(NetBoxModelForm):
super().__init__(*args, **kwargs)
if (parent_object_type_id := get_field_value(self, 'parent_object_type')):
if parent_object_type_id := get_field_value(self, 'parent_object_type'):
try:
parent_type = ContentType.objects.get(pk=parent_object_type_id)
model = parent_type.model_class()
if model == Device:
self.fields['ipaddresses'].widget.add_query_params({
'device_id': '$parent',
})
elif model == VirtualMachine:
self.fields['ipaddresses'].widget.add_query_params({
'virtual_machine_id': '$parent',
})
elif model == FHRPGroup:
self.fields['ipaddresses'].widget.add_query_params({
'fhrpgroup_id': '$parent',
})
self.fields['parent'].queryset = model.objects.all()
self.fields['parent'].widget.attrs['selector'] = model._meta.label_lower
self.fields['parent'].disabled = False

View File

@@ -222,6 +222,19 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMi
return Q()
return q
@strawberry_django.filter_field()
def contains(self, value: list[str], prefix) -> Q:
if not value:
return Q()
q = Q()
for subnet in value:
net = netaddr.IPNetwork(subnet.strip())
q |= Q(
start_address__host__inet__lte=str(netaddr.IPAddress(net.first)),
end_address__host__inet__gte=str(netaddr.IPAddress(net.last)),
)
return q
@strawberry_django.filter_type(models.Prefix, lookups=True)
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
@@ -238,6 +251,16 @@ class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, Pr
is_pool: FilterLookup[bool] | None = strawberry_django.filter_field()
mark_utilized: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_field()
def contains(self, value: list[str], prefix) -> Q:
if not value:
return Q()
q = Q()
for subnet in value:
query = str(netaddr.IPNetwork(subnet.strip()).cidr)
q |= Q(prefix__net_contains=query)
return q
@strawberry_django.filter_type(models.RIR, lookups=True)
class RIRFilter(OrganizationalModelFilterMixin):

View File

@@ -0,0 +1,20 @@
from django.contrib.postgres.indexes import GistIndex
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0210_macaddress_ordering'),
('extras', '0129_fix_script_paths'),
('ipam', '0081_remove_service_device_virtual_machine_add_parent_gfk_index'),
('tenancy', '0020_remove_contactgroupmembership'),
]
operations = [
migrations.AddIndex(
model_name='prefix',
index=GistIndex(fields=['prefix'], name='ipam_prefix_gist_idx', opclasses=['inet_ops']),
),
]

View File

@@ -1,5 +1,6 @@
import netaddr
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.postgres.indexes import GistIndex
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F
@@ -281,6 +282,13 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
ordering = (F('vrf').asc(nulls_first=True), 'prefix', 'pk') # (vrf, prefix) may be non-unique
verbose_name = _('prefix')
verbose_name_plural = _('prefixes')
indexes = [
GistIndex(
fields=['prefix'],
name='ipam_prefix_gist_idx',
opclasses=['inet_ops'],
),
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -53,8 +53,26 @@ class VRFView(GetRelatedModelsMixin, generic.ObjectView):
)
export_targets_table.configure(request)
related_models = self.get_related_models(
request,
instance,
omit=(Interface, VMInterface),
extra=(
(
Interface.objects.restrict(request.user, 'view').filter(vrf=instance),
'vrf_id',
_('Device Interfaces')
),
(
VMInterface.objects.restrict(request.user, 'view').filter(vrf=instance),
'vrf_id',
_('VM Interfaces')
),
),
)
return {
'related_models': self.get_related_models(request, instance, omit=[Interface, VMInterface]),
'related_models': related_models,
'import_targets_table': import_targets_table,
'export_targets_table': export_targets_table,
}

View File

@@ -29,6 +29,13 @@ __all__ = (
'OrganizationalModelFilterSet',
)
STANDARD_LOOKUPS = (
'exact',
'iexact',
'in',
'contains',
)
#
# FilterSets
@@ -159,7 +166,7 @@ class BaseFilterSet(django_filters.FilterSet):
return {}
# Skip nonstandard lookup expressions
if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'iexact', 'in']:
if existing_filter.method is not None or existing_filter.lookup_expr not in STANDARD_LOOKUPS:
return {}
# Choose the lookup expression map based on the filter type

View File

@@ -237,7 +237,11 @@ class ActionsColumn(tables.Column):
:param split_actions: When True, converts the actions dropdown menu into a split button with first action as the
direct button link and icon (default: True)
"""
attrs = {'td': {'class': 'text-end text-nowrap noprint'}}
attrs = {
'td': {
'class': 'text-end text-nowrap noprint p-1'
}
}
empty_values = ()
actions = {
'edit': ActionsItem('Edit', 'pencil', 'change', 'warning'),

View File

@@ -20,7 +20,7 @@ from netbox.search.backends import search_backend
from netbox.tables import SearchTable
from utilities.htmx import htmx_partial
from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.views import ConditionalLoginRequiredMixin
from utilities.views import ConditionalLoginRequiredMixin, TokenConditionalLoginRequiredMixin
__all__ = (
'HomeView',
@@ -119,7 +119,7 @@ class SearchView(ConditionalLoginRequiredMixin, View):
})
class MediaView(ConditionalLoginRequiredMixin, View):
class MediaView(TokenConditionalLoginRequiredMixin, View):
"""
Wrap Django's serve() view to enforce LOGIN_REQUIRED for static media.
"""

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

@@ -1 +1 @@
svg{--nbx-rack-bg: var(--tblr-bg-surface-secondary);--nbx-rack-border: #000;--nbx-rack-slot-bg: #e9ecef;--nbx-rack-slot-border: #adb5bd;--nbx-rack-slot-hover-bg: #ced4da;--nbx-rack-link-color: #0d6efd;--nbx-rack-unit-color: #6c757d}svg[data-bs-theme=dark]{--nbx-rack-bg: rgb(27, 41, 58);--nbx-rack-border: #6c757d;--nbx-rack-slot-bg: #343a40;--nbx-rack-slot-border: #495057;--nbx-rack-slot-hover-bg: #212529;--nbx-rack-link-color: #9ec5fe;--nbx-rack-unit-color: #adb5bd}*{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Noto Sans,Liberation Sans,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-size:.875rem}rect{box-sizing:border-box}text{text-anchor:middle;dominant-baseline:middle}svg{background-color:var(--nbx-rack-bg)}svg .unit{margin:0;padding:5px 0;fill:var(--nbx-rack-unit-color)}svg .hidden{visibility:hidden}svg rect.shaded,svg image.shaded{opacity:25%}svg text.shaded{opacity:50%}svg .rack{fill:none;stroke-width:2px;stroke:var(--nbx-rack-border)}svg .slot{fill:var(--nbx-rack-slot-bg);stroke:var(--nbx-rack-slot-border)}svg .slot:hover{fill:var(--nbx-rack-slot-hover-bg)}svg .slot+.add-device{fill:var(--nbx-rack-link-color);opacity:0;pointer-events:none}svg .slot:hover+.add-device{opacity:1}svg .slot.occupied[class],svg .slot.occupied:hover[class]{fill:url(#occupied)}svg .slot.blocked[class],svg .slot.blocked:hover[class]{fill:url(#blocked)}svg .slot.blocked:hover+.add-device{opacity:0}svg .reservation[class]{fill:url(#reserved)}
svg{--nbx-rack-bg: var(--tblr-bg-surface-secondary);--nbx-rack-border: #000;--nbx-rack-slot-bg: #e9ecef;--nbx-rack-slot-border: #adb5bd;--nbx-rack-slot-hover-bg: #ced4da;--nbx-rack-link-color: #0d6efd;--nbx-rack-unit-color: #6c757d}svg[data-bs-theme=dark]{--nbx-rack-bg: rgb(27, 41, 58);--nbx-rack-border: #6c757d;--nbx-rack-slot-bg: #343a40;--nbx-rack-slot-border: #495057;--nbx-rack-slot-hover-bg: #212529;--nbx-rack-link-color: #9ec5fe;--nbx-rack-unit-color: #adb5bd}rect{box-sizing:border-box}text{text-anchor:middle;dominant-baseline:middle}svg{background-color:var(--nbx-rack-bg);font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Noto Sans,Liberation Sans,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-size:.875rem}svg .unit{margin:0;padding:5px 0;fill:var(--nbx-rack-unit-color)}svg .hidden{visibility:hidden}svg rect.shaded,svg image.shaded{opacity:25%}svg text.shaded{opacity:50%}svg .rack{fill:none;stroke-width:2px;stroke:var(--nbx-rack-border)}svg .slot{fill:var(--nbx-rack-slot-bg);stroke:var(--nbx-rack-slot-border)}svg .slot:hover{fill:var(--nbx-rack-slot-hover-bg)}svg .slot+.add-device{fill:var(--nbx-rack-link-color);opacity:0;pointer-events:none}svg .slot:hover+.add-device{opacity:1}svg .slot.occupied[class],svg .slot.occupied:hover[class]{fill:url(#occupied)}svg .slot.blocked[class],svg .slot.blocked:hover[class]{fill:url(#blocked)}svg .slot.blocked:hover+.add-device{opacity:0}svg .reservation[class]{fill:url(#reserved)}

View File

@@ -24,13 +24,13 @@
"dependencies": {
"@mdi/font": "7.4.47",
"@tabler/core": "1.4.0",
"bootstrap": "5.3.7",
"bootstrap": "5.3.8",
"clipboard": "2.0.11",
"flatpickr": "4.6.13",
"gridstack": "12.2.2",
"gridstack": "12.3.3",
"htmx.org": "2.0.6",
"query-string": "9.2.2",
"sass": "1.89.2",
"sass": "1.91.0",
"tom-select": "2.4.3",
"typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13"

View File

@@ -38,7 +38,9 @@ function handleQuickSearchParams(event: Event): void {
if (quickSearchParameters != null) {
const link = document.getElementById('export_current_view') as HTMLLinkElement;
const search_parameter = `q=${quickSearchParameters.value}`;
const params = new URLSearchParams();
params.set('q', quickSearchParameters.value);
const search_parameter = params.toString();
const linkUpdated = link?.href + '&' + search_parameter;
link.setAttribute('href', linkUpdated);
}

View File

@@ -8,13 +8,17 @@ pre.change-data {
display: block;
padding-right: $spacer;
padding-left: $spacer;
width: 100%;
min-width: fit-content;
&.added {
background-color: $green;
color: var(--tblr-dark);
background-color: $green-300;
}
&.removed {
background-color: $red;
color: var(--tblr-dark);
background-color: $red-300;
}
}
}
@@ -24,11 +28,13 @@ pre.change-diff {
border-color: transparent;
&.change-added {
background-color: $green;
color: var(--tblr-dark);
background-color: $green-300;
}
&.change-removed {
background-color: $red;
color: var(--tblr-dark);
background-color: $red-300;
}
}

View File

@@ -6,3 +6,9 @@
}
}
}
// Replicate styling of tom-selected <select> fields tagged with .is-invalid to
// their corresponding TomSelect dropdowns
select.tomselected.is-invalid + div.ts-wrapper {
@extend .is-invalid;
}

View File

@@ -28,10 +28,6 @@ svg {
}
}
* {
font-family: $font-family-sans-serif;
font-size: $font-size-sm;
}
rect {
box-sizing: border-box;
}
@@ -42,6 +38,8 @@ text {
svg {
background-color: var(--nbx-rack-bg);
font-family: $font-family-sans-serif;
font-size: $font-size-sm;
// Rack unit numbers along left side of rack elevation.
.unit {

View File

@@ -845,78 +845,78 @@
"@types/estree" "*"
"@typescript-eslint/eslint-plugin@^8.37.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz#6e5220d16f2691ab6d983c1737dd5b36e17641b7"
integrity sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz#28dffcb5272d20afe250bfeec3173263db5528a0"
integrity sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.38.0"
"@typescript-eslint/type-utils" "8.38.0"
"@typescript-eslint/utils" "8.38.0"
"@typescript-eslint/visitor-keys" "8.38.0"
"@typescript-eslint/scope-manager" "8.39.1"
"@typescript-eslint/type-utils" "8.39.1"
"@typescript-eslint/utils" "8.39.1"
"@typescript-eslint/visitor-keys" "8.39.1"
graphemer "^1.4.0"
ignore "^7.0.0"
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@^8.37.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.38.0.tgz#6723a5ea881e1777956b1045cba30be5ea838293"
integrity sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.39.1.tgz#7f8f9ecfc7e172d67e42c366fa198e42324e5d50"
integrity sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==
dependencies:
"@typescript-eslint/scope-manager" "8.38.0"
"@typescript-eslint/types" "8.38.0"
"@typescript-eslint/typescript-estree" "8.38.0"
"@typescript-eslint/visitor-keys" "8.38.0"
"@typescript-eslint/scope-manager" "8.39.1"
"@typescript-eslint/types" "8.39.1"
"@typescript-eslint/typescript-estree" "8.39.1"
"@typescript-eslint/visitor-keys" "8.39.1"
debug "^4.3.4"
"@typescript-eslint/project-service@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.38.0.tgz#4900771f943163027fd7d2020a062892056b5e2f"
integrity sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==
"@typescript-eslint/project-service@8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.39.1.tgz#63525878d488ebf27c485f295e83434a1398f52d"
integrity sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.38.0"
"@typescript-eslint/types" "^8.38.0"
"@typescript-eslint/tsconfig-utils" "^8.39.1"
"@typescript-eslint/types" "^8.39.1"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz#5a0efcb5c9cf6e4121b58f87972f567c69529226"
integrity sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==
"@typescript-eslint/scope-manager@8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz#1253fe3e1f2f33f08a3e438a05b5dd7faf9fbca6"
integrity sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==
dependencies:
"@typescript-eslint/types" "8.38.0"
"@typescript-eslint/visitor-keys" "8.38.0"
"@typescript-eslint/types" "8.39.1"
"@typescript-eslint/visitor-keys" "8.39.1"
"@typescript-eslint/tsconfig-utils@8.38.0", "@typescript-eslint/tsconfig-utils@^8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz#6de4ce224a779601a8df667db56527255c42c4d0"
integrity sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==
"@typescript-eslint/tsconfig-utils@8.39.1", "@typescript-eslint/tsconfig-utils@^8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz#17f13b4ad481e7bec7c249ee1854078645b34b12"
integrity sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==
"@typescript-eslint/type-utils@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz#a56cd84765fa6ec135fe252b5db61e304403a85b"
integrity sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==
"@typescript-eslint/type-utils@8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz#642f9fb96173649e2928fea0375b1d74d31906c2"
integrity sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==
dependencies:
"@typescript-eslint/types" "8.38.0"
"@typescript-eslint/typescript-estree" "8.38.0"
"@typescript-eslint/utils" "8.38.0"
"@typescript-eslint/types" "8.39.1"
"@typescript-eslint/typescript-estree" "8.39.1"
"@typescript-eslint/utils" "8.39.1"
debug "^4.3.4"
ts-api-utils "^2.1.0"
"@typescript-eslint/types@8.38.0", "@typescript-eslint/types@^8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.38.0.tgz#297351c994976b93c82ac0f0e206c8143aa82529"
integrity sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==
"@typescript-eslint/types@8.39.1", "@typescript-eslint/types@^8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.39.1.tgz#f0ab996c8ab2c3b046bbf86bb1990b03529869a1"
integrity sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==
"@typescript-eslint/typescript-estree@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz#82262199eb6778bba28a319e25ad05b1158957df"
integrity sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==
"@typescript-eslint/typescript-estree@8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz#8825d3ea7ea2144c577859ae489eec24ef7318a5"
integrity sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==
dependencies:
"@typescript-eslint/project-service" "8.38.0"
"@typescript-eslint/tsconfig-utils" "8.38.0"
"@typescript-eslint/types" "8.38.0"
"@typescript-eslint/visitor-keys" "8.38.0"
"@typescript-eslint/project-service" "8.39.1"
"@typescript-eslint/tsconfig-utils" "8.39.1"
"@typescript-eslint/types" "8.39.1"
"@typescript-eslint/visitor-keys" "8.39.1"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
@@ -924,22 +924,22 @@
semver "^7.6.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/utils@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.38.0.tgz#5f10159899d30eb92ba70e642ca6f754bddbf15a"
integrity sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==
"@typescript-eslint/utils@8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.39.1.tgz#58a834f89f93b786ada2cd14d77fa63c3c8f408b"
integrity sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==
dependencies:
"@eslint-community/eslint-utils" "^4.7.0"
"@typescript-eslint/scope-manager" "8.38.0"
"@typescript-eslint/types" "8.38.0"
"@typescript-eslint/typescript-estree" "8.38.0"
"@typescript-eslint/scope-manager" "8.39.1"
"@typescript-eslint/types" "8.39.1"
"@typescript-eslint/typescript-estree" "8.39.1"
"@typescript-eslint/visitor-keys@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz#a9765a527b082cb8fc60fd8a16e47c7ad5b60ea5"
integrity sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==
"@typescript-eslint/visitor-keys@8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz#a467742a98f2fa3c03d7bed4979dc0db3850a77a"
integrity sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==
dependencies:
"@typescript-eslint/types" "8.38.0"
"@typescript-eslint/types" "8.39.1"
eslint-visitor-keys "^4.2.1"
"@ungap/structured-clone@^1.2.0":
@@ -1116,6 +1116,11 @@ bootstrap@5.3.7:
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.7.tgz#8640065036124d961d885d80b5945745e1154d90"
integrity sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==
bootstrap@5.3.8:
version "5.3.8"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.8.tgz#6401a10057a22752d21f4e19055508980656aeed"
integrity sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -1742,9 +1747,9 @@ eslint-plugin-import@^2.32.0:
tsconfig-paths "^3.15.0"
eslint-plugin-prettier@^5.5.1:
version "5.5.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz#1f88e9220a72ac8be171eec5f9d4e4d529b5f4a0"
integrity sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==
version "5.5.4"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz#9d61c4ea11de5af704d4edf108c82ccfa7f2e61c"
integrity sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==
dependencies:
prettier-linter-helpers "^1.0.0"
synckit "^0.11.7"
@@ -2173,10 +2178,10 @@ graphql@16.10.0:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c"
integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==
gridstack@12.2.2:
version "12.2.2"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.2.2.tgz#a9ec300cefc93516bcb8dd966510a728027be358"
integrity sha512-eK9XAbBWQp+QniqL6ipvofWSrCelm46j5USag73LNq8tOWSL2DeeGBWU9mTibLI6i66n0r7xYS+1/g2qqTqKcw==
gridstack@12.3.3:
version "12.3.3"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.3.3.tgz#0c4fc3cdf6e1c16e6095bc79ff7240a590d2c200"
integrity sha512-Bboi4gj7HXGnx1VFXQNde4Nwi5srdUSuCCnOSszKhFjBs8EtMEWhsKX02BjIKkErq/FjQUkNUbXUYeQaVMQ0jQ==
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
@@ -3185,10 +3190,10 @@ safe-regex-test@^1.1.0:
es-errors "^1.3.0"
is-regex "^1.2.1"
sass@1.89.2:
version "1.89.2"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.89.2.tgz#a771716aeae774e2b529f72c0ff2dfd46c9de10e"
integrity sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==
sass@1.91.0:
version "1.91.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.91.0.tgz#7d4f7f624b35d43f78da1c339cab24426e28d7fa"
integrity sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==
dependencies:
chokidar "^4.0.0"
immutable "^5.0.2"

View File

@@ -1,3 +1,3 @@
version: "4.3.5"
version: "4.3.7"
edition: "Community"
published: "2025-07-29"
published: "2025-08-26"

View File

@@ -4,22 +4,22 @@
<div class="card">
<h2 class="card-header d-flex justify-content-between">
{% blocktrans %}Termination{% endblocktrans %} {{ side }}
<div>
<div class="card-actions">
{% if not termination and perms.circuits.add_circuittermination %}
<a href="{% url 'circuits:circuittermination_add' %}?circuit={{ object.pk }}&term_side={{ side }}&return_url={{ object.get_absolute_url }}" class="btn btn-success lh-1">
<a href="{% url 'circuits:circuittermination_add' %}?circuit={{ object.pk }}&term_side={{ side }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-ghost-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add" %}
</a>
{% endif %}
{% if termination and perms.circuits.change_circuittermination %}
<a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning lh-1">
<a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-ghost-warning">
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
</a>
<a href="{% url 'circuits:circuit_terminations_swap' pk=object.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-primary lh-1">
<a href="{% url 'circuits:circuit_terminations_swap' pk=object.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-ghost-primary">
<span class="mdi mdi-swap-vertical" aria-hidden="true"></span> {% trans "Swap" %}
</a>
{% endif %}
{% if termination and perms.circuits.delete_circuittermination %}
<a href="{% url 'circuits:circuittermination_delete' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-danger lh-1">
<a href="{% url 'circuits:circuittermination_delete' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-ghost-danger">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Delete" %}
</a>
{% endif %}

View File

@@ -29,16 +29,16 @@
{{ peer|linkify }}{% if not forloop.last %},{% endif %}
{% endfor %}
<div class="mt-1">
<a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> {% trans "Trace" %}
</a>
{% if perms.dcim.change_cable %}
<a href="{% url 'dcim:cable_edit' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="{% trans "Edit cable" %}" class="btn btn-warning lh-1">
<a href="{% url 'dcim:cable_edit' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="{% trans "Edit cable" %}" class="btn btn-sm btn-warning">
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> {% trans "Edit" %}
</a>
{% endif %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="{% trans "Remove cable" %}" class="btn btn-danger lh-1">
<a href="{% url 'dcim:cable_delete' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="{% trans "Remove cable" %}" class="btn btn-sm btn-danger">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> {% trans "Disconnect" %}
</a>
{% endif %}

View File

@@ -77,7 +77,7 @@
<th scope="row">{% trans "Cable" %}</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:frontport_trace' pk=object.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url 'dcim:frontport_trace' pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>

View File

@@ -24,7 +24,7 @@
<i class="mdi mdi-chevron-right" aria-hidden="true"></i>
{{ term|linkify }}
{% with trace_url=term|viewname:"trace" %}
<a href="{% url trace_url pk=term.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url trace_url pk=term.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
{% endwith %}
@@ -48,7 +48,7 @@
{% for term in terminations %}
{{ term|linkify }}
{% with trace_url=term|viewname:"trace" %}
<a href="{% url trace_url pk=term.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url trace_url pk=term.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
{% endwith %}
@@ -68,7 +68,7 @@
{% for term in terminations %}
{{ term.circuit|linkify }} ({{ term }})
{% with trace_url=term|viewname:"trace" %}
<a href="{% url trace_url pk=term.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url trace_url pk=term.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
{% endwith %}

View File

@@ -4,7 +4,7 @@
<th scope="row">{% trans "Cable" %}</th>
<td>
{{ object.cable|linkify }}
<a href="{% url trace_url pk=object.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url trace_url pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>

View File

@@ -29,12 +29,12 @@
<td>{{ item.role|linkify|placeholder }}</td>
<td class="text-end d-print-none">
{% if perms.dcim.change_inventoryitem %}
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning lh-1" title="{% trans "Edit" %}">
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning" title="{% trans "Edit" %}">
<i class="mdi mdi-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.ipam.delete_inventoryitem %}
<a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-danger lh-1" title="{% trans "Delete" %}">
<a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-danger" title="{% trans "Delete" %}">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
</a>
{% endif %}

View File

@@ -208,7 +208,7 @@
<th scope="row">{% trans "Wireless Link" %}</th>
<td>
{{ object.wireless_link|linkify }}
<a href="{% url 'dcim:interface_trace' pk=object.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url 'dcim:interface_trace' pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>

View File

@@ -73,7 +73,7 @@
<th scope="row">{% trans "Cable" %}</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:rearport_trace' pk=object.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url 'dcim:rearport_trace' pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>

View File

@@ -57,7 +57,7 @@
{% trans "Output" %}
{% if job.completed %}
<div>
<a href="?export=output" class="btn btn-primary lh-1" role="button">
<a href="?export=output" class="btn btn-sm btn-primary" role="button">
<i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
</a>
</div>

View File

@@ -0,0 +1,139 @@
{% load buttons %}
{% load helpers %}
{% load perms %}
{% load i18n %}
{# Core script list content - used by both full page and embedded views #}
{% for module in script_modules %}
{% include 'inc/sync_warning.html' with object=module %}
<div class="card{% if embedded %} mb-3{% endif %}">
{% if not embedded %}
<h2 class="card-header" id="module{{ module.pk }}">
<i class="mdi mdi-file-document-outline"></i> {{ module }}
<div class="card-actions">
{% if perms.extras.edit_scriptmodule %}
<a href="{% url 'extras:scriptmodule_edit' pk=module.pk %}" class="btn btn-ghost-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
</a>
{% endif %}
{% if perms.extras.delete_scriptmodule %}
<a href="{% url 'extras:scriptmodule_delete' pk=module.pk %}" class="btn btn-ghost-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</a>
{% endif %}
</div>
</h2>
{% endif %}
{% with scripts=module.ordered_scripts %}
{% if scripts %}
<table class="table table-hover scripts{% if embedded %} object-list table-sm{% endif %}">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Last Run" %}</th>
<th>{% trans "Status" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for script in scripts %}
{% with last_job=script.get_latest_jobs|first %}
<tr>
<td>
{% if script.is_executable %}
<a href="{% url 'extras:script' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
{% else %}
<a href="{% url 'extras:script_jobs' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
<span class="text-danger">
<i class="mdi mdi-alert" title="{% trans "Script is no longer present in the source file" %}"></i>
</span>
{% endif %}
</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>
</td>
<td>
{% badge last_job.get_status_display last_job.get_status_color %}
</td>
{% else %}
<td class="text-muted">{% trans "Never" %}</td>
<td>{{ ''|placeholder }}</td>
{% endif %}
<td>
{% 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{% if embedded %} btn-sm{% endif %}">
{% if last_job %}
<i class="mdi mdi-replay"></i> {% if not embedded %}{% trans "Run Again" %}{% endif %}
{% else %}
<i class="mdi mdi-play"></i> {% if not embedded %}{% trans "Run Script" %}{% endif %}
{% endif %}
</button>
</form>
</div>
{% endif %}
</td>
</tr>
{% if last_job and not embedded %}
{% for test_name, data in last_job.data.tests.items %}
<tr>
<td colspan="4" class="method">
<span class="ps-3">{{ test_name }}</span>
</td>
<td class="text-end text-nowrap script-stats">
<span class="badge text-bg-success">{{ data.success }}</span>
<span class="badge text-bg-info">{{ data.info }}</span>
<span class="badge text-bg-warning">{{ data.warning }}</span>
<span class="badge text-bg-danger">{{ data.failure }}</span>
</td>
</tr>
{% endfor %}
{% elif last_job and not last_job.data.log and not embedded %}
{# legacy #}
{% for method, stats in last_job.data.items %}
<tr>
<td colspan="4" class="method">
<span class="ps-3">{{ method }}</span>
</td>
<td class="text-end text-nowrap report-stats">
<span class="badge bg-success">{{ stats.success }}</span>
<span class="badge bg-info">{{ stats.info }}</span>
<span class="badge bg-warning">{{ stats.warning }}</span>
<span class="badge bg-danger">{{ stats.failure }}</span>
</td>
</tr>
{% endfor %}
{% endif %}
{% endwith %}
{% endfor %}
</tbody>
</table>
{% else %}
<div class="card-body">
<div class="alert alert-warning" role="alert">
<i class="mdi mdi-alert"></i>
{% blocktrans with module=module.name %}Could not load scripts from module {{ module }}{% endblocktrans %}
</div>
</div>
{% endif %}
{% endwith %}
</div>
{% empty %}
<div class="alert alert-info" role="alert">
<h4 class="alert-heading">{% trans "No Scripts Found" %}</h4>
{% if perms.extras.add_scriptmodule and not embedded %}
{% url 'extras:scriptmodule_add' as create_script_url %}
{% blocktrans trimmed %}
Get started by <a href="{{ create_script_url }}">creating a script</a> from an uploaded file or data source.
{% endblocktrans %}
{% endif %}
</div>
{% endfor %}

View File

@@ -54,11 +54,11 @@
<div class="card">
<h2 class="card-header d-flex justify-content-between">
{% trans "Rendered Config" %}
<div>
{% copy_content "rendered_config" %}
<a href="?export=True" class="btn btn-primary lh-1" role="button">
<div class="card-actions">
<a href="?export=True" class="btn btn-sm btn-ghost-primary" role="button">
<i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
</a>
{% copy_content "rendered_config" %}
</div>
</h2>
<pre class="card-body" id="rendered_config">{{ rendered_config }}</pre>

View File

@@ -70,6 +70,10 @@
"poe_type": {
"type": "string",
"enum": {{ interface_poe_type_choices }}
},
"rf_role": {
"type": "string",
"enum": {{ interface_rf_role_choices }}
}
}
},

View File

@@ -19,135 +19,5 @@
{% endblock controls %}
{% block content %}
{% for module in script_modules %}
{% include 'inc/sync_warning.html' with object=module %}
<div class="card">
<h2 class="card-header" id="module{{ module.pk }}">
<i class="mdi mdi-file-document-outline"></i> {{ module }}
<div class="card-actions">
{% if perms.extras.edit_scriptmodule %}
<a href="{% url 'extras:scriptmodule_edit' pk=module.pk %}" class="btn btn-ghost-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
</a>
{% endif %}
{% if perms.extras.delete_scriptmodule %}
<a href="{% url 'extras:scriptmodule_delete' pk=module.pk %}" class="btn btn-ghost-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</a>
{% endif %}
</div>
</h2>
{% with scripts=module.ordered_scripts %}
{% if scripts %}
<table class="table table-hover scripts">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Last Run" %}</th>
<th>{% trans "Status" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for script in scripts %}
{% with last_job=script.get_latest_jobs|first %}
<tr>
<td>
{% if script.is_executable %}
<a href="{% url 'extras:script' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
{% else %}
<a href="{% url 'extras:script_jobs' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
<span class="text-danger">
<i class="mdi mdi-alert" title="{% trans "Script is no longer present in the source file" %}"></i>
</span>
{% endif %}
</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>
</td>
<td>
{% badge last_job.get_status_display last_job.get_status_color %}
</td>
{% else %}
<td class="text-muted">{% trans "Never" %}</td>
<td>{{ ''|placeholder }}</td>
{% endif %}
<td>
{% 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 %}
<i class="mdi mdi-replay"></i> {% trans "Run Again" %}
{% else %}
<i class="mdi mdi-play"></i> {% trans "Run Script" %}
{% endif %}
</button>
</form>
</div>
{% endif %}
</td>
</tr>
{% if last_job %}
{% for test_name, data in last_job.data.tests.items %}
<tr>
<td colspan="4" class="method">
<span class="ps-3">{{ test_name }}</span>
</td>
<td class="text-end text-nowrap script-stats">
<span class="badge text-bg-success">{{ data.success }}</span>
<span class="badge text-bg-info">{{ data.info }}</span>
<span class="badge text-bg-warning">{{ data.warning }}</span>
<span class="badge text-bg-danger">{{ data.failure }}</span>
</td>
</tr>
{% endfor %}
{% elif not last_job.data.log %}
{# legacy #}
{% for method, stats in last_job.data.items %}
<tr>
<td colspan="4" class="method">
<span class="ps-3">{{ method }}</span>
</td>
<td class="text-end text-nowrap report-stats">
<span class="badge bg-success">{{ stats.success }}</span>
<span class="badge bg-info">{{ stats.info }}</span>
<span class="badge bg-warning">{{ stats.warning }}</span>
<span class="badge bg-danger">{{ stats.failure }}</span>
</td>
</tr>
{% endfor %}
{% endif %}
{% endwith %}
{% endfor %}
</tbody>
</table>
{% else %}
<div class="card-body">
<div class="alert alert-warning" role="alert">
<i class="mdi mdi-alert"></i>
{% blocktrans with module=module.name %}Could not load scripts from module {{ module }}{% endblocktrans %}
</div>
</div>
{% endif %}
{% endwith %}
</div>
{% empty %}
<div class="alert alert-info" role="alert">
<h4 class="alert-heading">{% trans "No Scripts Found" %}</h4>
{% if perms.extras.add_scriptmodule %}
{% url 'extras:scriptmodule_add' as create_script_url %}
{% blocktrans trimmed %}
Get started by <a href="{{ create_script_url }}">creating a script</a> from an uploaded file or data source.
{% endblocktrans %}
{% endif %}
</div>
{% endfor %}
{% include 'extras/inc/script_list_content.html' with embedded=False %}
{% endblock content %}

View File

@@ -88,6 +88,21 @@
#
`;
let sharedQuery;
const hashArgs = new URLSearchParams(window.location.hash.substring(1));
if (hashArgs.has('query')) {
sharedQuery = hashArgs.get('query');
// reset url to not motivate copying of stale URL
hashArgs.delete('query');
let remainingHash = "";
if (hashArgs.size !== 0) {
remainingHash = `#${hashArgs.toString()}`;
}
history.pushState("", document.title,
window.location.pathname + window.location.search + remainingHash
);
}
const fetchURL = window.location.href;
function httpUrlToWebSockeUrl(url) {
@@ -123,6 +138,8 @@
defaultEditorToolsVisibility: true,
plugins: [explorerPlugin],
inputValueDeprecation: true,
defaultQuery: EXAMPLE_QUERY,
query: sharedQuery,
}),
);
</script>

View File

@@ -15,14 +15,14 @@
<div class="d-block text-secondary fs-5">{{ notification.event }} {{ notification.created|timesince }} {% trans "ago" %}</div>
</div>
<div class="col-auto">
<a href="#" hx-get="{% url 'extras:notification_dismiss' pk=notification.pk %}" hx-target="closest .notifications" class="list-group-item-actions text-secondary" title="{% trans "Dismiss" %}">
<a href="#" hx-get="{% url 'extras:notification_dismiss' pk=notification.pk %}" hx-target="closest .notifications" class="list-group-item-actions text-red" title="{% trans "Dismiss" %}">
<i class="mdi mdi-close"></i>
</a>
</div>
</div>
</div>
{% empty %}
<div class="dropdown-item text-muted">
<div class="dropdown-item disabled">
{% trans "No unread notifications" %}
</div>
{% endfor %}

View File

@@ -1,10 +1,10 @@
{% load i18n %}
<div class="d-flex ms-2">
<button class="btn color-mode-toggle hide-theme-dark" title="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<button class="nav-link color-mode-toggle hide-theme-dark fs-2 p-0 text-secondary" title="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb"></i>
</button>
<button class="btn color-mode-toggle hide-theme-light" title="{% trans "Enable light mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<button class="nav-link color-mode-toggle hide-theme-light fs-2 p-0 text-secondary" title="{% trans "Enable light mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb-on"></i>
</button>
</div>

View File

@@ -10,7 +10,7 @@
{% endblocktrans %}
</div>
<div>
{% add_button prerequisite_model %}
{% add_button prerequisite_model request.path %}
</div>
</div>
</div>

View File

@@ -1,6 +1,7 @@
{% if notifications %}
<span class="text-primary" id="notifications-alert" hx-swap-oob="true">
<i class="mdi mdi-bell-badge"></i>
<i class="mdi mdi-bell-ring"></i>
<span class="badge bg-red"></span>
</span>
{% else %}
<span class="text-muted" id="notifications-alert" hx-swap-oob="true">

View File

@@ -4,19 +4,19 @@
<div class="card">
<h2 class="card-header">{% trans "Related Objects" %}</h2>
<ul class="list-group list-group-flush" role="presentation">
{% for qs, filter_param in related_models %}
{% with viewname=qs.model|validated_viewname:"list" %}
{% for related_object_count in related_models %}
{% with viewname=related_object_count.queryset.model|validated_viewname:"list" %}
{% if viewname is not None %}
<a href="{% url viewname %}?{{ filter_param }}={{ object.pk }}" class="list-group-item list-group-item-action d-flex justify-content-between">
{{ qs.model|meta:"verbose_name_plural"|bettertitle }}
{% with count=qs.count %}
{% if count %}
<span class="badge text-bg-primary rounded-pill">{{ count }}</span>
{% else %}
<span class="badge text-bg-light rounded-pill">&mdash;</span>
{% endif %}
{% endwith %}
</a>
<a href="{% url viewname %}?{{ related_object_count.filter_param }}={{ object.pk }}" class="list-group-item list-group-item-action d-flex justify-content-between">
{{ related_object_count.name }}
{% with count=related_object_count.queryset.count %}
{% if count %}
<span class="badge text-bg-primary rounded-pill">{{ count }}</span>
{% else %}
<span class="badge text-bg-light rounded-pill">&mdash;</span>
{% endif %}
{% endwith %}
</a>
{% endif %}
{% endwith %}
{% empty %}

View File

@@ -12,9 +12,9 @@
{# Notifications #}
{% with notifications=request.user.notifications.unread.exists %}
<div class="dropdown">
<a href="#" class="nav-link px-1" data-bs-toggle="dropdown" hx-get="{% url 'extras:notifications' %}" hx-target="next .notifications" aria-label="{% trans "Notifications" %}">
<button class="nav-link fs-2 p-0" data-bs-toggle="dropdown" hx-get="{% url 'extras:notifications' %}" hx-target="next .notifications" aria-label="{% trans "Notifications" %}">
{% include 'inc/notification_bell.html' %}
</a>
</button>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow notifications"></div>
</div>
{% endwith %}

View File

@@ -40,12 +40,12 @@
<td>{{ assignment.priority }}</td>
<td class="text-end d-print-none">
{% if perms.ipam.change_fhrpgroupassignment %}
<a href="{% url 'ipam:fhrpgroupassignment_edit' pk=assignment.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning lh-1" title="{% trans "Edit" %}">
<a href="{% url 'ipam:fhrpgroupassignment_edit' pk=assignment.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning" title="{% trans "Edit" %}">
<i class="mdi mdi-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.ipam.delete_fhrpgroupassignment %}
<a href="{% url 'ipam:fhrpgroupassignment_delete' pk=assignment.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-danger lh-1" title="{% trans "Delete" %}">
<a href="{% url 'ipam:fhrpgroupassignment_delete' pk=assignment.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-danger" title="{% trans "Delete" %}">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
</a>
{% endif %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -160,9 +160,17 @@ def string_to_ranges(value):
return None
value.replace(' ', '') # Remove whitespace
values = []
for dash_range in value.split(','):
if '-' not in dash_range:
for data in value.split(','):
dash_range = data.strip().split('-')
if len(dash_range) == 1 and str(dash_range[0]).isdigit():
# Single integer value; expand to a range
lower = dash_range[0]
upper = dash_range[0]
elif len(dash_range) == 2 and str(dash_range[0]).isdigit() and str(dash_range[1]).isdigit():
# The range has two values and both are valid integers
lower = dash_range[0]
upper = dash_range[1]
else:
return None
lower, upper = dash_range.split('-')
values.append(NumericRange(int(lower), int(upper), bounds='[]'))
return values

View File

@@ -32,12 +32,14 @@ class NumericArrayField(SimpleArrayField):
class NumericRangeArrayField(forms.CharField):
"""
A field which allows for array of numeric ranges:
Example: 1-5,7-20,30-50
Example: 1-5,10,20-30
"""
def __init__(self, *args, help_text='', **kwargs):
if not help_text:
help_text = mark_safe(
_("Specify one or more numeric ranges separated by commas. Example: " + "<code>1-5,20-30</code>")
_(
"Specify one or more individual numbers or numeric ranges separated by commas. Example: {example}"
).format(example="<code>1-5,10,20-30</code>")
)
super().__init__(*args, help_text=help_text, **kwargs)

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