Skip to content

Commit

Permalink
Adds single table inheritance support for models
Browse files Browse the repository at this point in the history
This commit adds support for fetching, creating, updating, and deleting
at endpoints corresponding to APIs created for polymorphic models
defined using single table inheritance.

This also changes the default serializer so that it supports
heterogeneous collections directly.
  • Loading branch information
jfinkels committed Jun 5, 2016
1 parent d605996 commit 9834690
Show file tree
Hide file tree
Showing 11 changed files with 586 additions and 112 deletions.
5 changes: 4 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ Version 1.0.0b2-dev
Not yet released.

- Changes serialization/deserialization to class-based implementation instead
of a function-based implementation.
of a function-based implementation. This also adds support for serialization
of heterogeneous collections.
- :issue:`7`: allows filtering before function evaluation.
- :issue:`49`: deserializers now expect a complete JSON API document.
- :issue:`200`: be smarter about determining the ``collection_name`` for
polymorphic tables.
- :issue:`253`: don't assign to callable attributes of models.
- :issue:`481,488`: added negation (``not``) operator for search.
- :issue:`492`: support JSON API recommended "simple" filtering.
Expand Down
11 changes: 10 additions & 1 deletion flask_restless/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from uuid import uuid1
import sys

from sqlalchemy.inspection import inspect
from flask import Blueprint
from flask import url_for as flask_url_for

