From b21ed6a334eafab711e89680123556fd3717c733 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 30 Nov 2023 22:00:26 +0530 Subject: [PATCH 01/24] adds optional classes parameter #14390 --- netbox/utilities/templates/builtins/copy_content.html | 2 +- netbox/utilities/templatetags/builtins/tags.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/netbox/utilities/templates/builtins/copy_content.html b/netbox/utilities/templates/builtins/copy_content.html index 9025a71a1..4d9ad9431 100644 --- a/netbox/utilities/templates/builtins/copy_content.html +++ b/netbox/utilities/templates/builtins/copy_content.html @@ -1,3 +1,3 @@ - + diff --git a/netbox/utilities/templatetags/builtins/tags.py b/netbox/utilities/templatetags/builtins/tags.py index 68541ae5a..dc5d75f48 100644 --- a/netbox/utilities/templatetags/builtins/tags.py +++ b/netbox/utilities/templatetags/builtins/tags.py @@ -87,13 +87,14 @@ def checkmark(value, show_false=True, true='Yes', false='No'): @register.inclusion_tag('builtins/copy_content.html') -def copy_content(target, prefix=None, color='primary'): +def copy_content(target, prefix=None, color='primary', classes=None): """ Display a copy button to copy the content of a field. """ return { 'target': f'#{prefix or ""}{target}', - 'color': f'btn-{color}' + 'color': f'btn-{color}', + 'classes': classes or '', } From d10ac9b4a7a37ad9a441f81176f997df95052829 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 5 Dec 2023 14:03:38 -0500 Subject: [PATCH 02/24] Closes #12623: Document need for core.sync_datasource permission --- docs/features/synchronized-data.md | 4 +++- docs/integrations/synchronized-data.md | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/features/synchronized-data.md b/docs/features/synchronized-data.md index f266519b6..a070d0ce1 100644 --- a/docs/features/synchronized-data.md +++ b/docs/features/synchronized-data.md @@ -10,7 +10,6 @@ To enable remote data synchronization, the NetBox administrator first designates (Local disk paths are considered "remote" in this context as they exist outside NetBox's database. These paths could also be mapped to external network shares.) - !!! info Data backends which connect to external sources typically require the installation of one or more supporting Python libraries. The Git backend requires the [`dulwich`](https://www.dulwich.io/) package, and the S3 backend requires the [`boto3`](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) package. These must be installed within NetBox's environment to enable these backends. @@ -23,3 +22,6 @@ The following NetBox models can be associated with replicated data files: * Export templates Once a data has been designated for a local instance, its data will be replaced with the content of the replicated file. When the replicated file is updated in the future (via synchronization jobs), the local instance will be flagged as having out-of-date data. A user can then synchronize these objects individually or in bulk to effect the update. This two-stage process ensures that automated synchronization tasks do not immediately affect production data. + +!!! note "Permissions" + A user must be assigned the `core.sync_datasource` permission in order to synchronize local files from a remote data source. diff --git a/docs/integrations/synchronized-data.md b/docs/integrations/synchronized-data.md index 805cbe15b..d72501fd5 100644 --- a/docs/integrations/synchronized-data.md +++ b/docs/integrations/synchronized-data.md @@ -2,6 +2,9 @@ Some NetBox models support automatic synchronization of certain attributes from remote [data sources](../models/core/datasource.md), such as a git repository hosted on GitHub or GitLab. Data from the authoritative remote source is synchronized locally in NetBox as [data files](../models/core/datafile.md). +!!! note "Permissions" + A user must be assigned the `core.sync_datasource` permission in order to synchronize local files from a remote data source. This is accomplished by creating a permission for the "Core > Data Source" object type with the `sync` action, and assigning it to the desired user and/or group. + The following features support the use of synchronized data: * [Configuration templates](../features/configuration-rendering.md) From d2c3a39ebbdb0f5b4683193033b8e67e944ddbc5 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 5 Dec 2023 11:14:02 -0800 Subject: [PATCH 03/24] 14401 validate rack startion position > 0 --- .../0182_alter_rack_starting_unit.py | 18 ++++++++++++++++++ netbox/dcim/models/racks.py | 1 + 2 files changed, 19 insertions(+) create mode 100644 netbox/dcim/migrations/0182_alter_rack_starting_unit.py diff --git a/netbox/dcim/migrations/0182_alter_rack_starting_unit.py b/netbox/dcim/migrations/0182_alter_rack_starting_unit.py new file mode 100644 index 000000000..5b0fc6ce4 --- /dev/null +++ b/netbox/dcim/migrations/0182_alter_rack_starting_unit.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.5 on 2023-12-05 19:13 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0181_rename_device_role_device_role'), + ] + + operations = [ + migrations.AlterField( + model_name='rack', + name='starting_unit', + field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1)]), + ), + ] diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index ef0dde4da..a0614abcb 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -141,6 +141,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin): starting_unit = models.PositiveSmallIntegerField( default=RACK_STARTING_UNIT_DEFAULT, verbose_name=_('starting unit'), + validators=[MinValueValidator(1),], help_text=_('Starting unit for rack') ) desc_units = models.BooleanField( From b34daeaacbb3d18c30e18a6d34c63a16ddc0a1bd Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 5 Dec 2023 13:37:33 -0800 Subject: [PATCH 04/24] 14401 review changes - remove migration --- .../dcim/migrations/0174_rack_starting_unit.py | 3 ++- .../0182_alter_rack_starting_unit.py | 18 ------------------ 2 files changed, 2 insertions(+), 19 deletions(-) delete mode 100644 netbox/dcim/migrations/0182_alter_rack_starting_unit.py diff --git a/netbox/dcim/migrations/0174_rack_starting_unit.py b/netbox/dcim/migrations/0174_rack_starting_unit.py index e32738660..2d2b5f826 100644 --- a/netbox/dcim/migrations/0174_rack_starting_unit.py +++ b/netbox/dcim/migrations/0174_rack_starting_unit.py @@ -1,5 +1,6 @@ # Generated by Django 4.1.9 on 2023-05-31 15:47 +import django.core.validators from django.db import migrations, models @@ -12,6 +13,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='rack', name='starting_unit', - field=models.PositiveSmallIntegerField(default=1), + field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1)]), ), ] diff --git a/netbox/dcim/migrations/0182_alter_rack_starting_unit.py b/netbox/dcim/migrations/0182_alter_rack_starting_unit.py deleted file mode 100644 index 5b0fc6ce4..000000000 --- a/netbox/dcim/migrations/0182_alter_rack_starting_unit.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.5 on 2023-12-05 19:13 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('dcim', '0181_rename_device_role_device_role'), - ] - - operations = [ - migrations.AlterField( - model_name='rack', - name='starting_unit', - field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1)]), - ), - ] From 32264ac3e3496a9a9cd049eeef671179d3f9c41f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 5 Dec 2023 16:05:31 -0500 Subject: [PATCH 05/24] Fixes #14322: Populate default custom field values when instantiating templated device components --- netbox/dcim/models/devices.py | 19 ++++-- netbox/dcim/tests/test_models.py | 95 ++++++++++++++++++++-------- netbox/extras/models/customfields.py | 9 +++ 3 files changed, 88 insertions(+), 35 deletions(-) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index c9ebf898d..0ffee5c7b 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -16,7 +16,7 @@ from django.utils.translation import gettext_lazy as _ from dcim.choices import * from dcim.constants import * -from extras.models import ConfigContextModel +from extras.models import ConfigContextModel, CustomField from extras.querysets import ConfigContextModelQuerySet from netbox.config import ConfigItem from netbox.models import OrganizationalModel, PrimaryModel @@ -985,11 +985,17 @@ class Device( bulk_create: If True, bulk_create() will be called to create all components in a single query (default). Otherwise, save() will be called on each instance individually. """ + components = [obj.instantiate(device=self) for obj in queryset] + if not components: + return + + # Set default values for any applicable custom fields + model = queryset.model.component_model + if cf_defaults := CustomField.objects.get_defaults_for_model(model): + for component in components: + component.custom_field_data = cf_defaults + if bulk_create: - components = [obj.instantiate(device=self) for obj in queryset] - if not components: - return - model = components[0]._meta.model model.objects.bulk_create(components) # Manually send the post_save signal for each of the newly created components for component in components: @@ -1002,8 +1008,7 @@ class Device( update_fields=None ) else: - for obj in queryset: - component = obj.instantiate(device=self) + for component in components: component.save() def save(self, *args, **kwargs): diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 2e5ae0d5c..369f03ef5 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -1,9 +1,11 @@ +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.test import TestCase from circuits.models import * from dcim.choices import * from dcim.models import * +from extras.models import CustomField from tenancy.models import Tenant from utilities.utils import drange @@ -255,6 +257,23 @@ class DeviceTestCase(TestCase): ) DeviceRole.objects.bulk_create(roles) + # Create a CustomField with a default value & assign it to all component models + cf1 = CustomField.objects.create(name='cf1', default='foo') + cf1.content_types.set( + ContentType.objects.filter(app_label='dcim', model__in=[ + 'consoleport', + 'consoleserverport', + 'powerport', + 'poweroutlet', + 'interface', + 'rearport', + 'frontport', + 'modulebay', + 'devicebay', + 'inventoryitem', + ]) + ) + # Create DeviceType components ConsolePortTemplate( device_type=device_type, @@ -266,18 +285,18 @@ class DeviceTestCase(TestCase): name='Console Server Port 1' ).save() - ppt = PowerPortTemplate( + powerport = PowerPortTemplate( device_type=device_type, name='Power Port 1', maximum_draw=1000, allocated_draw=500 ) - ppt.save() + powerport.save() PowerOutletTemplate( device_type=device_type, name='Power Outlet 1', - power_port=ppt, + power_port=powerport, feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A ).save() @@ -288,19 +307,19 @@ class DeviceTestCase(TestCase): mgmt_only=True ).save() - rpt = RearPortTemplate( + rearport = RearPortTemplate( device_type=device_type, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=8 ) - rpt.save() + rearport.save() FrontPortTemplate( device_type=device_type, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, - rear_port=rpt, + rear_port=rearport, rear_port_position=2 ).save() @@ -314,73 +333,93 @@ class DeviceTestCase(TestCase): name='Device Bay 1' ).save() + InventoryItemTemplate( + device_type=device_type, + name='Inventory Item 1' + ).save() + def test_device_creation(self): """ Ensure that all Device components are copied automatically from the DeviceType. """ - d = Device( + device = Device( site=Site.objects.first(), device_type=DeviceType.objects.first(), role=DeviceRole.objects.first(), name='Test Device 1' ) - d.save() + device.save() - ConsolePort.objects.get( - device=d, + consoleport = ConsolePort.objects.get( + device=device, name='Console Port 1' ) + self.assertEqual(consoleport.cf['cf1'], 'foo') - ConsoleServerPort.objects.get( - device=d, + consoleserverport = ConsoleServerPort.objects.get( + device=device, name='Console Server Port 1' ) + self.assertEqual(consoleserverport.cf['cf1'], 'foo') - pp = PowerPort.objects.get( - device=d, + powerport = PowerPort.objects.get( + device=device, name='Power Port 1', maximum_draw=1000, allocated_draw=500 ) + self.assertEqual(powerport.cf['cf1'], 'foo') - PowerOutlet.objects.get( - device=d, + poweroutlet = PowerOutlet.objects.get( + device=device, name='Power Outlet 1', - power_port=pp, + power_port=powerport, feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A ) + self.assertEqual(poweroutlet.cf['cf1'], 'foo') - Interface.objects.get( - device=d, + interface = Interface.objects.get( + device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED, mgmt_only=True ) + self.assertEqual(interface.cf['cf1'], 'foo') - rp = RearPort.objects.get( - device=d, + rearport = RearPort.objects.get( + device=device, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=8 ) + self.assertEqual(rearport.cf['cf1'], 'foo') - FrontPort.objects.get( - device=d, + frontport = FrontPort.objects.get( + device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, - rear_port=rp, + rear_port=rearport, rear_port_position=2 ) + self.assertEqual(frontport.cf['cf1'], 'foo') - ModuleBay.objects.get( - device=d, + modulebay = ModuleBay.objects.get( + device=device, name='Module Bay 1' ) + self.assertEqual(modulebay.cf['cf1'], 'foo') - DeviceBay.objects.get( - device=d, + devicebay = DeviceBay.objects.get( + device=device, name='Device Bay 1' ) + self.assertEqual(devicebay.cf['cf1'], 'foo') + + inventoryitem = InventoryItem.objects.get( + device=device, + name='Inventory Item 1' + ) + self.assertEqual(inventoryitem.cf['cf1'], 'foo') def test_multiple_unnamed_devices(self): diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 2bed464bb..f70812bc0 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -57,6 +57,15 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)): content_type = ContentType.objects.get_for_model(model._meta.concrete_model) return self.get_queryset().filter(content_types=content_type) + def get_defaults_for_model(self, model): + """ + Return a dictionary of serialized default values for all CustomFields applicable to the given model. + """ + custom_fields = self.get_for_model(model).filter(default__isnull=False) + return { + cf.name: cf.default for cf in custom_fields + } + class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): content_types = models.ManyToManyField( From fe3f21105ce1a32691cee4c04955bcfb6b64a34b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 6 Dec 2023 15:28:47 -0500 Subject: [PATCH 06/24] Fixes #14448: Fix exception when creating a power feed with rack and panel in different sites --- netbox/dcim/models/power.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index a852ea5cd..62578d6c4 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -175,7 +175,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel): # Rack must belong to same Site as PowerPanel if self.rack and self.rack.site != self.power_panel.site: raise ValidationError(_( - "Rack {rack} ({site}) and power panel {powerpanel} ({powerpanel_site}) are in different sites" + "Rack {rack} ({rack_site}) and power panel {powerpanel} ({powerpanel_site}) are in different sites." ).format( rack=self.rack, rack_site=self.rack.site, From 92bdaa2120b2cbb4944d7e679aa516e14b214274 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 7 Dec 2023 20:15:30 +0530 Subject: [PATCH 07/24] Fixes IPv6 detection from headers (#14456) * fixes client ip detection for v6 * adds test for get_client_ip * Employ urlparse() to strip port numbers from IPs --------- Co-authored-by: Jeremy Stretch --- netbox/utilities/request.py | 18 ++++++++++++----- netbox/utilities/tests/test_request.py | 28 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 netbox/utilities/tests/test_request.py diff --git a/netbox/utilities/request.py b/netbox/utilities/request.py index 0f8ee9cae..a5ca145e9 100644 --- a/netbox/utilities/request.py +++ b/netbox/utilities/request.py @@ -1,4 +1,5 @@ -from netaddr import IPAddress +from netaddr import AddrFormatError, IPAddress +from urllib.parse import urlparse __all__ = ( 'get_client_ip', @@ -17,11 +18,18 @@ def get_client_ip(request, additional_headers=()): ) for header in HTTP_HEADERS: if header in request.META: - client_ip = request.META[header].split(',')[0].partition(':')[0] + ip = request.META[header].split(',')[0].strip() try: - return IPAddress(client_ip) - except ValueError: - raise ValueError(f"Invalid IP address set for {header}: {client_ip}") + return IPAddress(ip) + except AddrFormatError: + # Parse the string with urlparse() to remove port number or any other cruft + ip = urlparse(f'//{ip}').hostname + + try: + return IPAddress(ip) + except AddrFormatError: + # We did our best + raise ValueError(f"Invalid IP address set for {header}: {ip}") # Could not determine the client IP address from request headers return None diff --git a/netbox/utilities/tests/test_request.py b/netbox/utilities/tests/test_request.py new file mode 100644 index 000000000..69f677323 --- /dev/null +++ b/netbox/utilities/tests/test_request.py @@ -0,0 +1,28 @@ +from django.test import TestCase, RequestFactory + +from netaddr import IPAddress +from utilities.request import get_client_ip + + +class GetClientIPTests(TestCase): + def setUp(self): + self.factory = RequestFactory() + + def test_ipv4_address(self): + request = self.factory.get('/', HTTP_X_FORWARDED_FOR='192.168.1.1') + self.assertEqual(get_client_ip(request), IPAddress('192.168.1.1')) + request = self.factory.get('/', HTTP_X_FORWARDED_FOR='192.168.1.1:8080') + self.assertEqual(get_client_ip(request), IPAddress('192.168.1.1')) + + def test_ipv6_address(self): + request = self.factory.get('/', HTTP_X_FORWARDED_FOR='2001:db8::8a2e:370:7334') + self.assertEqual(get_client_ip(request), IPAddress('2001:db8::8a2e:370:7334')) + request = self.factory.get('/', HTTP_X_FORWARDED_FOR='[2001:db8::8a2e:370:7334]') + self.assertEqual(get_client_ip(request), IPAddress('2001:db8::8a2e:370:7334')) + request = self.factory.get('/', HTTP_X_FORWARDED_FOR='[2001:db8::8a2e:370:7334]:8080') + self.assertEqual(get_client_ip(request), IPAddress('2001:db8::8a2e:370:7334')) + + def test_invalid_ip_address(self): + request = self.factory.get('/', HTTP_X_FORWARDED_FOR='invalid_ip') + with self.assertRaises(ValueError): + get_client_ip(request) From e59ee3e01e29408609a4b91486fccc65456f7c7f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 5 Dec 2023 10:29:17 -0500 Subject: [PATCH 08/24] Fixes #14397: Pass a mutable copy of request data when provisioning available IPs --- netbox/ipam/api/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 662b393de..8e815817f 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -1,3 +1,5 @@ +from copy import deepcopy + from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction from django.shortcuts import get_object_or_404 @@ -290,7 +292,7 @@ class AvailableObjectsView(ObjectValidationMixin, APIView): ) # Prepare object data for deserialization - requested_objects = self.prep_object_data(requested_objects, available_objects, parent) + requested_objects = self.prep_object_data(deepcopy(requested_objects), available_objects, parent) # Initialize the serializer with a list or a single object depending on what was requested serializer_class = get_serializer_for_model(self.queryset.model) From 95a8415e2d027a11c7067e0f9235bedf27349855 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 7 Dec 2023 16:21:15 -0500 Subject: [PATCH 09/24] Add deployment type to bug report template --- .github/ISSUE_TEMPLATE/bug_report.yaml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 5e936c5ec..dcc3f0a97 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -10,16 +10,25 @@ body: installation. If you're having trouble with installation or just looking for assistance with using NetBox, please visit our [discussion forum](https://github.com/netbox-community/netbox/discussions) instead. + - type: dropdown + attributes: + label: Deployment Type + description: How are you running NetBox? + options: + - Self-hosted + - NetBox Cloud + validations: + required: true - type: input attributes: - label: NetBox version + label: NetBox Version description: What version of NetBox are you currently running? placeholder: v3.6.6 validations: required: true - type: dropdown attributes: - label: Python version + label: Python Version description: What version of Python are you currently running? options: - "3.8" From 9d7192202d793b7eaf8d67b8100ecc2ad5494bd2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 5 Dec 2023 15:09:59 -0500 Subject: [PATCH 10/24] Fixes #14392: Fix admin UI bulk actions --- netbox/templates/django/forms/widgets/checkbox.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/netbox/templates/django/forms/widgets/checkbox.html b/netbox/templates/django/forms/widgets/checkbox.html index bbe201a29..359657136 100644 --- a/netbox/templates/django/forms/widgets/checkbox.html +++ b/netbox/templates/django/forms/widgets/checkbox.html @@ -1,6 +1,7 @@ {% comment %} Include a hidden field of the same name to ensure that unchecked checkboxes - are always included in the submitted form data. + are always included in the submitted form data. Omit fields names + _selected_action to avoid breaking the admin UI. {% endcomment %} - +{% if widget.name != '_selected_action' %}{% endif %} {% include "django/forms/widgets/input.html" %} From 2ef023a16045b848f9c1cb3dbb9c2ee180589dac Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 7 Dec 2023 16:34:49 -0500 Subject: [PATCH 11/24] Changelog for #14249, #14390, #14392, #14397, #14401, #14432, #14448 --- docs/release-notes/version-3.6.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index 10e93be1e..a4234f8dd 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -2,6 +2,19 @@ ## v3.6.7 (FUTURE) +### Enhancements + +* [#14390](https://github.com/netbox-community/netbox/issues/14390) - Add `classes` parameter to `copy_content` template tag + +### Bug Fixes + +* [#14249](https://github.com/netbox-community/netbox/issues/14249) - Fix server error when authenticating via IP-restricted API tokens using IPv6 +* [#14392](https://github.com/netbox-community/netbox/issues/14392) - Fix bulk operations for plugin models under admin UI +* [#14397](https://github.com/netbox-community/netbox/issues/14397) - Fix exception on non-JSON request to `/available-ips/` API endpoints +* [#14401](https://github.com/netbox-community/netbox/issues/14401) - Rack `starting_unit` cannot be zero +* [#14432](https://github.com/netbox-community/netbox/issues/14432) - Populate custom field default values for components when creating a device +* [#14448](https://github.com/netbox-community/netbox/issues/14448) - Fix exception when creating a power feed with rack and panel in different sites + --- ## v3.6.6 (2023-11-29) From 35be4f05ef376e28d9af4d7245ba10cc286bb62a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 11 Dec 2023 10:10:28 -0500 Subject: [PATCH 12/24] Add note to bug reports section --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 301fac079..471846427 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,8 @@ NetBox users are welcome to participate in either role, on stage or in the crowd ## :bug: Reporting Bugs +:warning: Bug reports are used to call attention to some unintended or unexpected behavior in NetBox, such as when an error occurs or when the result of taking some action is inconsistent with the documentation. **Bug reports may not be used to suggest new functionality**; please see "feature requests" below if that is your goal. + * 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. From 81fa4265da51268e0f2652ac36a76a724ebdc4ab Mon Sep 17 00:00:00 2001 From: Prince Kumar Date: Mon, 11 Dec 2023 16:01:33 +0530 Subject: [PATCH 13/24] add tags field in L2VPN Termination --- netbox/ipam/forms/model_forms.py | 2 +- netbox/ipam/tables/l2vpn.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index bfd4f952d..41b31dc76 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -818,7 +818,7 @@ class L2VPNTerminationForm(NetBoxModelForm): class Meta: model = L2VPNTermination - fields = ('l2vpn', ) + fields = ('l2vpn', 'tags') def __init__(self, *args, **kwargs): instance = kwargs.get('instance') diff --git a/netbox/ipam/tables/l2vpn.py b/netbox/ipam/tables/l2vpn.py index 8635ab62a..6678d184c 100644 --- a/netbox/ipam/tables/l2vpn.py +++ b/netbox/ipam/tables/l2vpn.py @@ -73,12 +73,15 @@ class L2VPNTerminationTable(NetBoxTable): orderable=False, verbose_name=_('Object Site') ) + tags = columns.TagColumn( + url_name='ipam:l2vpntermination_list' + ) class Meta(NetBoxTable.Meta): model = L2VPNTermination fields = ( 'pk', 'l2vpn', 'assigned_object_type', 'assigned_object', 'assigned_object_parent', 'assigned_object_site', - 'actions', + 'tags', 'actions', ) default_columns = ( 'pk', 'l2vpn', 'assigned_object_type', 'assigned_object_parent', 'assigned_object', 'actions', From 6939ae4a47192d3d6e87061cc741a9b51f7ea215 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Tue, 12 Dec 2023 11:31:39 -0800 Subject: [PATCH 14/24] 14467 change ChoiceField separator from comma to colon (#14469) * 14467 change ChoiceField separator from comma to colon * 14467 fix test * 14467 fix test * 14467 use regex for colon detection * 14467 update tests --- netbox/extras/forms/model_forms.py | 7 ++++--- netbox/extras/tests/test_views.py | 9 ++++++++- netbox/utilities/forms/widgets/misc.py | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 83a346420..4e4a6e0de 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -1,4 +1,5 @@ import json +import re from django import forms from django.conf import settings @@ -95,8 +96,8 @@ class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm): required=False, help_text=mark_safe(_( 'Enter one choice per line. An optional label may be specified for each choice by appending it with a ' - 'comma. Example:' - ) + ' choice1,First Choice') + 'colon. Example:' + ) + ' choice1:First Choice') ) class Meta: @@ -107,7 +108,7 @@ class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm): data = [] for line in self.cleaned_data['extra_choices'].splitlines(): try: - value, label = line.split(',', maxsplit=1) + value, label = re.split(r'(? Date: Tue, 12 Dec 2023 13:53:04 -0800 Subject: [PATCH 15/24] Fixes #14081: Fix cached counters on delete for parent-child items (#14131) * 14081 fixed cached counters on delete for parent-child items * Misc cleanup --------- Co-authored-by: Jeremy Stretch --- netbox/utilities/counters.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/netbox/utilities/counters.py b/netbox/utilities/counters.py index 0ee2606db..589dacbdb 100644 --- a/netbox/utilities/counters.py +++ b/netbox/utilities/counters.py @@ -1,6 +1,6 @@ from django.apps import apps from django.db.models import F, Count, OuterRef, Subquery -from django.db.models.signals import post_delete, post_save +from django.db.models.signals import post_delete, post_save, pre_delete from netbox.registry import registry from .fields import CounterCacheField @@ -62,6 +62,12 @@ def post_save_receiver(sender, instance, created, **kwargs): update_counter(parent_model, new_pk, counter_name, 1) +def pre_delete_receiver(sender, instance, origin, **kwargs): + model = instance._meta.model + if not model.objects.filter(pk=instance.pk).exists(): + instance._previously_removed = True + + def post_delete_receiver(sender, instance, origin, **kwargs): """ Update counter fields on related objects when a TrackingModelMixin subclass is deleted. @@ -71,10 +77,8 @@ def post_delete_receiver(sender, instance, origin, **kwargs): parent_pk = getattr(instance, field_name, None) # Decrement the parent's counter by one - if parent_pk is not None: - # MPTT sends two delete signals for child elements so guard against multiple decrements - if not origin or origin == instance: - update_counter(parent_model, parent_pk, counter_name, -1) + if parent_pk is not None and not hasattr(instance, "_previously_removed"): + update_counter(parent_model, parent_pk, counter_name, -1) # @@ -106,6 +110,12 @@ def connect_counters(*models): weak=False, dispatch_uid=f'{model._meta.label}.{field.name}' ) + pre_delete.connect( + pre_delete_receiver, + sender=to_model, + weak=False, + dispatch_uid=f'{model._meta.label}.{field.name}' + ) post_delete.connect( post_delete_receiver, sender=to_model, From 69bf1472d2e80bc2916020b06cb71d87eb9ccfaa Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Thu, 14 Dec 2023 12:18:56 -0800 Subject: [PATCH 16/24] 13983 Add nested arrays for extra_choices in CustomFieldChoiceSet (#14470) * 13983 split array fields in CSV data for CustomFieldChoices * 13983 fix help text * 13983 update tests * 13983 use re for split * 13983 replace escaped chars * 13983 fix escape handling * 13983 fix escape handling * 13983 fix escape handling --- netbox/extras/forms/bulk_import.py | 20 +++++++++++++++++++- netbox/extras/forms/model_forms.py | 14 ++++++++++++++ netbox/extras/tests/test_views.py | 12 +++++++++--- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 79023a74d..745268f33 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -1,3 +1,5 @@ +import re + from django import forms from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.forms import SimpleArrayField @@ -76,7 +78,10 @@ class CustomFieldChoiceSetImportForm(CSVModelForm): extra_choices = SimpleArrayField( base_field=forms.CharField(), required=False, - help_text=_('Comma-separated list of field choices') + help_text=_( + 'Quoted string of comma-separated field choices with optional labels separated by colon: ' + '"choice1:First Choice,choice2:Second Choice"' + ) ) class Meta: @@ -85,6 +90,19 @@ class CustomFieldChoiceSetImportForm(CSVModelForm): 'name', 'description', 'extra_choices', 'order_alphabetically', ) + def clean_extra_choices(self): + if isinstance(self.cleaned_data['extra_choices'], list): + data = [] + for line in self.cleaned_data['extra_choices']: + try: + value, label = re.split(r'(? Date: Thu, 14 Dec 2023 13:33:11 -0500 Subject: [PATCH 17/24] Fixes #14512: Omit unused queryset annotations for REST API requests using brief mode --- netbox/netbox/api/viewsets/mixins.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index fde486fe9..315563e1a 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -56,8 +56,15 @@ class BriefModeMixin: def get_queryset(self): qs = super().get_queryset() - # If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any) if self.brief: + serializer_class = self.get_serializer_class() + + # Clear any annotations for fields not present on the nested serializer + for annotation in list(qs.query.annotations.keys()): + if annotation not in serializer_class().fields: + qs.query.annotations.pop(annotation) + + # Clear any prefetches from the queryset and append only brief_prefetch_fields (if any) return qs.prefetch_related(None).prefetch_related(*self.brief_prefetch_fields) return qs From 8a4233aca15e9aa62ac9b72bb5db304807cb1240 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Thu, 14 Dec 2023 08:08:09 -0600 Subject: [PATCH 18/24] Update create_userconfig to receive signals from NetBoxUser model in addition to User model. --- netbox/users/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/users/models.py b/netbox/users/models.py index 80fd0dd09..e9ee85960 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -218,6 +218,7 @@ class UserConfig(models.Model): @receiver(post_save, sender=User) +@receiver(post_save, sender=NetBoxUser) def create_userconfig(instance, created, raw=False, **kwargs): """ Automatically create a new UserConfig when a new User is created. Skip this if importing a user from a fixture. From d14e4ab52ba6e9ed537d576b193169b4e1a34bd2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 14 Dec 2023 17:12:29 -0500 Subject: [PATCH 19/24] Changelog for #13983, #14081, #14148, #14467, #14505, #14512, #14515 --- docs/release-notes/version-3.6.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index a4234f8dd..b8d237ce1 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -4,16 +4,23 @@ ### Enhancements +* [#14148](https://github.com/netbox-community/netbox/issues/14148) - Add tags column to L2VPN terminations column * [#14390](https://github.com/netbox-community/netbox/issues/14390) - Add `classes` parameter to `copy_content` template tag +* [#14467](https://github.com/netbox-community/netbox/issues/14467) - Change custom field choice delimiter from comma to colon ### Bug Fixes +* [#13983](https://github.com/netbox-community/netbox/issues/13983) - Fix bulk import support for custom field choices +* [#14081](https://github.com/netbox-community/netbox/issues/14081) - Ensure accuracy of parent object counters when deleting related objects * [#14249](https://github.com/netbox-community/netbox/issues/14249) - Fix server error when authenticating via IP-restricted API tokens using IPv6 * [#14392](https://github.com/netbox-community/netbox/issues/14392) - Fix bulk operations for plugin models under admin UI * [#14397](https://github.com/netbox-community/netbox/issues/14397) - Fix exception on non-JSON request to `/available-ips/` API endpoints * [#14401](https://github.com/netbox-community/netbox/issues/14401) - Rack `starting_unit` cannot be zero * [#14432](https://github.com/netbox-community/netbox/issues/14432) - Populate custom field default values for components when creating a device * [#14448](https://github.com/netbox-community/netbox/issues/14448) - Fix exception when creating a power feed with rack and panel in different sites +* [#14505](https://github.com/netbox-community/netbox/issues/14505) - Fix the assignment of tags to L2VPN terminations +* [#14512](https://github.com/netbox-community/netbox/issues/14512) - Remove unneeded annotations from queries when using REST API brief mode +* [#14515](https://github.com/netbox-community/netbox/issues/14515) - Ensure user config is created automatically for all user accounts --- From 929d4d2c95d7ed4a1ffdd8f56437ce316917d62e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 14 Dec 2023 16:58:11 -0500 Subject: [PATCH 20/24] Fixes #14522: Fix filtering contact assignments by group --- netbox/tenancy/filtersets.py | 13 +++++ netbox/tenancy/tests/test_filtersets.py | 71 +++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index 0f4900f54..8bc659a88 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -91,6 +91,19 @@ class ContactAssignmentFilterSet(ChangeLoggedModelFilterSet): queryset=Contact.objects.all(), label=_('Contact (ID)'), ) + group_id = TreeNodeMultipleChoiceFilter( + queryset=ContactGroup.objects.all(), + field_name='contact__group', + lookup_expr='in', + label=_('Contact group (ID)'), + ) + group = TreeNodeMultipleChoiceFilter( + queryset=ContactGroup.objects.all(), + field_name='contact__group', + lookup_expr='in', + to_field_name='slug', + label=_('Contact group (slug)'), + ) role_id = django_filters.ModelMultipleChoiceFilter( queryset=ContactRole.objects.all(), label=_('Contact role (ID)'), diff --git a/netbox/tenancy/tests/test_filtersets.py b/netbox/tenancy/tests/test_filtersets.py index e427c90ce..d7337396e 100644 --- a/netbox/tenancy/tests/test_filtersets.py +++ b/netbox/tenancy/tests/test_filtersets.py @@ -1,5 +1,7 @@ +from django.contrib.contenttypes.models import ContentType from django.test import TestCase +from dcim.models import Manufacturer, Site from tenancy.filtersets import * from tenancy.models import * from utilities.testing import ChangeLoggedFilterSetTests @@ -192,3 +194,72 @@ class ContactTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) params = {'group': [group[0].slug, group[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class ContactAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests): + queryset = ContactAssignment.objects.all() + filterset = ContactAssignmentFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + Site(name='Site 3', slug='site-3'), + ) + Site.objects.bulk_create(sites) + + contact_groups = ( + ContactGroup(name='Contact Group 1', slug='contact-group-1'), + ContactGroup(name='Contact Group 2', slug='contact-group-2'), + ContactGroup(name='Contact Group 3', slug='contact-group-3'), + ) + for contactgroup in contact_groups: + contactgroup.save() + + contact_roles = ( + ContactRole(name='Contact Role 1', slug='contact-role-1'), + ContactRole(name='Contact Role 2', slug='contact-role-2'), + ContactRole(name='Contact Role 3', slug='contact-role-3'), + ) + ContactRole.objects.bulk_create(contact_roles) + + contacts = ( + Contact(name='Contact 1', group=contact_groups[0]), + Contact(name='Contact 2', group=contact_groups[1]), + Contact(name='Contact 3', group=contact_groups[2]), + ) + Contact.objects.bulk_create(contacts) + + assignments = ( + ContactAssignment(object=sites[0], contact=contacts[0], role=contact_roles[0]), + ContactAssignment(object=sites[1], contact=contacts[1], role=contact_roles[1]), + ContactAssignment(object=sites[2], contact=contacts[2], role=contact_roles[2]), + ContactAssignment(object=manufacturer, contact=contacts[2], role=contact_roles[2]), + ) + ContactAssignment.objects.bulk_create(assignments) + + def test_content_type(self): + params = {'content_type_id': ContentType.objects.get_by_natural_key('dcim', 'site')} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + + def test_contact(self): + contacts = Contact.objects.all()[:2] + params = {'contact_id': [contacts[0].pk, contacts[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_group(self): + group = ContactGroup.objects.all()[:2] + params = {'group_id': [group[0].pk, group[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'group': [group[0].slug, group[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_role(self): + role = ContactRole.objects.all()[:2] + params = {'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]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) From c81869c795160c01e052533f65ca8461d337f969 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 15 Dec 2023 08:21:34 -0500 Subject: [PATCH 21/24] Fixes #14533: Fix quick search under VLAN group VLANs list --- netbox/ipam/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 48ea637d9..1de53b6d2 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -953,7 +953,7 @@ class VLANGroupVLANsView(generic.ObjectChildrenView): def prep_table_data(self, request, queryset, parent): if not get_table_ordering(request, self.table): - return add_available_vlans(parent.get_child_vlans(), parent) + return add_available_vlans(queryset, parent) return queryset From 8d391818429c4b2728de1bab42b52de31d2e8830 Mon Sep 17 00:00:00 2001 From: kkthxbye <400797+kkthxbye-code@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:07:15 +0100 Subject: [PATCH 22/24] Fixes #12751 - Usability improvements for object selector (#14387) * Usability improvements for object selector: * Adds preselected filters * Applies the filter on selection instead of requiring the search button to be pushed * Declare selector_fields on base form class --------- Co-authored-by: Jeremy Stretch --- netbox/circuits/forms/filtersets.py | 1 + netbox/dcim/forms/filtersets.py | 8 ++++++++ netbox/ipam/forms/filtersets.py | 2 ++ netbox/netbox/forms/base.py | 4 ++++ netbox/project-static/dist/netbox.js | Bin 529867 -> 529929 bytes netbox/project-static/dist/netbox.js.map | Bin 450255 -> 450302 bytes .../src/select/api/apiSelect.ts | 5 +++++ netbox/templates/htmx/object_selector.html | 6 +++--- netbox/virtualization/forms/filtersets.py | 2 ++ 9 files changed, 25 insertions(+), 3 deletions(-) diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index 1fb239023..643071be8 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -110,6 +110,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi (_('Tenant'), ('tenant_group_id', 'tenant_id')), (_('Contacts'), ('contact', 'contact_role', 'contact_group')), ) + selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id') type_id = DynamicModelMultipleChoiceField( queryset=CircuitType.objects.all(), required=False, diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index d0d321187..41bb417aa 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -164,6 +164,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte (_('Tenant'), ('tenant_group_id', 'tenant_id')), (_('Contacts'), ('contact', 'contact_role', 'contact_group')), ) + selector_fields = ('filter_id', 'q', 'region_id', 'group_id') status = forms.MultipleChoiceField( label=_('Status'), choices=SiteStatusChoices, @@ -247,6 +248,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte (_('Contacts'), ('contact', 'contact_role', 'contact_group')), (_('Weight'), ('weight', 'max_weight', 'weight_unit')), ) + selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id') region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -419,6 +421,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm): )), (_('Weight'), ('weight', 'weight_unit')), ) + selector_fields = ('filter_id', 'q', 'manufacturer_id') manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, @@ -543,6 +546,7 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm): )), (_('Weight'), ('weight', 'weight_unit')), ) + selector_fields = ('filter_id', 'q', 'manufacturer_id') manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, @@ -619,6 +623,7 @@ class DeviceRoleFilterForm(NetBoxModelFilterSetForm): class PlatformFilterForm(NetBoxModelFilterSetForm): model = Platform + selector_fields = ('filter_id', 'q', 'manufacturer_id') manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, @@ -653,6 +658,7 @@ class DeviceFilterForm( 'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data', )) ) + selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id') region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -996,6 +1002,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')), (_('Contacts'), ('contact', 'contact_role', 'contact_group')), ) + selector_fields = ('filter_id', 'q', 'site_id', 'location_id') region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -1227,6 +1234,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')), (_('Connection'), ('cabled', 'connected', 'occupied')), ) + selector_fields = ('filter_id', 'q', 'device_id') vdc_id = DynamicModelMultipleChoiceField( queryset=VirtualDeviceContext.objects.all(), required=False, diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index a8ca91901..b72788387 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -300,6 +300,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): (_('Tenant'), ('tenant_group_id', 'tenant_id')), (_('Device/VM'), ('device_id', 'virtual_machine_id')), ) + selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role') parent = forms.CharField( required=False, widget=forms.TextInput( @@ -452,6 +453,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): (_('Attributes'), ('group_id', 'status', 'role_id', 'vid', 'l2vpn_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')), ) + selector_fields = ('filter_id', 'q', 'site_id') region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 43d0850f0..51e664a39 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -145,12 +145,16 @@ class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, SavedFiltersMi model: The model class associated with the form fieldsets: An iterable of two-tuples which define a heading and field set to display per section of the rendered form (optional). If not defined, the all fields will be rendered as a single section. + selector_fields: An iterable of names of fields to display by default when rendering the form as + a selector widget """ q = forms.CharField( required=False, label=_('Search') ) + selector_fields = ('filter_id', 'q') + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 426302ea83c97ad0ae968919b4ede00f323f214e..97c4ba79c3e5b51b62097e2d1b2dbf904da8a917 100644 GIT binary patch delta 56 zcmV-80LTB!t00N2Ab^AcgaU*Egam{Iv<4!|m-J!<6A2=7Xm58SDJCYD=gI~immlH< O7Kb#-2DdcI2bW7sdK85K delta 36 scmeBNqi}k)LPHB<3sVbo3rh>@7B;0*)0H2w3bhxVV%uJHiha5_00L7E4FCWD diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 077c4bcc0b0e1e6c7dbb1eb1722ea94132e260b2..bbb2a3cc01cdba0309a6ead9b117254ba9d182be 100644 GIT binary patch delta 115 zcmX@VSNh*x>4p}@7N!>F7M3ln>(3gyM(B7tItJ^6Iy#2x1cO+?j?R`&I)RRkfgl-A z9cM>JXB}@xM{k`(N8bV`9rx*iUzo+WpE}F>O@u3>(y7W>$KNrta{H-AtiJ3ldD>3K Q)6d(mifuQ1#X9c`0J+*HXaE2J delta 68 zcmV-K0K5PGy&KQH8-RoXgaU*Egaot&ugwHWW-LjU5%dEWxB1Nl_7n;;ctUzZEKfl+ ac(?iD1x*J8IXOZzm(M%}7PnyP1)lV-*Bbr+ diff --git a/netbox/project-static/src/select/api/apiSelect.ts b/netbox/project-static/src/select/api/apiSelect.ts index 53996910e..279340c12 100644 --- a/netbox/project-static/src/select/api/apiSelect.ts +++ b/netbox/project-static/src/select/api/apiSelect.ts @@ -264,6 +264,11 @@ export class APISelect { switch (this.trigger) { case 'collapse': if (collapse !== null) { + // If the element is collapsible but already shown, load the data immediately. + if (collapse.classList.contains('show')) { + Promise.all([this.loadData()]); + } + // If this element is part of a collapsible element, only load the data when the // collapsible element is shown. // See: https://getbootstrap.com/docs/5.0/components/collapse/#events diff --git a/netbox/templates/htmx/object_selector.html b/netbox/templates/htmx/object_selector.html index 0febb1069..280102ada 100644 --- a/netbox/templates/htmx/object_selector.html +++ b/netbox/templates/htmx/object_selector.html @@ -10,18 +10,18 @@
{% for field in form.visible_fields %} - + {{ field.label }} {% endfor %}
-
+
{% for field in form.visible_fields %} -
{% render_field field %}
+
{% render_field field %}
{% endfor %}
diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py index 99ac0cb77..4028bcc64 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -44,6 +44,7 @@ class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi (_('Tenant'), ('tenant_group_id', 'tenant_id')), (_('Contacts'), ('contact', 'contact_role', 'contact_group')), ) + selector_fields = ('filter_id', 'q', 'group_id') type_id = DynamicModelMultipleChoiceField( queryset=ClusterType.objects.all(), required=False, @@ -186,6 +187,7 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm): (_('Virtual Machine'), ('cluster_id', 'virtual_machine_id')), (_('Attributes'), ('enabled', 'mac_address', 'vrf_id', 'l2vpn_id')), ) + selector_fields = ('filter_id', 'q', 'virtual_machine_id') cluster_id = DynamicModelMultipleChoiceField( queryset=Cluster.objects.all(), required=False, From 4cdc30a7c524ba01cb85eaf3801e11293d16f09c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 15 Dec 2023 16:25:24 -0500 Subject: [PATCH 23/24] Release v3.6.7 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- docs/release-notes/version-3.6.md | 5 ++++- netbox/netbox/settings.py | 2 +- requirements.txt | 18 +++++++++--------- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index dcc3f0a97..974527bd3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -23,7 +23,7 @@ body: attributes: label: NetBox Version description: What version of NetBox are you currently running? - placeholder: v3.6.6 + placeholder: v3.6.7 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 34103e616..9fb14742a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.6.6 + placeholder: v3.6.7 validations: required: true - type: dropdown diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index b8d237ce1..3478eb081 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -1,9 +1,10 @@ # NetBox v3.6 -## v3.6.7 (FUTURE) +## v3.6.7 (2023-12-15) ### Enhancements +* [#12751](https://github.com/netbox-community/netbox/issues/12751) - Designate fields to expand by default for object selector widget * [#14148](https://github.com/netbox-community/netbox/issues/14148) - Add tags column to L2VPN terminations column * [#14390](https://github.com/netbox-community/netbox/issues/14390) - Add `classes` parameter to `copy_content` template tag * [#14467](https://github.com/netbox-community/netbox/issues/14467) - Change custom field choice delimiter from comma to colon @@ -21,6 +22,8 @@ * [#14505](https://github.com/netbox-community/netbox/issues/14505) - Fix the assignment of tags to L2VPN terminations * [#14512](https://github.com/netbox-community/netbox/issues/14512) - Remove unneeded annotations from queries when using REST API brief mode * [#14515](https://github.com/netbox-community/netbox/issues/14515) - Ensure user config is created automatically for all user accounts +* [#14522](https://github.com/netbox-community/netbox/issues/14522) - Fix filtering contact assignments by group +* [#14533](https://github.com/netbox-community/netbox/issues/14533) - Fix quick search under VLAN group VLANs list --- diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 824bc4605..9cc6820bd 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.6.7-dev' +VERSION = '3.6.7' # Hostname HOSTNAME = platform.node() diff --git a/requirements.txt b/requirements.txt index b99f16e76..b2771b445 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ bleach==6.1.0 -Django==4.2.7 +Django==4.2.8 django-cors-headers==4.3.1 django-debug-toolbar==4.2.0 -django-filter==23.4 +django-filter==23.5 django-graphiql-debug-toolbar==0.2.0 django-mptt==0.14.0 django-pglocks==1.0.4 @@ -10,25 +10,25 @@ django-prometheus==2.3.1 django-redis==5.4.0 django-rich==1.8.0 django-rq==2.9.0 -django-tables2==2.6.0 +django-tables2==2.7.0 django-taggit==4.0.0 django-timezone-field==6.1.0 djangorestframework==3.14.0 -drf-spectacular==0.26.5 -drf-spectacular-sidecar==2023.10.1 -feedparser==6.0.10 +drf-spectacular==0.27.0 +drf-spectacular-sidecar==2023.12.1 +feedparser==6.0.11 graphene-django==3.0.0 gunicorn==21.2.0 Jinja2==3.1.2 Markdown==3.3.7 -mkdocs-material==9.4.14 +mkdocs-material==9.5.2 mkdocstrings[python-legacy]==0.24.0 netaddr==0.9.0 Pillow==10.1.0 -psycopg[binary,pool]==3.1.13 +psycopg[binary,pool]==3.1.15 PyYAML==6.0.1 requests==2.31.0 -sentry-sdk==1.38.0 +sentry-sdk==1.39.1 social-auth-app-django==5.4.0 social-auth-core[openidconnect]==4.5.1 svgwrite==1.4.3 From ec245b968f50bdbafaadd5d6b885832d858fa167 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 15 Dec 2023 16:46:53 -0500 Subject: [PATCH 24/24] PRVB --- docs/release-notes/version-3.6.md | 4 ++++ netbox/netbox/settings.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index 3478eb081..fc2328897 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -1,5 +1,9 @@ # NetBox v3.6 +## v3.6.8 (FUTURE) + +--- + ## v3.6.7 (2023-12-15) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 9cc6820bd..3fd7f1122 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.6.7' +VERSION = '3.6.8-dev' # Hostname HOSTNAME = platform.node()