Issue #90 new

Integrating handlers for Collections and Members

James Fisher
created an issue

REST defines two types of resource: 'collections' and 'members' (http://en.wikipedia.org/wiki/REST#RESTful_web_services). Each type can have GET, POST, PUT and DELETE performed on them.

In Django terms this is the difference between a queryset (collection) and a model instance (member).

I have these accessed by different URLs - e.g. the collection always accessed at /people and the members accessed at "/people/<id>" or "/person/<id>".

This requires me to make two handlers and two resources - one PersonHandler and one PeopleHandler. This is mostly well and good, except that these look almost identical for each mode I use. Therefore I've taken the opportunity to write two abstract subclasses of BaseHandler, namely CollectionHandler and ModelHandler.

These are work in progress but the following is how I would lay this out. (Note I've also taken the liberty of allowing the programmer to define "form = PersonForm" immediately after "model = Person" in his model; thus completely automating validation.)

{{{

!python

""" The following two classes subclass BaseHandler to form classes for handling the django Model in two different ways: as a collection (CollectionHandler) and as a member (MemberHandler).

A collection basically corresponds to a queryset, e.g. Person.objects.all(), whereas a member corresponds to a single Model instance, e.g. "Joe Bloggs".

An easy analogy can be drawn with Django's generic views object_list (a collection) and object_detail (a member).

A pretty good article on collections and members: http://www.projectzero.org/sMash/1.0.x/docs/zero.devguide.doc/zero.resource/rest-api.html For a table of how HTTP methods are interpreted for collections and members: http://en.wikipedia.org/wiki/REST#RESTful_web_services """

class CollectionHandler(BaseHandler): """ This class implements appropriate definitions of read, create, update, and delete for the interpretation of the resource as a COLLECTION. """

@has_model
def read(self, request, *args, **kwargs):
    """
    Get an entire collection from the model, filtered by the arguments given.
    """
    return self.queryset(request).filter(*args, **kwargs)

@has_model
def create(self, request, *args, **kwargs):
    """
    Using the data in POST, create a new member of this collection,
    and return it.
    """
    try:
        inst = self.queryset(request).get(**request.data)
        return rc.DUPLICATE_ENTRY
    except self.model.DoesNotExist:
        if self.form:
            form = self.form(**request.data)
            if form.is_valid():
                return form.save()
            else:
                return rc.BAD_REQUEST
        else:
            inst = self.model(**request.data)
            inst.save()
            return inst
    except self.model.MultipleObjectsReturned:
        return rc.DUPLICATE_ENTRY

@has_model
def update(self, request, *args, **kwargs):
    """
    Meaning defined as "replace the entire collection with another collection".
    """
    pass
    """
    The difference between this and every other REST method is that the 
    client is giving us the data of an entire COLLECTION rather than one member
    (or filtering data that looks like it could specify a member).

    Therefore we have to decide how data in request.POST or request.PUT should
    be structured in order to specify a collection.

    This is easy if the client is giving us structured JSON or XML data,
    which is capable of a sensible hierarchical structure.

    However, if the client wants to use standard url-encoding, we have to fall back
    on that trick used in django formsets (and elsewhere), like so:

    name_0=james&age_0=22&name_1=jesper&age_1=25&name_2= ...

    IMPLEMENT THIS.
    """

@has_model
def delete(self, request, *args, **kwargs):
    """
    Delete the entire collection (filtered by the parameters given).
    THIS IS DANGEROUS, DUDE!
    """
    self.queryset(request).delete()
    return rc.ALL_OK    # Is this correct?

class MemberHandler(BaseHandler): """ This class implements appropriate definitions of read, create, update, and delete for the interpretation of the resource as a MEMBER. """

def _inst(self, request, kwargs):
    """
    Get the django Model instance object specified by the client's request data.
    """
    pkfield = self.model._meta.pk.name
    try:
        member_pk = kwargs[pkfield]
    except KeyError: # The client has not specified the primary key of this member.
        return rc.BAD_REQUEST
    else:
        try:
            return self.queryset(request).get(pk=member_pk)
        except ObjectDoesNotExist:
            return rc.NOT_FOUND
        except MultipleObjectsReturned: # should never happen, since we're using a PK
            return rc.BAD_REQUEST

@has_model
def read(self, request, *args, **kwargs):
    """
    Get a single member, identified by primary key, and return it.
    """
    return self._inst(request, kwargs)

@has_model
def create(self, request, *args, **kwargs):
    """
    Returns 400 Bad Request.

    Method POST on an individual member doesn't make much sense in the abstract.
    Wikipedia says this should "[treat] the addressed member as a collection 
    in its own right and [create] a new subordinate of it".

    If you want this functionality then you'll have to implement it by
    overriding this function.
    """
    return rc.BAD_REQUEST

@has_model
def update(self, request, *args, **kwargs):
    """
    Update the addressed member of the collection or create it with the specified ID.
    """
    inst = self._inst(request, kwargs)

    if not isinstance(inst, QuerySet):  # The request failed to generate a unique object.
        return inst # In this case, inst is the rc.<ERROR> message.

    if self.form:
        form = self.form(request.data, instance=inst)
        if form.is_valid():
            form.save()
            return rc.ALL_OK
        else:
            return rc.BAD_REQUEST
    else:
        for k,v in request.data.iteritems():
            setattr( inst, k, v )
        inst.save()
        return rc.ALL_OK

@has_model
def delete(self, request, *args, **kwargs):
    """
    Delete the addressed member of the collection.
    """
    self._inst(request, kwargs).delete()
    return rc.ALL_OK    # Is this correct?

}}}

This relies on a little decorator in utils.py:

{{{

!python

def has_model(): """ Decorator to use on Handler methods to indicate that they only make sense if the Handler is tied to a model. If it is not, a call to that function returns 501 Not Implemented. """ @decorator def wrap(f, self, request, *args, kwargs): if not self.has_model(): return rc.NOT_IMPLEMENTED return f(self, request, *args, kwargs) return wrap }}}

This isn't tested yet. But I'll get it done and fork when I have time.