Jelajahi Sumber

Merge branch 'develop' into feature

jeremystretch 3 tahun lalu
induk
melakukan
29eb37857c

+ 2 - 0
docs/release-notes/version-3.2.md

@@ -14,6 +14,8 @@
 * [#8854](https://github.com/netbox-community/netbox/issues/8854) - Fix `REMOTE_AUTH_DEFAULT_GROUPS` for social-auth backends
 * [#8854](https://github.com/netbox-community/netbox/issues/8854) - Fix `REMOTE_AUTH_DEFAULT_GROUPS` for social-auth backends
 * [#9575](https://github.com/netbox-community/netbox/issues/9575) - Fix AttributeError exception for FHRP group with an IP address assigned
 * [#9575](https://github.com/netbox-community/netbox/issues/9575) - Fix AttributeError exception for FHRP group with an IP address assigned
 * [#9597](https://github.com/netbox-community/netbox/issues/9597) - Include `installed_module` in module bay REST API serializer
 * [#9597](https://github.com/netbox-community/netbox/issues/9597) - Include `installed_module` in module bay REST API serializer
+* [#9657](https://github.com/netbox-community/netbox/issues/9657) - Fix filtering for custom fields and webhooks in the UI
+* [#9682](https://github.com/netbox-community/netbox/issues/9682) - Fix bulk assignment of ASNs to sites
 
 
 ---
 ---
 
 

+ 11 - 2
netbox/extras/filtersets.py

@@ -32,6 +32,9 @@ class WebhookFilterSet(BaseFilterSet):
         method='search',
         method='search',
         label='Search',
         label='Search',
     )
     )
+    content_type_id = MultiValueNumberFilter(
+        field_name='content_types__id'
+    )
     content_types = ContentTypeFilter()
     content_types = ContentTypeFilter()
     http_method = django_filters.MultipleChoiceFilter(
     http_method = django_filters.MultipleChoiceFilter(
         choices=WebhookHttpMethodChoices
         choices=WebhookHttpMethodChoices
@@ -40,8 +43,8 @@ class WebhookFilterSet(BaseFilterSet):
     class Meta:
     class Meta:
         model = Webhook
         model = Webhook
         fields = [
         fields = [
-            'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled',
-            'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
+            'id', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled', 'http_method',
+            'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
         ]
         ]
 
 
     def search(self, queryset, name, value):
     def search(self, queryset, name, value):
@@ -58,6 +61,12 @@ class CustomFieldFilterSet(BaseFilterSet):
         method='search',
         method='search',
         label='Search',
         label='Search',
     )
     )
+    type = django_filters.MultipleChoiceFilter(
+        choices=CustomFieldTypeChoices
+    )
+    content_type_id = MultiValueNumberFilter(
+        field_name='content_types__id'
+    )
     content_types = ContentTypeFilter()
     content_types = ContentTypeFilter()
 
 
     class Meta:
     class Meta:

+ 8 - 6
netbox/extras/forms/filtersets.py

@@ -32,12 +32,13 @@ __all__ = (
 class CustomFieldFilterForm(FilterForm):
 class CustomFieldFilterForm(FilterForm):
     fieldsets = (
     fieldsets = (
         (None, ('q',)),
         (None, ('q',)),
-        ('Attributes', ('content_types', 'type', 'group_name', 'weight', 'required', 'ui_visibility')),
+        ('Attributes', ('type', 'content_type_id', 'group_name', 'weight', 'required', 'ui_visibility')),
     )
     )
-    content_types = ContentTypeMultipleChoiceField(
+    content_type_id = ContentTypeMultipleChoiceField(
         queryset=ContentType.objects.all(),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_fields'),
         limit_choices_to=FeatureQuery('custom_fields'),
-        required=False
+        required=False,
+        label='Object type'
     )
     )
     type = MultipleChoiceField(
     type = MultipleChoiceField(
         choices=CustomFieldTypeChoices,
         choices=CustomFieldTypeChoices,
@@ -119,13 +120,14 @@ class ExportTemplateFilterForm(FilterForm):
 class WebhookFilterForm(FilterForm):
 class WebhookFilterForm(FilterForm):
     fieldsets = (
     fieldsets = (
         (None, ('q',)),
         (None, ('q',)),
-        ('Attributes', ('content_types', 'http_method', 'enabled')),
+        ('Attributes', ('content_type_id', 'http_method', 'enabled')),
         ('Events', ('type_create', 'type_update', 'type_delete')),
         ('Events', ('type_create', 'type_update', 'type_delete')),
     )
     )
-    content_types = ContentTypeMultipleChoiceField(
+    content_type_id = ContentTypeMultipleChoiceField(
         queryset=ContentType.objects.all(),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('webhooks'),
         limit_choices_to=FeatureQuery('webhooks'),
-        required=False
+        required=False,
+        label='Object type'
     )
     )
     http_method = MultipleChoiceField(
     http_method = MultipleChoiceField(
         choices=WebhookHttpMethodChoices,
         choices=WebhookHttpMethodChoices,

+ 71 - 2
netbox/extras/tests/test_filtersets.py

@@ -6,8 +6,9 @@ from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 from django.test import TestCase
 
 
 from circuits.models import Provider
 from circuits.models import Provider
-from dcim.models import DeviceRole, DeviceType, Location, Manufacturer, Platform, Rack, Region, Site, SiteGroup
-from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
+from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
+from dcim.models import Location
+from extras.choices import *
 from extras.filtersets import *
 from extras.filtersets import *
 from extras.models import *
 from extras.models import *
 from ipam.models import IPAddress
 from ipam.models import IPAddress
@@ -16,6 +17,72 @@ from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests, cr
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 
 
 
 
+class CustomFieldTestCase(TestCase, BaseFilterSetTests):
+    queryset = CustomField.objects.all()
+    filterset = CustomFieldFilterSet
+
+    @classmethod
+    def setUpTestData(cls):
+        content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
+
+        custom_fields = (
+            CustomField(
+                name='Custom Field 1',
+                type=CustomFieldTypeChoices.TYPE_TEXT,
+                required=True,
+                weight=100,
+                filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE,
+                ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE
+            ),
+            CustomField(
+                name='Custom Field 2',
+                type=CustomFieldTypeChoices.TYPE_INTEGER,
+                required=False,
+                weight=200,
+                filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT,
+                ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY
+            ),
+            CustomField(
+                name='Custom Field 3',
+                type=CustomFieldTypeChoices.TYPE_BOOLEAN,
+                required=False,
+                weight=300,
+                filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED,
+                ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
+            ),
+        )
+        CustomField.objects.bulk_create(custom_fields)
+        custom_fields[0].content_types.add(content_types[0])
+        custom_fields[1].content_types.add(content_types[1])
+        custom_fields[2].content_types.add(content_types[2])
+
+    def test_name(self):
+        params = {'name': ['Custom Field 1', 'Custom Field 2']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_content_types(self):
+        params = {'content_types': 'dcim.site'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+        params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+    def test_required(self):
+        params = {'required': True}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+    def test_weight(self):
+        params = {'weight': [100, 200]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_filter_logic(self):
+        params = {'filter_logic': CustomFieldFilterLogicChoices.FILTER_LOOSE}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+    def test_ui_visibility(self):
+        params = {'ui_visibility': CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+
 class WebhookTestCase(TestCase, BaseFilterSetTests):
 class WebhookTestCase(TestCase, BaseFilterSetTests):
     queryset = Webhook.objects.all()
     queryset = Webhook.objects.all()
     filterset = WebhookFilterSet
     filterset = WebhookFilterSet
@@ -62,6 +129,8 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
     def test_content_types(self):
     def test_content_types(self):
         params = {'content_types': 'dcim.site'}
         params = {'content_types': 'dcim.site'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+        params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_type_create(self):
     def test_type_create(self):
         params = {'type_create': True}
         params = {'type_create': True}

+ 0 - 2
netbox/ipam/tests/test_views.py

@@ -789,8 +789,6 @@ class L2VPNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'export_targets': [rts[1].pk]
             'export_targets': [rts[1].pk]
         }
         }
 
 
-        print(cls.form_data)
-
 
 
 class L2VPNTerminationTestCase(
 class L2VPNTerminationTestCase(
         ViewTestCases.GetObjectViewTestCase,
         ViewTestCases.GetObjectViewTestCase,

+ 2 - 1
netbox/netbox/views/generic/bulk_views.py

@@ -7,6 +7,7 @@ from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import FieldDoesNotExist, ValidationError
 from django.core.exceptions import FieldDoesNotExist, ValidationError
 from django.db import transaction, IntegrityError
 from django.db import transaction, IntegrityError
 from django.db.models import ManyToManyField, ProtectedError
 from django.db.models import ManyToManyField, ProtectedError
+from django.db.models.fields.reverse_related import ManyToManyRel
 from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
 from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
 from django.http import HttpResponse
 from django.http import HttpResponse
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
@@ -455,7 +456,7 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
                         setattr(obj, name, None if model_field.null else '')
                         setattr(obj, name, None if model_field.null else '')
 
 
                 # ManyToManyFields
                 # ManyToManyFields
-                elif isinstance(model_field, ManyToManyField):
+                elif isinstance(model_field, (ManyToManyField, ManyToManyRel)):
                     if form.cleaned_data[name]:
                     if form.cleaned_data[name]:
                         getattr(obj, name).set(form.cleaned_data[name])
                         getattr(obj, name).set(form.cleaned_data[name])
                 # Normal fields
                 # Normal fields