diff --git a/netbox/extras/events.py b/netbox/extras/events.py index 0ee4cffa8..a33ac213c 100644 --- a/netbox/extras/events.py +++ b/netbox/extras/events.py @@ -1,9 +1,6 @@ -import logging - from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ObjectDoesNotExist from django.utils import timezone from django.utils.module_loading import import_string from django.utils.translation import gettext as _ @@ -15,9 +12,9 @@ from netbox.constants import RQ_QUEUE_DEFAULT from netbox.registry import registry from utilities.api import get_serializer_for_model from utilities.rqworker import get_rq_retry -from utilities.utils import serialize_object +from utilities.serialization import serialize_object from .choices import * -from .models import EventRule, ScriptModule +from .models import EventRule logger = logging.getLogger('netbox.events_processor') diff --git a/netbox/extras/models/staging.py b/netbox/extras/models/staging.py index f15d8d470..6e381ce70 100644 --- a/netbox/extras/models/staging.py +++ b/netbox/extras/models/staging.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _ from extras.choices import ChangeActionChoices from netbox.models import ChangeLoggedModel from netbox.models.features import * -from utilities.utils import deserialize_object +from utilities.serialization import deserialize_object __all__ = ( 'Branch', diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index bff9ee59f..000e717a4 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -17,7 +17,7 @@ from netbox.config import get_config from netbox.registry import registry from netbox.signals import post_clean from utilities.json import CustomFieldJSONEncoder -from utilities.utils import serialize_object +from utilities.serialization import serialize_object from utilities.views import register_model_view __all__ = ( diff --git a/netbox/netbox/staging.py b/netbox/netbox/staging.py index ec38dcadc..4d37fb7ad 100644 --- a/netbox/netbox/staging.py +++ b/netbox/netbox/staging.py @@ -6,7 +6,7 @@ from django.db.models.signals import m2m_changed, pre_delete, post_save from extras.choices import ChangeActionChoices from extras.models import StagedChange -from utilities.utils import serialize_object +from utilities.serialization import serialize_object logger = logging.getLogger('netbox.staging') diff --git a/netbox/utilities/serialization.py b/netbox/utilities/serialization.py new file mode 100644 index 000000000..f7a2002d1 --- /dev/null +++ b/netbox/utilities/serialization.py @@ -0,0 +1,75 @@ +import json + +from django.contrib.contenttypes.models import ContentType +from django.core import serializers +from mptt.models import MPTTModel + +from extras.utils import is_taggable + +__all__ = ( + 'deserialize_object', + 'serialize_object', +) + + +def serialize_object(obj, resolve_tags=True, extra=None, exclude=None): + """ + Return a generic JSON representation of an object using Django's built-in serializer. (This is used for things like + change logging, not the REST API.) Optionally include a dictionary to supplement the object data. A list of keys + can be provided to exclude them from the returned dictionary. Private fields (prefaced with an underscore) are + implicitly excluded. + + Args: + obj: The object to serialize + resolve_tags: If true, any assigned tags will be represented by their names + extra: Any additional data to include in the serialized output. Keys provided in this mapping will + override object attributes. + exclude: An iterable of attributes to exclude from the serialized output + """ + json_str = serializers.serialize('json', [obj]) + data = json.loads(json_str)[0]['fields'] + exclude = exclude or [] + + # Exclude any MPTTModel fields + if issubclass(obj.__class__, MPTTModel): + for field in ['level', 'lft', 'rght', 'tree_id']: + data.pop(field) + + # Include custom_field_data as "custom_fields" + if hasattr(obj, 'custom_field_data'): + data['custom_fields'] = data.pop('custom_field_data') + + # Resolve any assigned tags to their names. Check for tags cached on the instance; + # fall back to using the manager. + if resolve_tags and is_taggable(obj): + tags = getattr(obj, '_tags', None) or obj.tags.all() + data['tags'] = sorted([tag.name for tag in tags]) + + # Skip excluded and private (prefixes with an underscore) attributes + for key in list(data.keys()): + if key in exclude or (isinstance(key, str) and key.startswith('_')): + data.pop(key) + + # Append any extra data + if extra is not None: + data.update(extra) + + return data + + +def deserialize_object(model, fields, pk=None): + """ + Instantiate an object from the given model and field data. Functions as + the complement to serialize_object(). + """ + content_type = ContentType.objects.get_for_model(model) + if 'custom_fields' in fields: + fields['custom_field_data'] = fields.pop('custom_fields') + data = { + 'model': '.'.join(content_type.natural_key()), + 'pk': pk, + 'fields': fields, + } + instance = list(serializers.deserialize('python', [data]))[0] + + return instance diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 94e377dd7..7abb30cbd 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -1,12 +1,9 @@ import datetime import decimal -import json from itertools import count, groupby from urllib.parse import urlencode import nh3 -from django.contrib.contenttypes.models import ContentType -from django.core import serializers from django.db.models import Count, ManyToOneRel, OuterRef, Subquery from django.db.models.functions import Coalesce from django.http import QueryDict @@ -14,9 +11,7 @@ from django.utils import timezone from django.utils.datastructures import MultiValueDict from django.utils.timezone import localtime from jinja2.sandbox import SandboxedEnvironment -from mptt.models import MPTTModel -from extras.utils import is_taggable from netbox.config import get_config from .constants import HTML_ALLOWED_ATTRIBUTES, HTML_ALLOWED_TAGS from .string import title @@ -96,69 +91,6 @@ def count_related(model, field): return Coalesce(subquery, 0) -def serialize_object(obj, resolve_tags=True, extra=None, exclude=None): - """ - Return a generic JSON representation of an object using Django's built-in serializer. (This is used for things like - change logging, not the REST API.) Optionally include a dictionary to supplement the object data. A list of keys - can be provided to exclude them from the returned dictionary. Private fields (prefaced with an underscore) are - implicitly excluded. - - Args: - obj: The object to serialize - resolve_tags: If true, any assigned tags will be represented by their names - extra: Any additional data to include in the serialized output. Keys provided in this mapping will - override object attributes. - exclude: An iterable of attributes to exclude from the serialized output - """ - json_str = serializers.serialize('json', [obj]) - data = json.loads(json_str)[0]['fields'] - exclude = exclude or [] - - # Exclude any MPTTModel fields - if issubclass(obj.__class__, MPTTModel): - for field in ['level', 'lft', 'rght', 'tree_id']: - data.pop(field) - - # Include custom_field_data as "custom_fields" - if hasattr(obj, 'custom_field_data'): - data['custom_fields'] = data.pop('custom_field_data') - - # Resolve any assigned tags to their names. Check for tags cached on the instance; - # fall back to using the manager. - if resolve_tags and is_taggable(obj): - tags = getattr(obj, '_tags', None) or obj.tags.all() - data['tags'] = sorted([tag.name for tag in tags]) - - # Skip excluded and private (prefixes with an underscore) attributes - for key in list(data.keys()): - if key in exclude or (isinstance(key, str) and key.startswith('_')): - data.pop(key) - - # Append any extra data - if extra is not None: - data.update(extra) - - return data - - -def deserialize_object(model, fields, pk=None): - """ - Instantiate an object from the given model and field data. Functions as - the complement to serialize_object(). - """ - content_type = ContentType.objects.get_for_model(model) - if 'custom_fields' in fields: - fields['custom_field_data'] = fields.pop('custom_fields') - data = { - 'model': '.'.join(content_type.natural_key()), - 'pk': pk, - 'fields': fields, - } - instance = list(serializers.deserialize('python', [data]))[0] - - return instance - - def dict_to_filter_params(d, prefix=''): """ Translate a dictionary of attributes to a nested set of parameters suitable for QuerySet filtering. For example: