1. RJ Ryan
  2. django-rest-interface

Source

django-rest-interface / django_restapi / model_resource.py

"""
Model-bound resource class.
"""
from django import forms
from django.conf.urls.defaults import patterns
from django.http import *
from django.forms import ModelForm, models
from django.forms.util import ErrorDict
from django.utils.functional import curry
from django.utils.translation.trans_null import _
from resource import ResourceBase, load_put_and_files, reverse, HttpMethodNotAllowed
from receiver import FormReceiver

class InvalidModelData(Exception):
    """
    Raised if create/update fails because the PUT/POST 
    data is not appropriate.
    """
    def __init__(self, errors=None):
        if not errors:
            errors = ErrorDict()
        self.errors = errors

class Collection(ResourceBase):
    """
    Resource for a collection of models (queryset).
    """
    def __init__(self, queryset, responder, receiver=None, authentication=None,
                 permitted_methods=None, expose_fields=None, entry_class=None,
                 form_class=None):
        """
        queryset:
            determines the subset of objects (of a Django model)
            that make up this resource
        responder:
            the data format instance that creates HttpResponse
            objects from single or multiple model objects and
            renders forms
        receiver:
            the data format instance that handles POST and
            PUT data
        authentication:
            the authentication instance that checks whether a
            request is authenticated
        permitted_methods:
            the HTTP request methods that are allowed for this 
            resource e.g. ('GET', 'PUT')
        expose_fields:
            the model fields that can be accessed
            by the HTTP methods described in permitted_methods
        entry_class:
            class used for entries in create() and get_entry();
            default: class Entry (see below)
        form_class:
            base form class used for data validation and
            conversion in self.create() and Entry.update()
        """
        # Available data
        self.queryset = queryset
        
        # Input format
        if not receiver:
            receiver = FormReceiver()
        self.receiver = receiver
        
        # Input validation
        if not form_class:
            form_class = ModelForm
        self.form_class = form_class
        
        # Output format / responder setup
        self.responder = responder
        if not expose_fields:
            expose_fields = [field.name for field in queryset.model._meta.fields]
        responder.expose_fields = expose_fields
        if hasattr(responder, 'create_form'):
            responder.create_form = curry(responder.create_form, queryset=queryset, form_class=form_class)
        if hasattr(responder, 'update_form'):
            responder.update_form = curry(responder.update_form, queryset=queryset, form_class=form_class)
        
        # Resource class for individual objects of the collection
        if not entry_class:
            entry_class = Entry
        self.entry_class = entry_class
        
        ResourceBase.__init__(self, authentication, permitted_methods)
    
    def __call__(self, request, *args, **kwargs):
        """
        Redirects to one of the CRUD methods depending 
        on the HTTP method of the request. Checks whether
        the requested method is allowed for this resource.
        Catches errors.
        """
        # Check authentication
        if not self.authentication.is_authenticated(request):
            response = self.responder.error(request, 401)
            challenge_headers = self.authentication.challenge_headers()
            for k,v in challenge_headers.items():
                response[k] = v
            return response
        
        # Remove queryset cache
        self.queryset = self.queryset._clone()
        
        # Determine whether the collection or a specific
        # entry is requested. If not specified as a keyword
        # argument, assume that any args/kwargs are used to
        # select a specific entry from the collection.
        if kwargs.has_key('is_entry'):
            is_entry = kwargs.pop('is_entry')
        else:
            eval_args = tuple([x for x in args if x != ''])
            is_entry = bool(eval_args or kwargs)
        
        # Redirect either to entry method
        # or to collection method. Catch errors.
        try:
            if is_entry:
                entry = self.get_entry(*args, **kwargs)
                return self.dispatch(request, entry)
            else:
                return self.dispatch(request, self)
        except HttpMethodNotAllowed:
            response = self.responder.error(request, 405)
            response['Allow'] = ', '.join(self.permitted_methods)
            return response
        except (self.queryset.model.DoesNotExist, Http404):
            return self.responder.error(request, 404)
        except InvalidModelData, i:
            return self.responder.error(request, 400, i.errors)
        
        # No other methods allowed: 400 Bad Request
        return self.responder.error(request, 400)
    
    def create(self, request):
        """
        Creates a resource with attributes given by POST, then
        redirects to the resource URI. 
        """
        # Create form filled with POST data
        ResourceForm = models.modelform_factory(self.queryset.model, form=self.form_class)
        data = self.receiver.get_post_data(request)
        form = ResourceForm(data)
        
        # If the data contains no errors, save the model,
        # return a "201 Created" response with the model's
        # URI in the location header and a representation
        # of the model in the response body.
        if form.is_valid():
            new_model = form.save()
            model_entry = self.entry_class(self, new_model)
            response = model_entry.read(request)
            response.status_code = 201
            response['Location'] = model_entry.get_url()
            return response

        # Otherwise return a 400 Bad Request error.
        raise InvalidModelData(form.errors)
    
    def read(self, request):
        """
        Returns a representation of the queryset.
        The format depends on which responder (e.g. JSONResponder)
        is assigned to this ModelResource instance. Usually called by a
        HTTP request to the factory URI with method GET.
        """
        return self.responder.list(request, self.queryset)
    
    def get_entry(self, pk_value):
        """
        Returns a single entry retrieved by filtering the 
        collection queryset by primary key value.
        """
        model = self.queryset.get(**{self.queryset.model._meta.pk.name : pk_value})
        entry = self.entry_class(self, model)
        return entry

class Entry(object):
    """
    Resource for a single model.
    """
    def __init__(self, collection, model):
        self.collection = collection
        self.model = model
        
    def get_url(self):
        """
        Returns the URL for this resource object.
        """
        pk_value = getattr(self.model, self.model._meta.pk.name)
        return reverse(self.collection, (pk_value,))
    
    def create(self, request):
        raise Http404
    
    def read(self, request):
        """
        Returns a representation of a single model.
        The format depends on which responder (e.g. JSONResponder)
        is assigned to this ModelResource instance. Usually called by a
        HTTP request to the resource URI with method GET.
        """
        return self.collection.responder.element(request, self.model)
    
    def update(self, request):
        """
        Changes the attributes of the resource identified by 'ident'
        and redirects to the resource URI. Usually called by a HTTP
        request to the resource URI with method PUT.
        """
        # Create a form from the model/PUT data
        ResourceForm = models.modelform_factory(self.model.__class__, form=self.collection.form_class)
        data = self.collection.receiver.get_put_data(request)

        form = ResourceForm(data, instance=self.model)
        
        
        # If the data contains no errors, save the model,
        # return a "200 Ok" response with the model's
        # URI in the location header and a representation
        # of the model in the response body.
        if form.is_valid():
            form.save()
            response = self.read(request)
            response.status_code = 200
            response['Location'] = self.get_url()
            return response
        
        # Otherwise return a 400 Bad Request error.
        raise InvalidModelData(form.errors)
    
    def delete(self, request):
        """
        Deletes the model associated with the current entry.
        Usually called by a HTTP request to the entry URI
        with method DELETE.
        """
        self.model.delete()
        return HttpResponse(_("Object successfully deleted."), self.collection.responder.mimetype)