From a97a4e9f2378eb03a3597099d6c8cbb8405dfcec Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Wed, 16 Jul 2025 13:49:53 -0500 Subject: [PATCH] Closes: #18588: Relabel Service model to Application Service Updates the `verbose_name` of the `Service` and `ServiceTemplate` models to "Application Service" and "Application Service Template" respectively. This serves as the foundational change for relabeling the model throughout the user interface to reduce ambiguity. To preserve backward compatibility for the REST and GraphQL APIs, the test suites have been updated to assert the stability of the original field and parameter names. This includes: * Using `filter_name_map` in the filterset test case to ensure API query parameters remain `service` and `service_id`. * Employing the GraphQL test suite's aliasing mechanism to ensure the public schema remains unchanged despite the underlying `verbose_name` modification. Subsequent commits will address UI-specific labels in navigation, tables, forms, and templates. --- netbox/ipam/models/services.py | 8 ++++---- netbox/ipam/tests/test_api.py | 2 ++ netbox/ipam/tests/test_filtersets.py | 3 +++ netbox/utilities/testing/filtersets.py | 18 +++++++++++++----- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/netbox/ipam/models/services.py b/netbox/ipam/models/services.py index 2afd16076..000a8c764 100644 --- a/netbox/ipam/models/services.py +++ b/netbox/ipam/models/services.py @@ -55,8 +55,8 @@ class ServiceTemplate(ServiceBase, PrimaryModel): class Meta: ordering = ('name',) - verbose_name = _('service template') - verbose_name_plural = _('service templates') + verbose_name = _('application service template') + verbose_name_plural = _('application service templates') class Service(ContactsMixin, ServiceBase, PrimaryModel): @@ -94,5 +94,5 @@ class Service(ContactsMixin, ServiceBase, PrimaryModel): models.Index(fields=('parent_object_type', 'parent_object_id')), ) ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique - verbose_name = _('service') - verbose_name_plural = _('services') + verbose_name = _('application service') + verbose_name_plural = _('application services') diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index a7562a53b..5e2733577 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -1162,6 +1162,7 @@ class ServiceTemplateTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + graphql_base_name = 'service_template' @classmethod def setUpTestData(cls): @@ -1197,6 +1198,7 @@ class ServiceTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + graphql_base_name = 'service' @classmethod def setUpTestData(cls): diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 852fd3ea9..54ad5df90 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -1101,6 +1101,9 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = IPAddress.objects.all() filterset = IPAddressFilterSet ignore_fields = ('fhrpgroup',) + filter_name_map = { + 'application_service': 'service', + } @classmethod def setUpTestData(cls): diff --git a/netbox/utilities/testing/filtersets.py b/netbox/utilities/testing/filtersets.py index 0b3d4b198..260c092db 100644 --- a/netbox/utilities/testing/filtersets.py +++ b/netbox/utilities/testing/filtersets.py @@ -33,6 +33,7 @@ class BaseFilterSetTests: queryset = None filterset = None ignore_fields = tuple() + filter_name_map = {} def get_m2m_filter_name(self, field): """ @@ -46,7 +47,13 @@ class BaseFilterSetTests: """ Given a model field, return an iterable of (name, class) for each filter that should be defined on the model's FilterSet class. If the appropriate filter class cannot be determined, it will be None. + + filter_name_map provides a mechanism for developers to provide an actual field name for the + filter that is being resolved, given the field's actual name. """ + # If an alias is not present in filter_name_map, then use field.name + filter_name = self.filter_name_map.get(field.name, field.name) + # ForeignKey & OneToOneField if issubclass(field.__class__, ForeignKey) or type(field) is OneToOneRel: @@ -57,19 +64,20 @@ class BaseFilterSetTests: # ForeignKeys to ObjectType need two filters: 'app.model' & PK if field.related_model is ObjectType: return [ - (field.name, ContentTypeFilter), - (f'{field.name}_id', django_filters.ModelMultipleChoiceFilter), + (filter_name, ContentTypeFilter), + (f'{filter_name}_id', django_filters.ModelMultipleChoiceFilter), ] # ForeignKey to an MPTT-enabled model if issubclass(field.related_model, MPTTModel) and field.model is not field.related_model: - return [(f'{field.name}_id', TreeNodeMultipleChoiceFilter)] + return [(f'{filter_name}_id', TreeNodeMultipleChoiceFilter)] - return [(f'{field.name}_id', django_filters.ModelMultipleChoiceFilter)] + return [(f'{filter_name}_id', django_filters.ModelMultipleChoiceFilter)] # Many-to-many relationships (forward & backward) elif type(field) in (ManyToManyField, ManyToManyRel): filter_name = self.get_m2m_filter_name(field) + filter_name = self.filter_name_map.get(filter_name, filter_name) # ManyToManyFields to ObjectType need two filters: 'app.model' & PK if field.related_model is ObjectType: @@ -85,7 +93,7 @@ class BaseFilterSetTests: return [('tag', TagFilter)] # Unable to determine the correct filter class - return [(field.name, None)] + return [(filter_name, None)] def test_id(self): """