diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md index 941a1239e..c43d7feb0 100644 --- a/docs/release-notes/version-4.0.md +++ b/docs/release-notes/version-4.0.md @@ -11,10 +11,16 @@ ### Bug Fixes +* [#16385](https://github.com/netbox-community/netbox/issues/16385) - Restore support for white, gray, and black background colors * [#16640](https://github.com/netbox-community/netbox/issues/16640) - Fix potential corruption of JSON values in custom fields that are not UI-editable +* [#16825](https://github.com/netbox-community/netbox/issues/16825) - Avoid `NoReverseMatch` exception when displaying count of related object type with no list view +* [#16946](https://github.com/netbox-community/netbox/issues/16946) - GraphQL API requests with an invalid filter should return an empty set +* [#16959](https://github.com/netbox-community/netbox/issues/16959) - Fix function of "reset" button on objects filter form +* [#16973](https://github.com/netbox-community/netbox/issues/16973) - Fix support for evaluating user token (`$user`) against custom field values in permission constraints * [#17070](https://github.com/netbox-community/netbox/issues/17070) - Image height & width values should not be required when creating an image attachment via the REST API * [#17108](https://github.com/netbox-community/netbox/issues/17108) - Ensure template date & time filters always return localtime-aware values * [#17117](https://github.com/netbox-community/netbox/issues/17117) - Work around Safari rendering bug +* [#17230](https://github.com/netbox-community/netbox/issues/17230) - Ensure consistent rendering for all dashboard widget colors --- diff --git a/netbox/core/api/schema.py b/netbox/core/api/schema.py index bcc49d3fc..7c4ae722e 100644 --- a/netbox/core/api/schema.py +++ b/netbox/core/api/schema.py @@ -126,9 +126,18 @@ class NetBoxAutoSchema(AutoSchema): return response_serializers + def _get_serializer_name(self, serializer, direction, bypass_extensions=False) -> str: + name = super()._get_serializer_name(serializer, direction, bypass_extensions) + + # If this serializer is nested, prepend its name with "Brief" + if getattr(serializer, 'nested', False): + name = f'Brief{name}' + + return name + def get_serializer_ref_name(self, serializer): # from drf-yasg.utils - """Get serializer's ref_name (or None for ModelSerializer if it is named 'NestedSerializer') + """Get serializer's ref_name :param serializer: Serializer instance :return: Serializer's ``ref_name`` or ``None`` for inline serializer :rtype: str or None @@ -137,8 +146,6 @@ class NetBoxAutoSchema(AutoSchema): serializer_name = type(serializer).__name__ if hasattr(serializer_meta, 'ref_name'): ref_name = serializer_meta.ref_name - elif serializer_name == 'NestedSerializer' and isinstance(serializer, serializers.ModelSerializer): - ref_name = None else: ref_name = serializer_name if ref_name.endswith('Serializer'): diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py index c5e0f5fc3..df41cd34b 100644 --- a/netbox/extras/dashboard/widgets.py +++ b/netbox/extras/dashboard/widgets.py @@ -131,22 +131,6 @@ class DashboardWidget: def name(self): return f'{self.__class__.__module__.split(".")[0]}.{self.__class__.__name__}' - @property - def fg_color(self): - """ - Return the appropriate foreground (text) color for the widget's color. - """ - if self.color in ( - ButtonColorChoices.CYAN, - ButtonColorChoices.GRAY, - ButtonColorChoices.GREY, - ButtonColorChoices.TEAL, - ButtonColorChoices.WHITE, - ButtonColorChoices.YELLOW, - ): - return ButtonColorChoices.BLACK - return ButtonColorChoices.WHITE - @property def form_data(self): return { diff --git a/netbox/netbox/graphql/filter_mixins.py b/netbox/netbox/graphql/filter_mixins.py index 5075e9aa2..76cfd8915 100644 --- a/netbox/netbox/graphql/filter_mixins.py +++ b/netbox/netbox/graphql/filter_mixins.py @@ -4,7 +4,7 @@ from typing import List import django_filters import strawberry import strawberry_django -from django.core.exceptions import FieldDoesNotExist +from django.core.exceptions import FieldDoesNotExist, ValidationError from strawberry import auto from ipam.fields import ASNField from netbox.graphql.scalars import BigInt @@ -201,4 +201,9 @@ def autotype_decorator(filterset): class BaseFilterMixin: def filter_by_filterset(self, queryset, key): - return self.filterset(data={key: getattr(self, key)}, queryset=queryset).qs + filterset = self.filterset(data={key: getattr(self, key)}, queryset=queryset) + if not filterset.is_valid(): + # We could raise validation error but strawberry logs it all to the + # console i.e. raise ValidationError(f"{k}: {v[0]}") + return filterset.qs.none() + return filterset.qs diff --git a/netbox/netbox/tests/test_graphql.py b/netbox/netbox/tests/test_graphql.py index 2cf9ee87b..ab80c79c7 100644 --- a/netbox/netbox/tests/test_graphql.py +++ b/netbox/netbox/tests/test_graphql.py @@ -1,7 +1,13 @@ +import json + from django.test import override_settings from django.urls import reverse +from rest_framework import status -from utilities.testing import disable_warnings, TestCase +from core.models import ObjectType +from dcim.models import Site, Location +from users.models import ObjectPermission +from utilities.testing import disable_warnings, APITestCase, TestCase class GraphQLTestCase(TestCase): @@ -34,3 +40,45 @@ class GraphQLTestCase(TestCase): response = self.client.get(url, **header) with disable_warnings('django.request'): self.assertHttpStatus(response, 302) # Redirect to login page + + +class GraphQLAPITestCase(APITestCase): + + @override_settings(LOGIN_REQUIRED=True) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*', 'auth.user']) + def test_graphql_filter_objects(self): + """ + Test the operation of filters for GraphQL API requests. + """ + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + ) + Site.objects.bulk_create(sites) + Location.objects.create(site=sites[0], name='Location 1', slug='location-1'), + Location.objects.create(site=sites[1], name='Location 2', slug='location-2'), + + # Add object-level permission + obj_perm = ObjectPermission( + name='Test permission', + actions=['view'] + ) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Location)) + + # A valid request should return the filtered list + url = reverse('graphql') + query = '{location_list(filters: {site_id: "' + str(sites[0].pk) + '"}) {id site {id}}}' + response = self.client.post(url, data={'query': query}, format="json", **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + data = json.loads(response.content) + self.assertNotIn('errors', data) + self.assertEqual(len(data['data']['location_list']), 1) + + # An invalid request should return an empty list + query = '{location_list(filters: {site_id: "99999"}) {id site {id}}}' # Invalid site ID + response = self.client.post(url, data={'query': query}, format="json", **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + data = json.loads(response.content) + self.assertEqual(len(data['data']['location_list']), 0) diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index 7d95ac540..b599521fa 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 0b29c2213..afdbea0f8 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 c4f1ecd8c..212be3659 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/elements.ts b/netbox/project-static/src/forms/elements.ts index 356a8d51e..e047ea738 100644 --- a/netbox/project-static/src/forms/elements.ts +++ b/netbox/project-static/src/forms/elements.ts @@ -39,10 +39,17 @@ export function initFormElements(): void { // Find each of the form's submitters. Most object edit forms have a "Create" and // a "Create & Add", so we need to add a listener to both. const submitters = form.querySelectorAll('button[type=submit]'); - for (const submitter of submitters) { // Add the event listener to each submitter. submitter.addEventListener('click', (event: Event) => handleFormSubmit(event, form)); } + + // Initialize any reset buttons so that when clicked, the page is reloaded without query parameters. + const resetButton = document.querySelector('button[data-reset-select]'); + if (resetButton !== null) { + resetButton.addEventListener('click', () => { + window.location.assign(window.location.origin + window.location.pathname); + }); + } } } diff --git a/netbox/project-static/styles/overrides/_bootstrap.scss b/netbox/project-static/styles/overrides/_bootstrap.scss index 59c248541..3b58767fd 100644 --- a/netbox/project-static/styles/overrides/_bootstrap.scss +++ b/netbox/project-static/styles/overrides/_bootstrap.scss @@ -20,3 +20,14 @@ hr.dropdown-divider { margin-bottom: 0.25rem; margin-top: 0.25rem; } + +// Restore support for old Bootstrap v3 colors +.text-bg-black { + @extend .text-bg-dark; +} +.text-bg-gray { + @extend .text-bg-secondary; +} +.text-bg-white { + @extend .text-bg-light; +} diff --git a/netbox/templates/extras/dashboard/widget.html b/netbox/templates/extras/dashboard/widget.html index 39be16145..18be2cc92 100644 --- a/netbox/templates/extras/dashboard/widget.html +++ b/netbox/templates/extras/dashboard/widget.html @@ -9,31 +9,35 @@ gs-id="{{ widget.id }}" >
-
- - - -
- {% if widget.title %} - {{ widget.title }} - {% endif %} + {% with bg_color=widget.color|default:"secondary" %} +
+ + + +
+ {% if widget.title %} + {{ widget.title }} + {% endif %} +
+ + +
- - - -
-
- {% render_widget widget %} -
+
+ {% render_widget widget %} +
+ {% endwith %}
diff --git a/netbox/templates/inc/panels/related_objects.html b/netbox/templates/inc/panels/related_objects.html index 57605f110..321f5f869 100644 --- a/netbox/templates/inc/panels/related_objects.html +++ b/netbox/templates/inc/panels/related_objects.html @@ -5,7 +5,8 @@
{% trans "Related Objects" %}
diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 905beb442..dbccc7a78 100644 --- a/netbox/translations/en/LC_MESSAGES/django.po +++ b/netbox/translations/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-08-23 05:01+0000\n" +"POT-Creation-Date: 2024-08-27 05:01+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -14154,14 +14154,14 @@ msgstr "" msgid "Missing required value for static query param: '{static_params}'" msgstr "" -#: netbox/utilities/permissions.py:39 +#: netbox/utilities/permissions.py:42 #, python-brace-format msgid "" "Invalid permission name: {name}. Must be in the format ." "_" msgstr "" -#: netbox/utilities/permissions.py:57 +#: netbox/utilities/permissions.py:60 #, python-brace-format msgid "Unknown app_label/model_name for {name}" msgstr "" diff --git a/netbox/utilities/permissions.py b/netbox/utilities/permissions.py index 893cc619e..ba245dae1 100644 --- a/netbox/utilities/permissions.py +++ b/netbox/utilities/permissions.py @@ -1,7 +1,10 @@ from django.conf import settings +from django.apps import apps from django.db.models import Q from django.utils.translation import gettext_lazy as _ +from users.constants import CONSTRAINT_TOKEN_USER + __all__ = ( 'get_permission_for_model', 'permission_is_exempt', @@ -90,6 +93,11 @@ def qs_filter_from_constraints(constraints, tokens=None): if tokens is None: tokens = {} + User = apps.get_model('users.User') + for token, value in tokens.items(): + if token == CONSTRAINT_TOKEN_USER and isinstance(value, User): + tokens[token] = value.id + def _replace_tokens(value, tokens): if type(value) is list: return list(map(lambda v: tokens.get(v, v), value))