Add serializer handling natural keys (refs #5510)
This commit is contained in:
parent
6ee88e9fb9
commit
a58b57d465
|
@ -0,0 +1,156 @@
|
|||
import json
|
||||
import sys
|
||||
|
||||
from django.utils import six
|
||||
from django.core.serializers.json import Serializer as JSONSerializer
|
||||
from django.core.serializers.python import _get_model
|
||||
from django.core.serializers import base
|
||||
from django.core.serializers.base import DeserializationError
|
||||
from django.contrib.contenttypes.generic import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import FieldDoesNotExist
|
||||
from django.utils.encoding import smart_text
|
||||
from django.conf import settings
|
||||
from django.db import models, DEFAULT_DB_ALIAS
|
||||
|
||||
|
||||
def PythonDeserializer(object_list, **options):
|
||||
"""
|
||||
Deserialize simple Python objects back into Django ORM instances.
|
||||
|
||||
It's expected that you pass the Python objects themselves (instead of a
|
||||
stream or a string) to the constructor
|
||||
"""
|
||||
db = options.pop('using', DEFAULT_DB_ALIAS)
|
||||
ignore = options.pop('ignorenonexistent', False)
|
||||
|
||||
for d in object_list:
|
||||
# Look up the model and starting build a dict of data for it.
|
||||
Model = _get_model(d["model"])
|
||||
data = {}
|
||||
if 'pk' in d:
|
||||
data[Model._meta.pk.attname] = Model._meta.pk.to_python(d.get("pk", None))
|
||||
m2m_data = {}
|
||||
model_fields = Model._meta.get_all_field_names()
|
||||
|
||||
# Handle each field
|
||||
for (field_name, field_value) in six.iteritems(d["fields"]):
|
||||
|
||||
if ignore and field_name not in model_fields:
|
||||
# skip fields no longer on model
|
||||
continue
|
||||
|
||||
if isinstance(field_value, str):
|
||||
field_value = smart_text(field_value, options.get("encoding", settings.DEFAULT_CHARSET), strings_only=True)
|
||||
|
||||
try:
|
||||
field = Model._meta.get_field(field_name)
|
||||
except FieldDoesNotExist:
|
||||
for field in Model._meta.virtual_fields:
|
||||
if field.name == field_name:
|
||||
break
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
# Handle M2M relations
|
||||
if hasattr(field, 'rel'):
|
||||
if field.rel and isinstance(field.rel, models.ManyToManyRel):
|
||||
if hasattr(field.rel.to._default_manager, 'get_by_natural_key'):
|
||||
def m2m_convert(value):
|
||||
if hasattr(value, '__iter__') and not isinstance(value, six.text_type):
|
||||
try:
|
||||
return field.rel.to._default_manager.db_manager(db).get_by_natural_key(*value).pk
|
||||
except Exception:
|
||||
import pdb
|
||||
pdb.set_trace()
|
||||
else:
|
||||
return smart_text(field.rel.to._meta.pk.to_python(value))
|
||||
else:
|
||||
m2m_convert = lambda v: smart_text(field.rel.to._meta.pk.to_python(v))
|
||||
m2m_data[field.name] = [m2m_convert(pk) for pk in field_value]
|
||||
|
||||
# Handle FK fields
|
||||
elif field.rel and isinstance(field.rel, models.ManyToOneRel):
|
||||
if field_value is not None:
|
||||
if hasattr(field.rel.to._default_manager, 'get_by_natural_key'):
|
||||
if hasattr(field_value, '__iter__') and not isinstance(field_value, six.text_type):
|
||||
obj = field.rel.to._default_manager.db_manager(db).get_by_natural_key(*field_value)
|
||||
value = getattr(obj, field.rel.field_name)
|
||||
# If this is a natural foreign key to an object that
|
||||
# has a FK/O2O as the foreign key, use the FK value
|
||||
if field.rel.to._meta.pk.rel:
|
||||
value = value.pk
|
||||
else:
|
||||
value = field.rel.to._meta.get_field(field.rel.field_name).to_python(field_value)
|
||||
data[field.attname] = value
|
||||
else:
|
||||
data[field.attname] = field.rel.to._meta.get_field(field.rel.field_name).to_python(field_value)
|
||||
else:
|
||||
data[field.attname] = None
|
||||
else:
|
||||
data[field.name] = field.to_python(field_value)
|
||||
# Handle Generic foreign key
|
||||
elif isinstance(field, GenericForeignKey):
|
||||
ct_natural_key, fk_natural_key = field_value
|
||||
ct = ContentType.objects.get_by_natural_key(*ct_natural_key)
|
||||
data[field.name] = ct.model_class().objects.get_by_natural_key(*fk_natural_key)
|
||||
# Handle all other fields
|
||||
else:
|
||||
raise NotImplementedError('unsupported field type %s' % field)
|
||||
|
||||
yield base.DeserializedObject(Model(**data), m2m_data)
|
||||
|
||||
|
||||
class Serializer(JSONSerializer):
|
||||
def get_dump_object(self, obj):
|
||||
d = super(Serializer, self).get_dump_object(obj)
|
||||
if self.use_natural_keys:
|
||||
if hasattr(obj, 'natural_key'):
|
||||
d['pk'] = obj.natural_key()
|
||||
for vfield in obj.__class__._meta.virtual_fields:
|
||||
if not isinstance(vfield, GenericForeignKey):
|
||||
continue
|
||||
ct = getattr(obj, vfield.ct_field)
|
||||
if ct is None:
|
||||
continue
|
||||
sub_obj = getattr(obj, vfield.name)
|
||||
assert not sub_obj is None, 'should not happen'
|
||||
if not hasattr(sub_obj, 'natural_key'):
|
||||
# abort if no natural key
|
||||
continue
|
||||
# delete non natural keys
|
||||
fields = d['fields']
|
||||
del fields[vfield.ct_field]
|
||||
del fields[vfield.fk_field]
|
||||
fields[vfield.name] = (ct.natural_key(), sub_obj.natural_key())
|
||||
return d
|
||||
|
||||
def Deserializer(stream_or_string, **options):
|
||||
"""
|
||||
Deserialize a stream or string of JSON data.
|
||||
"""
|
||||
if not isinstance(stream_or_string, (bytes, six.string_types)):
|
||||
stream_or_string = stream_or_string.read()
|
||||
if isinstance(stream_or_string, bytes):
|
||||
stream_or_string = stream_or_string.decode('utf-8')
|
||||
try:
|
||||
objects = json.loads(stream_or_string)
|
||||
for obj in objects:
|
||||
Model = _get_model(obj['model'])
|
||||
if isinstance(obj['pk'], (tuple, list)):
|
||||
try:
|
||||
o = Model.objects.get_by_natural_key(*obj['pk'])
|
||||
except Model.DoesNotExist:
|
||||
obj['pk'] = None
|
||||
else:
|
||||
obj['pk'] = o.pk
|
||||
for obj in PythonDeserializer(objects, **options):
|
||||
yield obj
|
||||
except GeneratorExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
# Map to deserializer error
|
||||
six.reraise(DeserializationError, DeserializationError(e), sys.exc_info()[2])
|
||||
|
||||
|
Reference in New Issue