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.
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/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/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/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)
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/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 = (
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)