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 / unb-djutils   python

Repository URL to install this package:

Version: 0.0.24 

/ unb_djutils / rest / resources.py

"""
Resources
=========

Resources consolidate queries and permissions into one location.

For example, if you have both a user-facing site and a public data api, you
would generally have to duplicate the permissions and query logic in at least 2
places (maybe more if you include admin functionality).

One way to reduce that burden is to consolidate your queries into custom
``QuerySet``s.  Ignoring for the moment, that you still have to duplicate
permissions code (something you don't want to "forget to update").  However,
not all resources have a 1-to-1 relationship with a model, or for that matter,
any relation to a model at all.  For those cases, you're still left building
the resource data somewhere other than the query set.

You can try to shoehorn all of this into the serializers, but dealing with
permissions in serializers gets weird quickly, and would definitely be a WTF
for someone new to the project.


TODOs
-----

TODO(nick):  Define Serializers in the Resource?!?

             1) It helps to make the resource a bit more self-documenting as
                to the input/output it expects.  Also, the Resource *should*
                be responsible for CRUD, and is tightly coupled to the
                serializer anyway (raising ValidationErrors and whatnot), so
                why not define the serializer here and save a bit of
                indirection?

             2) Also, should we add `Resource.model` as a class attribute?  I
                think it might be useful to do as a convention, if nothing
                else, rather than using the models directly in something like
                `get_object` or `get_qs`.

             3) It might also be nice to add some type of `QuerySet` to the
                `Resource`.  Though I'm not sure if that should be a
                `django.db.models.QuerySet` or if we should make some kind of
                `Resource.QuerySet` or something.  I don't like how
                `get_object` is basically responsible for both querying the
                models/database **and** converting the model/db
                representation to the resource representation.

                Maybe a simple solution is to have something like
                `get_resource` that returns an arbitrary resource
                representation (dict/list/string/image/mp3/whatever) and a
                convention of using `get_object` to return a
                `django.db.models.Model`?

Example:

.. code-block::

    class EmailPreferences(Resource):
      '''Preferences for user-sent emails (from address, signature, etc.).'''

      # (2)
      model = models.EmailPreferences

      # (1)
      class Serializer(HALModelSerializer):
        _links = LinksField(
          ('self', 'api:prefs-email'),
        )

        class Meta:
          model = models.EmailPreferences
          fields = (
            '_links',
            'default_send_address',
            'signature',
          )

      def get_object(self):
        return self.model.objects.user(self.request.user)

      # (3)
      def get_resource(self):
        obj = self.get_object()
        # do some conversion or whatever with obj
        return {...} or [] or "" or PIL.Image or ...

      def _delete(self):
        pass


TODO(nick): How to represent a Resource id/pk when there's not a backing model?

            The HALSerializer is looking for a `pk` attribute, but not finding
            one because we're returning a dict from `get_object`.

            A workaround for this is to simply make this a `HALModelSerializer`
            using `models.Phone` as the backing instance and just attaching
            `sms_enabled` as an extra field.  That works in this instance, but
            there may be a case where a Resource is not attached to any model
            at all, or (probably more commonly) a Resource may dynamically
            select a model to query based on other attributes in the Resource.

Example:

.. code-block::

    class Phone(Resource):
      serializer = serializers.PhoneSerializer

      def get_object(self):
        # NOTE: This breaks if we allow users to have multiple phone numbers.
        phone = models.Phone.objects.user(self.request.user).first()
        reminder_prefs = models.ReminderPrefs.objects.user(self.request.user)

        return {
          'pk': phone.pk if phone else None,  # What to do here?!?
          'number': phone.number if phone else '',
          'sms_enabled': reminder_prefs.sms_enabled,
        }


"""

from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404

from rest_framework import renderers


# Exceptions
# ==========

class ResourceError(Exception):
  pass


class ValidationError(ResourceError):
  pass


class PermissionsError(ResourceError):
  pass


class NotFoundError(ResourceError, Http404):
  pass


# Resource
# ========

class Resource(object):
  """Consolidate queries and permissions into one location.

  Example
  -------

  .. code-block::

    class MyResource(Resource):
      serializer = serializers.MyModelSerializer

      def get_qs(self):
        return models.MyModel.objects.filter(user=self.request.user)

      def get_object(self):
        return self.get_qs().get(pk=self.kwargs['pk'])

      def _create(self):
        self.serialized.save(creator=self.request.user)

      def _update(self):
        self.serialized.save(updated_by=self.request.user)

      def _delete(self):
        self.object.deleted_by = self.request.user
        self.object.deleted_at = timezone.now()
        self.object.save()

  """

  serializer = None

  ValidationError = ValidationError
  NotFoundError = NotFoundError

  def __init__(self, request, queryset=None, serializer=None, *args, **kwargs):
    self.request = request
    self.args = args
    self.kwargs = kwargs
    self._queryset = queryset
    if serializer is not None:
      self.serializer = serializer

  def _resource_permissions(self, action):
    return True

  def _object_permissions(self, obj_or_qs):
    return True

  def get_object(self):
    return None  # models.Model.objects.get(user=request.user)

  # DEPRECATED: Use `queryset`
  def get_qs(self):
    return self._queryset  # models.Model.objects.filter(user=request.user)

  def get_queryset(self):
    """Hook for subclasses to override the queryset."""
    return self._queryset  # models.Model.objects.filter(user=request.user)

  def queryset(self):
    if self._queryset is not None:
      return self._queryset
    return self.get_queryset()

  def list_queryset(self):
    """Hook for subclasses to override the queryset for `list` operations.

    For example, you may want to `order_by` a field.  If you did the `order_by`
    on the `get_queryset` method, you would have to pay the price of that
    ordering for all queries.
    """
    return self.queryset()

  def _get_object(self):
    try:
      return self.get_object()
    except ObjectDoesNotExist:
      raise NotFoundError('Resource not found.')

  def _create(self):
    self.serialized.save()

  def create(self):
    self._resource_permissions('create')
    self.serialized = self.serializer(data=self.request.data,
                                      request=self.request)
    self.validate()
    self._create()
    return self

  def _read(self):
    pass

  def read(self, *args, **kwargs):
    self._resource_permissions('read')
    self.object = self._get_object()
    self._object_permissions(self.object)
    self.serialized = self.serializer(self.object, request=self.request)
    self._read()
    return self

  def _update(self):
    self.serialized.save()

  def update(self):
    self._resource_permissions('update')
    self.object = self._get_object()
    self._object_permissions(self.object)
    self.serialized = self.serializer(self.object,
                                      data=self.request.data,
                                      request=self.request)
    self.validate()
    self._update()
    return self

  def _delete(self):
    self.object.delete()

  def delete(self):
    self._resource_permissions('delete')
    self.object = self._get_object()
    self._object_permissions(self.object)
    self._delete()
    return self

  def _list(self):
    pass

  def list(self, *args, **kwargs):
    self._resource_permissions('list')
    qs = self.list_queryset(*args, **kwargs)
    self._object_permissions(qs)
    self.serialized = self.serializer(qs, request=self.request, many=True)
    self._list()
    return self

  def validate(self):
    if not self.serialized.is_valid():
      raise ValidationError

  @property
  def validated_data(self):
    return self.serialized.validated_data

  @property
  def data(self):
    return self.serialized.data

  @property
  def errors(self):
    return self.serialized.errors

  @property
  def json(self):
    return renderers.JSONRenderer().render(self.data)