Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

nickfrez / django-hal   python

Repository URL to install this package:

Version: 0.0.9 

/ django_hal / serializers.py

"""
HAL serializers for Django REST Framework.

"""

from __future__ import print_function

from collections import OrderedDict

from django.core import urlresolvers
from django.utils.http import urlencode
from rest_framework import serializers
from rest_framework.fields import SkipField
from rest_framework.utils.serializer_helpers import ReturnDict

from .utils import reverse, move_request_from_kwargs_to_context

# DEPRECATED: Import fields to make them available as:
#             ``from django_hal.serializers import LinksField, QueryField``
#
from .fields import LinksField, QueryField


def _to_link(self, request, instance, urlpattern, kwargs=None,
            query_kwargs=None):
    """Return an absolute url for the given urlpattern."""
    if query_kwargs:
        query_kwargs = {k: getattr(instance, v) for k, v in query_kwargs.items()}
    if not kwargs:
        url = reverse(urlpattern, request=request)
        if not query_kwargs:
            return {'href': url}
        return {'href': '%s?%s' % (url, urlencode(query_kwargs))}

    if isinstance(kwargs, basestring):
        # `(ref, urlpattern, string)` where `string` is equivalent to
        # `{string: string}`
        url = reverse(urlpattern,
                      kwargs={kwargs: getattr(instance, kwargs)},
                      request=request)
        if not query_kwargs:
            return {'href': url}
        return {'href': '%s?%s' % (url, urlencode(query_kwargs))}

    reverse_kwargs = {}
    if kwargs:
        for k, v in kwargs.items():
            reverse_kwargs[k] = getattr(instance, v)
    try:
        url = reverse(urlpattern, kwargs=reverse_kwargs, request=request)
        if not query_kwargs:
            return {'href': url}
        return {'href': '%s?%s' % (url, urlencode(query_kwargs))}
    except urlresolvers.NoReverseMatch:
        return None


def _link_to_dict(request, instance, link):
    assert link['pattern'] or link['href'], "Link must have an href or pattern."

    try:
        if link['pattern']:
            pattern = link['pattern'].get('pattern')
            kwargs = link['pattern'].get('kwargs')
            query = link['pattern'].get('query')

            if kwargs:
                kwargs = {k: getattr(instance, v) for k, v in kwargs.items()}
                url = reverse(pattern, kwargs=kwargs, request=request)
            else:
                url = reverse(pattern, request=request)

            if query:
                query = {k: getattr(instance, v) for k, v in query.items()}
                url = u'%s?%s' % (url, urlencode(query))

    except urlresolvers.NoReverseMatch:
        url = None
        return None  # ?

    ret = {
        'href': url,
    }
    if link.get('name'):
        ret['name'] = link['name']
    if link.get('profile'):
        ret['profile'] = link['profile']

    return ret


def _process_links(links_dict, request, instance, links):
    ret = links_dict
    for link in links:
        link_dict = _link_to_dict(request, instance, link)

        # If there is an existing entry for ``rel``, ``rel`` becomes
        # an array of link objects.
        #
        # .. seealso:: https://tools.ietf.org/html/draft-kelly-json-hal-08#section-4.1.1
        #
        current = ret.get(link['rel'])
        if not current:
            ret[link['rel']] = link_dict
        else:
            if isinstance(current, dict):
                ret[link['rel']] = [current]
            ret[link['rel']].append(link_dict)
    return ret



