"""
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)