general: add new "full" serializer to include parent model fields (#17645)
This commit is contained in:
parent
e626863893
commit
3aba43b569
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -186,6 +186,9 @@ HAYSTACK_CONNECTIONS = {
|
|||
},
|
||||
}
|
||||
|
||||
SERIALIZATION_MODULES = {
|
||||
'fulljson': 'combo.data.fulljson_serializer'
|
||||
}
|
||||
|
||||
COMBO_DEFAULT_PUBLIC_TEMPLATE = 'standard'
|
||||
COMBO_PUBLIC_TEMPLATES = {
|
||||
|
|
Loading…
Reference in New Issue