class BaseSerializerDataMixin(object):
    """Mixin to include ``rest_framework.serializers.BaseSerializer.data``.

    The only reason this exists is to provide access to the BaseSerializer's
    implementation of ``.data``, when you've super-classed a super-class of
    ``BaseSerializer`` that overrides ``.data`` with an incompatible
    implementation.

    This is implemented as a mixin to make it simple to keep this up-to-date
    with ``rest_framework.serializers.BaseSerializer`` and avoid coupling this
    to any specific implementation.

    Motivation:

    Due to the way super works, you can't call ``.data`` on the superclass's
    superclass, without also calling it on the superclass.  This is a problem
    for ``HALListSerializer`` because it's super class,
    ``rest_framework.serializers.ListSerializer``, implements ``.data`` as
    ``return ReturnList(super(ListSerializer, self).data)``.  This essentially
    converts the HAL-format dict we return in ``to_representation`` to a list
    of the dict's keys (``['_links', '_embedded']``).

    """
    def base_serializer_data(self):
        if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'):
            msg = (
                'When a serializer is passed a `data` keyword argument you '
                'must call `.is_valid()` before attempting to access the '
                'serialized `.data` representation.\n'
                'You should either call `.is_valid()` first, '
                'or access `.initial_data` instead.'
            )
            raise AssertionError(msg)

        if not hasattr(self, '_data'):
            if self.instance is not None and not getattr(self, '_errors', None):  # noqa
                self._data = self.to_representation(self.instance)
            elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None):  # noqa
                self._data = self.to_representation(self.validated_data)
            else:
                self._data = self.get_initial()

        return self._data


class HALListSerializer(BaseSerializerDataMixin, serializers.ListSerializer):
    def _get_meta(self, key, default=None):
        meta = getattr(self.child, 'Meta', None)
        if meta is None:
            return None
        return getattr(meta, key, default)

    def to_representation(self, data):
        results = super(HALListSerializer, self).to_representation(data)

        list_reverse = self._get_meta('list_reverse')
        assert list_reverse, "HALSerializer Meta class must define list_reverse."  # noqa

        # Handle reverse-relationships where the API route/url is defined by an
        # attribute on the parent.  Example, `/api/users/32/emails/`, the
        # list-view url is defined by the `pk` on the `User` model, so we need
        # to access `data.instance.pk` to reverse the url, essentially
        # preforming something like: `reverse('api:user-email-list',
        # pk=data.instance.pk)`.
        #
        kwargs = {}
        if isinstance(list_reverse, tuple):
            list_reverse, kwargs = list_reverse

            if not isinstance(kwargs, dict):
                # kwargs is a string where the key == the instance attribute
                # name.
                #
                kwargs = {kwargs: kwargs}

            # DEBUG: Verify that we have been passed a RelatedManager.
            #
            if hasattr(data, 'instance'):
                kwargs = {k: getattr(data.instance, v)
                          for k, v in kwargs.items()}
            else:
                # If the QuerySet is not a RelatedManager, there is no
                # `instance` to get attributes from.
                #
                raise Exception((
                    "Keyword arguments can only be used with "
                    "RelatedManagers.  Good: User.email_set "
                    "Bad: Emails.objects.filter(user=user)  "
                    "[[TODO(nick): This exception message is confusing...]]"))

        # resource_name = self._get_meta('resource_name', 'items') # DEPRECATED
        # rel = self._get_meta('rel', 'item')  # DEPRECATED
        profile = self._get_meta('profile', 'item')

        request = self.context.get('request')
        self_url = reverse(list_reverse, request=self.context.get('request'),
                           kwargs=kwargs)
        if request.GET:
            query_args = {k: v for k, v in request.GET.items()}
            self_url = u'%s?%s' % (self_url, urlencode(query_args))

        self_link = {
            'href': self_url,
        }
        profile = self._get_meta('profile')
        if profile:
            self_link['profile'] = profile

        ret = OrderedDict(
            _links={
                'self': self_link,
            },
            _embedded={
                profile: results,
            },
        )
        return ret

    @property
    def data(self):
        data = self.base_serializer_data()
        return ReturnDict(data, serializer=self)


class HALSerializer(BaseSerializerDataMixin, serializers.Serializer):
    """Serializer that returns data in HAL-format.

    For the ListSerializer to work correctly, you must define ``rel``
    and ``list_reverse`` on your subclass's ``Meta`` class.

    """
    def __init__(self, *args, **kwargs):
        move_request_from_kwargs_to_context(kwargs)
        super(HALSerializer, self).__init__(*args, **kwargs)

    @classmethod
    def many_init(cls, *args, **kwargs):
        kwargs['child'] = cls()
        move_request_from_kwargs_to_context(kwargs)
        return HALListSerializer(*args, **kwargs)


