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

View File

@ -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)
#

View File

@ -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):
"""

View File

@ -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:

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)
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}'
#