Commits

Anonymous committed aa61bfb

Initial commit of r81 of django-rest-interface.

  • Participants

Comments (0)

Files changed (8)

django_restapi/__init__.py

Empty file added.

django_restapi/authentication.py

+from django.http import HttpResponse
+from django.utils.translation import ugettext as _
+import md5, time, random
+
+def djangouser_auth(username, password):
+    """
+    Check username and password against
+    django.contrib.auth.models.User
+    """
+    from django.contrib.auth.models import User
+    try:
+        user = User.objects.get(username=username)
+        if user.check_password(password):
+            return True
+        else:
+            return False
+    except User.DoesNotExist:
+        return False
+
+class NoAuthentication(object):
+    """
+    No authentication: Permit every request.
+    """
+    def is_authenticated(self, request):
+        return True
+
+    def challenge_headers(self):
+        return {}
+
+class HttpBasicAuthentication(object):
+    """
+    HTTP/1.0 basic authentication.
+    """    
+    def __init__(self, authfunc=djangouser_auth, realm=_('Restricted Access')):
+        """
+        authfunc:
+            A user-defined function which takes a username and
+            password as its first and second arguments respectively
+            and returns True if the user is authenticated
+        realm:
+            An identifier for the authority that is requesting
+            authorization
+        """
+        self.realm = realm
+        self.authfunc = authfunc
+    
+    def challenge_headers(self):
+        """
+        Returns the http headers that ask for appropriate
+        authorization.
+        """
+        return {'WWW-Authenticate' : 'Basic realm="%s"' % self.realm}
+    
+    def is_authenticated(self, request):
+        """
+        Checks whether a request comes from an authorized user.
+        """
+        if not request.META.has_key('HTTP_AUTHORIZATION'):
+            return False
+        (authmeth, auth) = request.META['HTTP_AUTHORIZATION'].split(' ', 1)
+        if authmeth.lower() != 'basic':
+            return False
+        auth = auth.strip().decode('base64')
+        username, password = auth.split(':', 1)
+        return self.authfunc(username=username, password=password)
+
+def digest_password(realm, username, password):
+    """
+    Construct the appropriate hashcode needed for HTTP digest
+    """
+    return md5.md5("%s:%s:%s" % (username, realm, password)).hexdigest()
+
+class HttpDigestAuthentication(object):
+    """
+    HTTP/1.1 digest authentication (RFC 2617).
+    Uses code from the Python Paste Project (MIT Licence).
+    """    
+    def __init__(self, authfunc, realm=_('Restricted Access')):
+        """
+        authfunc:
+            A user-defined function which takes a username and
+            a realm as its first and second arguments respectively
+            and returns the combined md5 hash of username,
+            authentication realm and password.
+        realm:
+            An identifier for the authority that is requesting
+            authorization
+        """
+        self.realm = realm
+        self.authfunc = authfunc
+        self.nonce    = {} # prevention of replay attacks
+
+    def get_auth_dict(self, auth_string):
+        """
+        Splits WWW-Authenticate and HTTP_AUTHORIZATION strings
+        into a dictionaries, e.g.
+        {
+            nonce  : "951abe58eddbb49c1ed77a3a5fb5fc2e"',
+            opaque : "34de40e4f2e4f4eda2a3952fd2abab16"',
+            realm  : "realm1"',
+            qop    : "auth"'
+        }
+        """
+        amap = {}
+        for itm in auth_string.split(", "):
+            (k, v) = [s.strip() for s in itm.split("=", 1)]
+            amap[k] = v.replace('"', '')
+        return amap
+
+    def get_auth_response(self, http_method, fullpath, username, nonce, realm, qop, cnonce, nc):
+        """
+        Returns the server-computed digest response key.
+        
+        http_method:
+            The request method, e.g. GET
+        username:
+            The user to be authenticated
+        fullpath:
+            The absolute URI to be accessed by the user
+        nonce:
+            A server-specified data string which should be 
+            uniquely generated each time a 401 response is made
+        realm:
+            A string to be displayed to users so they know which 
+            username and password to use
+        qop:
+            Indicates the "quality of protection" values supported 
+            by the server.  The value "auth" indicates authentication.
+        cnonce:
+            An opaque quoted string value provided by the client 
+            and used by both client and server to avoid chosen 
+            plaintext attacks, to provide mutual authentication, 
+            and to provide some message integrity protection.
+        nc:
+            Hexadecimal request counter
+        """
+        ha1 = self.authfunc(realm, username)
+        ha2 = md5.md5('%s:%s' % (http_method, fullpath)).hexdigest()
+        if qop:
+            chk = "%s:%s:%s:%s:%s:%s" % (ha1, nonce, nc, cnonce, qop, ha2)
+        else:
+            chk = "%s:%s:%s" % (ha1, nonce, ha2)
+        computed_response = md5.md5(chk).hexdigest()
+        return computed_response
+    
+    def challenge_headers(self, stale=''):
+        """
+        Returns the http headers that ask for appropriate
+        authorization.
+        """
+        nonce  = md5.md5(
+            "%s:%s" % (time.time(), random.random())).hexdigest()
+        opaque = md5.md5(
+            "%s:%s" % (time.time(), random.random())).hexdigest()
+        self.nonce[nonce] = None
+        parts = {'realm': self.realm, 'qop': 'auth',
+                 'nonce': nonce, 'opaque': opaque }
+        if stale:
+            parts['stale'] = 'true'
+        head = ", ".join(['%s="%s"' % (k, v) for (k, v) in parts.items()])
+        return {'WWW-Authenticate':'Digest %s' % head}
+    
+    def is_authenticated(self, request):
+        """
+        Checks whether a request comes from an authorized user.
+        """
+        
+        # Make sure the request is a valid HttpDigest request
+        if not request.META.has_key('HTTP_AUTHORIZATION'):
+            return False
+        fullpath = request.META['SCRIPT_NAME'] + request.META['PATH_INFO']
+        (authmeth, auth) = request.META['HTTP_AUTHORIZATION'].split(" ", 1)
+        if authmeth.lower() != 'digest':
+            return False
+        
+        # Extract auth parameters from request
+        amap = self.get_auth_dict(auth)
+        try:
+            username = amap['username']
+            authpath = amap['uri']
+            nonce    = amap['nonce']
+            realm    = amap['realm']
+            response = amap['response']
+            assert authpath.split("?", 1)[0] in fullpath
+            assert realm == self.realm
+            qop      = amap.get('qop', '')
+            cnonce   = amap.get('cnonce', '')
+            nc       = amap.get('nc', '00000000')
+            if qop:
+                assert 'auth' == qop
+                assert nonce and nc
+        except:
+            return False
+
+        # Compute response key    
+        computed_response = self.get_auth_response(request.method, fullpath, username, nonce, realm, qop, cnonce, nc)
+        
+        # Compare server-side key with key from client
+        # Prevent replay attacks
+        if not computed_response or computed_response != response:
+            if nonce in self.nonce:
+                del self.nonce[nonce]
+            return False
+        pnc = self.nonce.get(nonce,'00000000')
+        if nc <= pnc:
+            if nonce in self.nonce:
+                del self.nonce[nonce]
+            return False # stale = True
+        self.nonce[nonce] = nc
+        return True
+    

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