class HALModelSerializer(BaseSerializerDataMixin, serializers.ModelSerializer):
    """ModelSerializer that returns data in HAL-format.

    For the ListSerializer to work correctly, you must define ``resource_name``
    and ``list_reverse`` on your subclass's ``Meta`` class.

    """
    def __init__(self, *args, **kwargs):
        move_request_from_kwargs_to_context(kwargs)
        super(HALModelSerializer, self).__init__(*args, **kwargs)

    @classmethod
    def many_init(cls, *args, **kwargs):
        kwargs['child'] = cls()
        move_request_from_kwargs_to_context(kwargs)
        return HALListSerializer(*args, **kwargs)

    def _get_self_link(self, instance):
        """Return the "self" link relation as a dict.

        Returns
        -------
        dict
            The ``self`` link relation.

            At minimum, the return value will include an ``href`` attribute:

            .. code:: python

                { "href": "https://some.url/and/path" }

            Optionally (if the serializer's ``Meta`` class defines them) this
            may include other attributes as defined in `Section 5 of the HAL
            Spec
            <https://tools.ietf.org/html/draft-kelly-json-hal-07#section-5>`_.

        """
        request = self.context.get('request')
        pattern = getattr(self.Meta, 'detail_reverse')

        if isinstance(pattern, basestring):
            pattern = (pattern, {'pk': 'pk'})
        elif isinstance(pattern[1], basestring):
            pattern = (pattern[0], {pattern[1]: pattern[1]})

        ret = {'href': reverse(pattern[0],
                               kwargs={k:getattr(instance, v)
                                       for k, v in pattern[1].items()},
                               request=request)}

        profile = getattr(self.Meta, 'profile', None)
        if profile:
            ret['profile'] = profile

        return ret

    def to_representation(self, instance):
        """
        Object instance -> Dict of primitive datatypes.
        """
        ret = OrderedDict()
        fields = self._readable_fields

        request = self.context.get('request')

        for field in fields:
            try:
                attribute = field.get_attribute(instance)
            except SkipField:
                continue

            if attribute is None:
                # We skip `to_representation` for `None` values so that
                # fields do not have to explicitly deal with that case.
                ret[field.field_name] = None
            else:
                ret[field.field_name] = field.to_representation(attribute)

        meta_links = getattr(self.Meta, '_links', None)
        if meta_links:
            self_link = self._get_self_link(instance)
            links_dict = OrderedDict((
                ('self', self_link),
            ))
            _links = _process_links(links_dict, request, instance, meta_links)
            ret['_links'] = _links

        return ret


class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
    """`HyperlinkedModelSerializer` that supports url namespacing.

    DEPRECATED: In favor of HAL-style _links with `LinksField`.

    NOTE: This assumes that the namespace for self and related fields is equal
    to the app_name for that particular model.

    """
    # TODO(nick): Allow the view_name for the self field to be overridden by a
    #   class attribute.
    # TODO(nick): Allow the namespace for the self field to be overridden by a
    #   class attribute.
    # TODO(nick): If both view_name and namespace are provided, view_name
    #   should take presidence.
    # TODO(nick): Allow this to be used without a namespace.  Maybe a class
    #   attribute like `use_namespace = True` or implied if any other namespace
    #   related class attribute is set?
    # TODO(nick): Add some more detail to this docstring and some examples of
    #   how this differs from the original implementation.

    def build_relational_field(self, field_name, relation_info):
        cls, kwargs = super(
            HyperlinkedModelSerializer, self).build_relational_field(
                field_name, relation_info)
        namespace = relation_info.related_model._meta.app_label
        view_name = ':'.join((namespace, kwargs['view_name']))
        kwargs['view_name'] = view_name
        return cls, kwargs

    def build_url_field(self, field_name, model_class):
      cls, kwargs = super(HyperlinkedModelSerializer, self).build_url_field(
          field_name, model_class)
      namespace = model_class._meta.app_label
      view_name = ':'.join((namespace, kwargs['view_name']))
      kwargs['view_name'] = view_name
      return cls, kwargs