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

This commit is contained in:
jeremystretch 2021-11-17 11:02:22 -05:00
parent 6f7fbf7686
commit 9de179cba8
5 changed files with 35 additions and 11 deletions

View File

@ -1,11 +1,16 @@
## v3.1-beta2 (FUTURE) ## 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 ### 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 * [#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 * [#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 * [#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` * [#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 ### Bug Fixes

View File

@ -15,7 +15,7 @@ from django.forms.fields import JSONField as _JSONField, InvalidJSONInput
from django.urls import reverse from django.urls import reverse
from utilities.choices import unpack_grouped_choices 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 utilities.validators import EnhancedURLValidator
from . import widgets from . import widgets
from .constants import * from .constants import *
@ -302,7 +302,7 @@ class CSVContentTypeField(CSVModelChoiceField):
STATIC_CHOICES = True STATIC_CHOICES = True
def prepare_value(self, value): def prepare_value(self, value):
return f'{value.app_label}.{value.model}' return content_type_identifier(value)
def to_python(self, value): def to_python(self, value):
if not value: if not value:
@ -328,7 +328,7 @@ class CSVMultipleContentTypeField(forms.ModelMultipleChoiceField):
app_label, model = name.split('.') app_label, model = name.split('.')
ct_filter |= Q(app_label=app_label, model=model) ct_filter |= Q(app_label=app_label, model=model)
return list(ContentType.objects.filter(ct_filter).values_list('pk', flat=True)) return list(ContentType.objects.filter(ct_filter).values_list('pk', flat=True))
return f'{value.app_label}.{value.model}' return content_type_identifier(value)
# #

View File

@ -13,7 +13,7 @@ from django_tables2.utils import Accessor
from extras.choices import CustomFieldTypeChoices from extras.choices import CustomFieldTypeChoices
from extras.models import CustomField 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 from .paginator import EnhancedPaginator, get_paginate_count
@ -289,16 +289,27 @@ class ContentTypeColumn(tables.Column):
def value(self, value): def value(self, value):
if value is None: if value is None:
return None return None
return f"{value.app_label}.{value.model}" return content_type_identifier(value)
class ContentTypesColumn(tables.ManyToManyColumn): class ContentTypesColumn(tables.ManyToManyColumn):
""" """
Display a list of ContentType instances. 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): def transform(self, obj):
return content_type_name(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): class ColorColumn(tables.Column):
""" """

View File

@ -10,6 +10,7 @@ from taggit.managers import TaggableManager
from users.models import ObjectPermission from users.models import ObjectPermission
from utilities.permissions import resolve_permission_ct from utilities.permissions import resolve_permission_ct
from utilities.utils import content_type_identifier
from .utils import extract_form_failures from .utils import extract_form_failures
__all__ = ( __all__ = (
@ -110,7 +111,7 @@ class ModelTestCase(TestCase):
if value and type(field) in (ManyToManyField, TaggableManager): if value and type(field) in (ManyToManyField, TaggableManager):
if field.related_model is ContentType and api: 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: else:
model_dict[key] = sorted([obj.pk for obj in value]) 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> # Replace ContentType numeric IDs with <app_label>.<model>
if type(getattr(instance, key)) is ContentType: if type(getattr(instance, key)) is ContentType:
ct = ContentType.objects.get(pk=value) 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 # Convert IPNetwork instances to strings
elif type(value) is IPNetwork: elif type(value) is IPNetwork:

View File

@ -344,16 +344,23 @@ def array_to_string(array):
return ', '.join('-'.join(map(str, (g[0], g[-1])[:len(g)])) for g in group) 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: try:
meta = contenttype.model_class()._meta meta = ct.model_class()._meta
return f'{meta.app_config.verbose_name} > {meta.verbose_name}' return f'{meta.app_config.verbose_name} > {meta.verbose_name}'
except AttributeError: except AttributeError:
# Model no longer exists # 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}'
# #