django_restapi/receiver.py

+"""
+Data format classes that can be plugged into 
+model_resource.ModelResource and determine how submissions
+of model data need to look like (e.g. form submission MIME types,
+XML, JSON, ...).
+"""
+from django.core import serializers
+from django.forms import model_to_dict
+
+class InvalidFormData(Exception):
+    """
+    Raised if form data can not be decoded into key-value
+    pairs.
+    """
+
+class Receiver(object):
+    """
+    Base class for all "receiver" data format classes.
+    All subclasses need to implement the method
+    get_data(self, request, method).
+    """
+    def get_data(self, request, method):
+        raise Exception("Receiver subclass needs to implement get_data!")
+    
+    def get_post_data(self, request):
+        return self.get_data(request, 'POST')
+    
+    def get_put_data(self, request):
+        return self.get_data(request, 'PUT')
+
+class FormReceiver(Receiver):
+    """
+    Data format class with standard Django behavior: 
+    POST and PUT data is in form submission format.
+    """
+    def get_data(self, request, method):
+        return getattr(request, method)
+
+class SerializeReceiver(Receiver):
+    """
+    Base class for all data formats possible
+    within Django's serializer framework.
+    """
+    def __init__(self, format):
+        self.format = format
+    
+    def get_data(self, request, method):
+        try:
+            deserialized_objects = list(serializers.deserialize(self.format, request.raw_post_data))
+        except serializers.base.DeserializationError:
+            raise InvalidFormData
+        if len(deserialized_objects) != 1:
+            raise InvalidFormData
+        model = deserialized_objects[0].object
+        
+        return model_to_dict(model)
+
+class JSONReceiver(SerializeReceiver):
+    """
+    Data format class for form submission in JSON, 
+    e.g. for web browsers.
+    """
+    def __init__(self):
+        self.format = 'json'
+
+class XMLReceiver(SerializeReceiver):
+    """
+    Data format class for form submission in XML, 
+    e.g. for software clients.
+    """
+    def __init__(self):
+        self.format = 'xml'

