mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Closes #7858: Standardize the representation of content types across import & export functions
This commit is contained in:
parent
6f7fbf7686
commit
9de179cba8
@ -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
|
||||
|
||||
|
@ -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,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):
|
||||
"""
|
||||
|
@ -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:
|
||||
|
@ -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}'
|
||||
|
||||
|
||||
#
|
||||
|
Loading…
Reference in New Issue
Block a user