May 20th, 2013

Permission based CRUD app in Django

Tutorial for: Django

Still confused on how to use Django's built-in permission system? Look no further than this tutorial to get you up and running quickly! You will need nothing more than Django and maybe South if you want smooth Database migrations.

If you followed yesterday's tutorial on a simple CRUD app in Django, you'll be pleased to know that this tutorial is a spiritual successor to that one. This tutorial will only have a single model, which will be a simple ToDo list, which will incorporate the user permission system built into Django.

Let's begin by explaining the default Django permission system and see if it will fit the CRUD model properly. The default permission on every model created, but not enforced by default are, add, change, and delete. The only one missing is a view permission. Permissions are by default only enforced in the admin interface, and can only be used if the particular user has access to the admin interface. Notice it's called an admin interface, you normally do not want your everyday website users to use this to modify data. Lets begin by creating our Todo model:

from django.db import models
from django.contrib.auth.models import User

class Todo(models.Model):
    PRIORITY_LIST = (
        (0, 'Low'),
        (5, 'Normal'),
        (10, 'High'),
        (15, 'Urgent'),
    )
    title = models.CharField(max_length=40)
    owner = models.ForeignKey(User)
    priority = models.PositiveSmallIntegerField(choices=PRIORITY_LIST, default=5)
    added_on = models.DateTimeField(auto_now_add=True)
    due_on = models.DateField()
    class Meta:
        permissions = (('view_todo', 'Can view todo'),)
        ordering = ['due_on']
    def __unicode__(self):
        return u"%s" % self.title
    @models.permalink
    def get_absolute_url(self):
        return ('todo_detail', [int(self.pk)])

Here you will notice the permission being created in the model's Meta. Besides that, the concepts from the previous CRUD tutorial are the same.

from django import forms
from todo.models import Todo

class TodoForm(forms.ModelForm):
    class Meta:
        model = Todo
        fields = ('title', 'priority', 'due_on',)

Here is the form, same concepts from the first tutorial.

from django.views.generic.list import ListView
from todo.models import Todo
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from todo.forms import TodoForm
from django.shortcuts import redirect
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import permission_required
from django.utils.decorators import method_decorator

class TodoList(ListView):
    model = Todo

class TodoDetail(DetailView):
    model = Todo
    @method_decorator(permission_required('todo.view_todo'))
    def dispatch(self, *args, **kwargs):
        return super(TodoDetail, self).dispatch(*args, **kwargs)

class TodoCreate(CreateView):
    model = Todo
    form_class = TodoForm
    @method_decorator(permission_required('todo.add_todo'))
    def dispatch(self, *args, **kwargs):
        return super(TodoCreate, self).dispatch(*args, **kwargs)
    def form_valid(self, form):
        self.object = form.save(commit=False)
        self.object.owner = self.request.user
        self.object.save()
        return redirect(self.object)

class TodoUpdate(UpdateView):
    model = Todo
    form_class = TodoForm
    @method_decorator(permission_required('todo.change_todo'))
    def dispatch(self, *args, **kwargs):
        return super(TodoUpdate, self).dispatch(*args, **kwargs)

class TodoDelete(DeleteView):
    model = Todo
    @method_decorator(permission_required('todo.delete_todo'))
    def dispatch(self, *args, **kwargs):
        return super(TodoDelete, self).dispatch(*args, **kwargs)
    def get_success_url(self):
        # To do this because the success_url class variable isn't reversed...
        return reverse('todo_list')

This is where the permissions are enforced, you just need to decorate the dispatch(), there are other ways as well to check for the permission in the views. The permissions are not enforced on the TodoList, but everything else it is.

from django.conf.urls import patterns, url
from todo.views import TodoList, TodoDetail, TodoCreate, TodoDelete, TodoUpdate

urlpatterns = patterns('todo.views',
    url(r'^$', TodoList.as_view(), name='todo_list'),
    url(r'^Todo(?P<pk>\d+)$', TodoDetail.as_view(), name='todo_detail'),
    url(r'^New$', TodoCreate.as_view(), name='todo_create'),
    url(r'^Todo(?P<pk>\d+)/Update$', TodoUpdate.as_view(), name='todo_update'),
    url(r'^Todo(?P<pk>\d+)/Delete$', TodoDelete.as_view(), name='todo_delete'),
)

