general: add new "full" serializer to include parent model fields (#17645)

This commit is contained in:
Frédéric Péters 2018-01-08 10:44:35 +01:00
parent e626863893
commit 3aba43b569
3 changed files with 177 additions and 3 deletions

View File

@ -0,0 +1,171 @@
# combo - content management system
# Copyright (C) 2015-2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import sys
from django.core.serializers import base
from django.core.serializers.base import DeserializationError
from django.core.serializers.json import Serializer as JsonSerializer
from django.core.serializers.python import _get_model
from django.db import DEFAULT_DB_ALIAS, models
from django.utils import six
from django.utils.encoding import force_text
class Serializer(JsonSerializer):
def serialize(self, queryset, **options):
"""
Serialize a queryset, get fields from both concrete and parent models.
"""
self.options = options
self.stream = options.pop("stream", six.StringIO())
self.selected_fields = options.pop("fields", None)
self.use_natural_keys = options.pop("use_natural_keys", False)
self.use_natural_foreign_keys = options.pop('use_natural_foreign_keys', False) or self.use_natural_keys
self.use_natural_primary_keys = options.pop('use_natural_primary_keys', False)
self.start_serialization()
self.first = True
for obj in queryset:
self.start_object(obj)
# Use the concrete parent class' _meta instead of the object's _meta
# This is to avoid local_fields problems for proxy models. Refs #17717.
for field in obj._meta.get_fields():
if field.serialize:
if isinstance(field, models.ManyToManyField):
continue
if field.rel is None:
if self.selected_fields is None or field.attname in self.selected_fields:
self.handle_field(obj, field)
else:
if self.selected_fields is None or field.attname[:-3] in self.selected_fields:
self.handle_fk_field(obj, field)
for field in obj._meta.get_fields():
if field.serialize and isinstance(field, models.ManyToManyField):
if self.selected_fields is None or field.attname in self.selected_fields:
self.handle_m2m_field(obj, field)
self.end_object(obj)
if self.first:
self.first = False
self.end_serialization()
return self.getvalue()
def Deserializer(stream_or_string, **options):
"""
Deserialize a stream or string of JSON data.
"""
# this merges both JsonDeserializer and PythonDeserializer, with the only
# relevant change being to use our own DeserializedObject
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')
db = options.pop('using', DEFAULT_DB_ALIAS)
ignore = options.pop('ignorenonexistent', False)
try:
object_list = json.loads(stream_or_string)
for d in object_list:
# Look up the model and starting build a dict of data for it.
try:
Model = _get_model(d["model"])
except base.DeserializationError:
if ignore:
continue
else:
raise
data = {}
if 'pk' in d:
data[Model._meta.pk.attname] = Model._meta.pk.to_python(d.get("pk", None))
m2m_data = {}
field_names = {f.name for f in Model._meta.get_fields()}
# Handle each field
for (field_name, field_value) in six.iteritems(d["fields"]):
if ignore and field_name not in field_names:
# skip fields no longer on model
continue
if isinstance(field_value, str):
field_value = force_text(
field_value, options.get("encoding", settings.DEFAULT_CHARSET), strings_only=True
)
field = Model._meta.get_field(field_name)
# Handle M2M relations
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):
return field.rel.to._default_manager.db_manager(db).get_by_natural_key(*value).pk
else:
return force_text(field.rel.to._meta.pk.to_python(value), strings_only=True)
else:
m2m_convert = lambda v: force_text(field.rel.to._meta.pk.to_python(v), strings_only=True)
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
# Handle all other fields
else:
data[field.name] = field.to_python(field_value)
obj = base.build_instance(Model, data, db)
yield DeserializedObject(obj, m2m_data)
except GeneratorExit:
raise
except Exception as e:
# Map to deserializer error
six.reraise(DeserializationError, DeserializationError(e), sys.exc_info()[2])
class DeserializedObject(base.DeserializedObject):
def save(self, save_m2m=True, using=None):
# change base.DeserializedObject to actually save parent models
# (raw=False)
models.Model.save_base(self.object, using=using, raw=False)
if self.m2m_data and save_m2m:
for accessor_name, object_list in self.m2m_data.items():
setattr(self.object, accessor_name, object_list)
# prevent a second (possibly accidental) call to save() from saving
# the m2m data twice.
self.m2m_data = None

View File

@ -264,13 +264,13 @@ class Page(models.Model):
serialized_page = json.loads(serializers.serialize('json', [self],
use_natural_foreign_keys=True, use_natural_primary_keys=True))[0]
del serialized_page['model']
serialized_page['cells'] = json.loads(serializers.serialize('json',
serialized_page['cells'] = json.loads(serializers.serialize('fulljson',
cells, use_natural_foreign_keys=True, use_natural_primary_keys=True))
for cell in serialized_page['cells']:
del cell['pk']
del cell['fields']['page']
for key in cell['fields'].keys():
if key.startswith('cached_'):
if key in ('subclass', 'subclassid') or key.startswith('cached_'):
del cell['fields'][key]
return serialized_page
@ -291,7 +291,7 @@ class Page(models.Model):
@classmethod
def load_serialized_cells(cls, cells):
# load new cells
for cell in serializers.deserialize('json', json.dumps(cells)):
for cell in serializers.deserialize('fulljson', json.dumps(cells)):
cell.save()
@classmethod

View File

@ -186,6 +186,9 @@ HAYSTACK_CONNECTIONS = {
},
}
SERIALIZATION_MODULES = {
'fulljson': 'combo.data.fulljson_serializer'
}
COMBO_DEFAULT_PUBLIC_TEMPLATE = 'standard'
COMBO_PUBLIC_TEMPLATES = {