Coverage for utilities/serialization.py: 56%

38 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1import json 

2 

3from django.apps import apps 

4from django.contrib.contenttypes.models import ContentType 

5from django.core import serializers 

6 

7from extras.utils import is_taggable 

8 

9__all__ = ( 

10 'deserialize_object', 

11 'serialize_object', 

12) 

13 

14 

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. 

20 

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 [] 

31 

32 # Include custom_field_data as "custom_fields" 

33 if 'custom_field_data' in data: 

34 data['custom_fields'] = data.pop('custom_field_data') 

35 

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

41 

42 # Skip any excluded attributes 

43 for key in list(data.keys()): 

44 if key in exclude: 

45 data.pop(key) 

46 

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) 

50 

51 return data 

52 

53 

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 = {} 

62 

63 # Account for custom field data 

64 if 'custom_fields' in data: 

65 data['custom_field_data'] = data.pop('custom_fields') 

66 

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

71 

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 } 

80 

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] 

88 

89 # Assign non-field attributes 

90 for name, value in attrs.items(): 

91 setattr(instance.object, name, value) 

92 

93 # Apply any additional M2M assignments 

94 instance.m2m_data.update(**m2m_data) 

95 

96 return instance