From 9de179cba8e974789d64961d2f0d46573f2477ad Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 17 Nov 2021 11:02:22 -0500 Subject: [PATCH] Closes #7858: Standardize the representation of content types across import & export functions --- docs/release-notes/version-3.1.md | 5 +++++ netbox/utilities/forms/fields.py | 6 +++--- netbox/utilities/tables.py | 15 +++++++++++++-- netbox/utilities/testing/base.py | 5 +++-- netbox/utilities/utils.py | 15 +++++++++++---- 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 4c8379d48..efd9883bb 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -1,11 +1,16 @@ ## v3.1-beta2 (FUTURE) +### Breaking Changes + +* Exported webhooks and custom fields now reference associated content types by raw string value (e.g. "dcim.site") rather than by human-friendly name. + ### Enhancements * [#7619](https://github.com/netbox-community/netbox/issues/7619) - Permit custom validation rules to be defined as plain data or dotted path to class * [#7761](https://github.com/netbox-community/netbox/issues/7761) - Extend cable tracing across bridged interfaces * [#7769](https://github.com/netbox-community/netbox/issues/7769) - Enable assignment of IP addresses to an existing FHRP group * [#7775](https://github.com/netbox-community/netbox/issues/7775) - Enable dynamic config for `CHANGELOG_RETENTION`, `CUSTOM_VALIDATORS`, and `GRAPHQL_ENABLED` +* [#7858](https://github.com/netbox-community/netbox/issues/7858) - Standardize the representation of content types across import & export functions ### Bug Fixes diff --git a/netbox/utilities/forms/fields.py b/netbox/utilities/forms/fields.py index 57e6c45a4..007215b6e 100644 --- a/netbox/utilities/forms/fields.py +++ b/netbox/utilities/forms/fields.py @@ -15,7 +15,7 @@ from django.forms.fields import JSONField as _JSONField, InvalidJSONInput from django.urls import reverse from utilities.choices import unpack_grouped_choices -from utilities.utils import content_type_name +from utilities.utils import content_type_identifier, content_type_name from utilities.validators import EnhancedURLValidator from . import widgets from .constants import * @@ -302,7 +302,7 @@ class CSVContentTypeField(CSVModelChoiceField): STATIC_CHOICES = True def prepare_value(self, value): - return f'{value.app_label}.{value.model}' + return content_type_identifier(value) def to_python(self, value): if not value: @@ -328,7 +328,7 @@ class CSVMultipleContentTypeField(forms.ModelMultipleChoiceField): app_label, model = name.split('.') ct_filter |= Q(app_label=app_label, model=model) return list(ContentType.objects.filter(ct_filter).values_list('pk', flat=True)) - return f'{value.app_label}.{value.model}' + return content_type_identifier(value) # diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index b92cde47c..6ad8ce6ca 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -13,7 +13,7 @@ from django_tables2.utils import Accessor from extras.choices import CustomFieldTypeChoices from extras.models import CustomField -from .utils import content_type_name +from .utils import content_type_identifier, content_type_name from .paginator import EnhancedPaginator, get_paginate_count @@ -289,16 +289,27 @@ class ContentTypeColumn(tables.Column): def value(self, value): if value is None: return None - return f"{value.app_label}.{value.model}" + return content_type_identifier(value) class ContentTypesColumn(tables.ManyToManyColumn): """ Display a list of ContentType instances. """ + def __init__(self, separator=None, *args, **kwargs): + # Use a line break as the default separator + if separator is None: + separator = mark_safe('
') + super().__init__(separator=separator, *args, **kwargs) + def transform(self, obj): return content_type_name(obj) + def value(self, value): + return ','.join([ + content_type_identifier(ct) for ct in self.filter(value) + ]) + class ColorColumn(tables.Column): """ diff --git a/netbox/utilities/testing/base.py b/netbox/utilities/testing/base.py index dd7ca4236..499a5e2e7 100644 --- a/netbox/utilities/testing/base.py +++ b/netbox/utilities/testing/base.py @@ -10,6 +10,7 @@ from taggit.managers import TaggableManager from users.models import ObjectPermission from utilities.permissions import resolve_permission_ct +from utilities.utils import content_type_identifier from .utils import extract_form_failures __all__ = ( @@ -110,7 +111,7 @@ class ModelTestCase(TestCase): if value and type(field) in (ManyToManyField, TaggableManager): if field.related_model is ContentType and api: - model_dict[key] = sorted([f'{ct.app_label}.{ct.model}' for ct in value]) + model_dict[key] = sorted([content_type_identifier(ct) for ct in value]) else: model_dict[key] = sorted([obj.pk for obj in value]) @@ -119,7 +120,7 @@ class ModelTestCase(TestCase): # Replace ContentType numeric IDs with . if type(getattr(instance, key)) is ContentType: ct = ContentType.objects.get(pk=value) - model_dict[key] = f'{ct.app_label}.{ct.model}' + model_dict[key] = content_type_identifier(ct) # Convert IPNetwork instances to strings elif type(value) is IPNetwork: diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 9ddb072f3..1a81369fc 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -344,16 +344,23 @@ def array_to_string(array): return ', '.join('-'.join(map(str, (g[0], g[-1])[:len(g)])) for g in group) -def content_type_name(contenttype): +def content_type_name(ct): """ - Return a proper ContentType name. + Return a human-friendly ContentType name (e.g. "DCIM > Site"). """ try: - meta = contenttype.model_class()._meta + meta = ct.model_class()._meta return f'{meta.app_config.verbose_name} > {meta.verbose_name}' except AttributeError: # Model no longer exists - return f'{contenttype.app_label} > {contenttype.model}' + return f'{ct.app_label} > {ct.model}' + + +def content_type_identifier(ct): + """ + Return a "raw" ContentType identifier string suitable for bulk import/export (e.g. "dcim.site"). + """ + return f'{ct.app_label}.{ct.model}' #