Coverage for utilities/serialization.py: 56%
38 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1import json
3from django.apps import apps
4from django.contrib.contenttypes.models import ContentType
5from django.core import serializers
7from extras.utils import is_taggable
9__all__ = (
10 'deserialize_object',
11 'serialize_object',
12)
15def serialize_object(obj, resolve_tags=True, extra=None, exclude=None):
16 """
17 Return a generic JSON representation of an object using Django's built-in serializer. (This is used for things like
18 change logging, not the REST API.) Optionally include a dictionary to supplement the object data. A list of keys
19 can be provided to exclude them from the returned dictionary.
21 Args:
22 obj: The object to serialize
23 resolve_tags: If true, any assigned tags will be represented by their names
24 extra: Any additional data to include in the serialized output. Keys provided in this mapping will
25 override object attributes.
26 exclude: An iterable of attributes to exclude from the serialized output
27 """
28 json_str = serializers.serialize('json', [obj])
29 data = json.loads(json_str)[0]['fields']
30 exclude = exclude or []
32 # Include custom_field_data as "custom_fields"
33 if 'custom_field_data' in data:
34 data['custom_fields'] = data.pop('custom_field_data')
36 # Resolve any assigned tags to their names. Check for tags cached on the instance;
37 # fall back to using the manager.
38 if resolve_tags and is_taggable(obj):
39 tags = getattr(obj, '_tags', None) or obj.tags.all()
40 data['tags'] = sorted([tag.name for tag in tags])
42 # Skip any excluded attributes
43 for key in list(data.keys()):
44 if key in exclude:
45 data.pop(key)
47 # Append any extra data
48 if extra is not None: 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true
49 data.update(extra)
51 return data
54def deserialize_object(model, data, pk=None):
55 """
56 Instantiate an object from the given model and field data. Functions as
57 the complement to serialize_object().
58 """
59 content_type = ContentType.objects.get_for_model(model)
60 data = data.copy()
61 m2m_data = {}
63 # Account for custom field data
64 if 'custom_fields' in data:
65 data['custom_field_data'] = data.pop('custom_fields')
67 # Pop any assigned tags to handle the M2M relationships manually
68 if is_taggable(model) and data.get('tags'):
69 Tag = apps.get_model('extras', 'Tag')
70 m2m_data['tags'] = Tag.objects.filter(name__in=data.pop('tags'))
72 # Separate any non-field attributes for assignment after deserialization of the object
73 model_fields = [
74 field.name for field in model._meta.get_fields()
75 ]
76 attrs = {
77 name: data.pop(name) for name in list(data.keys())
78 if name not in model_fields
79 }
81 # Employ Django's native Python deserializer to produce the instance
82 data = {
83 'model': '.'.join(content_type.natural_key()),
84 'pk': pk,
85 'fields': data,
86 }
87 instance = list(serializers.deserialize('python', [data]))[0]
89 # Assign non-field attributes
90 for name, value in attrs.items():
91 setattr(instance.object, name, value)
93 # Apply any additional M2M assignments
94 instance.m2m_data.update(**m2m_data)
96 return instance