Source

django-rest-interface / 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})