Compare commits

...

98 Commits

Author SHA1 Message Date
Jeremy Stretch
09d6b9c62f Merge pull request #17157 from netbox-community/develop
Release v4.0.9
2024-08-14 10:23:47 -04:00
Jeremy Stretch
4747cdef0b Bump version to v4.0.9 for release 2024-08-14 10:06:44 -04:00
Jeremy Stretch
f3f1aa3841 Release v4.0.9 2024-08-14 09:30:41 -04:00
transifex-integration[bot]
727cb65c50 Updates for project NetBox (#17153)
* Translate django.po in ru

100% translated source file: 'django.po'
on 'ru'.

* Translate django.po in de

100% translated source file: 'django.po'
on 'de'.

* Translate django.po in ja [Manual Sync]

98% of minimum 1% translated source file: 'django.po'
on 'ja'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in fr [Manual Sync]

98% of minimum 1% translated source file: 'django.po'
on 'fr'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in es [Manual Sync]

98% of minimum 1% translated source file: 'django.po'
on 'es'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in pt [Manual Sync]

98% of minimum 1% translated source file: 'django.po'
on 'pt'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in tr [Manual Sync]

98% of minimum 1% translated source file: 'django.po'
on 'tr'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in it [Manual Sync]

98% of minimum 1% translated source file: 'django.po'
on 'it'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in zh [Manual Sync]

99% of minimum 1% translated source file: 'django.po'
on 'zh'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in pl [Manual Sync]

98% of minimum 1% translated source file: 'django.po'
on 'pl'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in nl [Manual Sync]

98% of minimum 1% translated source file: 'django.po'
on 'nl'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in uk [Manual Sync]

99% of minimum 1% translated source file: 'django.po'
on 'uk'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in cs [Manual Sync]

98% of minimum 1% translated source file: 'django.po'
on 'cs'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in da [Manual Sync]

98% of minimum 1% translated source file: 'django.po'
on 'da'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in zh

100% translated source file: 'django.po'
on 'zh'.

* Translate django.po in cs

100% translated source file: 'django.po'
on 'cs'.

* Translate django.po in da

100% translated source file: 'django.po'
on 'da'.

* Translate django.po in nl

100% translated source file: 'django.po'
on 'nl'.

* Translate django.po in fr

100% translated source file: 'django.po'
on 'fr'.

* Translate django.po in it

100% translated source file: 'django.po'
on 'it'.

* Translate django.po in ja

100% translated source file: 'django.po'
on 'ja'.

* Translate django.po in pl

100% translated source file: 'django.po'
on 'pl'.

* Translate django.po in pt

100% translated source file: 'django.po'
on 'pt'.

* Translate django.po in es

100% translated source file: 'django.po'
on 'es'.

* Translate django.po in tr

100% translated source file: 'django.po'
on 'tr'.

* Translate django.po in uk

100% translated source file: 'django.po'
on 'uk'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-08-14 09:28:28 -04:00
Arthur Hanson
872af72b8e 16073 set default custom fields on CSV import (#17152)
* 16073 set default custom fields on CSV import

* 16073 add test case

* Remove second for loop

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-08-14 08:22:14 -04:00
Jeremy Stretch
5a9f9af2fa Fixes #16871: Sanitize device ID when bulk editing components to prevent exception 2024-08-14 07:43:10 -04:00
github-actions
09d36469dd Update source translation strings 2024-08-14 05:02:17 +00:00
Jeremy Stretch
8789aaaa39 Changelog for #16692, #17006, #17124, #17131, #17144 2024-08-13 16:12:58 -04:00
Jeremy Stretch
d5c1a5acda Fixes #17144: Avoid displaying duplicate pop-up messages 2024-08-13 15:36:15 -04:00
PieterL75
6feb8bf0e3 add 'vlan' to prefix bulk edit (#17142)
* add 'vlan' to prefix bulk edit

* Move VLAN fields to a separate field set in bulk edit form

---------

Co-authored-by: Pieter Lambrecht <pieter.lambrecht@accenture.com>
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-08-13 09:54:07 -04:00
Jeremy Stretch
9e54cfe340 Fixes #17131: Fix exception when creating object-type custom field without selecting related object type 2024-08-13 08:14:16 -04:00
github-actions
6a663e2a3e Update source translation strings 2024-08-13 05:02:12 +00:00
Matthew Mehrtens
7c9a77b77f 17006 Add Wi-Fi 7 IEEE 802.11be (#17125)
* Add .devcontainer to .gitignore

* Closes #17006: Add Wi-Fi 7 IEEE 802.11be

* Revert out-of-scope change

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-08-12 08:03:46 -04:00
github-actions
81fe12a7d9 Update source translation strings 2024-08-11 05:02:11 +00:00
Jeremy Stretch
9c7002f691 Fixes #17124: BaseTable should follow reverse one-to-one relationships when prefetching related objects 2024-08-10 11:58:14 -04:00
Jeremy Stretch
20967bf88d Changelog for #13459, #16176, #17038, #17064 2024-08-10 11:49:34 -04:00
Arthur Hanson
34d20fccd5 17036 international messages (#17041)
* 17036 international messages

* 17036 fix typo

* 17036 fix _

* Misc cleanup & fixes

* More cleanup

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-08-10 11:47:06 -04:00
Arthur Hanson
f6c1642116 16176 Add UI for multi-termination cables 2024-08-10 10:30:56 -04:00
Arthur Hanson
b7b0ab16f5 17064 fix markdown padding for first line 2024-08-10 10:28:01 -04:00
Arthur Hanson
6ae3af2f26 13459 Fix OpenAPI type for TreeNodeMultipleChoiceFilter (#17095)
* 13459 Correct OpenAPI type for TreeNodeMultipleChoiceFilter

* 13459 Correct OpenAPI type for TreeNodeMultipleChoiceFilter
2024-08-10 10:24:02 -04:00
Jeremy Stretch
6c845bd5de Update CONTRIBUTING.md
Add warning about intentionally submitting duplicate issues
2024-08-06 07:53:43 -04:00
github-actions
597fc926c0 Update source translation strings 2024-08-02 05:02:14 +00:00
bubu
fa2b3bcfcc Adjust HTML template for Chinese translation order. 2024-08-01 09:08:06 -04:00
Arthur Hanson
dc173a5508 17038 fix export system data 2024-08-01 08:47:26 -04:00
Arthur Hanson
408f8b4964 17054 upgrade sass to upgrade braces 2024-08-01 08:43:50 -04:00
github-actions
f949aa334b Update source translation strings 2024-07-27 05:02:14 +00:00
Jeremy Stretch
8bfcb1c816 PRVB 2024-07-26 16:25:32 -04:00
Jeremy Stretch
630c6fb43d Merge branch 'master' into develop 2024-07-26 16:24:55 -04:00
Jeremy Stretch
c8b4faefcb Update static assets 2024-07-26 16:20:46 -04:00
Jeremy Stretch
cbf84a6b95 Release v4.0.8 2024-07-26 16:20:46 -04:00
Jeremy Stretch
173c339993 Update import statements for Strawberry 0.236.0 2024-07-26 16:20:46 -04:00
transifex-integration[bot]
5ebdf7fc0f Updates for project NetBox (#17004)
* Translate django.po in de

100% translated source file: 'django.po'
on 'de'.

* Translate django.po in pt

100% translated source file: 'django.po'
on 'pt'.

* Translate django.po in zh

100% translated source file: 'django.po'
on 'zh'.

* Translate django.po in pl

100% translated source file: 'django.po'
on 'pl'.

* Translate django.po in ja

100% translated source file: 'django.po'
on 'ja'.

* Translate django.po in nl

100% translated source file: 'django.po'
on 'nl'.

* Translate django.po in cs

100% translated source file: 'django.po'
on 'cs'.

* Translate django.po in uk

100% translated source file: 'django.po'
on 'uk'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-07-26 16:20:46 -04:00
Jonathan Senecal
0d30ab3462 Use provider_id instead of account_id in url_params 2024-07-26 16:20:46 -04:00
Jeremy Stretch
17ddbdd3b8 Changelog for #16933, #16943, #16964 2024-07-26 16:20:46 -04:00
Jeremy Stretch
cb59f6e6f7 Fixes #16964: Ensure configured password validators are enforced (#16990)
* Closes #16964: Validate password when creating a new user or updating password for an existing user

* Add serializer validation & tests

---------

Co-authored-by: Nishant Gaglani <nishantgaglani@gmail.com>
2024-07-26 16:20:46 -04:00
Jeremy Stretch
93cebae55c Remove jeffgdotorg from triage rotation 2024-07-26 16:20:46 -04:00
github-actions
2620d6067a Update source translation strings 2024-07-26 16:20:46 -04:00
Jeremy Stretch
3c9d173139 Closes #16933: Enable toggling true/false marks on BooleanColumn 2024-07-26 16:20:46 -04:00
Jeremy Stretch
181fe0a3cc Closes #16943: Expand navigation breadcrumbs on job view to include parent object 2024-07-26 16:20:46 -04:00
github-actions
70b2451209 Update source translation strings 2024-07-26 16:20:46 -04:00
Jeremy Stretch
dab07d653f Closes #16929: Add version & user details as data attributes (#16939)
* Closes #16929: Add version & user details as data attributes

* Fix typo
2024-07-26 16:20:46 -04:00
Benjamin Dale
8b2f9bf700 Update CONTRIBUTING.md
The GitHub reactions icon has been moved from the top right to the bottom left of messages in Issues - I was going insane trying to find it, so this might help someone in the future ; )
2024-07-26 16:20:46 -04:00
Jeremy Stretch
8f54724ac1 Changelog for #16402, #16536, #16624, #16819 2024-07-26 16:20:46 -04:00
Arthur Hanson
c9452db6cf 16819 highlight parent device in rack (#16881)
* 16819 highlight parent device in rack

* 16819 review changes
2024-07-26 16:20:46 -04:00
Eric Oswald
a9dadfd179 Fix incorrect import in rest-api.md 2024-07-26 16:20:46 -04:00
Thomas Fargeix
5019a67a61 Fixes 16536 - Fix filtering of device component by device role (#16553)
* Fixes 16536 - Fix filtering of device component by device role

Rename role and role_id fields to device_role and device_role_id in
DeviceComponentFilterSet

* Update tests for DeviceComponentFilterSet

* Use device_role filter name for DeviceComponentFilterSetTests

* Add test_device_role test in InventoryItemTestCase
2024-07-26 16:20:46 -04:00
Fabian Geisberger
95347cfd0f Fixes #16624: Set allow_null=True on method fields that can return None 2024-07-26 16:20:46 -04:00
Arthur Hanson
eb74393070 16402 remove links from script result table 2024-07-26 16:20:46 -04:00
github-actions
874677b359 Update source translation strings 2024-07-26 16:20:46 -04:00
Jeremy Stretch
3cde4da4a9 Changelog for #14640, #14792, #15660, #15696, #16793 2024-07-26 16:20:46 -04:00
transifex-integration[bot]
0b1b9caea4 Updates for project NetBox (#16888)
* Translate django.po in pt

100% reviewed source file: 'django.po'
on 'pt'.

* Translate django.po in es

100% translated source file: 'django.po'
on 'es'.

* Translate django.po in de

100% translated source file: 'django.po'
on 'de'.

* Translate django.po in fr

100% translated source file: 'django.po'
on 'fr'.

* Translate django.po in ru

100% translated source file: 'django.po'
on 'ru'.

* Translate django.po in ja

100% translated source file: 'django.po'
on 'ja'.

* Translate django.po in it

100% translated source file: 'django.po'
on 'it'.

* Translate django.po in cs

100% translated source file: 'django.po'
on 'cs'.

* Translate django.po in zh

100% translated source file: 'django.po'
on 'zh'.

* Translate django.po in nl

100% translated source file: 'django.po'
on 'nl'.

* Translate django.po in da

100% translated source file: 'django.po'
on 'da'.

* Translate django.po in uk

100% translated source file: 'django.po'
on 'uk'.

* Translate django.po in pl

100% translated source file: 'django.po'
on 'pl'.

* Translate django.po in tr

100% translated source file: 'django.po'
on 'tr'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-07-26 16:20:46 -04:00
Jeff Gehlbach
2969c4188c Added CS, DA, IT, NL, and PL, minus the .po and .mo starting point files (#16810)
* Added CS, DA, IT, NL, and PL, minus the .po and .mo starting point files

* Add initial PO files for new languages

* Revert updates to EN django.po

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-07-26 16:20:46 -04:00
github-actions
68013cb554 Update source translation strings 2024-07-26 16:20:46 -04:00
Jeremy Stretch
5c272f8e6e Changelog for #15375, #16357, #16760, #16838, #16867 2024-07-26 16:20:46 -04:00
Jeremy Stretch
e51d67c72a Update contact email 2024-07-26 16:20:46 -04:00
the.friendly.net
0e0d6172a4 Fixes #16760: datasource git on local file system fails (#16872)
* Fixes #16760: datasource git on local file system fails

* Fixes #16760: datasource git on local file system fails

* Set depth & quiet parameters only if using a remote URL

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-07-26 16:20:46 -04:00
Jeff Gehlbach
b3fbcb3afc Small additions and tweaks to release checklist from releasing v4.0.7 2024-07-26 16:20:46 -04:00
Arthur Hanson
4f60b26bf3 16838 show extra_buttons if no actions defined 2024-07-26 16:20:46 -04:00
Arthur Hanson
0bc17850fd 16867 render dashboard if model no longer available 2024-07-26 16:20:46 -04:00
Arthur Hanson
30b9fcf4f8 16357 clone tenant and type for cable 2024-07-26 16:20:46 -04:00
Théophile Bastian
ef5c0256f8 SSO: custom name for identity providers (#16732) 2024-07-26 16:20:46 -04:00
github-actions
db081e2b5e Update source translation strings 2024-07-26 16:20:46 -04:00
Jeremy Stretch
11f13bf2a4 PRVB 2024-07-26 16:20:46 -04:00
Jeremy Stretch
7b5e8d5f2a Update static assets 2024-07-26 15:47:57 -04:00
Jeremy Stretch
303c1ce00c Release v4.0.8 2024-07-26 15:37:05 -04:00
Jeremy Stretch
b4240cdd67 Update import statements for Strawberry 0.236.0 2024-07-26 15:35:08 -04:00
transifex-integration[bot]
1e7a71969e Updates for project NetBox (#17004)
* Translate django.po in de

100% translated source file: 'django.po'
on 'de'.

* Translate django.po in pt

100% translated source file: 'django.po'
on 'pt'.

* Translate django.po in zh

100% translated source file: 'django.po'
on 'zh'.

* Translate django.po in pl

100% translated source file: 'django.po'
on 'pl'.

* Translate django.po in ja

100% translated source file: 'django.po'
on 'ja'.

* Translate django.po in nl

100% translated source file: 'django.po'
on 'nl'.

* Translate django.po in cs

100% translated source file: 'django.po'
on 'cs'.

* Translate django.po in uk

100% translated source file: 'django.po'
on 'uk'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-07-26 15:34:47 -04:00
Jonathan Senecal
f3d1924453 Use provider_id instead of account_id in url_params 2024-07-26 15:10:25 -04:00
Jeremy Stretch
4d55d7d964 Changelog for #16933, #16943, #16964 2024-07-26 08:01:08 -04:00
Jeremy Stretch
d8c7282fdb Fixes #16964: Ensure configured password validators are enforced (#16990)
* Closes #16964: Validate password when creating a new user or updating password for an existing user

* Add serializer validation & tests

---------

Co-authored-by: Nishant Gaglani <nishantgaglani@gmail.com>
2024-07-26 07:58:14 -04:00
Jeremy Stretch
cc72a58c1e Remove jeffgdotorg from triage rotation 2024-07-24 17:15:53 -04:00
github-actions
36df9228a6 Update source translation strings 2024-07-20 05:02:12 +00:00
Jeremy Stretch
424dda5be6 Closes #16933: Enable toggling true/false marks on BooleanColumn 2024-07-19 07:54:41 -04:00
Jeremy Stretch
3028f262cc Closes #16943: Expand navigation breadcrumbs on job view to include parent object 2024-07-19 07:52:27 -04:00
github-actions
11cadf3a8a Update source translation strings 2024-07-19 05:02:01 +00:00
Jeremy Stretch
954d0cfcd0 Closes #16929: Add version & user details as data attributes (#16939)
* Closes #16929: Add version & user details as data attributes

* Fix typo
2024-07-18 12:39:23 -04:00
Benjamin Dale
0830ebb34a Update CONTRIBUTING.md
The GitHub reactions icon has been moved from the top right to the bottom left of messages in Issues - I was going insane trying to find it, so this might help someone in the future ; )
2024-07-16 20:12:17 -04:00
Jeremy Stretch
95cb7b2c34 Changelog for #16402, #16536, #16624, #16819 2024-07-15 09:12:36 -04:00
Arthur Hanson
dde77c83b4 16819 highlight parent device in rack (#16881)
* 16819 highlight parent device in rack

* 16819 review changes
2024-07-15 09:09:26 -04:00
Eric Oswald
e216bebd41 Fix incorrect import in rest-api.md 2024-07-13 10:28:43 -04:00
Thomas Fargeix
d39acfd3f2 Fixes 16536 - Fix filtering of device component by device role (#16553)
* Fixes 16536 - Fix filtering of device component by device role

Rename role and role_id fields to device_role and device_role_id in
DeviceComponentFilterSet

* Update tests for DeviceComponentFilterSet

* Use device_role filter name for DeviceComponentFilterSetTests

* Add test_device_role test in InventoryItemTestCase
2024-07-12 09:13:33 -04:00
Fabian Geisberger
4ea4e57f33 Fixes #16624: Set allow_null=True on method fields that can return None 2024-07-12 09:03:40 -04:00
Arthur Hanson
377543cd9c 16402 remove links from script result table 2024-07-12 08:31:32 -04:00
github-actions
a8827c8472 Update source translation strings 2024-07-12 05:02:03 +00:00
Jeremy Stretch
b2ca6df50a Changelog for #14640, #14792, #15660, #15696, #16793 2024-07-11 14:21:41 -04:00
transifex-integration[bot]
ab6ddd50a8 Updates for project NetBox (#16888)
* Translate django.po in pt

100% reviewed source file: 'django.po'
on 'pt'.

* Translate django.po in es

100% translated source file: 'django.po'
on 'es'.

* Translate django.po in de

100% translated source file: 'django.po'
on 'de'.

* Translate django.po in fr

100% translated source file: 'django.po'
on 'fr'.

* Translate django.po in ru

100% translated source file: 'django.po'
on 'ru'.

* Translate django.po in ja

100% translated source file: 'django.po'
on 'ja'.

* Translate django.po in it

100% translated source file: 'django.po'
on 'it'.

* Translate django.po in cs

100% translated source file: 'django.po'
on 'cs'.

* Translate django.po in zh

100% translated source file: 'django.po'
on 'zh'.

* Translate django.po in nl

100% translated source file: 'django.po'
on 'nl'.

* Translate django.po in da

100% translated source file: 'django.po'
on 'da'.

* Translate django.po in uk

100% translated source file: 'django.po'
on 'uk'.

* Translate django.po in pl

100% translated source file: 'django.po'
on 'pl'.

* Translate django.po in tr

100% translated source file: 'django.po'
on 'tr'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-07-11 14:20:06 -04:00
Jeff Gehlbach
499da4fdcf Added CS, DA, IT, NL, and PL, minus the .po and .mo starting point files (#16810)
* Added CS, DA, IT, NL, and PL, minus the .po and .mo starting point files

* Add initial PO files for new languages

* Revert updates to EN django.po

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-07-11 14:04:36 -04:00
github-actions
4fa396716e Update source translation strings 2024-07-11 05:02:02 +00:00
Jeremy Stretch
6f3a2a599f Changelog for #15375, #16357, #16760, #16838, #16867 2024-07-10 09:16:23 -04:00
Jeremy Stretch
960d2b82b7 Update contact email 2024-07-10 09:14:20 -04:00
the.friendly.net
f2e1de027f Fixes #16760: datasource git on local file system fails (#16872)
* Fixes #16760: datasource git on local file system fails

* Fixes #16760: datasource git on local file system fails

* Set depth & quiet parameters only if using a remote URL

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-07-10 09:10:28 -04:00
Jeff Gehlbach
bf97138c78 Small additions and tweaks to release checklist from releasing v4.0.7 2024-07-10 08:45:36 -04:00
Arthur Hanson
30d711d24a 16838 show extra_buttons if no actions defined 2024-07-10 08:43:27 -04:00
Arthur Hanson
2a8bec1cbf 16867 render dashboard if model no longer available 2024-07-10 08:39:40 -04:00
Arthur Hanson
013139aa20 16357 clone tenant and type for cable 2024-07-10 08:34:48 -04:00
Théophile Bastian
4ca1494127 SSO: custom name for identity providers (#16732) 2024-07-10 13:09:03 +07:00
github-actions
70311a9db5 Update source translation strings 2024-07-10 05:02:10 +00:00
Jeremy Stretch
dd413b248a PRVB 2024-07-09 13:49:24 -04:00
83 changed files with 101339 additions and 19237 deletions

View File

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

View File

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

View File

@@ -16,6 +16,6 @@ jobs:
if: "contains(github.event.issue.labels.*.name, 'status: needs triage')"
with:
# Weighted assignments
assignees: arthanson:3, jeffgdotorg:3, jeremystretch:3, DanSheps
assignees: arthanson:3, jeremystretch:3, DanSheps
numOfAssignee: 1
abortIfPreviousAssignees: true

View File

@@ -40,7 +40,7 @@ NetBox users are welcome to participate in either role, on stage or in the crowd
* First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases) of NetBox. If you're running an older version, it's likely that the bug has already been fixed.
* Next, search our [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the bug you've found has already been reported. If you come across a bug report that seems to match, please click "add a reaction" in the top right corner of the issue and add a thumbs up (:thumbsup:). This will help draw more attention to it. Any comments you can add to provide additional information or context would also be much appreciated.
* Next, search our [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the bug you've found has already been reported. If you come across a bug report that seems to match, please click "add a reaction" in the bottom left corner of the issue and add a thumbs up ( :thumbsup: ). This will help draw more attention to it. Any comments you can add to provide additional information or context would also be much appreciated.
* If you can't find any existing issues (open or closed) that seem to match yours, you're welcome to [submit a new bug report](https://github.com/netbox-community/netbox/issues/new?label=type%3A+bug&template=bug_report.yaml). Be sure to complete the entire report template, including detailed steps that someone triaging your issue can follow to confirm the reported behavior. (If we're not able to replicate the bug based on the information provided, we'll ask for additional detail.)
@@ -56,7 +56,9 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli
## :bulb: Feature Requests
* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the feature you have in mind has already been proposed. If you happen to find an open feature request that matches your idea, click "add a reaction" in the top right corner of the issue and add a thumbs up (:thumbsup:). This ensures that the issue has a better chance of receiving attention. Also feel free to add a comment with any additional justification for the feature.
* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the feature you have in mind has already been proposed. If you happen to find an open feature request that matches your idea, click "add a reaction" in the top right corner of the issue and add a thumbs up ( :thumbsup: ). This ensures that the issue has a better chance of receiving attention. Also feel free to add a comment with any additional justification for the feature.
* Please don't submit duplicate issues! Sometimes we reject feature requests, for various reasons. Even if you disagree with those reasons, please **do not** submit a duplicate feature request. It is very disrepectful of the maintainers' time, and you may be barred from opening future issues.
* If you have a rough idea that's not quite ready for formal submission yet, start a [GitHub discussion](https://github.com/netbox-community/netbox/discussions) instead. This is a great way to test the viability and narrow down the scope of a new feature prior to submitting a formal proposal, and can serve to generate interest in your idea from other community members.

View File

@@ -5,7 +5,7 @@
<a href="https://github.com/netbox-community/netbox/blob/master/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a>
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-10-blue" alt="Languages supported" /></a>
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master" alt="CI status" /></a>
<p></p>
</div>

View File

@@ -16,7 +16,7 @@ Administrators are encouraged to adhere to industry best practices concerning th
## Reporting a Suspected Vulnerability
If you believe you've uncovered a security vulnerability and wish to report it confidentially, you may do so via email. Please note that any reported vulnerabilities **MUST** meet all the following conditions:
If you believe you've uncovered a security vulnerability and wish to report it confidentially, you may do so by emailing `security@netboxlabs.com`. Please ensure that your report meets all the following conditions:
* Affects the most recent stable release of NetBox, or a current beta release
* Affects a NetBox instance installed and configured per the official documentation
@@ -24,7 +24,7 @@ If you believe you've uncovered a security vulnerability and wish to report it c
Please note that we **DO NOT** accept reports generated by automated tooling which merely suggest that a file or file(s) _may_ be vulnerable under certain conditions, as these are most often innocuous.
If you believe that you've found a vulnerability which meets all of these conditions, please [submit a draft security advisory](https://github.com/netbox-community/netbox/security/advisories/new) on GitHub. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project.
For any security concerns regarding the community-maintained Docker image for NetBox, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project.
### Bug Bounties

View File

@@ -10,7 +10,7 @@ django-cors-headers
# https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst
# Pinned for DNS looukp bug; see https://github.com/netbox-community/netbox/issues/16454
# and https://github.com/jazzband/django-debug-toolbar/issues/1927
django-debug-toolbar==4.3.0
django-debug-toolbar
# Library for writing reusable URL query filters
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst

View File

@@ -377,6 +377,7 @@
"ieee802.11ad",
"ieee802.11ax",
"ieee802.11ay",
"ieee802.11be",
"ieee802.15.1",
"other-wireless",
"gsm",

View File

@@ -40,3 +40,22 @@ REMOTE_AUTH_BACKEND = 'social_core.backends.google.GoogleOAuth2'
NetBox supports single sign-on authentication via the [python-social-auth](https://github.com/python-social-auth) library. To enable SSO, specify the path to the desired authentication backend within the `social_core` Python package. Please see the complete list of [supported authentication backends](https://github.com/python-social-auth/social-core/tree/master/social_core/backends) for the available options.
Most remote authentication backends require some additional configuration through settings prefixed with `SOCIAL_AUTH_`. These will be automatically imported from NetBox's `configuration.py` file. Additionally, the [authentication pipeline](https://python-social-auth.readthedocs.io/en/latest/pipeline.html) can be customized via the `SOCIAL_AUTH_PIPELINE` parameter. (NetBox's default pipeline is defined in `netbox/settings.py` for your reference.)
#### Configuring the SSO module's appearance
The way a remote authentication backend is displayed to the user on the login
page may be adjusted via the `SOCIAL_AUTH_BACKEND_ATTRS` parameter, defaulting
to an empty dictionary. This dictionary maps a `social_core` module's name (ie.
`REMOTE_AUTH_BACKEND.name`) to a couple of parameters, `(display_name, icon)`.
The `display_name` is the name displayed to the user on the login page. The
icon may either be the URL of an icon; refer to a [Material Design
Icons](https://github.com/google/material-design-icons) icon's name; or be
`None` for no icon.
For instance, the OIDC backend may be customized with
```python
SOCIAL_AUTH_BACKEND_ATTRS = {
'oidc': ("My awesome SSO", "login"),
}
```

View File

@@ -113,7 +113,7 @@ Create a [new release](https://github.com/netbox-community/netbox/releases/new)
* **Tag:** Current version (e.g. `v3.3.1`)
* **Target:** `master`
* **Title:** Version and date (e.g. `v3.3.1 - 2022-08-25`)
* **Description:** Copy from the pull request body
* **Description:** Copy from the pull request body, then promote the `###` headers to `##` ones
Once created, the release will become available for users to install.
@@ -135,6 +135,6 @@ First, run the `build-site` action, by navigating to Actions > build-site > Run
Once the documentation files have been compiled, they must be published by running the `deploy-kinsta` action. Select the desired deployment environment (staging or production) and specify `latest` as the deploy tag.
Clear the CDN cache from the [Kinsta](https://my.kinsta.com/) portal. Navigate to _Sites_ / _NetBox Labs_ / _Live_, select _CDN_ in the left-nav, click the _Clear CDN cache_ button, and confirm the clear operation.
Clear the CDN cache from the [Kinsta](https://my.kinsta.com/) portal. Navigate to _Sites_ / _NetBox Labs_ / _Live_, select _Cache_ in the left-nav, click the _Clear Cache_ button, and confirm the clear operation.
Finally, verify that the documentation at <https://netboxlabs.com/docs/netbox/en/stable/> has been updated.

View File

@@ -20,6 +20,8 @@ Then, commit the change and push to the `develop` branch on GitHub. Any new stri
Typically, translated strings need to be updated only as part of the NetBox [release process](./release-checklist.md).
Check the Transifex dashboard for languages that are not marked _ready for use_, being sure to click _Show all languages_ if it appears at the bottom of the list. Use machine translation to round out any not-ready languages. It's not necessary to review the machine translation immediately as the translation teams will handle that aspect; the goal at this stage is to get translations included in the Transifex pull request.
To update translated strings, start by initiating a sync from Transifex. From the Transifex dashboard, navigate to Settings > Integrations > GitHub > Manage, and click the **Manual Sync** button at top right.
![Transifex manual sync](../media/development/transifex_sync.png)

View File

@@ -84,11 +84,11 @@ To create a viewset for a plugin model, subclass `NetBoxModelViewSet` in `api/vi
```python
# api/views.py
from netbox.api.viewsets import ModelViewSet
from netbox.api.viewsets import NetBoxModelViewSet
from my_plugin.models import MyModel
from .serializers import MyModelSerializer
class MyModelViewSet(ModelViewSet):
class MyModelViewSet(NetBoxModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
```

View File

@@ -1,5 +1,54 @@
# NetBox v4.0
## v4.0.9 (2024-08-14)
### Enhancements
* [#16692](https://github.com/netbox-community/netbox/issues/16692) - Enable modifying VLAN assignment while bulk editing prefixes
* [#17006](https://github.com/netbox-community/netbox/issues/17006) - Add IEEE 802.11be interface type
### Bug Fixes
* [#13459](https://github.com/netbox-community/netbox/issues/13459) - Correct OpenAPI schema type for `TreeNodeMultipleChoiceFilter`
* [#16073](https://github.com/netbox-community/netbox/issues/16073) - Respect default values for custom fields during bulk import of objects
* [#16176](https://github.com/netbox-community/netbox/issues/16176) - Restore ability to select multiple terminating devices when connecting a cable
* [#16871](https://github.com/netbox-community/netbox/issues/16871) - Sanitize device ID query parameter when bulk editing components to prevent exception
* [#17038](https://github.com/netbox-community/netbox/issues/17038) - Fix AttributeError exception when attempting to export system status data
* [#17064](https://github.com/netbox-community/netbox/issues/17064) - Fix misaligned text within rendered Markdown code blocks
* [#17124](https://github.com/netbox-community/netbox/issues/17124) - `BaseTable` should follow reverse one-to-one relationships when prefetching related objects
* [#17131](https://github.com/netbox-community/netbox/issues/17131) - Fix exception when creating object-type custom field without selecting related object type
* [#17144](https://github.com/netbox-community/netbox/issues/17144) - Avoid showing duplicated pop-up messages
---
## v4.0.8 (2024-07-26)
### Enhancements
* [#14640](https://github.com/netbox-community/netbox/issues/14640) - Add Dutch language support
* [#14792](https://github.com/netbox-community/netbox/issues/14792) - Add Polish language support
* [#15375](https://github.com/netbox-community/netbox/issues/15375) - Enable customization of SSO backend name & icon
* [#15660](https://github.com/netbox-community/netbox/issues/15660) - Add Czech language support
* [#15696](https://github.com/netbox-community/netbox/issues/15696) - Add Danish language support
* [#16793](https://github.com/netbox-community/netbox/issues/16793) - Add Italian language support
* [#16933](https://github.com/netbox-community/netbox/issues/16933) - Enable toggling true/false marks on BooleanColumn
* [#16943](https://github.com/netbox-community/netbox/issues/16943) - Expand navigation breadcrumbs on job view to include the parent object
### Bug Fixes
* [#16357](https://github.com/netbox-community/netbox/issues/16357) - Replicate assigned type & tenant for cable when clicking "create an add another"
* [#16402](https://github.com/netbox-community/netbox/issues/16402) - Remove inoperative links from report result view
* [#16536](https://github.com/netbox-community/netbox/issues/16536) - Revert `role` & `role_id` filters for device components to `device_role` & `device_role_id` to avoid conflict with inventory item `role` field
* [#16624](https://github.com/netbox-community/netbox/issues/16624) - Correct OpenAPI schema definitions for several fields
* [#16760](https://github.com/netbox-community/netbox/issues/16760) - Fix data source syncing using git via a local path
* [#16819](https://github.com/netbox-community/netbox/issues/16819) - Highlight parent device in rack when viewing child device
* [#16838](https://github.com/netbox-community/netbox/issues/16838) - ActionsColumn should render extra buttons even when no stock actions are enabled
* [#16867](https://github.com/netbox-community/netbox/issues/16867) - Fix exception when a dashboard list widget references a model which has been removed
* [#16963](https://github.com/netbox-community/netbox/issues/16963) - Fix filtering of "accounts" link under providers list
* [#16964](https://github.com/netbox-community/netbox/issues/16964) - Ensure configured password validators are enforced
---
## v4.0.7 (2024-07-09)
### Enhancements

View File

@@ -44,10 +44,20 @@ class LoginView(View):
return super().dispatch(*args, **kwargs)
def gen_auth_data(self, name, url, params):
display_name, icon_name = get_auth_backend_display(name)
display_name, icon_source = get_auth_backend_display(name)
icon_name = None
icon_img = None
if icon_source:
if '://' in icon_source:
icon_img = icon_source
else:
icon_name = icon_source
return {
'display_name': display_name,
'icon_name': icon_name,
'icon_img': icon_img,
'url': f'{url}?{urlencode(params)}',
}
@@ -99,7 +109,7 @@ class LoginView(View):
# Authenticate user
auth_login(request, form.get_user())
logger.info(f"User {request.user} successfully authenticated")
messages.success(request, f"Logged in as {request.user}.")
messages.success(request, _("Logged in as {user}.").format(user=request.user))
# Ensure the user has a UserConfig defined. (This should normally be handled by
# create_userconfig() on user creation.)
@@ -149,7 +159,7 @@ class LogoutView(View):
username = request.user
auth_logout(request)
logger.info(f"User {username} has logged out")
messages.info(request, "You have logged out.")
messages.info(request, _("You have logged out."))
# Delete session key & language cookies (if set) upon logout
response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
@@ -224,7 +234,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
def get(self, request):
# LDAP users cannot change their password here
if getattr(request.user, 'ldap_username', None):
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
messages.warning(request, _("LDAP-authenticated user credentials cannot be changed within NetBox."))
return redirect('account:profile')
form = PasswordChangeForm(user=request.user)
@@ -239,7 +249,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
if form.is_valid():
form.save()
update_session_auth_hash(request, form.user)
messages.success(request, "Your password has been changed successfully.")
messages.success(request, _("Your password has been changed successfully."))
return redirect('account:profile')
return render(request, self.template_name, {

View File

@@ -25,7 +25,7 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
account_count = columns.LinkedCountColumn(
accessor=tables.A('accounts__count'),
viewname='circuits:provideraccount_list',
url_params={'account_id': 'pk'},
url_params={'provider_id': 'pk'},
verbose_name=_('Account Count')
)
asns = columns.ManyToManyColumn(

View File

@@ -1,6 +1,7 @@
from django.contrib import messages
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import gettext_lazy as _
from dcim.views import PathTraceView
from netbox.views import generic
@@ -326,7 +327,9 @@ class CircuitSwapTerminations(generic.ObjectEditView):
# Circuit must have at least one termination to swap
if not circuit.termination_a and not circuit.termination_z:
messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
messages.error(request, _(
"No terminations have been defined for circuit {circuit}."
).format(circuit=circuit))
return redirect('circuits:circuit', pk=circuit.pk)
return render(request, 'circuits/circuit_terminations_swap.html', {
@@ -374,7 +377,7 @@ class CircuitSwapTerminations(generic.ObjectEditView):
circuit.termination_z = None
circuit.save()
messages.success(request, f"Swapped terminations for circuit {circuit}.")
messages.success(request, _("Swapped terminations for circuit {circuit}.").format(circuit=circuit))
return redirect('circuits:circuit', pk=circuit.pk)
return render(request, 'circuits/circuit_terminations_swap.html', {

View File

@@ -84,9 +84,7 @@ class GitBackend(DataBackend):
clone_args = {
"branch": self.params.get('branch'),
"config": self.config,
"depth": 1,
"errstream": porcelain.NoneStream(),
"quiet": True,
}
if self.url_scheme in ('http', 'https'):
@@ -97,6 +95,9 @@ class GitBackend(DataBackend):
"password": self.params.get('password'),
}
)
if self.url_scheme:
clone_args["quiet"] = True
clone_args["depth"] = 1
logger.debug(f"Cloning git repo: {self.url}")
try:

View File

@@ -19,6 +19,7 @@ REVISION_BUTTONS = """
class ConfigRevisionTable(NetBoxTable):
is_active = columns.BooleanColumn(
verbose_name=_('Is Active'),
false_mark=None
)
actions = columns.ActionsColumn(
actions=('delete',),

View File

@@ -76,7 +76,10 @@ class DataSourceSyncView(BaseObjectView):
datasource = get_object_or_404(self.queryset, pk=pk)
job = datasource.enqueue_sync_job(request)
messages.success(request, f"Queued job #{job.pk} to sync {datasource}")
messages.success(
request,
_("Queued job #{id} to sync {datasource}").format(id=job.pk, datasource=datasource)
)
return redirect(datasource.get_absolute_url())
@@ -235,7 +238,7 @@ class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
candidate_config = get_object_or_404(ConfigRevision, pk=pk)
candidate_config.activate()
messages.success(request, f"Restored configuration revision #{pk}")
messages.success(request, _("Restored configuration revision #{id}").format(id=pk))
return redirect(candidate_config.get_absolute_url())
@@ -379,9 +382,9 @@ class BackgroundTaskDeleteView(BaseRQView):
# Remove job id from queue and delete the actual job
queue.connection.lrem(queue.key, 0, job.id)
job.delete()
messages.success(request, f'Deleted job {job_id}')
messages.success(request, _('Job {id} has been deleted.').format(id=job_id))
else:
messages.error(request, f'Error deleting job: {form.errors[0]}')
messages.error(request, _('Error deleting job {id}: {error}').format(id=job_id, error=form.errors[0]))
return redirect(reverse('core:background_queue_list'))
@@ -394,13 +397,13 @@ class BackgroundTaskRequeueView(BaseRQView):
try:
job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),)
except NoSuchJobError:
raise Http404(_("Job {job_id} not found").format(job_id=job_id))
raise Http404(_("Job {id} not found.").format(id=job_id))
queue_index = QUEUES_MAP[job.origin]
queue = get_queue_by_index(queue_index)
requeue_job(job_id, connection=queue.connection, serializer=queue.serializer)
messages.success(request, f'You have successfully requeued: {job_id}')
messages.success(request, _('Job {id} has been re-enqueued.').format(id=job_id))
return redirect(reverse('core:background_task', args=[job_id]))
@@ -412,7 +415,7 @@ class BackgroundTaskEnqueueView(BaseRQView):
try:
job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),)
except NoSuchJobError:
raise Http404(_("Job {job_id} not found").format(job_id=job_id))
raise Http404(_("Job {id} not found.").format(id=job_id))
queue_index = QUEUES_MAP[job.origin]
queue = get_queue_by_index(queue_index)
@@ -435,7 +438,7 @@ class BackgroundTaskEnqueueView(BaseRQView):
registry = ScheduledJobRegistry(queue.name, queue.connection)
registry.remove(job)
messages.success(request, f'You have successfully enqueued: {job_id}')
messages.success(request, _('Job {id} has been enqueued.').format(id=job_id))
return redirect(reverse('core:background_task', args=[job_id]))
@@ -452,11 +455,11 @@ class BackgroundTaskStopView(BaseRQView):
queue_index = QUEUES_MAP[job.origin]
queue = get_queue_by_index(queue_index)
stopped, _ = stop_jobs(queue, job_id)
if len(stopped) == 1:
messages.success(request, f'You have successfully stopped {job_id}')
stopped_jobs = stop_jobs(queue, job_id)[0]
if len(stopped_jobs) == 1:
messages.success(request, _('Job {id} has been stopped.').format(id=job_id))
else:
messages.error(request, f'Failed to stop {job_id}')
messages.error(request, _('Failed to stop job {id}').format(id=job_id))
return redirect(reverse('core:background_task', args=[job_id]))
@@ -559,13 +562,14 @@ class SystemView(UserPassesTestMixin, View):
# Raw data export
if 'export' in request.GET:
params = [param.name for param in PARAMS]
data = {
**stats,
'plugins': {
plugin.name: plugin.version for plugin in plugins
},
'config': {
k: config.data[k] for k in sorted(config.data)
k: getattr(config, k) for k in sorted(params)
},
}
response = HttpResponse(json.dumps(data, indent=4), content_type='text/json')

View File

@@ -13,7 +13,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
"""
Legacy serializer for pre-v3.3 connections
"""
connected_endpoints_type = serializers.SerializerMethodField(read_only=True)
connected_endpoints_type = serializers.SerializerMethodField(read_only=True, allow_null=True)
connected_endpoints = serializers.SerializerMethodField(read_only=True)
connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True)
@@ -22,7 +22,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
if endpoints := obj.connected_endpoints:
return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}'
@extend_schema_field(serializers.ListField)
@extend_schema_field(serializers.ListField(allow_null=True))
def get_connected_endpoints(self, obj):
"""
Return the appropriate serializer for the type of connected object.

View File

@@ -91,7 +91,7 @@ class CablePathSerializer(serializers.ModelSerializer):
class CabledObjectSerializer(serializers.ModelSerializer):
cable = CableSerializer(nested=True, read_only=True, allow_null=True)
cable_end = serializers.CharField(read_only=True)
link_peers_type = serializers.SerializerMethodField(read_only=True)
link_peers_type = serializers.SerializerMethodField(read_only=True, allow_null=True)
link_peers = serializers.SerializerMethodField(read_only=True)
_occupied = serializers.SerializerMethodField(read_only=True)

View File

@@ -88,7 +88,7 @@ class DeviceSerializer(NetBoxModelSerializer):
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
@extend_schema_field(NestedDeviceSerializer)
@extend_schema_field(NestedDeviceSerializer(allow_null=True))
def get_parent_device(self, obj):
try:
device_bay = obj.parent_bay

View File

@@ -886,6 +886,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_80211AD = 'ieee802.11ad'
TYPE_80211AX = 'ieee802.11ax'
TYPE_80211AY = 'ieee802.11ay'
TYPE_80211BE = 'ieee802.11be'
TYPE_802151 = 'ieee802.15.1'
TYPE_OTHER_WIRELESS = 'other-wireless'
@@ -1057,6 +1058,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_80211AD, 'IEEE 802.11ad'),
(TYPE_80211AX, 'IEEE 802.11ax'),
(TYPE_80211AY, 'IEEE 802.11ay'),
(TYPE_80211BE, 'IEEE 802.11be'),
(TYPE_802151, 'IEEE 802.15.1 (Bluetooth)'),
(TYPE_OTHER_WIRELESS, 'Other (Wireless)'),
)

View File

@@ -49,6 +49,7 @@ WIRELESS_IFACE_TYPES = [
InterfaceTypeChoices.TYPE_80211AD,
InterfaceTypeChoices.TYPE_80211AX,
InterfaceTypeChoices.TYPE_80211AY,
InterfaceTypeChoices.TYPE_80211BE,
InterfaceTypeChoices.TYPE_802151,
InterfaceTypeChoices.TYPE_OTHER_WIRELESS,
]

View File

@@ -1389,12 +1389,12 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
to_field_name='model',
label=_('Device type (model)'),
)
role_id = django_filters.ModelMultipleChoiceFilter(
device_role_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__role',
queryset=DeviceRole.objects.all(),
label=_('Device role (ID)'),
)
role = django_filters.ModelMultipleChoiceFilter(
device_role = django_filters.ModelMultipleChoiceFilter(
field_name='device__role__slug',
queryset=DeviceRole.objects.all(),
to_field_name='slug',

View File

@@ -1188,12 +1188,17 @@ class ComponentBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __init__(self, *args, initial=None, **kwargs):
try:
self.device_id = int(initial.get('device'))
except (TypeError, ValueError):
self.device_id = None
super().__init__(*args, initial=initial, **kwargs)
# Limit module queryset to Modules which belong to the parent Device
if 'device' in self.initial:
device = Device.objects.filter(pk=self.initial['device']).first()
if self.device_id:
device = Device.objects.filter(pk=self.device_id).first()
self.fields['module'].queryset = Module.objects.filter(device=device)
else:
self.fields['module'].choices = ()
@@ -1201,8 +1206,8 @@ class ComponentBulkEditForm(NetBoxModelBulkEditForm):
class ConsolePortBulkEditForm(
form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description']),
ComponentBulkEditForm
ComponentBulkEditForm,
form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description'])
):
mark_connected = forms.NullBooleanField(
label=_('Mark connected'),
@@ -1218,8 +1223,8 @@ class ConsolePortBulkEditForm(
class ConsoleServerPortBulkEditForm(
form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description']),
ComponentBulkEditForm
ComponentBulkEditForm,
form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description'])
):
mark_connected = forms.NullBooleanField(
label=_('Mark connected'),
@@ -1235,8 +1240,8 @@ class ConsoleServerPortBulkEditForm(
class PowerPortBulkEditForm(
form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description']),
ComponentBulkEditForm
ComponentBulkEditForm,
form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description'])
):
mark_connected = forms.NullBooleanField(
label=_('Mark connected'),
@@ -1253,8 +1258,8 @@ class PowerPortBulkEditForm(
class PowerOutletBulkEditForm(
form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']),
ComponentBulkEditForm
ComponentBulkEditForm,
form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description'])
):
mark_connected = forms.NullBooleanField(
label=_('Mark connected'),
@@ -1273,8 +1278,8 @@ class PowerOutletBulkEditForm(
super().__init__(*args, **kwargs)
# Limit power_port queryset to PowerPorts which belong to the parent Device
if 'device' in self.initial:
device = Device.objects.filter(pk=self.initial['device']).first()
if self.device_id:
device = Device.objects.filter(pk=self.device_id).first()
self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
else:
self.fields['power_port'].choices = ()
@@ -1282,12 +1287,12 @@ class PowerOutletBulkEditForm(
class InterfaceBulkEditForm(
ComponentBulkEditForm,
form_from_model(Interface, [
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
'tx_power', 'wireless_lans'
]),
ComponentBulkEditForm
])
):
enabled = forms.NullBooleanField(
label=_('Enabled'),
@@ -1416,8 +1421,8 @@ class InterfaceBulkEditForm(
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'device' in self.initial:
device = Device.objects.filter(pk=self.initial['device']).first()
if self.device_id:
device = Device.objects.filter(pk=self.device_id).first()
# Restrict parent/bridge/LAG interface assignment by device
self.fields['parent'].widget.add_query_param('virtual_chassis_member_id', device.pk)
@@ -1480,8 +1485,8 @@ class InterfaceBulkEditForm(
class FrontPortBulkEditForm(
form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']),
ComponentBulkEditForm
ComponentBulkEditForm,
form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description'])
):
mark_connected = forms.NullBooleanField(
label=_('Mark connected'),
@@ -1497,8 +1502,8 @@ class FrontPortBulkEditForm(
class RearPortBulkEditForm(
form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']),
ComponentBulkEditForm
ComponentBulkEditForm,
form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description'])
):
mark_connected = forms.NullBooleanField(
label=_('Mark connected'),

View File

@@ -19,7 +19,7 @@ def get_cable_form(a_type, b_type):
# Device component
if hasattr(term_cls, 'device'):
attrs[f'termination_{cable_end}_device'] = DynamicModelChoiceField(
attrs[f'termination_{cable_end}_device'] = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
label=_('Device'),
required=False,
@@ -33,6 +33,7 @@ def get_cable_form(a_type, b_type):
label=term_cls._meta.verbose_name.title(),
context={
'disabled': '_occupied',
'parent': 'device',
},
query_params={
'device_id': f'$termination_{cable_end}_device',
@@ -43,7 +44,7 @@ def get_cable_form(a_type, b_type):
# PowerFeed
elif term_cls == PowerFeed:
attrs[f'termination_{cable_end}_powerpanel'] = DynamicModelChoiceField(
attrs[f'termination_{cable_end}_powerpanel'] = DynamicModelMultipleChoiceField(
queryset=PowerPanel.objects.all(),
label=_('Power Panel'),
required=False,
@@ -57,6 +58,7 @@ def get_cable_form(a_type, b_type):
label=_('Power Feed'),
context={
'disabled': '_occupied',
'parent': 'powerpanel',
},
query_params={
'power_panel_id': f'$termination_{cable_end}_powerpanel',
@@ -66,7 +68,7 @@ def get_cable_form(a_type, b_type):
# CircuitTermination
elif term_cls == CircuitTermination:
attrs[f'termination_{cable_end}_circuit'] = DynamicModelChoiceField(
attrs[f'termination_{cable_end}_circuit'] = DynamicModelMultipleChoiceField(
queryset=Circuit.objects.all(),
label=_('Circuit'),
selector=True,
@@ -79,6 +81,7 @@ def get_cable_form(a_type, b_type):
label=_('Side'),
context={
'disabled': '_occupied',
'parent': 'circuit',
},
query_params={
'circuit_id': f'$termination_{cable_end}_circuit',

View File

@@ -88,6 +88,8 @@ class Cable(PrimaryModel):
null=True
)
clone_fields = ('tenant', 'type',)
class Meta:
ordering = ('pk',)
verbose_name = _('cable')

View File

@@ -63,7 +63,10 @@ class DeviceRoleTable(NetBoxTable):
verbose_name=_('VMs')
)
color = columns.ColorColumn()
vm_role = columns.BooleanColumn()
vm_role = columns.BooleanColumn(
verbose_name=_('VM role'),
false_mark=None
)
config_template = tables.Column(
linkify=True
)
@@ -329,6 +332,7 @@ class CableTerminationTable(NetBoxTable):
)
mark_connected = columns.BooleanColumn(
verbose_name=_('Mark Connected'),
false_mark=None
)
class Meta:
@@ -586,7 +590,8 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
}
)
mgmt_only = columns.BooleanColumn(
verbose_name=_('Management Only')
verbose_name=_('Management Only'),
false_mark=None
)
speed_formatted = columns.TemplateColumn(
template_code='{% load helpers %}{{ value|humanize_speed }}',
@@ -913,6 +918,7 @@ class InventoryItemTable(DeviceComponentTable):
)
discovered = columns.BooleanColumn(
verbose_name=_('Discovered'),
false_mark=None
)
parent = tables.Column(
linkify=True,

View File

@@ -86,7 +86,8 @@ class DeviceTypeTable(NetBoxTable):
linkify=True
)
is_full_depth = columns.BooleanColumn(
verbose_name=_('Full Depth')
verbose_name=_('Full Depth'),
false_mark=None
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
@@ -98,7 +99,10 @@ class DeviceTypeTable(NetBoxTable):
verbose_name=_('U Height'),
template_code='{{ value|floatformat }}'
)
exclude_from_utilization = columns.BooleanColumn()
exclude_from_utilization = columns.BooleanColumn(
verbose_name=_('Exclude from utilization'),
false_mark=None
)
weight = columns.TemplateColumn(
verbose_name=_('Weight'),
template_code=WEIGHT,
@@ -221,7 +225,8 @@ class InterfaceTemplateTable(ComponentTemplateTable):
verbose_name=_('Enabled'),
)
mgmt_only = columns.BooleanColumn(
verbose_name=_('Management Only')
verbose_name=_('Management Only'),
false_mark=None
)
actions = columns.ActionsColumn(
actions=('edit', 'delete'),

View File

@@ -32,11 +32,11 @@ class DeviceComponentFilterSetTests:
params = {'device_type': [device_types[0].model, device_types[1].model]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_role(self):
def test_device_role(self):
role = DeviceRole.objects.all()[:2]
params = {'role_id': [role[0].pk, role[1].pk]}
params = {'device_role_id': [role[0].pk, role[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'role': [role[0].slug, role[1].slug]}
params = {'device_role': [role[0].slug, role[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -4547,6 +4547,13 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'device_type': [device_types[0].model, device_types[1].model]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_device_role(self):
role = DeviceRole.objects.all()[:2]
params = {'device_role_id': [role[0].pk, role[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'device_role': [role[0].slug, role[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_role(self):
role = DeviceRole.objects.all()[:2]
params = {'role_id': [role[0].pk, role[1].pk]}

View File

@@ -2059,7 +2059,7 @@ class DeviceRenderConfigView(generic.ObjectView):
try:
rendered_config = config_template.render(context=context_data)
except TemplateError as e:
messages.error(request, f"An error occurred while rendering the template: {e}")
messages.error(request, _("An error occurred while rendering the template: {error}").format(error=e))
rendered_config = traceback.format_exc()
return {
@@ -2823,7 +2823,13 @@ class DeviceBayPopulateView(generic.ObjectEditView):
device_bay.snapshot()
device_bay.installed_device = form.cleaned_data['installed_device']
device_bay.save()
messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay))
messages.success(
request,
_("Installed device {device} in bay {device_bay}.").format(
device=device_bay.installed_device,
device_bay=device_bay
)
)
return_url = self.get_return_url(request)
return redirect(return_url)
@@ -2858,7 +2864,13 @@ class DeviceBayDepopulateView(generic.ObjectEditView):
removed_device = device_bay.installed_device
device_bay.installed_device = None
device_bay.save()
messages.success(request, f"{removed_device} has been removed from {device_bay}.")
messages.success(
request,
_("Removed device {device} from bay {device_bay}.").format(
device=removed_device,
device_bay=device_bay
)
)
return_url = self.get_return_url(request, device_bay.device)
return redirect(return_url)
@@ -3426,7 +3438,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
membership_form.save()
messages.success(request, mark_safe(
f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>'
_('Added member <a href="{url}">{escape(device)}</a>').format(url=device.get_absolute_url())
))
if '_addanother' in request.POST:
@@ -3471,7 +3483,10 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
# Protect master device from being removed
virtual_chassis = VirtualChassis.objects.filter(master=device).first()
if virtual_chassis is not None:
messages.error(request, f'Unable to remove master device {device} from the virtual chassis.')
messages.error(
request,
_('Unable to remove master device {device} from the virtual chassis.').format(device=device)
)
return redirect(device.get_absolute_url())
if form.is_valid():
@@ -3483,7 +3498,10 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
device.vc_priority = None
device.save()
msg = 'Removed {} from virtual chassis {}'.format(device, device.virtual_chassis)
msg = _('Removed {device} from virtual chassis {chassis}').format(
device=device,
chassis=device.virtual_chassis
)
messages.success(request, msg)
return redirect(self.get_return_url(request, device))

View File

@@ -39,7 +39,7 @@ class ScriptSerializer(ValidatedModelSerializer):
def get_display(self, obj):
return f'{obj.name} ({obj.module})'
@extend_schema_field(serializers.CharField())
@extend_schema_field(serializers.CharField(allow_null=True))
def get_description(self, obj):
if obj.python_class:
return obj.python_class().description

View File

@@ -251,6 +251,10 @@ class ObjectListWidget(DashboardWidget):
def render(self, request):
app_label, model_name = self.config['model'].split('.')
model = ObjectType.objects.get_by_natural_key(app_label, model_name).model_class()
if not model:
logger.debug(f"Dashboard Widget model_class not found: {app_label}:{model_name}")
return
viewname = get_viewname(model, action='list')
# Evaluate user's permission. Note that this controls only whether the HTMX element is

View File

@@ -352,13 +352,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
if not self.related_object_type:
raise ValidationError({
'object_type': _("Object fields must define an object type.")
'related_object_type': _("Object fields must define an object type.")
})
elif self.related_object_type:
raise ValidationError({
'object_type': _(
"{type} fields may not define an object type.")
.format(type=self.get_type_display())
'type': _("{type} fields may not define an object type.") .format(type=self.get_type_display())
})
def serialize(self, value):

View File

@@ -47,7 +47,8 @@ class CustomFieldTable(NetBoxTable):
verbose_name=_('Object Types')
)
required = columns.BooleanColumn(
verbose_name=_('Required')
verbose_name=_('Required'),
false_mark=None
)
ui_visible = columns.ChoiceFieldColumn(
verbose_name=_('Visible')
@@ -72,6 +73,7 @@ class CustomFieldTable(NetBoxTable):
)
is_cloneable = columns.BooleanColumn(
verbose_name=_('Is Cloneable'),
false_mark=None
)
class Meta(NetBoxTable.Meta):
@@ -105,6 +107,7 @@ class CustomFieldChoiceSetTable(NetBoxTable):
)
order_alphabetically = columns.BooleanColumn(
verbose_name=_('Order Alphabetically'),
false_mark=None
)
class Meta(NetBoxTable.Meta):
@@ -129,6 +132,7 @@ class CustomLinkTable(NetBoxTable):
)
new_window = columns.BooleanColumn(
verbose_name=_('New Window'),
false_mark=None
)
class Meta(NetBoxTable.Meta):
@@ -150,6 +154,7 @@ class ExportTemplateTable(NetBoxTable):
)
as_attachment = columns.BooleanColumn(
verbose_name=_('As Attachment'),
false_mark=None
)
data_source = tables.Column(
verbose_name=_('Data Source'),
@@ -218,6 +223,7 @@ class SavedFilterTable(NetBoxTable):
)
shared = columns.BooleanColumn(
verbose_name=_('Shared'),
false_mark=None
)
def value_parameters(self, value):

View File

@@ -221,6 +221,19 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
'group_id': '$site_group',
}
)
vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
label=_('VLAN Group')
)
vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
label=_('VLAN'),
query_params={
'group_id': '$vlan_group',
}
)
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
@@ -269,9 +282,10 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
FieldSet('tenant', 'status', 'role', 'description'),
FieldSet('region', 'site_group', 'site', name=_('Site')),
FieldSet('vrf', 'prefix_length', 'is_pool', 'mark_utilized', name=_('Addressing')),
FieldSet('vlan_group', 'vlan', name=_('VLAN Assignment')),
)
nullable_fields = (
'site', 'vrf', 'tenant', 'role', 'description', 'comments',
'site', 'vlan', 'vrf', 'tenant', 'role', 'description', 'comments',
)

View File

@@ -86,7 +86,8 @@ class RIRTable(NetBoxTable):
linkify=True
)
is_private = columns.BooleanColumn(
verbose_name=_('Private')
verbose_name=_('Private'),
false_mark=None
)
aggregate_count = columns.LinkedCountColumn(
viewname='ipam:aggregate_list',
@@ -258,10 +259,12 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
linkify=True
)
is_pool = columns.BooleanColumn(
verbose_name=_('Pool')
verbose_name=_('Pool'),
false_mark=None
)
mark_utilized = columns.BooleanColumn(
verbose_name=_('Marked Utilized')
verbose_name=_('Marked Utilized'),
false_mark=None
)
utilization = PrefixUtilizationColumn(
verbose_name=_('Utilization'),
@@ -314,7 +317,8 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
linkify=True
)
mark_utilized = columns.BooleanColumn(
verbose_name=_('Marked Utilized')
verbose_name=_('Marked Utilized'),
false_mark=None
)
utilization = columns.UtilizationColumn(
verbose_name=_('Utilization'),
@@ -386,7 +390,8 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
assigned = columns.BooleanColumn(
accessor='assigned_object_id',
linkify=lambda record: record.assigned_object.get_absolute_url(),
verbose_name=_('Assigned')
verbose_name=_('Assigned'),
false_mark=None
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),

View File

@@ -211,6 +211,7 @@ class InterfaceVLANTable(NetBoxTable):
)
tagged = columns.BooleanColumn(
verbose_name=_('Tagged'),
false_mark=None
)
site = tables.Column(
verbose_name=_('Site'),

View File

@@ -30,7 +30,8 @@ class VRFTable(TenancyColumnsMixin, NetBoxTable):
verbose_name=_('RD')
)
enforce_unique = columns.BooleanColumn(
verbose_name=_('Unique')
verbose_name=_('Unique'),
false_mark=None
)
import_targets = columns.TemplateColumn(
verbose_name=_('Import Targets'),

View File

@@ -49,12 +49,15 @@ AUTH_BACKEND_ATTRS = {
'okta-openidconnect': ('Okta (OIDC)', None),
'salesforce-oauth2': ('Salesforce', 'salesforce'),
}
# Override with potential user configuration
AUTH_BACKEND_ATTRS.update(getattr(settings, 'SOCIAL_AUTH_BACKEND_ATTRS', {}))
def get_auth_backend_display(name):
"""
Return the user-friendly name and icon name for a remote authentication backend, if known. Defaults to the
raw backend name and no icon.
Return the user-friendly name and icon name for a remote authentication backend, if
known. Obtained from the defaults dictionary AUTH_BACKEND_ATTRS, overridden by the
setting `SOCIAL_AUTH_BACKEND_ATTRS`. Defaults to the raw backend name and no icon.
"""
return AUTH_BACKEND_ATTRS.get(name, (name, None))

View File

@@ -25,7 +25,7 @@ from utilities.string import trailing_slash
# Environment setup
#
VERSION = '4.0.7'
VERSION = '4.0.9'
HOSTNAME = platform.node()
# Set the base directory two levels up
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -739,11 +739,16 @@ RQ_QUEUES.update({
# Supported translation languages
LANGUAGES = (
('cs', _('Czech')),
('da', _('Danish')),
('de', _('German')),
('en', _('English')),
('es', _('Spanish')),
('fr', _('French')),
('it', _('Italian')),
('ja', _('Japanese')),
('nl', _('Dutch')),
('pl', _('Polish')),
('pt', _('Portuguese')),
('ru', _('Russian')),
('tr', _('Turkish')),

View File

@@ -194,14 +194,23 @@ class BooleanColumn(tables.Column):
Custom implementation of BooleanColumn to render a nicely-formatted checkmark or X icon instead of a Unicode
character.
"""
TRUE_MARK = mark_safe('<span class="text-success"><i class="mdi mdi-check-bold"></i></span>')
FALSE_MARK = mark_safe('<span class="text-danger"><i class="mdi mdi-close-thick"></i></span>')
EMPTY_MARK = mark_safe('<span class="text-muted">&mdash;</span>') # Placeholder
def __init__(self, *args, true_mark=TRUE_MARK, false_mark=FALSE_MARK, **kwargs):
self.true_mark = true_mark
self.false_mark = false_mark
super().__init__(*args, **kwargs)
def render(self, value):
if value:
rendered = '<span class="text-success"><i class="mdi mdi-check-bold"></i></span>'
elif value is None:
rendered = '<span class="text-muted">&mdash;</span>'
else:
rendered = '<span class="text-danger"><i class="mdi mdi-close-thick"></i></span>'
return mark_safe(rendered)
if value is None:
return self.EMPTY_MARK
if value and self.true_mark:
return self.true_mark
if not value and self.false_mark:
return self.false_mark
return self.EMPTY_MARK
def value(self, value):
return str(value)
@@ -249,7 +258,7 @@ class ActionsColumn(tables.Column):
def render(self, record, table, **kwargs):
# Skip dummy records (e.g. available VLANs) or those with no actions
if not getattr(record, 'pk', None) or not self.actions:
if not getattr(record, 'pk', None) or not (self.actions or self.extra_buttons):
return ''
model = table.Meta.model

View File

@@ -6,6 +6,7 @@ from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields.related import RelatedField
from django.db.models.fields.reverse_related import ManyToOneRel
from django.urls import reverse
from django.urls.exceptions import NoReverseMatch
from django.utils.safestring import mark_safe
@@ -102,7 +103,7 @@ class BaseTable(tables.Table):
field = model._meta.get_field(field_name)
except FieldDoesNotExist:
break
if isinstance(field, RelatedField):
if isinstance(field, (RelatedField, ManyToOneRel)):
# Follow ForeignKeys to the related model
prefetch_path.append(field_name)
model = field.remote_field.model

View File

@@ -2,6 +2,7 @@ from django.test import override_settings
from core.models import ObjectType
from dcim.models import *
from extras.models import CustomField
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
from users.models import ObjectPermission
from utilities.testing import ModelViewTestCase, create_tags
@@ -116,3 +117,28 @@ class CSVImportTestCase(ModelViewTestCase):
# Test POST with permission
self.assertHttpStatus(self.client.post(self._get_url('import'), data), 200)
self.assertEqual(Region.objects.count(), 0)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_custom_field_defaults(self):
self.add_permissions('dcim.add_region')
csv_data = [
'name,slug,description',
'Region 1,region-1,abc',
]
data = {
'format': ImportFormatChoices.CSV,
'data': self._get_csv_data(csv_data),
'csv_delimiter': CSVDelimiterChoices.AUTO,
}
cf = CustomField.objects.create(
name='tcf',
type='text',
required=False,
default='def-cf-text'
)
cf.object_types.set([ObjectType.objects.get_for_model(self.model)])
self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302)
region = Region.objects.get(slug='region-1')
self.assertEqual(region.cf['tcf'], 'def-cf-text')

View File

@@ -4,6 +4,7 @@ from copy import deepcopy
from django.contrib import messages
from django.contrib.contenttypes.fields import GenericRel
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError
from django.db import transaction, IntegrityError
from django.db.models import ManyToManyField, ProtectedError, RestrictedError
@@ -17,7 +18,8 @@ from django.utils.translation import gettext as _
from django_tables2.export import TableExport
from core.models import ObjectType
from extras.models import ExportTemplate
from extras.choices import CustomFieldUIEditableChoices
from extras.models import CustomField, ExportTemplate
from extras.signals import clear_events
from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
@@ -106,7 +108,13 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
try:
return template.render_to_response(self.queryset)
except Exception as e:
messages.error(request, f"There was an error rendering the selected export template ({template.name}): {e}")
messages.error(
request,
_("There was an error rendering the selected export template ({template}): {error}").format(
template=template.name,
error=e
)
)
# Strip the `export` param and redirect user to the filtered objects list
query_params = request.GET.copy()
query_params.pop('export')
@@ -409,6 +417,17 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
if instance.pk and hasattr(instance, 'snapshot'):
instance.snapshot()
else:
# For newly created objects, apply any default custom field values
custom_fields = CustomField.objects.filter(
object_types=ContentType.objects.get_for_model(self.queryset.model),
ui_editable=CustomFieldUIEditableChoices.YES
)
for cf in custom_fields:
field_name = f'cf_{cf.name}'
if field_name not in record:
record[field_name] = cf.default
# Instantiate the model form for the object
model_form_kwargs = {
'data': record,
@@ -668,7 +687,10 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
# Retrieve objects being edited
table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False)
if not table.rows:
messages.warning(request, "No {} were selected.".format(model._meta.verbose_name_plural))
messages.warning(
request,
_("No {object_type} were selected.").format(object_type=model._meta.verbose_name_plural)
)
return redirect(self.get_return_url(request))
return render(request, self.template_name, {
@@ -745,8 +767,13 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects):
raise PermissionsViolation
model_name = self.queryset.model._meta.verbose_name_plural
messages.success(request, f"Renamed {len(selected_objects)} {model_name}")
messages.success(
request,
_("Renamed {count} {object_type}").format(
count=len(selected_objects),
object_type=self.queryset.model._meta.verbose_name_plural
)
)
return redirect(self.get_return_url(request))
except (AbortRequest, PermissionsViolation) as e:
@@ -838,7 +865,10 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
messages.error(request, mark_safe(e.message))
return redirect(self.get_return_url(request))
msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
msg = _("Deleted {count} {object_type}").format(
count=deleted_count,
object_type=model._meta.verbose_name_plural
)
logger.info(msg)
messages.success(request, msg)
return redirect(self.get_return_url(request))
@@ -855,7 +885,10 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
# Retrieve objects being deleted
table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False)
if not table.rows:
messages.warning(request, "No {} were selected for deletion.".format(model._meta.verbose_name_plural))
messages.warning(
request,
_("No {object_type} were selected.").format(object_type=model._meta.verbose_name_plural)
)
return redirect(self.get_return_url(request))
return render(request, self.template_name, {
@@ -900,7 +933,10 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
selected_objects = self.parent_model.objects.filter(pk__in=pk_list)
if not selected_objects:
messages.warning(request, "No {} were selected.".format(self.parent_model._meta.verbose_name_plural))
messages.warning(
request,
_("No {object_type} were selected.").format(object_type=self.parent_model._meta.verbose_name_plural)
)
return redirect(self.get_return_url(request))
table = self.table(selected_objects, orderable=False)

View File

@@ -202,11 +202,14 @@ class ObjectSyncDataView(View):
obj = get_object_or_404(qs, **kwargs)
if not obj.data_file:
messages.error(request, f"Unable to synchronize data: No data file set.")
messages.error(request, _("Unable to synchronize data: No data file set."))
return redirect(obj.get_absolute_url())
obj.sync(save=True)
messages.success(request, f"Synchronized data for {model._meta.verbose_name} {obj}.")
messages.success(request, _("Synchronized data for {object_type} {object}.").format(
object_type=model._meta.verbose_name,
object=obj
))
return redirect(obj.get_absolute_url())
@@ -228,7 +231,9 @@ class BulkSyncDataView(GetReturnURLMixin, BaseMultiObjectView):
for obj in selected_objects:
obj.sync(save=True)
model_name = self.queryset.model._meta.verbose_name_plural
messages.success(request, f"Synced {len(selected_objects)} {model_name}")
messages.success(request, _("Synced {count} {object_type}").format(
count=len(selected_objects),
object_type=self.queryset.model._meta.verbose_name_plural
))
return redirect(self.get_return_url(request))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -27,10 +27,10 @@
"bootstrap": "5.3.3",
"clipboard": "2.0.11",
"flatpickr": "4.6.13",
"gridstack": "10.3.0",
"gridstack": "10.3.1",
"htmx.org": "1.9.12",
"query-string": "9.0.0",
"sass": "1.77.6",
"query-string": "9.1.0",
"sass": "1.77.8",
"tom-select": "2.3.1",
"typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13"

View File

@@ -10,7 +10,9 @@ export function initMessages(): void {
for (const element of elements) {
if (element !== null) {
const toast = new Toast(element);
toast.show();
if (!toast.isShown()) {
toast.show();
}
}
}
}

View File

@@ -44,3 +44,7 @@ table a {
[data-bs-theme=dark] ::selection {
background-color: rgba(var(--tblr-primary-rgb),.48)
}
pre code {
padding: unset;
}

View File

@@ -867,13 +867,20 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
braces@^3.0.2, braces@~3.0.2:
braces@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
braces@~3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.1.1"
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
@@ -1520,10 +1527,10 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
fill-range@^7.0.1, fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
@@ -1754,10 +1761,10 @@ graphql@16.8.1:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
gridstack@10.3.0:
version "10.3.0"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.3.0.tgz#8fa065f896d0a880c5c54c24d189f3197184488a"
integrity sha512-eGKsmU2TppV4coyDu9IIdIkm4qjgLLdjlEOFwQyQMuSwfOpzSfLdPc8du0HuebGr7CvAIrJxN4lBOmGrWSBg9g==
gridstack@10.3.1:
version "10.3.1"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.3.1.tgz#4ed704279c40094fc1b9e3318f20b573f2fe9f40"
integrity sha512-Ra82k/88gdeiu3ZP40COS4bI4sGhNQlZAaAQ6szfPfr68zVpsXxiyLKr5zYcTpKX4jjcwyNsNNdcV1tDJc71fA==
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
@@ -1816,9 +1823,9 @@ ignore@^5.2.0:
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
immutable@^4.0.0:
version "4.3.6"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447"
integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==
version "4.3.7"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381"
integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==
import-fresh@^3.2.1:
version "3.3.0"
@@ -2346,10 +2353,10 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
query-string@9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.0.0.tgz#1fe177cd95545600f0deab93f5fb02fd4e3e7273"
integrity sha512-4EWwcRGsO2H+yzq6ddHcVqkCQ2EFUSfDMEjF8ryp8ReymyZhIuaFRGLomeOQLkrzacMHoyky2HW0Qe30UbzkKw==
query-string@9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.1.0.tgz#5f12a4653a4ba56021e113b5cf58e56581823e7a"
integrity sha512-t6dqMECpCkqfyv2FfwVS1xcB6lgXW/0XZSaKdsCNGYkqMO76AFiJEg4vINzoDKcZa6MS7JX+OHIjwh06K5vczw==
dependencies:
decode-uri-component "^0.4.1"
filter-obj "^5.1.0"
@@ -2482,10 +2489,10 @@ safe-regex-test@^1.0.3:
es-errors "^1.3.0"
is-regex "^1.1.4"
sass@1.77.6:
version "1.77.6"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.6.tgz#898845c1348078c2e6d1b64f9ee06b3f8bd489e4"
integrity sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==
sass@1.77.8:
version "1.77.8"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.8.tgz#9f18b449ea401759ef7ec1752a16373e296b52bd"
integrity sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"

View File

@@ -7,7 +7,11 @@
<html
lang="en"
data-netbox-url-name="{{ request.resolver_match.url_name }}"
data-netbox-base-path="{{ settings.BASE_PATH }}"
data-netbox-version="{{ settings.VERSION }}"
{% if request.user.is_authenticated %}
data-netbox-user-name="{{ request.user.username }}"
data-netbox-user-id="{{ request.user.pk }}"
{% endif %}
>
<head>
<meta charset="UTF-8" />

View File

@@ -4,6 +4,18 @@
{% load perms %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item">
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object|meta:"verbose_name_plural"|bettertitle }}</a>
</li>
{% with parent_jobs_viewname=object.object|viewname:"jobs" %}
<li class="breadcrumb-item">
<a href="{% url parent_jobs_viewname pk=object.object.pk %}">{{ object.object }}</a>
</li>
{% endwith %}
{% endblock breadcrumbs %}
{% block control-buttons %}
{% if request.user|can_delete:object %}
{% delete_button object %}

View File

@@ -24,7 +24,12 @@
</div>
{% endblock page-header %}
{% block title %}{{ status|capfirst }} {% trans "Workers in " %}{{ queue.name }}{% endblock %}
{% block title %}
{{ status|capfirst }}
{% blocktrans trimmed with queue_name=queue.name %}
Workers in {{ queue_name }}
{% endblocktrans %}
{% endblock %}
{% block controls %}{% endblock %}

View File

@@ -31,7 +31,7 @@
<td class="d-flex justify-content-between align-items-start">
{% if object.rack %}
{{ object.rack|linkify }}
<a href="{{ object.rack.get_absolute_url }}?device={{ object.pk }}" class="btn btn-primary btn-sm d-print-none" title="{% trans "Highlight device in rack" %}">
<a href="{{ object.rack.get_absolute_url }}?device={% firstof object.parent_bay.device.pk object.pk %}" class="btn btn-primary btn-sm d-print-none" title="{% trans "Highlight device in rack" %}">
<i class="mdi mdi-view-day-outline"></i>
</a>
{% else %}

View File

@@ -24,7 +24,7 @@
<table class="table table-hover">
{% for test, data in tests.items %}
<tr>
<td class="font-monospace"><a href="#{{ test }}">{{ test }}</a></td>
<td class="font-monospace">{{ test }}</td>
<td class="text-end report-stats">
<span class="badge text-bg-success">{{ data.success }}</span>
<span class="badge text-bg-info">{{ data.info }}</span>

View File

@@ -78,7 +78,8 @@
{% for backend in auth_backends %}
<div class="col">
<a href="{{ backend.url }}" class="btn w-100">
{% if backend.icon_name %}<i class="mdi mdi-{{ backend.icon_name }}"></i>{% endif %}
{% if backend.icon_name %}<i class="mdi mdi-{{ backend.icon_name }}"></i>
{% elif backend.icon_img %}<img src="{{ backend.icon_img }}" height="24" class="me-2" />{% endif %}
{{ backend.display_name }}
</a>
</div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
from django.contrib.auth import get_user_model
from django.contrib.auth import get_user_model, password_validation
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
@@ -61,6 +61,14 @@ class UserSerializer(ValidatedModelSerializer):
'password': {'write_only': True}
}
def validate(self, data):
# Enforce password validation rules (if configured)
if not self.nested and data.get('password'):
password_validation.validate_password(data['password'], self.instance)
return super().validate(data)
def create(self, validated_data):
"""
Extract the password from validated data and set it separately to ensure proper hash generation.

View File

@@ -1,6 +1,6 @@
from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth import get_user_model, password_validation
from django.contrib.postgres.forms import SimpleArrayField
from django.core.exceptions import FieldError
from django.utils.safestring import mark_safe
@@ -227,6 +227,10 @@ class UserForm(forms.ModelForm):
if self.cleaned_data['password'] and self.cleaned_data['password'] != self.cleaned_data['confirm_password']:
raise forms.ValidationError(_("Passwords do not match! Please check your input and try again."))
# Enforce password validation rules (if configured)
if self.cleaned_data['password']:
password_validation.validate_password(self.cleaned_data['password'], self.instance)
class GroupForm(forms.ModelForm):
users = DynamicModelMultipleChoiceField(

View File

@@ -1,4 +1,5 @@
from django.contrib.auth import get_user_model
from django.test import override_settings
from django.urls import reverse
from core.models import ObjectType
@@ -93,6 +94,31 @@ class UserTest(APIViewTestCases.APIViewTestCase):
user.refresh_from_db()
self.assertTrue(user.check_password(data['password']))
@override_settings(AUTH_PASSWORD_VALIDATORS=[{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {'min_length': 8}
}])
def test_password_validation_enforced(self):
"""
Test that any configured password validation rules (AUTH_PASSWORD_VALIDATORS) are enforced.
"""
self.add_permissions('users.add_user')
data = {
'username': 'new_user',
'password': 'foo',
}
url = reverse('users-api:user-list')
# Password too short
response = self.client.post(url, data, format='json', **self.header)
self.assertEqual(response.status_code, 400)
# Password long enough
data['password'] = 'foobar123'
response = self.client.post(url, data, format='json', **self.header)
self.assertEqual(response.status_code, 201)
class GroupTest(APIViewTestCases.APIViewTestCase):
model = Group

View File

@@ -1,6 +1,8 @@
from django.test import override_settings
from core.models import ObjectType
from users.models import *
from utilities.testing import ViewTestCases, create_test_user
from utilities.testing import ViewTestCases, create_test_user, extract_form_failures
class UserTestCase(
@@ -58,6 +60,34 @@ class UserTestCase(
'last_name': 'newlastname',
}
@override_settings(AUTH_PASSWORD_VALIDATORS=[{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {'min_length': 8}
}])
def test_password_validation_enforced(self):
"""
Test that any configured password validation rules (AUTH_PASSWORD_VALIDATORS) are enforced.
"""
self.add_permissions('users.add_user')
data = {
'username': 'new_user',
'password': 'foo',
'confirm_password': 'foo',
}
# Password too short
request = {
'path': self._get_url('add'),
'data': data,
}
response = self.client.post(**request)
self.assertHttpStatus(response, 200)
# Password long enough
data['password'] = 'foobar123'
data['confirm_password'] = 'foobar123'
self.assertHttpStatus(self.client.post(**request), 302)
class GroupTestCase(
ViewTestCases.GetObjectViewTestCase,

View File

@@ -3,8 +3,8 @@ from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django_filters.constants import EMPTY_VALUES
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
__all__ = (
'ContentTypeFilter',
@@ -116,6 +116,7 @@ class MultiValueWWNFilter(django_filters.MultipleChoiceFilter):
field_class = multivalue_field_factory(forms.CharField)
@extend_schema_field(OpenApiTypes.STR)
class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
"""
Filters for a set of Models, including all descendant models within a Tree. Example: [<Region: R1>,<Region: R2>]

View File

@@ -19,10 +19,9 @@ from .base import ModelTestCase
from .utils import disable_warnings
from ipam.graphql.types import IPAddressFamilyType
from strawberry.field import StrawberryField
from strawberry.lazy_type import LazyType
from strawberry.type import StrawberryList, StrawberryOptional
from strawberry.union import StrawberryUnion
from strawberry.types.lazy_type import LazyType
from strawberry.types.base import StrawberryList, StrawberryOptional
from strawberry.types.union import StrawberryUnion
__all__ = (
'APITestCase',

View File

@@ -271,8 +271,9 @@ class ClusterAddDevicesView(generic.ObjectEditView):
device.cluster = cluster
device.save()
messages.success(request, "Added {} devices to cluster {}".format(
len(device_pks), cluster
messages.success(request, _("Added {count} devices to cluster {cluster}").format(
count=len(device_pks),
cluster=cluster
))
return redirect(cluster.get_absolute_url())
@@ -305,8 +306,9 @@ class ClusterRemoveDevicesView(generic.ObjectEditView):
device.cluster = None
device.save()
messages.success(request, "Removed {} devices from cluster {}".format(
len(device_pks), cluster
messages.success(request, _("Removed {count} devices from cluster {cluster}").format(
count=len(device_pks),
cluster=cluster
))
return redirect(cluster.get_absolute_url())
@@ -444,7 +446,7 @@ class VirtualMachineRenderConfigView(generic.ObjectView):
try:
rendered_config = config_template.render(context=context_data)
except TemplateError as e:
messages.error(request, f"An error occurred while rendering the template: {e}")
messages.error(request, _("An error occurred while rendering the template: {error}").format(error=e))
rendered_config = traceback.format_exc()
return {

View File

@@ -1,14 +1,14 @@
Django==5.0.7
Django==5.0.8
django-cors-headers==4.4.0
django-debug-toolbar==4.3.0
django-debug-toolbar==4.4.6
django-filter==24.2
django-htmx==1.18.0
django-htmx==1.19.0
django-graphiql-debug-toolbar==0.2.0
django-mptt==0.16.0
django-pglocks==1.0.4
django-prometheus==2.3.1
django-redis==5.4.0
django-rich==1.9.0
django-rich==1.10.0
django-rq==2.10.2
django-taggit==5.0.1
django-tables2==2.7.0
@@ -17,21 +17,21 @@ djangorestframework==3.15.2
drf-spectacular==0.27.2
drf-spectacular-sidecar==2024.7.1
feedparser==6.0.11
gunicorn==22.0.0
gunicorn==23.0.0
Jinja2==3.1.4
Markdown==3.6
mkdocs-material==9.5.28
mkdocstrings[python-legacy]==0.25.1
mkdocs-material==9.5.30
mkdocstrings[python-legacy]==0.25.2
netaddr==1.3.0
nh3==0.2.18
Pillow==10.4.0
psycopg[c,pool]==3.2.1
PyYAML==6.0.1
PyYAML==6.0.2
requests==2.32.3
social-auth-app-django==5.4.1
social-auth-app-django==5.4.2
social-auth-core==4.5.4
strawberry-graphql==0.235.2
strawberry-graphql-django==0.46.1
strawberry-graphql==0.237.2
strawberry-graphql-django==0.47.1
svgwrite==1.4.3
tablib==3.6.1
tzdata==2024.1