Compare commits

...

141 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
Jeff Gehlbach
3f67b5d8cb Merge pull request #16859 from netbox-community/develop
Release v4.0.7
2024-07-09 13:43:51 -04:00
Jeff Gehlbach
596514ce74 Release v4.0.7 2024-07-09 13:27:13 -04:00
transifex-integration[bot]
aafb26662a Updates for project NetBox (#16811)
* Translate django.po in pt

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

* Translate django.po in fr [Manual Sync]

12% of minimum 1% reviewed 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 pt [Manual Sync]

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

* Translate django.po in de [Manual Sync]

78% of minimum 1% reviewed source file: 'django.po'
on 'de'.

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]

7% of minimum 1% reviewed 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 ru [Manual Sync]

30% of minimum 1% reviewed source file: 'django.po'
on 'ru'.

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]

16% of minimum 1% reviewed 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

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-07-09 13:18:19 -04:00
Jeremy Stretch
4c797bf755 Update dependencies for v4.0.7 2024-07-09 13:05:57 -04:00
Jeremy Stretch
aceed94787 Changelog for #14554, #16721, #16758, #16802, #16808, #16817, #16843 2024-07-09 12:19:39 -04:00
Martin
6a1245c792 Fixes #16758: Create language cookie if required (#16764)
* Fixes #16758: Create language cookie if required

