From a32992e166a5e4040bd190ed85ab2e6cbbbe6ea1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 17 Jul 2023 13:48:28 -0400 Subject: [PATCH] Add & update tests --- netbox/extras/filtersets.py | 4 +- netbox/extras/forms/bulk_import.py | 10 ++--- netbox/extras/forms/model_forms.py | 3 +- netbox/extras/graphql/schema.py | 4 +- netbox/extras/models/customfields.py | 9 ++-- netbox/extras/tests/test_api.py | 39 +++++++++++++++- netbox/extras/tests/test_changelog.py | 62 +++++++++++++++----------- netbox/extras/tests/test_filtersets.py | 22 +++++++++ netbox/extras/tests/test_forms.py | 15 +++++-- netbox/extras/tests/test_views.py | 46 ++++++++++++++++++- 10 files changed, 163 insertions(+), 51 deletions(-) diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 3c7fcfcf4..b4a2a25e7 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -118,11 +118,11 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet): return queryset.filter( Q(name__icontains=value) | Q(description__icontains=value) | - Q(choices__icontains=value) + Q(extra_choices__contains=value) ) def filter_by_choice(self, queryset, name, value): - return queryset.filter(choices__icontains=value.strip()) + return queryset.filter(extra_choices__overlap=value) class CustomLinkFilterSet(BaseFilterSet): diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 577d4e3ee..ed5d82e35 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -45,12 +45,8 @@ class CustomFieldImportForm(CSVModelForm): choice_set = CSVModelChoiceField( queryset=CustomFieldChoiceSet.objects.all(), to_field_name='name', - help_text=_('Choice set (for selection fields)') - ) - choices = SimpleArrayField( - base_field=forms.CharField(), required=False, - help_text=_('Comma-separated list of field choices') + help_text=_('Choice set (for selection fields)') ) ui_visibility = CSVChoiceField( choices=CustomFieldVisibilityChoices, @@ -61,8 +57,8 @@ class CustomFieldImportForm(CSVModelForm): model = CustomField fields = ( 'name', 'label', 'group_name', 'type', 'content_types', 'object_type', 'required', 'description', - 'search_weight', 'filter_logic', 'default', 'choices', 'weight', 'validation_minimum', 'validation_maximum', - 'validation_regex', 'ui_visibility', 'is_cloneable', + 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum', + 'validation_maximum', 'validation_regex', 'ui_visibility', 'is_cloneable', ) diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index b6b3820ec..472080d50 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -52,7 +52,8 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): help_text=_("Type of the related object (for object/multi-object fields only)") ) choice_set = DynamicModelChoiceField( - queryset=CustomFieldChoiceSet.objects.all() + queryset=CustomFieldChoiceSet.objects.all(), + required=False ) fieldsets = ( diff --git a/netbox/extras/graphql/schema.py b/netbox/extras/graphql/schema.py index dc935440b..e13cc0e9f 100644 --- a/netbox/extras/graphql/schema.py +++ b/netbox/extras/graphql/schema.py @@ -25,8 +25,8 @@ class ExtrasQuery(graphene.ObjectType): def resolve_custom_field_list(root, info, **kwargs): return gql_query_optimizer(models.CustomField.objects.all(), info) - custom_field_choices = ObjectField(CustomFieldChoiceSetType) - custom_field_choices_list = ObjectListField(CustomFieldChoiceSetType) + custom_field_choice_set = ObjectField(CustomFieldChoiceSetType) + custom_field_choice_set_list = ObjectListField(CustomFieldChoiceSetType) def resolve_custom_field_choices_list(root, info, **kwargs): return gql_query_optimizer(models.CustomFieldChoiceSet.objects.all(), info) diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index a78a284e1..d24a45374 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -290,10 +290,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): if self.type in ( CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT - ) and not self.choice_set: - raise ValidationError({ - 'choice_set': "Selection fields must define a set of choices." - }) + ): + if not self.choice_set: + raise ValidationError({ + 'choice_set': "Selection fields must define a set of choices." + }) elif self.choice_set: raise ValidationError({ 'choice_set': "Choices may be set only for selection fields." diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index cbf3b8529..3230d1da0 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -98,8 +98,7 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): { 'content_types': ['dcim.site'], 'name': 'cf6', - 'type': 'select', - 'choices': ['A', 'B', 'C'] + 'type': 'text', }, ] bulk_update_data = { @@ -134,6 +133,42 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): cf.content_types.add(site_ct) +class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase): + model = CustomFieldChoiceSet + brief_fields = ['display', 'id', 'name', 'url'] + create_data = [ + { + 'name': 'Choice Set 4', + 'extra_choices': ['4A', '4B', '4C'], + }, + { + 'name': 'Choice Set 5', + 'extra_choices': ['5A', '5B', '5C'], + }, + { + 'name': 'Choice Set 6', + 'extra_choices': ['6A', '6B', '6C'], + }, + ] + bulk_update_data = { + 'description': 'New description', + } + update_data = { + 'name': 'Choice Set X', + 'extra_choices': ['X1', 'X2', 'X3'], + 'description': 'New description', + } + + @classmethod + def setUpTestData(cls): + choice_sets = ( + CustomFieldChoiceSet(name='Choice Set 1', extra_choices=['1A', '1B', '1C', '1D', '1E']), + CustomFieldChoiceSet(name='Choice Set 2', extra_choices=['2A', '2B', '2C', '2D', '2E']), + CustomFieldChoiceSet(name='Choice Set 3', extra_choices=['3A', '3B', '3C', '3D', '3E']), + ) + CustomFieldChoiceSet.objects.bulk_create(choice_sets) + + class CustomLinkTest(APIViewTestCases.APIViewTestCase): model = CustomLink brief_fields = ['display', 'id', 'name', 'url'] diff --git a/netbox/extras/tests/test_changelog.py b/netbox/extras/tests/test_changelog.py index e0be8c3bd..9ebbeef5c 100644 --- a/netbox/extras/tests/test_changelog.py +++ b/netbox/extras/tests/test_changelog.py @@ -5,7 +5,7 @@ from rest_framework import status from dcim.choices import SiteStatusChoices from dcim.models import Site from extras.choices import * -from extras.models import CustomField, ObjectChange, Tag +from extras.models import CustomField, CustomFieldChoiceSet, ObjectChange, Tag from utilities.testing import APITestCase from utilities.testing.utils import create_tags, post_data from utilities.testing.views import ModelViewTestCase @@ -16,12 +16,16 @@ class ChangeLogViewTest(ModelViewTestCase): @classmethod def setUpTestData(cls): + choice_set = CustomFieldChoiceSet.objects.create( + name='Custom Field Choice Set 1', + extra_choices=['Bar', 'Foo'] + ) # Create a custom field on the Site model ct = ContentType.objects.get_for_model(Site) cf = CustomField( type=CustomFieldTypeChoices.TYPE_TEXT, - name='my_field', + name='cf1', required=False ) cf.save() @@ -30,9 +34,9 @@ class ChangeLogViewTest(ModelViewTestCase): # Create a select custom field on the Site model cf_select = CustomField( type=CustomFieldTypeChoices.TYPE_SELECT, - name='my_field_select', + name='cf2', required=False, - choices=['Bar', 'Foo'] + choice_set=choice_set ) cf_select.save() cf_select.content_types.set([ct]) @@ -43,8 +47,8 @@ class ChangeLogViewTest(ModelViewTestCase): 'name': 'Site 1', 'slug': 'site-1', 'status': SiteStatusChoices.STATUS_ACTIVE, - 'cf_my_field': 'ABC', - 'cf_my_field_select': 'Bar', + 'cf_cf1': 'ABC', + 'cf_cf2': 'Bar', 'tags': [tag.pk for tag in tags], } @@ -65,8 +69,8 @@ class ChangeLogViewTest(ModelViewTestCase): self.assertEqual(oc.changed_object, site) self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_CREATE) self.assertEqual(oc.prechange_data, None) - self.assertEqual(oc.postchange_data['custom_fields']['my_field'], form_data['cf_my_field']) - self.assertEqual(oc.postchange_data['custom_fields']['my_field_select'], form_data['cf_my_field_select']) + self.assertEqual(oc.postchange_data['custom_fields']['cf1'], form_data['cf_cf1']) + self.assertEqual(oc.postchange_data['custom_fields']['cf2'], form_data['cf_cf2']) self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2']) def test_update_object(self): @@ -79,8 +83,8 @@ class ChangeLogViewTest(ModelViewTestCase): 'name': 'Site X', 'slug': 'site-x', 'status': SiteStatusChoices.STATUS_PLANNED, - 'cf_my_field': 'DEF', - 'cf_my_field_select': 'Foo', + 'cf_cf1': 'DEF', + 'cf_cf2': 'Foo', 'tags': [tags[2].pk], } @@ -102,8 +106,8 @@ class ChangeLogViewTest(ModelViewTestCase): self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_UPDATE) self.assertEqual(oc.prechange_data['name'], 'Site 1') self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2']) - self.assertEqual(oc.postchange_data['custom_fields']['my_field'], form_data['cf_my_field']) - self.assertEqual(oc.postchange_data['custom_fields']['my_field_select'], form_data['cf_my_field_select']) + self.assertEqual(oc.postchange_data['custom_fields']['cf1'], form_data['cf_cf1']) + self.assertEqual(oc.postchange_data['custom_fields']['cf2'], form_data['cf_cf2']) self.assertEqual(oc.postchange_data['tags'], ['Tag 3']) def test_delete_object(self): @@ -111,8 +115,8 @@ class ChangeLogViewTest(ModelViewTestCase): name='Site 1', slug='site-1', custom_field_data={ - 'my_field': 'ABC', - 'my_field_select': 'Bar' + 'cf1': 'ABC', + 'cf2': 'Bar' } ) site.save() @@ -131,8 +135,8 @@ class ChangeLogViewTest(ModelViewTestCase): self.assertEqual(oc.changed_object, None) self.assertEqual(oc.object_repr, site.name) self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_DELETE) - self.assertEqual(oc.prechange_data['custom_fields']['my_field'], 'ABC') - self.assertEqual(oc.prechange_data['custom_fields']['my_field_select'], 'Bar') + self.assertEqual(oc.prechange_data['custom_fields']['cf1'], 'ABC') + self.assertEqual(oc.prechange_data['custom_fields']['cf2'], 'Bar') self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2']) self.assertEqual(oc.postchange_data, None) @@ -213,18 +217,22 @@ class ChangeLogAPITest(APITestCase): ct = ContentType.objects.get_for_model(Site) cf = CustomField( type=CustomFieldTypeChoices.TYPE_TEXT, - name='my_field', + name='cf1', required=False ) cf.save() cf.content_types.set([ct]) # Create a select custom field on the Site model + choice_set = CustomFieldChoiceSet.objects.create( + name='Choice Set 1', + extra_choices=['Bar', 'Foo'] + ) cf_select = CustomField( type=CustomFieldTypeChoices.TYPE_SELECT, - name='my_field_select', + name='cf2', required=False, - choices=['Bar', 'Foo'] + choice_set=choice_set ) cf_select.save() cf_select.content_types.set([ct]) @@ -242,8 +250,8 @@ class ChangeLogAPITest(APITestCase): 'name': 'Site 1', 'slug': 'site-1', 'custom_fields': { - 'my_field': 'ABC', - 'my_field_select': 'Bar', + 'cf1': 'ABC', + 'cf2': 'Bar', }, 'tags': [ {'name': 'Tag 1'}, @@ -276,8 +284,8 @@ class ChangeLogAPITest(APITestCase): 'name': 'Site X', 'slug': 'site-x', 'custom_fields': { - 'my_field': 'DEF', - 'my_field_select': 'Foo', + 'cf1': 'DEF', + 'cf2': 'Foo', }, 'tags': [ {'name': 'Tag 3'} @@ -305,8 +313,8 @@ class ChangeLogAPITest(APITestCase): name='Site 1', slug='site-1', custom_field_data={ - 'my_field': 'ABC', - 'my_field_select': 'Bar' + 'cf1': 'ABC', + 'cf2': 'Bar' } ) site.save() @@ -323,8 +331,8 @@ class ChangeLogAPITest(APITestCase): self.assertEqual(oc.changed_object, None) self.assertEqual(oc.object_repr, site.name) self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_DELETE) - self.assertEqual(oc.prechange_data['custom_fields']['my_field'], 'ABC') - self.assertEqual(oc.prechange_data['custom_fields']['my_field_select'], 'Bar') + self.assertEqual(oc.prechange_data['custom_fields']['cf1'], 'ABC') + self.assertEqual(oc.prechange_data['custom_fields']['cf2'], 'Bar') self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2']) self.assertEqual(oc.postchange_data, None) diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index b4b216244..91a842bd5 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -87,6 +87,28 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) +class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests): + queryset = CustomFieldChoiceSet.objects.all() + filterset = CustomFieldChoiceSetFilterSet + + @classmethod + def setUpTestData(cls): + choice_sets = ( + CustomFieldChoiceSet(name='Choice Set 1', extra_choices=['A', 'B', 'C']), + CustomFieldChoiceSet(name='Choice Set 2', extra_choices=['D', 'E', 'F']), + CustomFieldChoiceSet(name='Choice Set 3', extra_choices=['G', 'H', 'I']), + ) + CustomFieldChoiceSet.objects.bulk_create(choice_sets) + + def test_name(self): + params = {'name': ['Choice Set 1', 'Choice Set 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_choice(self): + params = {'choice': ['A', 'D']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + class WebhookTestCase(TestCase, BaseFilterSetTests): queryset = Webhook.objects.all() filterset = WebhookFilterSet diff --git a/netbox/extras/tests/test_forms.py b/netbox/extras/tests/test_forms.py index cc3625c7c..9d6054b86 100644 --- a/netbox/extras/tests/test_forms.py +++ b/netbox/extras/tests/test_forms.py @@ -5,7 +5,7 @@ from dcim.forms import SiteForm from dcim.models import Site from extras.choices import CustomFieldTypeChoices from extras.forms import SavedFilterForm -from extras.models import CustomField +from extras.models import CustomField, CustomFieldChoiceSet class CustomFieldModelFormTest(TestCase): @@ -13,7 +13,10 @@ class CustomFieldModelFormTest(TestCase): @classmethod def setUpTestData(cls): obj_type = ContentType.objects.get_for_model(Site) - CHOICES = ('A', 'B', 'C') + choice_set = CustomFieldChoiceSet.objects.create( + name='Custom Field Choice Set 1', + extra_choices=('A', 'B', 'C') + ) cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT) cf_text.content_types.set([obj_type]) @@ -42,13 +45,17 @@ class CustomFieldModelFormTest(TestCase): cf_json = CustomField.objects.create(name='json', type=CustomFieldTypeChoices.TYPE_JSON) cf_json.content_types.set([obj_type]) - cf_select = CustomField.objects.create(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choices=CHOICES) + cf_select = CustomField.objects.create( + name='select', + type=CustomFieldTypeChoices.TYPE_SELECT, + choice_set=choice_set + ) cf_select.content_types.set([obj_type]) cf_multiselect = CustomField.objects.create( name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT, - choices=CHOICES + choice_set=choice_set ) cf_multiselect.content_types.set([obj_type]) diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 57efc5be7..acfdcf1e3 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -21,6 +21,11 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase): def setUpTestData(cls): site_ct = ContentType.objects.get_for_model(Site) + CustomFieldChoiceSet.objects.create( + name='Choice Set 1', + extra_choices=('A', 'B', 'C') + ) + custom_fields = ( CustomField(name='field1', label='Field 1', type=CustomFieldTypeChoices.TYPE_TEXT), CustomField(name='field2', label='Field 2', type=CustomFieldTypeChoices.TYPE_TEXT), @@ -44,10 +49,10 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - 'name,label,type,content_types,object_type,weight,search_weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex,ui_visibility', + 'name,label,type,content_types,object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visibility', 'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},read-write', 'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,read-write', - 'field6,Field 6,select,dcim.site,,100,3000,exact,"A,B,C",,,,read-write', + 'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,read-write', 'field7,Field 7,object,dcim.site,dcim.region,100,4000,exact,,,,,read-write', ) @@ -64,6 +69,43 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase): } +class CustomFieldChoiceSetTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = CustomFieldChoiceSet + + @classmethod + def setUpTestData(cls): + + choice_sets = ( + CustomFieldChoiceSet(name='Choice Set 1', extra_choices=['1A', '1B', '1C', '1D', '1E']), + CustomFieldChoiceSet(name='Choice Set 2', extra_choices=['2A', '2B', '2C', '2D', '2E']), + CustomFieldChoiceSet(name='Choice Set 3', extra_choices=['3A', '3B', '3C', '3D', '3E']), + ) + CustomFieldChoiceSet.objects.bulk_create(choice_sets) + + cls.form_data = { + 'name': 'Choice Set X', + 'extra_choices': 'X1,X2,X3,X4,X5', + } + + cls.csv_data = ( + 'name,extra_choices', + 'Choice Set 4,"4A,4B,4C,4D,4E"', + 'Choice Set 5,"5A,5B,5C,5D,5E"', + 'Choice Set 6,"6A,6B,6C,6D,6E"', + ) + + cls.csv_update_data = ( + 'id,extra_choices', + f'{choice_sets[0].pk},"1X,1Y,1Z"', + f'{choice_sets[1].pk},"2X,2Y,2Z"', + f'{choice_sets[2].pk},"3X,3Y,3Z"', + ) + + cls.bulk_edit_data = { + 'description': 'New description', + } + + class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = CustomLink