django_restapi/resource.py

+"""
+Generic resource class.
+"""
+from django.utils.translation import ugettext as _
+from authentication import NoAuthentication
+from django.core.urlresolvers import reverse as _reverse
+from django.http import Http404, HttpResponse, HttpResponseNotAllowed
+
+def load_put_and_files(request):
+    """
+    Populates request.PUT and request.FILES from
+    request.raw_post_data. PUT and POST requests differ 
+    only in REQUEST_METHOD, not in the way data is encoded. 
+    Therefore we can use Django's POST data retrieval method 
+    for PUT.
+    """
+    if request.method == 'PUT':
+        request.method = 'POST'
+        request._load_post_and_files()
+        request.method = 'PUT'
+        request.PUT = request.POST
+        del request._post
+
+def reverse(viewname, args=(), kwargs=None):
+    """
+    Return the URL associated with a view and specified parameters.
+    If the regular expression used specifies an optional slash at 
+    the end of the URL, add the slash.
+    """
+    if not kwargs:
+        kwargs = {}
+    url = _reverse(viewname, None, args, kwargs)
+    if url[-2:] == '/?':
+        url = url[:-1]
+    return url
+
+class HttpMethodNotAllowed(Exception):
+    """
+    Signals that request.method was not part of
+    the list of permitted methods.
+    """
+
+class ResourceBase(object):
+    """
+    Base class for both model-based and non-model-based 
+    resources.
+    """
+    def __init__(self, authentication=None, permitted_methods=None):
+        """
+        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')
+        """
+        # Access restrictions
+        if not authentication:
+            authentication = NoAuthentication()
+        self.authentication = authentication
+        
+        if not permitted_methods:
+            permitted_methods = ["GET"]
+        self.permitted_methods = [m.upper() for m in permitted_methods]
+    
+    def dispatch(self, request, target, *args, **kwargs):
+        """
+        """
+        request_method = request.method.upper()
+        if request_method not in self.permitted_methods:
+            raise HttpMethodNotAllowed
+        
+        if request_method == 'GET':
+            return target.read(request, *args, **kwargs)
+        elif request_method == 'POST':
+            return target.create(request, *args, **kwargs)
+        elif request_method == 'PUT':
+            load_put_and_files(request)
+            return target.update(request, *args, **kwargs)
+        elif request_method == 'DELETE':
+            return target.delete(request, *args, **kwargs)
+        else:
+            raise Http404
+    
+    def get_url(self):
+        """
+        Returns resource URL.
+        """
+        return reverse(self)
+
+    # The four CRUD methods that any class that 
+    # inherits from Resource may implement:
+    
+    def create(self, request):
+        raise Http404
+    
+    def read(self, request):
+        raise Http404
+    
+    def update(self, request):
+        raise Http404
+    
+    def delete(self, request):
+        raise Http404
+
+class Resource(ResourceBase):
+    """
+    Generic resource class that can be used for
+    resources that are not based on Django models.
+    """
+    def __init__(self, authentication=None, permitted_methods=None,
+                 mimetype=None):
+        """
+        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')
+        mimetype:
+            if the default None is not changed, any HttpResponse calls 
+            use settings.DEFAULT_CONTENT_TYPE and settings.DEFAULT_CHARSET
+        """
+        ResourceBase.__init__(self, authentication, permitted_methods)
+        self.mimetype = mimetype
+    
+    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.
+        """
+        # Check permission
+        if not self.authentication.is_authenticated(request):
+            response = HttpResponse(_('Authorization Required'), mimetype=self.mimetype)
+            challenge_headers = self.authentication.challenge_headers()
+            for k,v in challenge_headers.items():
+                response[k] = v
+            response.status_code = 401
+            return response
+        
+        try:
+            return self.dispatch(request, self, *args, **kwargs)
+        except HttpMethodNotAllowed:
+            response = HttpResponseNotAllowed(self.permitted_methods)
+            response.mimetype = self.mimetype
+            return response
+    

django_restapi/responder.py

+"""
+Data format classes ("responders") that can be plugged 
+into model_resource.ModelResource and determine how
+the objects of a ModelResource instance are rendered
+(e.g. serialized to XML, rendered by templates, ...).
+"""
+from django.core import serializers
+from django.core.handlers.wsgi import STATUS_CODE_TEXT
+from django.core.paginator import QuerySetPaginator, InvalidPage
+# the correct paginator for Model objects is the QuerySetPaginator,
+# not the Paginator! (see Django doc)
+from django.core.xheaders import populate_xheaders
+from django import forms
+from django.http import Http404, HttpResponse
+from django.forms.util import ErrorDict
+from django.shortcuts import render_to_response
+from django.template import loader, RequestContext
+from django.utils import simplejson
+from django.utils.xmlutils import SimplerXMLGenerator
+from django.views.generic.simple import direct_to_template
+
+class SerializeResponder(object):
+    """
+    Class for all data formats that are possible
+    with Django's serializer framework.
+    """
+    def __init__(self, format, mimetype=None, paginate_by=None, allow_empty=False):
+        """
+        format:
+            may be every format that works with Django's serializer
+            framework. By default: xml, python, json, (yaml).
+        mimetype:
+            if the default None is not changed, any HttpResponse calls 
+            use settings.DEFAULT_CONTENT_TYPE and settings.DEFAULT_CHARSET
+        paginate_by:
+            Number of elements per page. Default: All elements.
+        """
+        self.format = format
+        self.mimetype = mimetype
+        self.paginate_by = paginate_by
+        self.allow_empty = allow_empty
+        self.expose_fields = []
+        
+    def render(self, object_list):
+        """
+        Serializes a queryset to the format specified in
+        self.format.
+        """
+        # Hide unexposed fields
+        hidden_fields = []
+        for obj in list(object_list):
+            for field in obj._meta.fields:
+                if not field.name in self.expose_fields and field.serialize:
+                    field.serialize = False
+                    hidden_fields.append(field)
+        response = serializers.serialize(self.format, object_list)
+        # Show unexposed fields again
+        for field in hidden_fields:
+            field.serialize = True
+        return response
+    
+    def element(self, request, elem):
+        """
+        Renders single model objects to HttpResponse.
+        """
+        return HttpResponse(self.render([elem]), self.mimetype)
+    
+    def error(self, request, status_code, error_dict=None):
+        """
+        Handles errors in a RESTful way.
+        - appropriate status code
+        - appropriate mimetype
+        - human-readable error message
+        """
+        if not error_dict:
+            error_dict = ErrorDict()
+        response = HttpResponse(mimetype = self.mimetype)
+        response.write('%d %s' % (status_code, STATUS_CODE_TEXT[status_code]))
+        if error_dict:
+            response.write('\n\nErrors:\n')
+            response.write(error_dict.as_text())
+        response.status_code = status_code
+        return response
+    
+    def list(self, request, queryset, page=None):
+        """
+        Renders a list of model objects to HttpResponse.
+        """
+        if self.paginate_by:
+            paginator = QuerySetPaginator(queryset, self.paginate_by)
+            if not page:
+                page = request.GET.get('page', 1)
+            try:
+                page = int(page)
+                object_list = paginator.page(page).object_list
+            except (InvalidPage, ValueError):
+                if page == 1 and self.allow_empty:
+                    object_list = []
+                else:
+                    return self.error(request, 404)
+        else:
+            object_list = list(queryset)
+        return HttpResponse(self.render(object_list), self.mimetype)
+    
+class JSONResponder(SerializeResponder):
+    """
+    JSON data format class.
+    """
+    def __init__(self, paginate_by=None, allow_empty=False):
+        SerializeResponder.__init__(self, 'json', 'application/json',
+                    paginate_by=paginate_by, allow_empty=allow_empty)
+
+    def error(self, request, status_code, error_dict=None):
+        """
+        Return JSON error response that includes a human readable error
+        message, application-specific errors and a machine readable
+        status code.
+        """
+        if not error_dict:
+            error_dict = ErrorDict()
+        response = HttpResponse(mimetype = self.mimetype)
+        response.status_code = status_code
+        response_dict = {
+            "error-message" : '%d %s' % (status_code, STATUS_CODE_TEXT[status_code]),
+            "status-code" : status_code,
+            "model-errors" : error_dict.as_ul()
+        }
+        simplejson.dump(response_dict, response)
+        return response
+
+class XMLResponder(SerializeResponder):
+    """
+    XML data format class.
+    """
+    def __init__(self, paginate_by=None, allow_empty=False):
+        SerializeResponder.__init__(self, 'xml', 'application/xml',
+                    paginate_by=paginate_by, allow_empty=allow_empty)
+
+    def error(self, request, status_code, error_dict=None):
+        """
+        Return XML error response that includes a human readable error
+        message, application-specific errors and a machine readable
+        status code.
+        """
+        from django.conf import settings
+        if not error_dict:
+            error_dict = ErrorDict()
+        response = HttpResponse(mimetype = self.mimetype)
+        response.status_code = status_code
+        xml = SimplerXMLGenerator(response, settings.DEFAULT_CHARSET)
+        xml.startDocument()
+        xml.startElement("django-error", {})
+        xml.addQuickElement(name="error-message", contents='%d %s' % (status_code, STATUS_CODE_TEXT[status_code]))
+        xml.addQuickElement(name="status-code", contents=str(status_code))
+        if error_dict:
+            xml.startElement("model-errors", {})
+            for (model_field, errors) in error_dict.items():
+                for error in errors:
+                    xml.addQuickElement(name=model_field, contents=error)
+            xml.endElement("model-errors")
+        xml.endElement("django-error")
+        xml.endDocument()
+        return response
+
+class TemplateResponder(object):
+    """
+    Data format class that uses templates (similar to Django's
+    generic views).
+    """
+    def __init__(self, template_dir, paginate_by=None, template_loader=loader,
+                 extra_context=None, allow_empty=False, context_processors=None,
+                 template_object_name='object', mimetype=None):
+        self.template_dir = template_dir
+        self.paginate_by = paginate_by
+        self.template_loader = template_loader
+        if not extra_context:
+            extra_context = {}
+        for key, value in extra_context.items():
+            if callable(value):
+                extra_context[key] = value()
+        self.extra_context = extra_context
+        self.allow_empty = allow_empty
+        self.context_processors = context_processors
+        self.template_object_name = template_object_name
+        self.mimetype = mimetype
+        self.expose_fields = None # Set by Collection.__init__
+            
+    def _hide_unexposed_fields(self, obj, allowed_fields):
+        """
+        Remove fields from a model that should not be public.
+        """
+        for field in obj._meta.fields:
+            if not field.name in allowed_fields and \
+               not field.name + '_id' in allowed_fields:
+                obj.__dict__.pop(field.name)    
+
+    def list(self, request, queryset, page=None):
+        """
+        Renders a list of model objects to HttpResponse.
+        """
+        template_name = '%s/%s_list.html' % (self.template_dir, queryset.model._meta.module_name)
+        if self.paginate_by:
+            paginator = QuerySetPaginator(queryset, self.paginate_by)
+            if not page:
+                page = request.GET.get('page', 1)
+            try:
+                page = int(page)
+                object_list = paginator.page(page).object_list
+            except (InvalidPage, ValueError):
+                if page == 1 and self.allow_empty:
+                    object_list = []
+                else:
+                    raise Http404
+            current_page = paginator.page(page)
+            c = RequestContext(request, {
+                '%s_list' % self.template_object_name: object_list,
+                'is_paginated': paginator.num_pages > 1,
+                'results_per_page': self.paginate_by,
+                'has_next': current_page.has_next(),
+                'has_previous': current_page.has_previous(),
+                'page': page,
+                'next': page + 1,
+                'previous': page - 1,
+                'last_on_page': current_page.end_index(),
+                'first_on_page': current_page.start_index(),
+                'pages': paginator.num_pages,
+                'hits' : paginator.count,
+            }, self.context_processors)
+        else:
+            object_list = queryset
+            c = RequestContext(request, {
+                '%s_list' % self.template_object_name: object_list,
+                'is_paginated': False
+            }, self.context_processors)
+            if not self.allow_empty and len(queryset) == 0:
+                raise Http404
+        # Hide unexposed fields
+        for obj in object_list:
+            self._hide_unexposed_fields(obj, self.expose_fields)
+        c.update(self.extra_context)        
+        t = self.template_loader.get_template(template_name)
+        return HttpResponse(t.render(c), mimetype=self.mimetype)
+
+    def element(self, request, elem):
+        """
+        Renders single model objects to HttpResponse.
+        """
+        template_name = '%s/%s_detail.html' % (self.template_dir, elem._meta.module_name)
+        t = self.template_loader.get_template(template_name)
+        c = RequestContext(request, {
+            self.template_object_name : elem,
+        }, self.context_processors)
+        # Hide unexposed fields
+        self._hide_unexposed_fields(elem, self.expose_fields)
+        c.update(self.extra_context)
+        response = HttpResponse(t.render(c), mimetype=self.mimetype)
+        populate_xheaders(request, response, elem.__class__, getattr(elem, elem._meta.pk.name))
+        return response
+    
+    def error(self, request, status_code, error_dict=None):
+        """
+        Renders error template (template name: error status code).
+        """
+        if not error_dict:
+            error_dict = ErrorDict()
+        response = direct_to_template(request, 
+            template = '%s/%s.html' % (self.template_dir, str(status_code)),
+            extra_context = { 'errors' : error_dict },
+            mimetype = self.mimetype)
+        response.status_code = status_code
+        return response
+    
+    def create_form(self, request, queryset, form_class):
+        """
+        Render form for creation of new collection entry.
+        """
+        ResourceForm = forms.form_for_model(queryset.model, form=form_class)
+        if request.POST:
+            form = ResourceForm(request.POST)
+        else:
+            form = ResourceForm()
+        template_name = '%s/%s_form.html' % (self.template_dir, queryset.model._meta.module_name)
+        return render_to_response(template_name, {'form':form})
+
+    def update_form(self, request, pk, queryset, form_class):
+        """
+        Render edit form for single entry.
+        """
+        # Remove queryset cache by cloning the queryset
+        queryset = queryset._clone()
+        elem = queryset.get(**{queryset.model._meta.pk.name : pk})
+        ResourceForm = forms.form_for_instance(elem, form=form_class)
+        if request.PUT:
+            form = ResourceForm(request.PUT)
+        else:
+            form = ResourceForm()
+        template_name = '%s/%s_form.html' % (self.template_dir, elem._meta.module_name)
+        return render_to_response(template_name, 
+                {'form':form, 'update':True, self.template_object_name:elem})
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+    from ez_setup import use_setuptools
+    use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c9"
+DEFAULT_URL     = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+    'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+    'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+    'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+    'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+    'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+    'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+    'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+    'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+    'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+    'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+    'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+    'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+    'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+    'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+    'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+    'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
+    'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
+    'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
+    'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
+    'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
+    'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
+    'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
+    'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
+    'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
+    'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
+    'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
+    'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
+    'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
+    'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
+    'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
+    'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
+    'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
+    'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
+    'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
+}
+
+import sys, os
+try: from hashlib import md5
+except ImportError: from md5 import md5
+
+def _validate_md5(egg_name, data):
+    if egg_name in md5_data:
+        digest = md5(data).hexdigest()
+        if digest != md5_data[egg_name]:
+            print >>sys.stderr, (
+                "md5 validation of %s failed!  (Possible download problem?)"
+                % egg_name
+            )
+            sys.exit(2)
+    return data
+
+def use_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    download_delay=15
+):
+    """Automatically find/download setuptools and make it available on sys.path
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end with
+    a '/').  `to_dir` is the directory where setuptools will be downloaded, if
+    it is not already available.  If `download_delay` is specified, it should
+    be the number of seconds that will be paused before initiating a download,
+    should one be required.  If an older version of setuptools is installed,
+    this routine will print a message to ``sys.stderr`` and raise SystemExit in
+    an attempt to abort the calling script.
+    """
+    was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
+    def do_download():
+        egg = download_setuptools(version, download_base, to_dir, download_delay)
+        sys.path.insert(0, egg)
+        import setuptools; setuptools.bootstrap_install_from = egg
+    try:
+        import pkg_resources
+    except ImportError:
+        return do_download()       
+    try:
+        pkg_resources.require("setuptools>="+version); return
+    except pkg_resources.VersionConflict, e:
+        if was_imported:
+            print >>sys.stderr, (
+            "The required version of setuptools (>=%s) is not available, and\n"
+            "can't be installed while this script is running. Please install\n"
+            " a more recent version first, using 'easy_install -U setuptools'."
+            "\n\n(Currently using %r)"
+            ) % (version, e.args[0])
+            sys.exit(2)
+        else:
+            del pkg_resources, sys.modules['pkg_resources']    # reload ok
+            return do_download()
+    except pkg_resources.DistributionNotFound:
+        return do_download()
+
+def download_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    delay = 15
+):
+    """Download setuptools from a specified location and return its filename
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end
+    with a '/'). `to_dir` is the directory where the egg will be downloaded.
+    `delay` is the number of seconds to pause before an actual download attempt.
+    """
+    import urllib2, shutil
+    egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+    url = download_base + egg_name
+    saveto = os.path.join(to_dir, egg_name)
+    src = dst = None
+    if not os.path.exists(saveto):  # Avoid repeated downloads
+        try:
+            from distutils import log
+            if delay:
+                log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help).  I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+   %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+                    version, download_base, delay, url
+                ); from time import sleep; sleep(delay)
+            log.warn("Downloading %s", url)
+            src = urllib2.urlopen(url)
+            # Read/write all in one block, so we don't create a corrupt file
+            # if the download is interrupted.
+            data = _validate_md5(egg_name, src.read())
+            dst = open(saveto,"wb"); dst.write(data)
+        finally:
+            if src: src.close()
+            if dst: dst.close()
+    return os.path.realpath(saveto)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def main(argv, version=DEFAULT_VERSION):
+    """Install or upgrade setuptools and EasyInstall"""
+    try:
+        import setuptools
+    except ImportError:
+        egg = None
+        try:
+            egg = download_setuptools(version, delay=0)
+            sys.path.insert(0,egg)
+            from setuptools.command.easy_install import main
+            return main(list(argv)+[egg])   # we're done here
+        finally:
+            if egg and os.path.exists(egg):
+                os.unlink(egg)
+    else:
+        if setuptools.__version__ == '0.0.1':
+            print >>sys.stderr, (
+            "You have an obsolete version of setuptools installed.  Please\n"
+            "remove it from your system entirely before rerunning this script."
+            )
+            sys.exit(2)
+
+    req = "setuptools>="+version
+    import pkg_resources
+    try:
+        pkg_resources.require(req)
+    except pkg_resources.VersionConflict:
+        try:
+            from setuptools.command.easy_install import main
+        except ImportError:
+            from easy_install import main
+        main(list(argv)+[download_setuptools(delay=0)])
+        sys.exit(0) # try to force an exit
+    else:
+        if argv:
+            from setuptools.command.easy_install import main
+            main(argv)
+        else:
+            print "Setuptools version",version,"or greater has been installed."
+            print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+def update_md5(filenames):
+    """Update our built-in md5 registry"""
+
+    import re
+
+    for name in filenames:
+        base = os.path.basename(name)
+        f = open(name,'rb')
+        md5_data[base] = md5(f.read()).hexdigest()
+        f.close()
+
+    data = ["    %r: %r,\n" % it for it in md5_data.items()]
+    data.sort()
+    repl = "".join(data)
+
+    import inspect
+    srcfile = inspect.getsourcefile(sys.modules[__name__])
+    f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+    match = re.search("\nmd5_data = {\n([^}]+)}", src)
+    if not match:
+        print >>sys.stderr, "Internal error!"
+        sys.exit(2)
+
+    src = src[:match.start(1)] + repl + src[match.end(1):]
+    f = open(srcfile,'w')
+    f.write(src)
+    f.close()
+
+
+if __name__=='__main__':
+    if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+        update_md5(sys.argv[2:])
+    else:
+        main(sys.argv[1:])
+
+
+
+
+
+
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Setuptools module description for django_restapi.  Packaged by RJ Ryan, since
+# the django-rest-interface project on Google Code does not have an installer.
+
+from ez_setup import use_setuptools
+use_setuptools()
+from setuptools import setup, find_packages
+
+setup(name='django-rest-interface',
+      version='r81',
+      description='The Django REST interface makes it easy to offer private and public APIs for existing Django models.',
+      long_description=u'The Django REST interface makes it easy to offer private and public APIs for existing Django models. New generic views simplify data retrieval and modification in a resource-centric architecture and provide model data in formats such as XML, JSON and YAML with very little custom code.',
+      author=u'Andreas Stuhlmüller',
+      packages=['django_restapi',],
+      package_data={},
+      )