diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md index 671f3ab17..bdc3f9104 100644 --- a/docs/customization/custom-scripts.md +++ b/docs/customization/custom-scripts.md @@ -476,7 +476,7 @@ class NewBranchScript(Script): name=f'{site.slug}-switch{i}', site=site, status=DeviceStatusChoices.STATUS_PLANNED, - device_role=switch_role + role=switch_role ) switch.full_clean() switch.save() diff --git a/docs/models/dcim/device.md b/docs/models/dcim/device.md index c9f05cd93..8b38d7c89 100644 --- a/docs/models/dcim/device.md +++ b/docs/models/dcim/device.md @@ -18,9 +18,9 @@ When a device has one or more interfaces with IP addresses assigned, a primary I The device's configured name. This field is optional; devices can be unnamed. However, if set, the name must be unique to the assigned site and tenant. -### Device Role +### Role -The functional [role](./devicerole.md) assigned to this device. +The functional [device role](./devicerole.md) assigned to this device. ### Device Type diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md index 9bf0a4db8..4bae93fa8 100644 --- a/docs/release-notes/version-4.0.md +++ b/docs/release-notes/version-4.0.md @@ -13,6 +13,10 @@ The NetBox user interface has been completely refreshed and updated. +#### Dynamic REST API Fields ([#15087](https://github.com/netbox-community/netbox/issues/15087)) + +The REST API now supports specifying which fields to include in the response data. + ### Enhancements * [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3 @@ -22,6 +26,9 @@ The NetBox user interface has been completely refreshed and updated. * [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12 * [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI * [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI +* [#14438](https://github.com/netbox-community/netbox/issues/14438) - Track individual custom scripts as database objects +* [#15131](https://github.com/netbox-community/netbox/issues/15131) - Automatically annotate related object counts on REST API querysets +* [#15238](https://github.com/netbox-community/netbox/issues/15238) - Include the `description` field in "brief" REST API serializations ### Other Changes @@ -34,5 +41,6 @@ The NetBox user interface has been completely refreshed and updated. * [#14657](https://github.com/netbox-community/netbox/issues/14657) - Remove backward compatibility for old permissions mapping under `ActionsMixin` * [#14658](https://github.com/netbox-community/netbox/issues/14658) - Remove backward compatibility for importing `process_webhook()` (now `extras.webhooks.send_webhook()`) * [#14740](https://github.com/netbox-community/netbox/issues/14740) - Remove the obsolete `BootstrapMixin` form mixin class +* [#15042](https://github.com/netbox-community/netbox/issues/15042) - Rearchitect the logic for registering models & model features * [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices * [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class diff --git a/netbox/dcim/api/serializers_/devices.py b/netbox/dcim/api/serializers_/devices.py index ca900732a..303c35532 100644 --- a/netbox/dcim/api/serializers_/devices.py +++ b/netbox/dcim/api/serializers_/devices.py @@ -32,11 +32,6 @@ class DeviceSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') device_type = DeviceTypeSerializer(nested=True) role = DeviceRoleSerializer(nested=True) - device_role = DeviceRoleSerializer( - nested=True, - read_only=True, - help_text='Deprecated in v3.6 in favor of `role`.' - ) tenant = TenantSerializer( nested=True, required=False, @@ -83,13 +78,13 @@ class DeviceSerializer(NetBoxModelSerializer): class Meta: model = Device fields = [ - 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', - 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', - 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', - 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', - 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', - 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', - 'device_bay_count', 'module_bay_count', 'inventory_item_count', + 'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', + 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow', + 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', + 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', + 'created', 'last_updated', 'console_port_count', 'console_server_port_count', 'power_port_count', + 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count', + 'module_bay_count', 'inventory_item_count', ] brief_fields = ('id', 'url', 'display', 'name', 'description') @@ -104,22 +99,19 @@ class DeviceSerializer(NetBoxModelSerializer): data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data return data - def get_device_role(self, obj): - return obj.role - class DeviceWithConfigContextSerializer(DeviceSerializer): config_context = serializers.SerializerMethodField(read_only=True) class Meta(DeviceSerializer.Meta): fields = [ - 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', - 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', - 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', - 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context', - 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count', - 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count', - 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count', + 'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', + 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow', + 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', + 'vc_priority', 'description', 'comments', 'config_template', 'config_context', 'local_context_data', 'tags', + 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', + 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', + 'device_bay_count', 'module_bay_count', 'inventory_item_count', ] @extend_schema_field(serializers.JSONField(allow_null=True)) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 5e773364a..c75757fa7 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -815,20 +815,6 @@ class Device( def get_absolute_url(self): return reverse('dcim:device', args=[self.pk]) - @property - def device_role(self): - """ - For backwards compatibility with pre-v3.6 code expecting a device_role to be present on Device. - """ - return self.role - - @device_role.setter - def device_role(self, value): - """ - For backwards compatibility with pre-v3.6 code expecting a device_role to be present on Device. - """ - self.role = value - def clean(self): super().clean() diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index a827939f7..49a71022e 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -2156,7 +2156,7 @@ class CablePathTestCase(TestCase): device = Device.objects.create( site=self.site, device_type=self.device.device_type, - device_role=self.device.device_role, + role=self.device.role, name='Test mid-span Device' ) interface1 = Interface.objects.create(device=self.device, name='Interface 1') diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index d56bf0741..8eb057020 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -533,30 +533,6 @@ class DeviceTestCase(TestCase): device2.full_clean() device2.save() - def test_old_device_role_field(self): - """ - Ensure that the old device role field sets the value in the new role field. - """ - - # Test getter method - device = Device( - site=Site.objects.first(), - device_type=DeviceType.objects.first(), - role=DeviceRole.objects.first(), - name='Test Device 1', - device_role=DeviceRole.objects.first() - ) - device.full_clean() - device.save() - - self.assertEqual(device.role, device.device_role) - - # Test setter method - device.device_role = DeviceRole.objects.last() - device.full_clean() - device.save() - self.assertEqual(device.role, device.device_role) - class CableTestCase(TestCase): diff --git a/netbox/netbox/authentication.py b/netbox/netbox/authentication.py index 10555b373..c70c68bc0 100644 --- a/netbox/netbox/authentication.py +++ b/netbox/netbox/authentication.py @@ -4,13 +4,13 @@ from collections import defaultdict from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as _RemoteUserBackend -from django.contrib.auth.models import Group, AnonymousUser +from django.contrib.auth.models import AnonymousUser from django.core.exceptions import ImproperlyConfigured from django.db.models import Q from django.utils.translation import gettext_lazy as _ from users.constants import CONSTRAINT_TOKEN_USER -from users.models import ObjectPermission +from users.models import Group, ObjectPermission from utilities.permissions import ( permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_ct, ) diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 2dba76e72..621bd4f5d 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -392,19 +392,19 @@ ADMIN_MENU = Menu( ), # Proxy model for auth.Group MenuItem( - link=f'users:netboxgroup_list', + link=f'users:group_list', link_text=_('Groups'), permissions=[f'auth.view_group'], staff_only=True, buttons=( MenuItemButton( - link=f'users:netboxgroup_add', + link=f'users:group_add', title='Add', icon_class='mdi mdi-plus-thick', permissions=[f'auth.add_group'] ), MenuItemButton( - link=f'users:netboxgroup_import', + link=f'users:group_import', title='Import', icon_class='mdi mdi-upload', permissions=[f'auth.add_group'] diff --git a/netbox/netbox/tests/test_authentication.py b/netbox/netbox/tests/test_authentication.py index 1804087d1..6a894edcd 100644 --- a/netbox/netbox/tests/test_authentication.py +++ b/netbox/netbox/tests/test_authentication.py @@ -2,7 +2,6 @@ import datetime from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.test import Client from django.test.utils import override_settings @@ -12,7 +11,7 @@ from rest_framework.test import APIClient from dcim.models import Site from ipam.models import Prefix -from users.models import ObjectPermission, Token +from users.models import Group, ObjectPermission, Token from utilities.testing import TestCase from utilities.testing.api import APITestCase diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index e016f0923..95ac3c1b1 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 30c0d1674..07e7d30b4 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 bd26af093..04cebd4c2 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/img/tint_20.png b/netbox/project-static/img/tint_20.png deleted file mode 100644 index a03a1f9ac..000000000 Binary files a/netbox/project-static/img/tint_20.png and /dev/null differ diff --git a/netbox/project-static/src/objectSelector.ts b/netbox/project-static/src/objectSelector.ts index 9de6c1750..633f5038a 100644 --- a/netbox/project-static/src/objectSelector.ts +++ b/netbox/project-static/src/objectSelector.ts @@ -18,11 +18,12 @@ function handleSelection(link: HTMLAnchorElement): void { const value = link.getAttribute('data-value'); //@ts-ignore - target.slim.setData([ - {text: label, value: value} - ]); - const change = new Event('change'); - target.dispatchEvent(change); + target.tomselect.addOption({ + id: value, + display: label, + }); + //@ts-ignore + target.tomselect.addItem(value); } diff --git a/netbox/project-static/src/select/dynamic.ts b/netbox/project-static/src/select/dynamic.ts index 20912140b..10ce955c2 100644 --- a/netbox/project-static/src/select/dynamic.ts +++ b/netbox/project-static/src/select/dynamic.ts @@ -42,7 +42,7 @@ function renderItem(data: TomOption, escape: typeof escape_html) { // Initialize elements with statically-defined options export function initStaticSelects(): void { for (const select of getElements( - 'select:not(.api-select):not(.color-select)', + 'select:not(.tomselected):not(.no-ts):not([size]):not(.api-select):not(.color-select)', )) { new TomSelect(select, { ...config, @@ -24,7 +24,7 @@ export function initColorSelects(): void { )}"> ${escape(item.text)}`; } - for (const select of getElements('select.color-select')) { + for (const select of getElements('select.color-select:not(.tomselected)')) { new TomSelect(select, { ...config, maxOptions: undefined, diff --git a/netbox/project-static/src/util.ts b/netbox/project-static/src/util.ts index e1ada2e19..3aa8b6676 100644 --- a/netbox/project-static/src/util.ts +++ b/netbox/project-static/src/util.ts @@ -244,29 +244,6 @@ export function getSelectedOptions( return selected; } -/** - * Get data that can only be accessed via Django context, and is thus already rendered in the HTML - * template. - * - * @see Templates requiring Django context data have a `{% block data %}` block. - * - * @param key Property name, which must exist on the HTML element. If not already prefixed with - * `data-`, `data-` will be prepended to the property. - * @returns Value if it exists, `null` if not. - */ -export function getNetboxData(key: string): string | null { - if (!key.startsWith('data-')) { - key = `data-${key}`; - } - for (const element of getElements('body > div#netbox-data > *')) { - const value = element.getAttribute(key); - if (isTruthy(value)) { - return value; - } - } - return null; -} - /** * Toggle visibility of an element. */ diff --git a/netbox/project-static/styles/custom/_markdown.scss b/netbox/project-static/styles/custom/_markdown.scss index 08de23581..cb4527f37 100644 --- a/netbox/project-static/styles/custom/_markdown.scss +++ b/netbox/project-static/styles/custom/_markdown.scss @@ -28,6 +28,13 @@ } +// Remove the bottom margin of

elements inside a table cell +td > .rendered-markdown { + p:last-of-type { + margin-bottom: 0; + } +} + // Markdown preview .markdown-widget { .preview { diff --git a/netbox/project-static/styles/custom/_misc.scss b/netbox/project-static/styles/custom/_misc.scss index ebf66d547..9779bf583 100644 --- a/netbox/project-static/styles/custom/_misc.scss +++ b/netbox/project-static/styles/custom/_misc.scss @@ -2,7 +2,7 @@ // Color labels span.color-label { - display: block; + display: inline-block; width: 5rem; height: 1rem; padding: $badge-padding-y $badge-padding-x; diff --git a/netbox/project-static/styles/overrides/_tabler.scss b/netbox/project-static/styles/overrides/_tabler.scss index a5ae3c647..f855daf0c 100644 --- a/netbox/project-static/styles/overrides/_tabler.scss +++ b/netbox/project-static/styles/overrides/_tabler.scss @@ -9,6 +9,10 @@ pre { // Tabler sets display: flex display: inline-block; } +.btn-sm { + // $border-radius-sm (2px) is too small + border-radius: $border-radius; +} // Tabs .nav-tabs { diff --git a/netbox/project-static/styles/transitional/_tables.scss b/netbox/project-static/styles/transitional/_tables.scss index 6ac17c59c..0af11f9cd 100644 --- a/netbox/project-static/styles/transitional/_tables.scss +++ b/netbox/project-static/styles/transitional/_tables.scss @@ -23,7 +23,6 @@ table.attr-table { // Restyle row header th { - color: $gray-700; font-weight: normal; width: min-content; } diff --git a/netbox/templates/base/base.html b/netbox/templates/base/base.html index b7d4f6fc6..1c58047ef 100644 --- a/netbox/templates/base/base.html +++ b/netbox/templates/base/base.html @@ -70,10 +70,5 @@ {# User messages #} {% include 'inc/messages.html' %} - {# Data container #} -

- {% block data %}{% endblock %} -
- diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 97be5e839..b28ee6e31 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -163,7 +163,7 @@
- diff --git a/netbox/templates/dcim/rack_elevation_list.html b/netbox/templates/dcim/rack_elevation_list.html index 752fe6913..e50380ba0 100644 --- a/netbox/templates/dcim/rack_elevation_list.html +++ b/netbox/templates/dcim/rack_elevation_list.html @@ -11,13 +11,11 @@ {% trans "View List" %} -
- -
+
{% trans "Front" %} {% trans "Rear" %} diff --git a/netbox/templates/inc/toast.html b/netbox/templates/inc/toast.html index 85eff2d7a..0cf04b93b 100644 --- a/netbox/templates/inc/toast.html +++ b/netbox/templates/inc/toast.html @@ -1,6 +1,6 @@ {% load helpers %} -