david / django-modelviews

Backup of an old repository with useful ideas. Initial goal: integrating REST to django admin (class-based views).

Clone this repository (size: 85.7 KB): HTTPS / SSH
$ hg clone http://code.welldev.org/django-modelviews
django-modelviews / generic / rest_views.py
r44:add8532b04a8 231 loc 8.8 KB embed / history / annotate / raw /
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotAllowed
from django.shortcuts import render_to_response
from django.newforms.models import ModelFormMetaclass, ModelForm
from responders import *

try:
    from django.views.generic.base import BaseView
except ImportError:
    from compatability import BaseView

def django_authentication(request, **kwargs):
    return request.user.is_authenticated()

def django_superuser_authentication(request, **kwargs):
    return request.user.is_superuser

def django_staff_authentication(request, **kwargs):
    return request.user.is_staff



class ModelView(BaseView):
    """
    ModelView: a RESTful class-based view of your resources
    =======================================================
    
    Philosophy
    ----------
    
    The goal of this class is to provide a lightweight REST interface, I know
    that django-rest-interface exists but it's abandoned and it suffers from
    some defaults. Given my experience with it, I always need to rewrite large
    parts of the library in order to customize it to fits with my own needs.
    
    In order to avoid this with ModelView, the philosophy is to allow 
    dead-easy subclassing with granularity at each point.
    
    Note: I totally respect what had been done before and this class is 
    inspired from a lot of existing projects so many thanks to all authors.
    
    
    Goals
    -----
    
    Basically, it dispatches requests to the appropriated function given the
    HTTP verb and it allows you to specify a responder in order to avoid 
    duplication of code at the resource logic level.
    
    You can specify your own responders, and restrict to allowed HTTP methods.
    
    Secret goal: to be honest, I dream of resource oriented stuff in 
    Django's trunk for years now. A RESTful Django's (newforms-)admin could be
    awesome too in order to provide a built-in API!
        
    
    Example
    -------
    
    A quick example in order to demonstrate what is possible.
    
    models.py::
    
        class Post(models.Model):
            title = models.Charfield(max_length=200)
            slug = models.SlugField(max_length=200, prepopulate_from=('title',), unique=True)
            content = models.TextField()
        
    urls.py::
    
        blog = ModelView(Post.objects.filter(is_online=True), responders=(HtmlResponder, JsonResponder), methods=('GET',))
        blog_admin = ModelView(Post.objects.all(), methods=('GET', 'POST', 'PUT', 'DELETE'))
        urlpatterns = pattern('', 
            url(^blog/(?P<slug>[-\w]+)/(?P<format>(html|json))?/?$), blog, name='blog'),
            url(^blog/admin/(?P<object_pk>\d)/), blog_admin, name='blog_admin'),
        )
    
    Note: here we consider that you want a custom admin for your blog,
    otherwise Django's built-in admin is much more interesting, of course.
    
    
    TODO
    ----
    
        * Add tests and documentation
        * Create a collection of generic Responders
        * Handle receivers in order to use it as an API (for the moment,
          it assumes that you receive formencoded data), need more reflexion.
        * Handle privacy, need more reflexion
        * See what can be done with APP, it could be fun!
        * Ideas?
    
    """
    def __init__(self, queryset, slug_field='slug', post_save_redirect=None,
            paginate_by=None, allow_empty=True,
            responders=(HtmlResponder,), methods=('GET', 'POST', 'PUT', 'DELETE')):
        self.slug_field = slug_field
        self.post_save_redirect = post_save_redirect
        self.paginate_by = paginate_by
        self.allow_empty = allow_empty
        self.responders = dict((responder.__name__, responder) for responder in responders)
        if isinstance(methods, tuple):
            self.methods = dict(zip(methods, len(methods)*(False,)))
        else:
            self.methods = methods
        super(ModelView, self).__init__(queryset)


    def __call__(self, request, object_pk=None, slug=None, format='html', **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.
        """
        # Restrict
        request_method = request.method.upper()
        if request_method not in self.methods.keys():
            return HttpResponseNotAllowed(request_method)
        if self.methods[request_method]:
            if not self.methods[request_method](request, **kwargs):
                return HttpResponseNotAllowed(request_method)

        # Unmatched regex groups in the url will not recieve
        # the default value specified for the method, but
        # instead recieve the value None. Thus we must
        # explicitly check for None and assign the desired
        # value if we want default values.
        if format is None:
            format = 'html'

        # Filter initial queryset given request arguments.
        subset = self.get_subset(request, object_pk, slug, **kwargs)
                
        # Dispatch
        if request_method == 'GET':
            return self.read(request, subset, self.get_responder(format, subset),
                             is_list=object_pk is None and slug is None)
        elif request_method == 'POST':
            return self.create(request, self.get_responder(format, subset))
        elif request_method == 'PUT':
            return self.update(request, subset[0], self.get_responder(format, subset))
        elif request_method == 'DELETE':
            return self.delete(request, subset[0], self.get_responder(format, subset))
        else:
            raise Http404

    def get_subset(self, request, object_pk=None, slug=None, **kwargs):
        """
        Returns the filtered queryset from arguments.
        """
        # Verify if lookup arguments exist
        if 'lookup_kwargs' in kwargs:
            lookup_kwargs = kwargs['lookup_kwargs']
            for k, v in lookup_kwargs.items():
                lookup_kwargs[k] = v % kwargs
        else:
            lookup_kwargs = {}
        
        if object_pk:
            lookup_kwargs['pk'] = object_pk
        elif slug and self.slug_field:
            lookup_kwargs[self.slug_field] = slug
        return self.get_query_set(request).filter(**lookup_kwargs)

    def get_responder(self, format, subset):
        """
        Returns a ``Responder`` instance given the format.
        """
        try:
            # get_query_set requires a request parameter, but doesn't
            # do anything with the parameter (presumably a subclass
            # might. Passing self is an arbitrary decision.
            return self.responders['%sResponder' % format.title()](subset)
        except KeyError:
            raise ImproperlyConfigured("%s responder doesn't exist." % '%sResponder' % format.title())

    def get_form(self, request):
        """
        Returns a ``ModelForm`` class to be used in this view.
        """
        # TODO: we should be able to construct a ModelForm without creating
        # and passing in a temporary inner class
        class Meta:
            model = self.model
        class_name = self.model.__name__ + 'Form'
        return ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta})

    def save_form(self, request, form):
        """
        Saves and returns the object represented by the given form. This
        method will only be called if the form is valid.
        """
        return form.save()

    def read(self, request, subset, responder, is_list):
        """
        Returns a rendered list or element whether ``obj`` exists, idempotent.
        """
        if is_list:
            return responder.list(request, self.paginate_by, self.allow_empty)
        else:
            return responder.element(request, subset[0])

    def create(self, request, responder):
        """
        Create a new resource.
        """
        Form = self.get_form(request)
        form = Form(request.POST, request.FILES)
        if form.is_valid():
            new_obj = self.save_form(request, form)
            return responder.create_success(request, new_obj, self.post_save_redirect)

    def update(self, request, obj, responder):
        """
        Update an existing resource (there is no PUT creation for now), idempotent.
        """
        Form = self.get_form(request)
        form = Form(request.POST, request.FILES, instance=obj)
        if form.is_valid():
            new_obj = self.save_form(request, form)
            return responder.update_success(request, obj, new_obj, self.post_save_redirect)

    def delete(self, request, obj, responder):
        """
        Delete an existing resource, idempotent.
        """
        if obj is not None:
            obj.delete()
        return responder.delete_success(self.post_save_redirect)