Bladeren bron

Closes #7858: Standardize the representation of content types across import & export functions

jeremystretch 4 jaren geleden
bovenliggende
commit
9de179cba8

+ 5 - 0
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
 

+ 3 - 3
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)
 
 
 #

+ 13 - 2
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('<br />')
+        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):
     """

+ 3 - 2
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 <app_label>.<model>
                 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:

+ 11 - 4
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}'
 
 
 #