Expand Down Expand Up @@ -601,7 +602,15 @@ def create_api_blueprint(self, name, model, methods=READONLY_METHODS,
msg = 'Collection name must be nonempty'
raise IllegalArgumentError(msg)
if collection_name is None:
collection_name = model.__table__.name
# If the model is polymorphic in a single table inheritance
# scenario, this should *not* be the tablename, but perhaps
# the polymorphic identity?
mapper = inspect(model)
if mapper.polymorphic_identity is not None:
collection_name = mapper.polymorphic_identity
else:
collection_name = model.__table__.name

# convert all method names to upper case
methods = frozenset((m.upper() for m in methods))
# the name of the API, for use in creating the view and the blueprint
Expand Down
1 change: 0 additions & 1 deletion flask_restless/serialization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from .exceptions import SerializationException
from .serializers import DefaultSerializer
from .serializers import JsonApiDocument
from .serializers import simple_heterogeneous_serialize_many
from .serializers import simple_serialize
from .serializers import simple_serialize_many
from .serializers import simple_relationship_serialize
Expand Down
25 changes: 19 additions & 6 deletions flask_restless/serialization/deserializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from ..helpers import get_by
from ..helpers import has_field
from ..helpers import is_like_list
from ..helpers import model_for
from ..helpers import strings_to_datetimes


Expand Down Expand Up @@ -129,31 +130,43 @@ def _load(self, data):
raise MissingType
if 'id' in data and not self.allow_client_generated_ids:
raise ClientGeneratedIDNotAllowed
# Determine the model from the type name that the the user is
# requesting. If no model is known with the given type, raise an
# exception.
type_ = data.pop('type')
expected_type = collection_name(self.model)
try:
model = model_for(type_)
except ValueError:
raise ConflictingType(expected_type, type_)
# If we wanted to allow deserializing a subclass of the model,
# we could use:
#
# if not issubclass(model, self.model) and type != expected_type:
#
if type_ != expected_type:
raise ConflictingType(expected_type, type_)
# Check for any request parameter naming a column which does not exist
# on the current model.
for field in data:
if field == 'relationships':
for relation in data['relationships']:
if not has_field(self.model, relation):
if not has_field(model, relation):
raise UnknownRelationship(relation)
elif field == 'attributes':
for attribute in data['attributes']:
if not has_field(self.model, attribute):
if not has_field(model, attribute):
raise UnknownAttribute(attribute)
# Determine which related instances need to be added.
links = {}
if 'relationships' in data:
links = data.pop('relationships', {})
for link_name, link_object in links.items():
related_model = get_related_model(self.model, link_name)
related_model = get_related_model(model, link_name)
DRD = DefaultRelationshipDeserializer
deserializer = DRD(self.session, related_model, link_name)
# Create the deserializer for this relationship object.
if is_like_list(self.model, link_name):
if is_like_list(model, link_name):
deserialize = deserializer.deserialize_many
else:
deserialize = deserializer.deserialize
Expand All @@ -168,9 +181,9 @@ def _load(self, data):
data.update(data.pop('attributes', {}))
# Special case: if there are any dates, convert the string form of the
# date into an instance of the Python ``datetime`` object.
data = strings_to_datetimes(self.model, data)
data = strings_to_datetimes(model, data)
# Create the new instance by keyword attributes.
instance = self.model(**data)
instance = model(**data)
# Set each relation specified in the links.
for relation_name, related_value in links.items():
setattr(instance, relation_name, related_value)
Expand Down
48 changes: 8 additions & 40 deletions flask_restless/serialization/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,51 +457,21 @@ def serialize(self, instance, only=None):
result['data'] = resource
return result

def serialize_many(self, instances, only=None):
# Here we are assuming the iterable of instances is homogeneous
# (i.e. each instance is of the same type).
#
# Since loading each instance from a given resource object
# representation could theoretically raise a
# DeserializationException, we collect all the errors and wrap
# them in a MultipleExceptions exception object.
resources = []
failed = []
for instance in instances:
try:
resource = self._dump(instance, only=only)
resources.append(resource)
except SerializationException as exception:
failed.append(exception)
if failed:
raise MultipleExceptions(failed)
result = JsonApiDocument()
result['data'] = resources
return result


class HeterogeneousSerializer(DefaultSerializer):
"""A serializer for heterogeneous collections of instances (that is,
collections in which each instance is of a different type).
This class overrides the :meth:`DefaultSerializer.serialize_many`
method, which currently works only for homogeneous collections, to
apply the correct model-specific serializer for each instance (as
registered at the time of invoking :meth:`APIManager.create_api`).
"""

def serialize_many(self, instances, only=None):
"""Serializes each instance using its model-specific serializer.
This method works for heterogeneous collections of instances
(that is, collections in which each instance is of a different
type).
The `only` keyword argument must be a dictionary mapping
resource type name to list of fields representing a sparse
fieldset. The values in this dictionary must be valid values for
the `only` keyword argument in the
:meth:`DefaultSerializer.serialize` method.
"""
result = []
resources = []
failed = []
for instance in instances:
# Determine the serializer for this instance.
Expand Down Expand Up @@ -530,11 +500,13 @@ def serialize_many(self, instances, only=None):
#
# TODO We could use `serializer._dump` instead.
serialized = serialized['data']
result.append(serialized)
resources.append(serialized)
except SerializationException as exception:
failed.append(exception)
if failed:
raise MultipleExceptions(failed)
result = JsonApiDocument()
result['data'] = resources
return result


Expand Down Expand Up @@ -588,8 +560,6 @@ def serialize_many(self, instances, only=None, _type=None):
#: serialization methods.
singleton_serializer = DefaultSerializer()

singleton_heterogeneous_serializer = HeterogeneousSerializer()

#: This is an instance of the default relationship serializer class,
#: :class:`DefaultRelationshipSerializer`.
#:
Expand All @@ -613,8 +583,6 @@ def serialize_many(self, instances, only=None, _type=None):
#: instantiation or customization necessary.
simple_serialize_many = singleton_serializer.serialize_many

simple_heterogeneous_serialize_many = singleton_heterogeneous_serializer.serialize_many

simple_relationship_dump = singleton_relationship_serializer._dump

#: Provides basic, uncustomized serialization functionality as provided
Expand Down
35 changes: 8 additions & 27 deletions flask_restless/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
from ..serialization import DeserializationException
from ..serialization import JsonApiDocument
from ..serialization import MultipleExceptions
from ..serialization import simple_heterogeneous_serialize_many
from ..serialization import simple_serialize_many
from ..serialization import simple_relationship_serialize
from ..serialization import simple_relationship_serialize_many
from ..serialization import SerializationException
Expand Down Expand Up @@ -1467,7 +1467,11 @@ def get_all_inclusions(self, instance_or_instances):
instance = instance_or_instances
to_include = self.resources_to_include(instance)
only = self.sparse_fields
return simple_heterogeneous_serialize_many(to_include, only=only)
# HACK We only need the primary data from the JSON API document,
# not the metadata (so really the serializer is doing more work
# than it needs to here).
result = simple_serialize_many(to_include, only=only)
return result['data']

def _paginated(self, items, filters=None, sort=None, group_by=None):
"""Returns a :class:`Paginated` object representing the
Expand Down Expand Up @@ -1698,32 +1702,9 @@ def _get_collection_helper(self, resource=None, relation_name=None,
# ...and this covers the primary resource collection and
# to-many relation cases.
else:
# TODO This doesn't handle the case of a
# heterogeneous to-many relationship, in which case
# each related resource could be an instance of a
# distinct model class.
if is_relation:
primary_model = get_model(resource)
model = get_related_model(primary_model, relation_name)
else:
model = self.model
# Determine the serializer for this instance. If there
# is no serializer, use the default serializer for the
# current resource, even though the current model may
# different from the model of the current instance.
try:
serializer = serializer_for(model)
except ValueError:
# TODO Should we fail instead, thereby effectively
# requiring that an API has been created for each
# type of resource? This is mainly a design
# question.
serializer = self.serializer
# This may raise ValueError
_type = collection_name(model)
only = self.sparse_fields.get(_type)
only = self.sparse_fields
try:
result = serializer.serialize_many(items, only=only)
result = self.serializer.serialize_many(items, only=only)
except MultipleExceptions as e:
return errors_from_serialization_exceptions(e.exceptions)
except SerializationException as exception:
Expand Down
Loading

0 comments on commit 9834690

Please sign in to comment.