This is the urls.py file which will map the views, you can technically also check for permissions here as well by wrapping the class in a decorator. I normally do this for the login_required decorator.

Now for the templates, they are mostly the same as the first tutorial from yesterday but with a few key differences, can you spot them?

<html>
<head>
  <title>{% block title %}Untitled{% endblock %} | Todo list with permissions</title>
</head>
<body>
<h1>Todo CRUD/Permissions example!</h1>
<a href="{% url 'todo_list' %}">Index</a>{% if perms.todo.add_todo %} | <a href="{% url 'todo_create' %}">Add Todo</a>{% endif %}
<hr/>
{% block content %}
The end user should never ever see this!
{% endblock %}
</body>
</html>

This is the crummy unpolished base.html but with permission checking.

{% extends 'todo/base.html' %}

{% block title %}Todo List{% endblock %}

{% block content %}
<table>
<thead><tr><th>Title</th><th>Action</th></tr></thead>
<tbody>
  {% for todo in todo_list %}
  <tr><td>{% if perms.todo.view_todo %}<a href="{{todo.get_absolute_url}}">{{todo}}</a>{% else %}{{todo}}{% endif %}</td>
  <td>{% if perms.todo.change_todo %}<a href="{% url 'todo_update' todo.pk %}">Update</a>{% endif %} | {% if perms.todo.delete_todo %}<a href="{% url 'todo_delete' todo.pk %}">Delete</a>{% endif %}</td></tr>
  {% endfor %}
</tbody>
</table>
{% endblock %}

This is the todo_list.html again with permission checking, if the user know the link anyways it wouldn't give them access, but hiding links which the user doesn't have permission to use just makes sense in my books.

{% extends 'todo/base.html' %}

{% block title %}{{todo}}{% endblock %}

{% block content %}
<table>
<tr><th>Title:</th><td>{{todo}}</td></tr>
<tr><th>Owner:</th><td>{{todo.owner}}</td></tr>
<tr><th>Priority:</th><td>{{todo.get_priority_display}}</td></tr>
<tr><th>Added on:</th><td>{{todo.added_on}}</td></tr>
<tr><th>Due on:</th><td>{{todo.due_on}}</td></tr>
</table>
{% if perms.todo.change_todo %}<a href="{% url 'todo_update' todo.pk %}">Update</a>{% endif %} | {% if perms.todo.delete_todo %}<a href="{% url 'todo_delete' todo.pk %}">Delete</a>{% endif %}
{% endblock %}

The detail template, similar to the template from the last tutorial but tailored specifically for this app.

{% extends 'todo/base.html' %}
{% load url from future %}

{% block title %}Todo Form{% endblock %}

{% block content %}
<h3>{% if object %}Updating {{object}}{% else %}Add a new Todo{% endif %}</h3>
<form action="" method="post">{% csrf_token %}
<table>
{{form}}
</table>
<input type="submit" value="Save Todo"/></form>
{% endblock %}

Here is the form template, similar but yet a bit different from the previous tutorial.

{% extends 'todo/base.html' %}
{% load url from future %}

{% block title %}{{object_name}} Confirm delete?{% endblock %}

{% block content %}
<h3>Are you sure you want to delete {{object}}?</h3>
<form action="" method="post">{% csrf_token %}
<input type="submit" value="Yes!"/>  <input type="button" value="No..." onclick="window.history.go(-1);"/>
</form>
{% endblock %}

The final template, the one that confirms the delete operation.

So there you have it, a fully working Django app with CRUD permissions, the superuser will always have all permissions, so you will need to create a regular or staff user to test out the permissions.

About Me

My Photo
Names Kevin, hugely into UNIX technologies, not just Linux. I've dabbled with the demons, played with the Sun, and now with the Penguins.




Kevin Veroneau Consulting Services
Do you require the services of a Django contractor? Do you need both a website and hosting services? Perhaps I can help.

This Month

If you like what you read, please consider donating to help with hosting costs, and to fund future books to review.

Python Powered | © 2012-2014 Kevin Veroneau