From 33004dfab0679547b2a33b84b39f6b96f08201ae Mon Sep 17 00:00:00 2001 From: Jeff Gehlbach Date: Tue, 25 Jun 2024 16:21:32 -0400 Subject: [PATCH 1/4] Added missing CDN cache clearing step to release checklist in docs --- docs/development/release-checklist.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md index 91162f08a..019eb2a6c 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -135,4 +135,6 @@ First, run the `build-site` action, by navigating to Actions > build-site > Run Once the documentation files have been compiled, they must be published by running the `deploy-kinsta` action. Select the desired deployment environment (staging or production) and specify `latest` as the deploy tag. +Clear the CDN cache from the [Kinsta](https://my.kinsta.com/) portal. Navigate to _Sites_ / _NetBox Labs_ / _Live_, select _CDN_ in the left-nav, click the _Clear CDN cache_ button, and confirm the clear operation. + Finally, verify that the documentation at has been updated. From b605dfcba09f36ff1d69d4140dea4074cad68026 Mon Sep 17 00:00:00 2001 From: Julio Oliveira at Encora <149191228+Julio-Oliveira-Encora@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:14:08 -0300 Subject: [PATCH 2/4] 16704 - Define a default help_text for ColorField (#16708) * Added `help_text` to ColorField. * Addressed PR comment to remove the redundant help_text from all the forms where ColorField was used. * Add space before example value --------- Co-authored-by: Jeremy Stretch --- netbox/circuits/forms/bulk_import.py | 3 --- netbox/dcim/forms/bulk_import.py | 12 ------------ netbox/extras/forms/bulk_import.py | 3 --- netbox/utilities/fields.py | 2 ++ 4 files changed, 2 insertions(+), 18 deletions(-) diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py index 1ceb44b60..88fdd2c71 100644 --- a/netbox/circuits/forms/bulk_import.py +++ b/netbox/circuits/forms/bulk_import.py @@ -66,9 +66,6 @@ class CircuitTypeImportForm(NetBoxModelImportForm): class Meta: model = CircuitType fields = ('name', 'slug', 'color', 'description', 'tags') - help_texts = { - 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'), - } class CircuitImportForm(NetBoxModelImportForm): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 5a64cad02..1c537512c 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -174,9 +174,6 @@ class RackRoleImportForm(NetBoxModelImportForm): class Meta: model = RackRole fields = ('name', 'slug', 'color', 'description', 'tags') - help_texts = { - 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'), - } class RackImportForm(NetBoxModelImportForm): @@ -384,9 +381,6 @@ class DeviceRoleImportForm(NetBoxModelImportForm): class Meta: model = DeviceRole fields = ('name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags') - help_texts = { - 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'), - } class PlatformImportForm(NetBoxModelImportForm): @@ -1104,9 +1098,6 @@ class InventoryItemRoleImportForm(NetBoxModelImportForm): class Meta: model = InventoryItemRole fields = ('name', 'slug', 'color', 'description') - help_texts = { - 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'), - } # @@ -1183,9 +1174,6 @@ class CableImportForm(NetBoxModelImportForm): 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags', ] - help_texts = { - 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'), - } def _clean_side(self, side): """ diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index c09eed3da..f2cf0b721 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -228,9 +228,6 @@ class TagImportForm(CSVModelForm): class Meta: model = Tag fields = ('name', 'slug', 'color', 'description') - help_texts = { - 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'), - } class JournalEntryImportForm(NetBoxModelImportForm): diff --git a/netbox/utilities/fields.py b/netbox/utilities/fields.py index 2640f6886..ee71223cb 100644 --- a/netbox/utilities/fields.py +++ b/netbox/utilities/fields.py @@ -2,6 +2,7 @@ from collections import defaultdict from django.contrib.contenttypes.fields import GenericForeignKey from django.db import models +from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from utilities.ordering import naturalize @@ -26,6 +27,7 @@ class ColorField(models.CharField): def formfield(self, **kwargs): kwargs['widget'] = ColorSelect + kwargs['help_text'] = mark_safe(_('RGB color in hexadecimal. Example: ') + '00ff00') return super().formfield(**kwargs) From b241c97e0059affc799d7f788c74b8326a8b66c3 Mon Sep 17 00:00:00 2001 From: Julio Oliveira at Encora <149191228+Julio-Oliveira-Encora@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:54:15 -0300 Subject: [PATCH 3/4] =?UTF-8?q?Was=20added=20to=20searching=20support=20la?= =?UTF-8?q?nguages=20other=20than=20English=20for=20objec=E2=80=A6=20(#167?= =?UTF-8?q?06)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Was added to searching support languages other than English for object types(s). * Fix SearchForm field label translation --------- Co-authored-by: Jeremy Stretch --- netbox/netbox/forms/__init__.py | 5 +++-- netbox/netbox/search/backends.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/netbox/netbox/forms/__init__.py b/netbox/netbox/forms/__init__.py index fa82689a5..f88fb18bc 100644 --- a/netbox/netbox/forms/__init__.py +++ b/netbox/netbox/forms/__init__.py @@ -1,7 +1,7 @@ import re from django import forms -from django.utils.translation import gettext as _ +from django.utils.translation import gettext_lazy as _ from netbox.search import LookupTypes from netbox.search.backends import search_backend @@ -36,7 +36,8 @@ class SearchForm(forms.Form): lookup = forms.ChoiceField( choices=LOOKUP_CHOICES, initial=LookupTypes.PARTIAL, - required=False + required=False, + label=_('Lookup') ) def __init__(self, *args, **kwargs): diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 227a79205..12243e9b6 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -8,6 +8,7 @@ from django.db.models.fields.related import ForeignKey from django.db.models.functions import window from django.db.models.signals import post_delete, post_save from django.utils.module_loading import import_string +from django.utils.translation import gettext_lazy as _ import netaddr from netaddr.core import AddrFormatError @@ -39,7 +40,7 @@ class SearchBackend: # Organize choices by category categories = defaultdict(dict) for label, idx in registry['search'].items(): - categories[idx.get_category()][label] = title(idx.model._meta.verbose_name) + categories[idx.get_category()][label] = _(title(idx.model._meta.verbose_name)) # Compile a nested tuple of choices for form rendering results = ( From c506f60f1208796deda36e6bea4c8f77a8d13a16 Mon Sep 17 00:00:00 2001 From: Julio Oliveira at Encora <149191228+Julio-Oliveira-Encora@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:13:32 -0300 Subject: [PATCH 4/4] 16424 - Allow filtering of Devices by Cluster and Cluster Group (#16674) * Allow filtering Devices by Cluster and Cluster Group. * Allow filtering Devices by Cluster and Cluster Group. * Added tests for cluster and cluster_groups filterset. * Add missing filter & complete tests --------- Co-authored-by: Jeremy Stretch --- netbox/dcim/filtersets.py | 13 ++++++++++++- netbox/dcim/forms/filtersets.py | 12 ++++++++++++ netbox/dcim/tests/test_filtersets.py | 21 +++++++++++++++++---- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 2fb1e9949..0848966e8 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -20,7 +20,7 @@ from utilities.filters import ( ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter, ) -from virtualization.models import Cluster +from virtualization.models import Cluster, ClusterGroup from vpn.models import L2VPN from wireless.choices import WirelessRoleChoices, WirelessChannelChoices from wireless.models import WirelessLAN, WirelessLink @@ -1018,6 +1018,17 @@ class DeviceFilterSet( queryset=Cluster.objects.all(), label=_('VM cluster (ID)'), ) + cluster_group = django_filters.ModelMultipleChoiceFilter( + field_name='cluster__group__slug', + queryset=ClusterGroup.objects.all(), + to_field_name='slug', + label=_('Cluster group (slug)'), + ) + cluster_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='cluster__group', + queryset=ClusterGroup.objects.all(), + label=_('Cluster group (ID)'), + ) model = django_filters.ModelMultipleChoiceFilter( field_name='device_type__slug', queryset=DeviceType.objects.all(), diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 0a28a4ec4..22e66763b 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -14,6 +14,7 @@ from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_ch from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField from utilities.forms.rendering import FieldSet from utilities.forms.widgets import NumberWithOptions +from virtualization.models import Cluster, ClusterGroup from vpn.models import L2VPN from wireless.choices import * @@ -655,6 +656,7 @@ class DeviceFilterForm( 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports', name=_('Components') ), + FieldSet('cluster_group_id', 'cluster_id', name=_('Cluster')), FieldSet( 'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data', 'has_virtual_device_context', @@ -821,6 +823,16 @@ class DeviceFilterForm( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + cluster_id = DynamicModelMultipleChoiceField( + queryset=Cluster.objects.all(), + required=False, + label=_('Cluster') + ) + cluster_group_id = DynamicModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + label=_('Cluster group') + ) tag = TagFilterField(model) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 0a22f5a82..d08e2707f 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -9,7 +9,7 @@ from ipam.models import ASN, IPAddress, RIR, VRF from netbox.choices import ColorChoices from tenancy.models import Tenant, TenantGroup from utilities.testing import ChangeLoggedFilterSetTests, create_test_device -from virtualization.models import Cluster, ClusterType +from virtualization.models import Cluster, ClusterType, ClusterGroup from wireless.choices import WirelessChannelChoices, WirelessRoleChoices User = get_user_model() @@ -1959,10 +1959,16 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests): Rack.objects.bulk_create(racks) cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') + cluster_groups = ( + ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'), + ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'), + ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'), + ) + ClusterGroup.objects.bulk_create(cluster_groups) clusters = ( - Cluster(name='Cluster 1', type=cluster_type), - Cluster(name='Cluster 2', type=cluster_type), - Cluster(name='Cluster 3', type=cluster_type), + Cluster(name='Cluster 1', type=cluster_type, group=cluster_groups[0]), + Cluster(name='Cluster 2', type=cluster_type, group=cluster_groups[1]), + Cluster(name='Cluster 3', type=cluster_type, group=cluster_groups[2]), ) Cluster.objects.bulk_create(clusters) @@ -2213,6 +2219,13 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'cluster_id': [clusters[0].pk, clusters[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_cluster_group(self): + cluster_groups = ClusterGroup.objects.all()[:2] + params = {'cluster_group_id': [cluster_groups[0].pk, cluster_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'cluster_group': [cluster_groups[0].slug, cluster_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_model(self): params = {'model': ['model-1', 'model-2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)