diff --git a/.gitignore b/.gitignore index 88faab27c..e04e44a30 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ netbox.pid .idea .coverage .vscode +.python-version diff --git a/SECURITY.md b/SECURITY.md index c434b6110..4ca6ef33a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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, or email a brief description of the suspected bug and instructions for reproduction to **security@netbox.dev**. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project. +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. ### Bug Bounties diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md index ae0578690..9ca245e04 100644 --- a/docs/release-notes/version-4.0.md +++ b/docs/release-notes/version-4.0.md @@ -2,6 +2,26 @@ ## v4.0.6 (FUTURE) +### Enhancements + +* [#15348](https://github.com/netbox-community/netbox/issues/15348) - Show saved filters alongside quick search on object list views +* [#15794](https://github.com/netbox-community/netbox/issues/15794) - Dynamically populate related objects in UI views +* [#16256](https://github.com/netbox-community/netbox/issues/16256) - Enable alphabetical ordering of bookmarks on dashboard + +### Bug Fixes + +* [#13925](https://github.com/netbox-community/netbox/issues/13925) - Fix support for "zulu" (UTC) timestamps for custom fields +* [#14829](https://github.com/netbox-community/netbox/issues/14829) - Fix support for simple conditions (without AND/OR) in event rules +* [#16143](https://github.com/netbox-community/netbox/issues/16143) - Display timestamps in tables in the configured timezone +* [#16416](https://github.com/netbox-community/netbox/issues/16416) - Retain dark/light mode toggle on mobile view +* [#16444](https://github.com/netbox-community/netbox/issues/16444) - Disable ordering circuits list by A/Z termination +* [#16450](https://github.com/netbox-community/netbox/issues/16450) - Searching for rack unit in form dropdown should be case-insensitive +* [#16452](https://github.com/netbox-community/netbox/issues/16452) - Fix sizing of buttons within object attribute panels +* [#16454](https://github.com/netbox-community/netbox/issues/16454) - Address DNS lookup bug in `django-debug-toolbar +* [#16460](https://github.com/netbox-community/netbox/issues/16460) - Omit spaces from telephone number URLs +* [#16512](https://github.com/netbox-community/netbox/issues/16512) - Restore a user's preferred language (if any) on login +* [#16542](https://github.com/netbox-community/netbox/issues/16542) - Fix bulk form operations when HTMX is enabled + --- ## v4.0.5 (2024-06-06) diff --git a/netbox/account/views.py b/netbox/account/views.py index 40ce78039..feb85fdfe 100644 --- a/netbox/account/views.py +++ b/netbox/account/views.py @@ -104,10 +104,16 @@ class LoginView(View): # Ensure the user has a UserConfig defined. (This should normally be handled by # create_userconfig() on user creation.) if not hasattr(request.user, 'config'): - config = get_config() - UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save() + request.user.config = get_config() + UserConfig(user=request.user, data=request.user.config.DEFAULT_USER_PREFERENCES).save() - return self.redirect_to_next(request, logger) + response = self.redirect_to_next(request, logger) + + # Set the user's preferred language (if any) + if language := request.user.config.get('locale.language'): + response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language) + + return response else: logger.debug(f"Login form validation failed for username: {form['username'].value()}") @@ -145,9 +151,10 @@ class LogoutView(View): logger.info(f"User {username} has logged out") messages.info(request, "You have logged out.") - # Delete session key cookie (if set) upon logout + # Delete session key & language cookies (if set) upon logout response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL)) response.delete_cookie('session_key') + response.delete_cookie(settings.LANGUAGE_COOKIE_NAME) return response diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py index 5d650df61..e1b99ff42 100644 --- a/netbox/circuits/tables/circuits.py +++ b/netbox/circuits/tables/circuits.py @@ -63,10 +63,12 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): status = columns.ChoiceFieldColumn() termination_a = tables.TemplateColumn( template_code=CIRCUITTERMINATION_LINK, + orderable=False, verbose_name=_('Side A') ) termination_z = tables.TemplateColumn( template_code=CIRCUITTERMINATION_LINK, + orderable=False, verbose_name=_('Side Z') ) commit_rate = CommitRateColumn( diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index d6ddd466b..be7a9c306 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -219,9 +219,9 @@ class RackViewSet(NetBoxModelViewSet): ) # Enable filtering rack units by ID - q = data['q'] - if q: - elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name'])] + if q := data['q']: + q = q.lower() + elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name']).lower()] page = self.paginate_queryset(elevation) if page is not None: diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index d5cc0e856..c12ddccde 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -465,7 +465,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm): label=_('Cluster'), queryset=Cluster.objects.all(), required=False, - selector=True + selector=True, + query_params={ + 'site_id': ['$site', 'null'] + }, ) comments = CommentField() local_context_data = JSONField( diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index cab1760ed..9056a66c0 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -8,6 +8,7 @@ from dcim.models import * from extras.models import CustomField from tenancy.models import Tenant from utilities.data import drange +from virtualization.models import Cluster, ClusterType class LocationTestCase(TestCase): @@ -533,6 +534,36 @@ class DeviceTestCase(TestCase): device2.full_clean() device2.save() + def test_device_mismatched_site_cluster(self): + cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') + Cluster.objects.create(name='Cluster 1', type=cluster_type) + + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + ) + Site.objects.bulk_create(sites) + + clusters = ( + Cluster(name='Cluster 1', type=cluster_type, site=sites[0]), + Cluster(name='Cluster 2', type=cluster_type, site=sites[1]), + Cluster(name='Cluster 3', type=cluster_type, site=None), + ) + Cluster.objects.bulk_create(clusters) + + device_type = DeviceType.objects.first() + device_role = DeviceRole.objects.first() + + # Device with site only should pass + Device(name='device1', site=sites[0], device_type=device_type, role=device_role).full_clean() + + # Device with site, cluster non-site should pass + Device(name='device1', site=sites[0], device_type=device_type, role=device_role, cluster=clusters[2]).full_clean() + + # Device with mismatched site & cluster should fail + with self.assertRaises(ValidationError): + Device(name='device1', site=sites[0], device_type=device_type, role=device_role, cluster=clusters[1]).full_clean() + class CableTestCase(TestCase): diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 974affb2e..240998146 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -660,6 +660,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): # Validate date & time elif self.type == CustomFieldTypeChoices.TYPE_DATETIME: if type(value) is not datetime: + # Work around UTC issue for Python < 3.11; see + # https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat + if type(value) is str and value.endswith('Z'): + value = f'{value[:-1]}+00:00' try: datetime.fromisoformat(value) except ValueError: diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index 0696d2e82..36ed4defc 100644 Binary files a/netbox/project-static/dist/netbox.css and b/netbox/project-static/dist/netbox.css differ diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 59f703d4b..f4d0311ab 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 00b92809a..c5cd1a402 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/src/forms/savedFiltersSelect.ts b/netbox/project-static/src/forms/savedFiltersSelect.ts new file mode 100644 index 000000000..1d06a8d0b --- /dev/null +++ b/netbox/project-static/src/forms/savedFiltersSelect.ts @@ -0,0 +1,30 @@ +import { isTruthy } from '../util'; + +/** + * Handle saved filter change event. + * + * @param event "change" event for the saved filter select + */ +function handleSavedFilterChange(event: Event): void { + const savedFilter = event.currentTarget as HTMLSelectElement; + let baseUrl = savedFilter.baseURI.split('?')[0]; + const preFilter = '?'; + + const selectedOptions = Array.from(savedFilter.options) + .filter(option => option.selected) + .map(option => `filter_id=${option.value}`) + .join('&'); + + baseUrl += `${preFilter}${selectedOptions}`; + document.location.href = baseUrl; +} + +export function initSavedFilterSelect(): void { + const divResults = document.getElementById('results'); + if (isTruthy(divResults)) { + const savedFilterSelect = document.getElementById('id_filter_id'); + if (isTruthy(savedFilterSelect)) { + savedFilterSelect.addEventListener('change', handleSavedFilterChange); + } + } +} diff --git a/netbox/project-static/src/netbox.ts b/netbox/project-static/src/netbox.ts index 59faab222..ce0aad93f 100644 --- a/netbox/project-static/src/netbox.ts +++ b/netbox/project-static/src/netbox.ts @@ -13,6 +13,7 @@ import { initSideNav } from './sidenav'; import { initDashboard } from './dashboard'; import { initRackElevation } from './racks'; import { initHtmx } from './htmx'; +import { initSavedFilterSelect } from './forms/savedFiltersSelect'; function initDocument(): void { for (const init of [ @@ -31,6 +32,7 @@ function initDocument(): void { initDashboard, initRackElevation, initHtmx, + initSavedFilterSelect, ]) { init(); } diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index b04b85fc9..af2905312 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/netbox/project-static/styles/netbox.scss @@ -7,6 +7,7 @@ // Overrides of external libraries @import 'overrides/bootstrap'; @import 'overrides/tabler'; +@import 'overrides/tomselect'; // Transitional styling to ease migration of templates from NetBox v3.x @import 'transitional/badges'; diff --git a/netbox/project-static/styles/overrides/_tomselect.scss b/netbox/project-static/styles/overrides/_tomselect.scss new file mode 100644 index 000000000..29aa9d361 --- /dev/null +++ b/netbox/project-static/styles/overrides/_tomselect.scss @@ -0,0 +1,8 @@ +.ts-wrapper.multi { + .ts-control { + padding: 7px 7px 3px 7px; + div { + margin: 0 4px 4px 0; + } + } +} diff --git a/netbox/templates/base/layout.html b/netbox/templates/base/layout.html index d53591cb4..9ba6fded3 100644 --- a/netbox/templates/base/layout.html +++ b/netbox/templates/base/layout.html @@ -35,6 +35,7 @@ Blocks: {# User menu (mobile view) #} @@ -52,14 +53,7 @@ Blocks: