mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into feature
This commit is contained in:
@ -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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -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',
|
'id', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled', 'http_method',
|
||||||
'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
|
'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:
|
||||||
|
@ -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,
|
||||||
|
@ -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 dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
|
||||||
from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
|
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}
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user