* Align language cookie with session lifetime
2024-07-09 08:51:12 -04:00
Mattias L
96ff796b94 Allowed configuration of Sentry send_default_pii parameter (#16803)
* Allowed configuration of Sentry send_default_pii parameter.

Also changed default value of send_default_pii to False to avoid sending sensitive data to Sentry.

Closes #16802

* Order alphabetically & link to Sentry parameter documentation

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-07-09 08:39:43 -04:00
locklearxd
d8d66581cc potential fix for IKE policy mode issue with version2 2024-07-09 08:35:02 -04:00
Jeremy Stretch
7564f6f538 Fixes #16808: Correct event type of webhooks emitted upon object deletion immediately following a modification 2024-07-09 08:17:46 -04:00
github-actions
f2e3c1a219 Update source translation strings 2024-07-09 05:02:43 +00:00
Arzhel Younsi
22348cdbfc Extend STORAGE_BACKEND config to support Swift (#16319)
* Extend STORAGE_BACKEND config to support Swift

Requires django-storage-swift >= 1.4.0 when used.

Bug: T310717
Change-Id: I67cf439e9152608cbba3a3de4173d54ba5fbddc2

* Update system.md from suggestions

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* Update settings.py from suggestions

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* Update system.md from suggestions 2

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* Remove SWIFT storage from configuration_example.py

* Load swift config as global instead of monkey path

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-07-08 12:04:17 -04:00
Adam Brutsaert
f4532dd4ab Closes #16817: Added 200 & 400 Gbps selections for circuit commit rate 2024-07-04 11:31:10 -04:00
Jeremy Stretch
e02796a64e Fixes #16721: Fix errant API request after deselecting a rack in device edit form 2024-07-04 10:28:52 -04:00
Jeremy Stretch
9ab7960a66 Changelog for #16679, #16716, #16779, #16780, #16791, #16796, #16806, #16807, #16813 2024-07-04 10:00:50 -04:00
Jeremy Stretch
7a88810a23 Fixes #16780: IKE proposal created via REST API should not require authentication_algorithm 2024-07-04 09:32:01 -04:00
Jeremy Stretch
a518579916 Fixes #16796: Allow assignment of VM with no site to a cluster with a site 2024-07-04 09:14:07 -04:00
Jeremy Stretch
e9dd5aa17b Fixes #16806: Fix redirect URL when creating contact assignments with "add another" button 2024-07-04 08:50:22 -04:00
Jeremy Stretch
8026f79cbb Fixes #16813: Fix ordering of bookmarks in dashboard widget when filtering by object type 2024-07-04 08:23:05 -04:00
github-actions
cf38c7724e Update source translation strings 2024-07-04 05:02:06 +00:00
Jeremy Stretch
b18a6b7c59 Fixes #16779: Fix saved filter selection for child object lists (#16789)
* Fixes #16779: Fix saved filter selection for child object lists

* Omit label_suffix
2024-07-03 08:51:30 -04:00
RobertH1993
98748d901b Closes #16716, add NAT IP to device view for OOB IP 2024-07-03 08:48:24 -04:00
Jeremy Stretch
a704708caa Fixes #16679: Avoid overwriting custom JSON fields during bulk edit 2024-07-03 08:42:37 -04:00
Jeremy Stretch
224f157b75 Fixes #16807: Fix layout of VLAN edit form when custom fields are present 2024-07-03 08:31:25 -04:00
Jeremy Stretch
94c2e7582e Closes #16791: Add 200 & 400 Gbps selections for circuit termination port speed 2024-07-03 08:28:12 -04:00
github-actions
4857a87be5 Update source translation strings 2024-07-02 05:02:15 +00:00
Jeremy Stretch
d3d27d8111 Changelog for #16424, #16523, #16654, #16657, #16689, #16714, #16723, #16725, #16735, #16747 2024-07-01 09:40:21 -04:00
Elliott Balsley
e2596587fa fix: allow cloning field value of 0 (#16741)
* fix: allow cloning field value of 0

* Fix evaluation of False value

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-07-01 09:22:09 -04:00
siku4
a12259fae7 fix: add missing parent field to inventory item import form 2024-07-01 09:20:49 -04:00
Peter Eckel
753ba5d3f4 Do not delete all search indexes when reindexing specific models (#16755)
* Do not delete all search indexes when reindexing specific models

* Clear all indexes only if neither --lazy nor a list of models are
  specified for "manage.py reindex"

* Otherwise, clear the index for a model immediately before rebuilding
  it

* Separated clearing from re-indexing the search cache
2024-07-01 09:12:02 -04:00
Jeremy Stretch
b5d8e657ad Fixes #16523: Restore highlighting of current device in virtual chassis members panel 2024-07-01 08:48:01 -04:00
github-actions
67983c6a75 Update source translation strings 2024-07-01 05:02:10 +00:00
prryplatypus
a00ed4b74d Quote VIRTUALENV 2024-06-30 15:21:46 -04:00
Tobias Genannt
a896b14c08 Fixes #16689: Load correct configuration
Loads the the current configuration if no ConfigRevisions are saved to
the database.
2024-06-30 15:20:26 -04:00
Julio-Oliveira-Encora
2c64a52d7d Added default:"0" to total_count in object_list.html 2024-06-30 15:10:38 -04:00
Peter Eckel
96338c002b Updated the documentation section about removing plugins 2024-06-30 15:08:11 -04:00
Julio Oliveira at Encora
00d23a0cff 16725 - The admin section should always come last in the navigation menu (#16762)
* I replaced `append` with `insert` into menu.py to make the admin section appear last in the navigation menu.

* Clean up ordering logic

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-06-30 11:30:39 -04:00
github-actions
c7dcded74f Update source translation strings 2024-06-27 05:02:05 +00:00
Julio Oliveira at Encora
c506f60f12 16424 - Allow filtering of Devices by Cluster and Cluster Group (#16674)
* Allow filtering Devices by Cluster and Cluster Group.

* Allow filtering Devices by Cluster and Cluster Group.

* Added tests for cluster and cluster_groups filterset.

* Add missing filter & complete tests

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-06-26 10:13:32 -04:00
Julio Oliveira at Encora
b241c97e00 Was added to searching support languages other than English for objec… (#16706)
* Was added to searching support languages other than English for object types(s).

* Fix SearchForm field label translation

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-06-26 09:54:15 -04:00
Julio Oliveira at Encora
b605dfcba0 16704 - Define a default help_text for ColorField (#16708)
* Added `help_text` to ColorField.

* Addressed PR comment to remove the redundant help_text from all the forms where ColorField was used.

* Add space before example value

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-06-26 09:14:08 -04:00
Jeff Gehlbach
33004dfab0 Added missing CDN cache clearing step to release checklist in docs 2024-06-25 16:21:32 -04:00
github-actions
65e40603ff Update source translation strings 2024-06-25 05:01:53 +00:00
Jeremy Stretch
7702b0ebb0 PRVB 2024-06-24 15:04:46 -04:00
117 changed files with 112371 additions and 29241 deletions

View File

@@ -26,7 +26,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.0.6
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.6
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

@@ -31,6 +31,17 @@ The sampling rate for errors. Must be a value between 0 (disabled) and 1.0 (repo
---
## SENTRY_SEND_DEFAULT_PII
Default: False
Maps to the Sentry SDK's [`send_default_pii`](https://docs.sentry.io/platforms/python/configuration/options/#send-default-pii) parameter. If enabled, certain personally identifiable information (PII) is added.
!!! warning "Sensitive data"
If you enable this option, be aware that sensitive data such as cookies and authentication tokens will be logged.
---
## SENTRY_TAGS
An optional dictionary of tag names and values to apply to Sentry error reports.For example:

View File

@@ -177,7 +177,7 @@ The dotted path to the desired search backend class. `CachedValueSearchBackend`
Default: None (local storage)
The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) package, which provides backends for several popular file storage services. If not configured, local filesystem storage will be used.
The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) and [`django-storage-swift`](https://github.com/dennisv/django-storage-swift) packages, which provide backends for several popular file storage services. If not configured, local filesystem storage will be used.
The configuration parameters for the specified storage backend are defined under the `STORAGE_CONFIG` setting.
@@ -187,7 +187,7 @@ The configuration parameters for the specified storage backend are defined under
Default: Empty
A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the [`django-storages` documentation](https://django-storages.readthedocs.io/en/stable/) for more detail.
A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the documentation for your selected backend ([`django-storages`](https://django-storages.readthedocs.io/en/stable/) or [`django-storage-swift`](https://github.com/dennisv/django-storage-swift)) for more detail.
If `STORAGE_BACKEND` is not defined, this setting will be ignored.

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,4 +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 _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

@@ -70,3 +70,19 @@ DROP TABLE
netbox=> DROP TABLE pluginname_bar;
DROP TABLE
```
### Remove the Django Migration Records
After removing the tables created by a plugin, the migrations that created the tables need to be removed from Django's migration history as well. This is necessary to make it possible to reinstall the plugin at a later time. If the migration history were left in place, Django would skip all migrations that were executed in the course of a previous installation, which would cause the plugin to fail after reinstallation.
```no-highlight
netbox=> SELECT * FROM django_migrations WHERE app='pluginname';
id | app | name | applied
-----+------------+------------------------+-------------------------------
492 | pluginname | 0001_initial | 2023-12-21 11:59:59.325995+00
493 | pluginname | 0002_add_foo | 2023-12-21 11:59:59.330026+00
netbox=> DELETE FROM django_migrations WHERE app='pluginname';
```
!!! warning
Exercise extreme caution when altering Django system tables. Users are strongly encouraged to perform a backup of their database immediately before taking these actions.

View File

@@ -1,5 +1,90 @@
# 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
* [#14554](https://github.com/netbox-community/netbox/issues/14554) - Add support for [django-storage-swift](https://github.com/dennisv/django-storage-swift) storage backend
* [#16424](https://github.com/netbox-community/netbox/issues/16424) - Enable filtering of devices by cluster and cluster group
* [#16716](https://github.com/netbox-community/netbox/issues/16716) - Display NAT address (if any) for OOB IP address under device view
* [#16725](https://github.com/netbox-community/netbox/issues/16725) - Always position the admin section last in the navigation menu
* [#16791](https://github.com/netbox-community/netbox/issues/16791) - Add 200 & 400 Gbps selections for circuit termination port speed
* [#16802](https://github.com/netbox-community/netbox/issues/16802) - Introduce `SENTRY_SEND_DEFAULT_PII` configuration parameter and disable PII export by default
* [#16817](https://github.com/netbox-community/netbox/issues/16817) - Add 200 & 400 Gbps selections for circuit commit rate
### Bug Fixes
* [#16523](https://github.com/netbox-community/netbox/issues/16523) - Restore highlighting of current device in virtual chassis members panel
* [#16654](https://github.com/netbox-community/netbox/issues/16654) - Fix parent item assignment for inventory item bulk import
* [#16657](https://github.com/netbox-community/netbox/issues/16657) - Fix translation of object types in global search
* [#16679](https://github.com/netbox-community/netbox/issues/16679) - Avoid overwriting custom JSON fields during bulk edit
* [#16689](https://github.com/netbox-community/netbox/issues/16689) - System configuration view should reflect static parameters when no config revisions exist
* [#16714](https://github.com/netbox-community/netbox/issues/16714) - Fix cloning of device types with 0U height
* [#16721](https://github.com/netbox-community/netbox/issues/16721) - Fix errant API request after deselecting a rack in device edit form
* [#16723](https://github.com/netbox-community/netbox/issues/16723) - Fix escaping of path to virtual environment in `upgrade.sh`
* [#16735](https://github.com/netbox-community/netbox/issues/16735) - Object list "results" tab should show a count of zero when empty
* [#16747](https://github.com/netbox-community/netbox/issues/16747) - Avoid clearing entire search cache when manually reindexing specific apps/models
* [#16758](https://github.com/netbox-community/netbox/issues/16758) - Ensure manually selected lagnuage persists across browser sessions
* [#16779](https://github.com/netbox-community/netbox/issues/16779) - Fix saved filter selection for child object lists
* [#16780](https://github.com/netbox-community/netbox/issues/16780) - IKE proposal created via REST API should not require authentication_algorithm
* [#16796](https://github.com/netbox-community/netbox/issues/16796) - Allow assignment of VM with no site to a cluster with a site
* [#16806](https://github.com/netbox-community/netbox/issues/16806) - Fix redirect URL when creating contact assignments with "add another" button
* [#16807](https://github.com/netbox-community/netbox/issues/16807) - Fix layout of VLAN edit form when custom fields are present
* [#16808](https://github.com/netbox-community/netbox/issues/16808) - Fix event rule triggering in scenario where objects are updated immediately prior to deletion
* [#16813](https://github.com/netbox-community/netbox/issues/16813) - Fix AttributeError exception when filtering bookmarks in dashboard widget by object type
* [#16843](https://github.com/netbox-community/netbox/issues/16843) - Permit creation of IKE policies via REST API without specifying an IKE mode
---
## v4.0.6 (2024-06-24)
### 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.)
@@ -111,7 +121,7 @@ class LoginView(View):
# Set the user's preferred language (if any)
if language := request.user.config.get('locale.language'):
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
return response
@@ -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))
@@ -206,7 +216,7 @@ class UserConfigView(LoginRequiredMixin, View):
# Set/clear language cookie
if language := form.cleaned_data['locale.language']:
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
else:
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
@@ -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

@@ -38,6 +38,8 @@ class CircuitCommitRateChoices(ChoiceSet):
(25000000, '25 Gbps'),
(40000000, '40 Gbps'),
(100000000, '100 Gbps'),
(200000000, '200 Gbps'),
(400000000, '400 Gbps'),
(1544, 'T1 (1.544 Mbps)'),
(2048, 'E1 (2.048 Mbps)'),
]
@@ -69,6 +71,8 @@ class CircuitTerminationPortSpeedChoices(ChoiceSet):
(25000000, '25 Gbps'),
(40000000, '40 Gbps'),
(100000000, '100 Gbps'),
(200000000, '200 Gbps'),
(400000000, '400 Gbps'),
(1544, 'T1 (1.544 Mbps)'),
(2048, 'E1 (2.048 Mbps)'),
]

View File

@@ -66,9 +66,6 @@ class CircuitTypeImportForm(NetBoxModelImportForm):
class Meta:
model = CircuitType
fields = ('name', 'slug', 'color', 'description', 'tags')
help_texts = {
'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' <code>00ff00</code>'),
}
class CircuitImportForm(NetBoxModelImportForm):

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]))
@@ -555,17 +558,18 @@ class SystemView(UserPassesTestMixin, View):
config = ConfigRevision.objects.get(pk=cache.get('config_version'))
except ConfigRevision.DoesNotExist:
# Fall back to using the active config data if no record is found
config = ConfigRevision(data=get_config().defaults)
config = get_config()
# 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

@@ -20,7 +20,7 @@ from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
)
from virtualization.models import Cluster
from virtualization.models import Cluster, ClusterGroup
from vpn.models import L2VPN
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
from wireless.models import WirelessLAN, WirelessLink
@@ -1018,6 +1018,17 @@ class DeviceFilterSet(
queryset=Cluster.objects.all(),
label=_('VM cluster (ID)'),
)
cluster_group = django_filters.ModelMultipleChoiceFilter(
field_name='cluster__group__slug',
queryset=ClusterGroup.objects.all(),
to_field_name='slug',
label=_('Cluster group (slug)'),
)
cluster_group_id = django_filters.ModelMultipleChoiceFilter(
field_name='cluster__group',
queryset=ClusterGroup.objects.all(),
label=_('Cluster group (ID)'),
)
model = django_filters.ModelMultipleChoiceFilter(
field_name='device_type__slug',
queryset=DeviceType.objects.all(),
@@ -1378,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

@@ -174,9 +174,6 @@ class RackRoleImportForm(NetBoxModelImportForm):
class Meta:
model = RackRole
fields = ('name', 'slug', 'color', 'description', 'tags')
help_texts = {
'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' <code>00ff00</code>'),
}
class RackImportForm(NetBoxModelImportForm):
@@ -384,9 +381,6 @@ class DeviceRoleImportForm(NetBoxModelImportForm):
class Meta:
model = DeviceRole
fields = ('name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags')
help_texts = {
'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' <code>00ff00</code>'),
}
class PlatformImportForm(NetBoxModelImportForm):
@@ -1052,7 +1046,7 @@ class InventoryItemImportForm(NetBoxModelImportForm):
class Meta:
model = InventoryItem
fields = (
'device', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'device', 'name', 'label', 'role', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag', 'discovered',
'description', 'tags', 'component_type', 'component_name',
)
@@ -1104,9 +1098,6 @@ class InventoryItemRoleImportForm(NetBoxModelImportForm):
class Meta:
model = InventoryItemRole
fields = ('name', 'slug', 'color', 'description')
help_texts = {
'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' <code>00ff00</code>'),
}
#
@@ -1183,9 +1174,6 @@ class CableImportForm(NetBoxModelImportForm):
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
]
help_texts = {
'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' <code>00ff00</code>'),
}
def _clean_side(self, side):
"""

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

@@ -14,6 +14,7 @@ from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_ch
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import NumberWithOptions
from virtualization.models import Cluster, ClusterGroup
from vpn.models import L2VPN
from wireless.choices import *
@@ -655,6 +656,7 @@ class DeviceFilterForm(
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
name=_('Components')
),
FieldSet('cluster_group_id', 'cluster_id', name=_('Cluster')),
FieldSet(
'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
'has_virtual_device_context',
@@ -821,6 +823,16 @@ class DeviceFilterForm(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(),
required=False,
label=_('Cluster')
)
cluster_group_id = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(),
required=False,
label=_('Cluster group')
)
tag = TagFilterField(model)

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

@@ -9,7 +9,7 @@ from ipam.models import ASN, IPAddress, RIR, VRF
from netbox.choices import ColorChoices
from tenancy.models import Tenant, TenantGroup
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
from virtualization.models import Cluster, ClusterType
from virtualization.models import Cluster, ClusterType, ClusterGroup
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
User = get_user_model()
@@ -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)
@@ -1959,10 +1959,16 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
Rack.objects.bulk_create(racks)
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
cluster_groups = (
ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'),
ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'),
)
ClusterGroup.objects.bulk_create(cluster_groups)
clusters = (
Cluster(name='Cluster 1', type=cluster_type),
Cluster(name='Cluster 2', type=cluster_type),
Cluster(name='Cluster 3', type=cluster_type),
Cluster(name='Cluster 1', type=cluster_type, group=cluster_groups[0]),
Cluster(name='Cluster 2', type=cluster_type, group=cluster_groups[1]),
Cluster(name='Cluster 3', type=cluster_type, group=cluster_groups[2]),
)
Cluster.objects.bulk_create(clusters)
@@ -2213,6 +2219,13 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'cluster_id': [clusters[0].pk, clusters[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cluster_group(self):
cluster_groups = ClusterGroup.objects.all()[:2]
params = {'cluster_group_id': [cluster_groups[0].pk, cluster_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'cluster_group': [cluster_groups[0].slug, cluster_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_model(self):
params = {'model': ['model-1', 'model-2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -4534,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

@@ -31,6 +31,7 @@ from utilities.views import (
GetRelatedModelsMixin, GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
)
from virtualization.filtersets import VirtualMachineFilterSet
from virtualization.forms import VirtualMachineFilterForm
from virtualization.models import VirtualMachine
from virtualization.tables import VirtualMachineTable
from . import filtersets, forms, tables
@@ -679,6 +680,7 @@ class RackRackReservationsView(generic.ObjectChildrenView):
child_model = RackReservation
table = tables.RackReservationTable
filterset = filtersets.RackReservationFilterSet
filterset_form = forms.RackReservationFilterForm
template_name = 'dcim/rack/reservations.html'
tab = ViewTab(
label=_('Reservations'),
@@ -697,6 +699,7 @@ class RackNonRackedView(generic.ObjectChildrenView):
child_model = Device
table = tables.DeviceTable
filterset = filtersets.DeviceFilterSet
filterset_form = forms.DeviceFilterForm
template_name = 'dcim/rack/non_racked_devices.html'
tab = ViewTab(
label=_('Non-Racked Devices'),
@@ -1835,6 +1838,7 @@ class DeviceConsolePortsView(DeviceComponentsView):
child_model = ConsolePort
table = tables.DeviceConsolePortTable
filterset = filtersets.ConsolePortFilterSet
filterset_form = forms.ConsolePortFilterForm
template_name = 'dcim/device/consoleports.html',
tab = ViewTab(
label=_('Console Ports'),
@@ -1850,6 +1854,7 @@ class DeviceConsoleServerPortsView(DeviceComponentsView):
child_model = ConsoleServerPort
table = tables.DeviceConsoleServerPortTable
filterset = filtersets.ConsoleServerPortFilterSet
filterset_form = forms.ConsoleServerPortFilterForm
template_name = 'dcim/device/consoleserverports.html'
tab = ViewTab(
label=_('Console Server Ports'),
@@ -1865,6 +1870,7 @@ class DevicePowerPortsView(DeviceComponentsView):
child_model = PowerPort
table = tables.DevicePowerPortTable
filterset = filtersets.PowerPortFilterSet
filterset_form = forms.PowerPortFilterForm
template_name = 'dcim/device/powerports.html'
tab = ViewTab(
label=_('Power Ports'),
@@ -1880,6 +1886,7 @@ class DevicePowerOutletsView(DeviceComponentsView):
child_model = PowerOutlet
table = tables.DevicePowerOutletTable
filterset = filtersets.PowerOutletFilterSet
filterset_form = forms.PowerOutletFilterForm
template_name = 'dcim/device/poweroutlets.html'
tab = ViewTab(
label=_('Power Outlets'),
@@ -1895,6 +1902,7 @@ class DeviceInterfacesView(DeviceComponentsView):
child_model = Interface
table = tables.DeviceInterfaceTable
filterset = filtersets.InterfaceFilterSet
filterset_form = forms.InterfaceFilterForm
template_name = 'dcim/device/interfaces.html'
tab = ViewTab(
label=_('Interfaces'),
@@ -1916,6 +1924,7 @@ class DeviceFrontPortsView(DeviceComponentsView):
child_model = FrontPort
table = tables.DeviceFrontPortTable
filterset = filtersets.FrontPortFilterSet
filterset_form = forms.FrontPortFilterForm
template_name = 'dcim/device/frontports.html'
tab = ViewTab(
label=_('Front Ports'),
@@ -1931,6 +1940,7 @@ class DeviceRearPortsView(DeviceComponentsView):
child_model = RearPort
table = tables.DeviceRearPortTable
filterset = filtersets.RearPortFilterSet
filterset_form = forms.RearPortFilterForm
template_name = 'dcim/device/rearports.html'
tab = ViewTab(
label=_('Rear Ports'),
@@ -1946,6 +1956,7 @@ class DeviceModuleBaysView(DeviceComponentsView):
child_model = ModuleBay
table = tables.DeviceModuleBayTable
filterset = filtersets.ModuleBayFilterSet
filterset_form = forms.ModuleBayFilterForm
template_name = 'dcim/device/modulebays.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
@@ -1965,6 +1976,7 @@ class DeviceDeviceBaysView(DeviceComponentsView):
child_model = DeviceBay
table = tables.DeviceDeviceBayTable
filterset = filtersets.DeviceBayFilterSet
filterset_form = forms.DeviceBayFilterForm
template_name = 'dcim/device/devicebays.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
@@ -1984,6 +1996,7 @@ class DeviceInventoryView(DeviceComponentsView):
child_model = InventoryItem
table = tables.DeviceInventoryItemTable
filterset = filtersets.InventoryItemFilterSet
filterset_form = forms.InventoryItemFilterForm
template_name = 'dcim/device/inventory.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
@@ -2046,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 {
@@ -2062,6 +2075,7 @@ class DeviceVirtualMachinesView(generic.ObjectChildrenView):
child_model = VirtualMachine
table = VirtualMachineTable
filterset = VirtualMachineFilterSet
filterset_form = VirtualMachineFilterForm
tab = ViewTab(
label=_('Virtual Machines'),
badge=lambda obj: VirtualMachine.objects.filter(cluster=obj.cluster, device=obj).count(),
@@ -2809,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)
@@ -2844,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)
@@ -2944,6 +2970,7 @@ class InventoryItemChildrenView(generic.ObjectChildrenView):
child_model = InventoryItem
table = tables.InventoryItemTable
filterset = filtersets.InventoryItemFilterSet
filterset_form = forms.InventoryItemFilterForm
tab = ViewTab(
label=_('Children'),
badge=lambda obj: obj.child_items.count(),
@@ -3411,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:
@@ -3456,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():
@@ -3468,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
@@ -381,17 +385,17 @@ class BookmarksWidget(DashboardWidget):
if request.user.is_anonymous:
bookmarks = list()
else:
user_bookmarks = Bookmark.objects.filter(user=request.user)
if self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_AZ:
bookmarks = sorted(user_bookmarks, key=lambda bookmark: bookmark.__str__().lower())
elif self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_ZA:
bookmarks = sorted(user_bookmarks, key=lambda bookmark: bookmark.__str__().lower(), reverse=True)
else:
bookmarks = user_bookmarks.order_by(self.config['order_by'])
bookmarks = Bookmark.objects.filter(user=request.user)
if object_types := self.config.get('object_types'):
models = get_models_from_content_types(object_types)
content_types = ObjectType.objects.get_for_models(*models).values()
bookmarks = bookmarks.filter(object_type__in=content_types)
if self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_AZ:
bookmarks = sorted(bookmarks, key=lambda bookmark: bookmark.__str__().lower())
elif self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_ZA:
bookmarks = sorted(bookmarks, key=lambda bookmark: bookmark.__str__().lower(), reverse=True)
else:
bookmarks = bookmarks.order_by(self.config['order_by'])
if max_items := self.config.get('max_items'):
bookmarks = bookmarks[:max_items]

View File

@@ -63,6 +63,9 @@ def enqueue_object(queue, instance, user, request_id, action):
if key in queue:
queue[key]['data'] = serialize_for_event(instance)
queue[key]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange']
# If the object is being deleted, update any prior "update" event to "delete"
if action == ObjectChangeActionChoices.ACTION_DELETE:
queue[key]['event'] = action
else:
queue[key] = {
'content_type': ContentType.objects.get_for_model(instance),

View File

@@ -228,9 +228,6 @@ class TagImportForm(CSVModelForm):
class Meta:
model = Tag
fields = ('name', 'slug', 'color', 'description')
help_texts = {
'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' <code>00ff00</code>'),
}
class JournalEntryImportForm(NetBoxModelImportForm):

View File

@@ -66,11 +66,16 @@ class Command(BaseCommand):
raise CommandError(_("No indexers found!"))
self.stdout.write(f'Reindexing {len(indexers)} models.')
# Clear all cached values for the specified models (if not being lazy)
# Clear cached values for the specified models (if not being lazy)
if not kwargs['lazy']:
if model_labels:
content_types = [ContentType.objects.get_for_model(model) for model in indexers.keys()]
else:
content_types = None
self.stdout.write('Clearing cached values... ', ending='')
self.stdout.flush()
deleted_count = search_backend.clear()
deleted_count = search_backend.clear(object_types=content_types)
self.stdout.write(f'{deleted_count} entries deleted.')
# Index models

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):
@@ -490,7 +488,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# JSON
elif self.type == CustomFieldTypeChoices.TYPE_JSON:
field = JSONField(required=required, initial=json.dumps(initial) if initial else '')
field = JSONField(required=required, initial=json.dumps(initial) if initial else None)
# Object
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:

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

@@ -390,13 +390,36 @@ class EventRuleTest(APITestCase):
request.id = uuid.uuid4()
request.user = self.user
self.assertEqual(self.queue.count, 0, msg="Unexpected jobs found in queue")
# Test create & update
with event_tracking(request):
site = Site(name='Site 1', slug='site-1')
site.save()
# Save the site a second time
site.description = 'foo'
site.save()
self.assertEqual(self.queue.count, 1, msg="Duplicate jobs found in queue")
job = self.queue.get_jobs()[0]
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_CREATE)
self.queue.empty()
# Test multiple updates
site = Site.objects.create(name='Site 2', slug='site-2')
with event_tracking(request):
site.description = 'foo'
site.save()
site.description = 'bar'
site.save()
self.assertEqual(self.queue.count, 1, msg="Duplicate jobs found in queue")
job = self.queue.get_jobs()[0]
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_UPDATE)
self.queue.empty()
# Test update & delete
site = Site.objects.create(name='Site 3', slug='site-3')
with event_tracking(request):
site.description = 'foo'
site.save()
site.delete()
self.assertEqual(self.queue.count, 1, msg="Duplicate jobs found in queue")
job = self.queue.get_jobs()[0]
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_DELETE)
self.queue.empty()

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

@@ -7,6 +7,7 @@ from django.utils.translation import gettext as _
from circuits.models import Provider
from dcim.filtersets import InterfaceFilterSet
from dcim.forms import InterfaceFilterForm
from dcim.models import Interface, Site
from netbox.views import generic
from tenancy.views import ObjectContactsView
@@ -14,6 +15,7 @@ from utilities.query import count_related
from utilities.tables import get_table_ordering
from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet
from virtualization.forms import VMInterfaceFilterForm
from virtualization.models import VMInterface
from . import filtersets, forms, tables
from .choices import PrefixStatusChoices
@@ -206,6 +208,7 @@ class ASNRangeASNsView(generic.ObjectChildrenView):
child_model = ASN
table = tables.ASNTable
filterset = filtersets.ASNFilterSet
filterset_form = forms.ASNFilterForm
tab = ViewTab(
label=_('ASNs'),
badge=lambda x: x.get_child_asns().count(),
@@ -337,6 +340,7 @@ class AggregatePrefixesView(generic.ObjectChildrenView):
child_model = Prefix
table = tables.PrefixTable
filterset = filtersets.PrefixFilterSet
filterset_form = forms.PrefixFilterForm
template_name = 'ipam/aggregate/prefixes.html'
tab = ViewTab(
label=_('Prefixes'),
@@ -523,6 +527,7 @@ class PrefixPrefixesView(generic.ObjectChildrenView):
child_model = Prefix
table = tables.PrefixTable
filterset = filtersets.PrefixFilterSet
filterset_form = forms.PrefixFilterForm
template_name = 'ipam/prefix/prefixes.html'
tab = ViewTab(
label=_('Child Prefixes'),
@@ -558,6 +563,7 @@ class PrefixIPRangesView(generic.ObjectChildrenView):
child_model = IPRange
table = tables.IPRangeTable
filterset = filtersets.IPRangeFilterSet
filterset_form = forms.IPRangeFilterForm
template_name = 'ipam/prefix/ip_ranges.html'
tab = ViewTab(
label=_('Child Ranges'),
@@ -584,6 +590,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
child_model = IPAddress
table = tables.IPAddressTable
filterset = filtersets.IPAddressFilterSet
filterset_form = forms.IPAddressFilterForm
template_name = 'ipam/prefix/ip_addresses.html'
tab = ViewTab(
label=_('IP Addresses'),
@@ -683,6 +690,7 @@ class IPRangeIPAddressesView(generic.ObjectChildrenView):
child_model = IPAddress
table = tables.IPAddressTable
filterset = filtersets.IPAddressFilterSet
filterset_form = forms.IPRangeFilterForm
template_name = 'ipam/iprange/ip_addresses.html'
tab = ViewTab(
label=_('IP Addresses'),
@@ -885,6 +893,7 @@ class IPAddressRelatedIPsView(generic.ObjectChildrenView):
child_model = IPAddress
table = tables.IPAddressTable
filterset = filtersets.IPAddressFilterSet
filterset_form = forms.IPAddressFilterForm
tab = ViewTab(
label=_('Related IPs'),
badge=lambda x: x.get_related_ips().count(),
@@ -957,6 +966,7 @@ class VLANGroupVLANsView(generic.ObjectChildrenView):
child_model = VLAN
table = tables.VLANTable
filterset = filtersets.VLANFilterSet
filterset_form = forms.VLANFilterForm
tab = ViewTab(
label=_('VLANs'),
badge=lambda x: x.get_child_vlans().count(),
@@ -1112,6 +1122,7 @@ class VLANInterfacesView(generic.ObjectChildrenView):
child_model = Interface
table = tables.VLANDevicesTable
filterset = InterfaceFilterSet
filterset_form = InterfaceFilterForm
tab = ViewTab(
label=_('Device Interfaces'),
badge=lambda x: x.get_interfaces().count(),
@@ -1129,6 +1140,7 @@ class VLANVMInterfacesView(generic.ObjectChildrenView):
child_model = VMInterface
table = tables.VLANVirtualMachinesTable
filterset = VMInterfaceFilterSet
filterset_form = VMInterfaceFilterForm
tab = ViewTab(
label=_('VM Interfaces'),
badge=lambda x: x.get_vminterfaces().count(),

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

@@ -1,7 +1,7 @@
import re
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from netbox.search import LookupTypes
from netbox.search.backends import search_backend
@@ -36,7 +36,8 @@ class SearchForm(forms.Form):
lookup = forms.ChoiceField(
choices=LOOKUP_CHOICES,
initial=LookupTypes.PARTIAL,
required=False
required=False,
label=_('Lookup')
)
def __init__(self, *args, **kwargs):

View File

@@ -47,6 +47,11 @@ class CoreMiddleware:
with event_tracking(request):
response = self.get_response(request)
# Check if language cookie should be renewed
if request.user.is_authenticated and settings.SESSION_SAVE_EVERY_REQUEST:
if language := request.user.config.get('locale.language'):
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
# Attach the unique request ID as an HTTP header.
response['X-Request-ID'] = request.id

View File

@@ -462,16 +462,13 @@ MENUS = [
PROVISIONING_MENU,
CUSTOMIZATION_MENU,
OPERATIONS_MENU,
ADMIN_MENU,
]
#
# Add plugin menus
#
# Add top-level plugin menus
for menu in registry['plugins']['menus']:
MENUS.append(menu)
# Add the default "plugins" menu
if registry['plugins']['menu_items']:
# Build the default plugins menu
@@ -485,3 +482,6 @@ if registry['plugins']['menu_items']:
groups=groups
)
MENUS.append(plugins_menu)
# Add the admin menu last
MENUS.append(ADMIN_MENU)

View File

@@ -8,6 +8,7 @@ from django.db.models.fields.related import ForeignKey
from django.db.models.functions import window
from django.db.models.signals import post_delete, post_save
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
import netaddr
from netaddr.core import AddrFormatError
@@ -39,7 +40,7 @@ class SearchBackend:
# Organize choices by category
categories = defaultdict(dict)
for label, idx in registry['search'].items():
categories[idx.get_category()][label] = title(idx.model._meta.verbose_name)
categories[idx.get_category()][label] = _(title(idx.model._meta.verbose_name))
# Compile a nested tuple of choices for form rendering
results = (

View File

@@ -25,7 +25,7 @@ from utilities.string import trailing_slash
# Environment setup
#
VERSION = '4.0.6'
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__)))
@@ -147,6 +147,7 @@ SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', None)
SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
SENTRY_SAMPLE_RATE = getattr(configuration, 'SENTRY_SAMPLE_RATE', 1.0)
SENTRY_SEND_DEFAULT_PII = getattr(configuration, 'SENTRY_SEND_DEFAULT_PII', False)
SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE', 0)
SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
@@ -225,6 +226,23 @@ if STORAGE_BACKEND is not None:
return globals().get(name, default)
storages.utils.setting = _setting
# django-storage-swift
elif STORAGE_BACKEND == 'swift.storage.SwiftStorage':
try:
import swift.utils # type: ignore
except ModuleNotFoundError as e:
if getattr(e, 'name') == 'swift':
raise ImproperlyConfigured(
f"STORAGE_BACKEND is set to {STORAGE_BACKEND} but django-storage-swift is not present. "
"It can be installed by running 'pip install django-storage-swift'."
)
raise e
# Load all SWIFT_* settings from the user configuration
for param, value in STORAGE_CONFIG.items():
if param.startswith('SWIFT_'):
globals()[param] = value
if STORAGE_CONFIG and STORAGE_BACKEND is None:
warnings.warn(
"STORAGE_CONFIG has been set in configuration.py but STORAGE_BACKEND is not defined. STORAGE_CONFIG will be "
@@ -536,7 +554,7 @@ if SENTRY_ENABLED:
release=VERSION,
sample_rate=SENTRY_SAMPLE_RATE,
traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
send_default_pii=True,
send_default_pii=SENTRY_SEND_DEFAULT_PII,
http_proxy=HTTP_PROXIES.get('http') if HTTP_PROXIES else None,
https_proxy=HTTP_PROXIES.get('https') if HTTP_PROXIES else None
)
@@ -721,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')
@@ -176,7 +184,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
'model': model,
'table': table,
'actions': actions,
'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
'filter_form': self.filterset_form(request.GET) if self.filterset_form else None,
'prerequisite_model': get_prerequisite_model(self.queryset),
**self.get_extra_context(request),
}
@@ -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))

View File

@@ -87,12 +87,14 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
child_model: The model class which represents the child objects
table: The django-tables2 Table class used to render the child objects list
filterset: A django-filter FilterSet that is applied to the queryset
filterset_form: The form class used to render filter options
actions: A mapping of supported actions to their required permissions. When adding custom actions, bulk
action names must be prefixed with `bulk_`. (See ActionsMixin.)
"""
child_model = None
table = None
filterset = None
filterset_form = None
template_name = 'generic/object_children.html'
def get_children(self, request, parent):
@@ -152,6 +154,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
'table': table,
'table_config': f'{table.name}_config',
'filter_form': self.filterset_form(request.GET) if self.filterset_form else None,
'actions': actions,
'tab': self.tab,
'return_url': request.get_full_path(),

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.2.1",
"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

@@ -74,20 +74,25 @@ export class DynamicTomSelect extends TomSelect {
load(value: string) {
const self = this;
const url = self.getRequestUrl(value);
// Automatically clear any cached options. (Only options included
// in the API response should be present.)
self.clearOptions();
addClasses(self.wrapper, self.settings.loadingClass);
self.loading++;
// Populate the null option (if any) if not searching
if (self.nullOption && !value) {
self.addOption(self.nullOption);
}
// Get the API request URL. If none is provided, abort as no request can be made.
const url = self.getRequestUrl(value);
if (!url) {
return;
}
addClasses(self.wrapper, self.settings.loadingClass);
self.loading++;
// Make the API request
fetch(url)
.then(response => response.json())
@@ -129,6 +134,9 @@ export class DynamicTomSelect extends TomSelect {
for (const result of this.api_url.matchAll(new RegExp(`({{${key}}})`, 'g'))) {
if (value) {
url = replaceAll(url, result[1], value.toString());
} else {
// No value is available to replace the token; abort.
return '';
}
}
}

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.2.1:
version "10.2.1"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.2.1.tgz#3ce6119ae86cfb0a533c5f0d15b03777a55384ca"
integrity sha512-UAPKnIvd9sIqPDFMtKMqj0G5GDj8MUFPcelRJq7FzQFSxSYBblKts/Gd52iEJg0EvTFP51t6ZuMWGx0pSSFBdw==
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

@@ -88,7 +88,7 @@
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">{% trans "Current Configuration" %}</h5>
{% include 'core/inc/config_data.html' with config=config.data %}
{% include 'core/inc/config_data.html' %}
</div>
</div>

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 %}
@@ -125,28 +125,30 @@
</div>
</h5>
<table class="table table-hover attr-table">
<tr>
<th>{% trans "Device" %}</th>
<th>{% trans "Position" %}</th>
<th>{% trans "Master" %}</th>
<th>{% trans "Priority" %}</th>
<thead>
<tr class="border-bottom">
<th>{% trans "Device" %}</th>
<th>{% trans "Position" %}</th>
<th>{% trans "Master" %}</th>
<th>{% trans "Priority" %}</th>
</tr>
</thead>
<tbody>
{% for vc_member in vc_members %}
<tr{% if vc_member == object %} class="info"{% endif %}>
<td>
{{ vc_member|linkify }}
</td>
<td>
{% badge vc_member.vc_position show_empty=True %}
</td>
<td>
{% if object.virtual_chassis.master == vc_member %}<i class="mdi mdi-check-bold"></i>{% endif %}
</td>
<td>
{{ vc_member.vc_priority|placeholder }}
</td>
</tr>
<tr{% if vc_member == object %} class="table-primary"{% endif %}>
<td>{{ vc_member|linkify }}</td>
<td>{% badge vc_member.vc_position show_empty=True %}</td>
<td>
{% if object.virtual_chassis.master == vc_member %}
{% checkmark True %}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td>{{ vc_member.vc_priority|placeholder }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
@@ -221,6 +223,11 @@
<td>
{% if object.oob_ip %}
<a href="{{ object.oob_ip.get_absolute_url }}" id="oob_ip">{{ object.oob_ip.address.ip }}</a>
{% if object.oob_ip.nat_inside %}
({% trans "NAT for" %} <a href="{{ object.oob_ip.nat_inside.get_absolute_url }}">{{ object.oob_ip.nat_inside.address.ip }}</a>)
{% elif object.oob_ip.nat_outside.exists %}
({% trans "NAT" %}: {% for nat in object.oob_ip.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %}
{% copy_content "oob_ip" %}
{% else %}
{{ ''|placeholder }}

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

@@ -48,7 +48,7 @@ Context:
<li class="nav-item" role="presentation">
<a class="nav-link active" id="object-list-tab" data-bs-toggle="tab" data-bs-target="#object-list" type="button" role="tab" aria-controls="edit-form" aria-selected="true">
{% trans "Results" %}
<span class="badge text-bg-secondary total-object-count">{% if table.page.paginator.count %}{{ table.page.paginator.count }}{% else %}{{ total_count }}{% endif %}</span>
<span class="badge text-bg-secondary total-object-count">{% if table.page.paginator.count %}{{ table.page.paginator.count }}{% else %}{{ total_count|default:"0" }}{% endif %}</span>
</a>
</li>
{% if filter_form %}

View File

@@ -13,14 +13,16 @@
</div>
</div>
<div class="col-auto d-print-none">
<div class="input-group">
<div class="input-group-text">
<i class="mdi mdi-filter" title="{% trans "Saved filter" %}"></i>
{% if filter_form %}
<div class="col-auto d-print-none">
<div class="input-group">
<div class="input-group-text">
<i class="mdi mdi-filter" title="{% trans "Saved filter" %}"></i>
</div>
{{ filter_form.filter_id }}
</div>
{{ filter_form.filter_id }}
</div>
</div>
{% endif %}
<div class="col-auto ms-auto d-print-none">
{% if request.user.is_authenticated and table_modal %}

View File

@@ -53,10 +53,6 @@
{% endwith %}
</div>
<div class="field-group my-5">
{% render_field form.comments %}
</div>
{% if form.custom_fields %}
<div class="field-group my-5">
<div class="row">
@@ -65,4 +61,8 @@
{% render_custom_fields form %}
</div>
{% endif %}
<div class="field-group my-5">
{% render_field form.comments %}
</div>
{% endblock %}

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>

View File

@@ -13,6 +13,7 @@ class ObjectContactsView(generic.ObjectChildrenView):
child_model = ContactAssignment
table = tables.ContactAssignmentTable
filterset = filtersets.ContactAssignmentFilterSet
filterset_form = forms.ContactAssignmentFilterForm
template_name = 'tenancy/object_contacts.html'
tab = ViewTab(
label=_('Contacts'),
@@ -364,7 +365,7 @@ class ContactAssignmentEditView(generic.ObjectEditView):
def get_extra_addanother_params(self, request):
return {
'content_type': request.GET.get('content_type'),
'object_type': request.GET.get('object_type'),
'object_id': request.GET.get('object_id'),
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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