Django Login Screen

Django Login Screen

I have worked on a couple of projects  that required some customization of the Django admin. Here are some of the ways I was able to customize the admin to add functionality.

Adding Buttons to the Change View

One of the major features needed for a project was the ability to manage a work flow for a model. The model progressed through different status stages, and the admin team needed to be able to make decisions based on that current status. To handle this, I added some new buttons to the model’s change view, and used my ModelAdmin and ModelForm subclasses to move the model around in the workflow accordingly.

Customizing the Template

In order to differentiate the workflow management options from the standard Admin delete & save buttons, I added a row of buttons directly above the standard delete & save row. I created a file in my templates directory with the path admin/myapp/mymodel/change_form.html that looked something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{% extends "admin/change_form.html" %}
 
{% block after_related_objects %}
  {% if original.pk %}
    <div class="submit-row">
      <p class="deletelink-box">Manage this request</p>
      {% if original.requested %}
        <input type="submit" value="Approve" name="approve">
        <input type="submit" value="Deny" name="deny">
      {% endif %}
      {% if original.submitted %}
        <input type="submit" value="Accept" name="accept">
        <input type="submit" value="Return" name="return">
      {% endif %}
      {% if original.submitted or original.assigned %}
        <input type="submit" value="Cancel" name="cancel">
      {% endif %}
      {% if original.accepted %}
        <input type="submit" value="Complete" name="complete">
      {% endif %}
      {% if original.cancelled %}
        <input type="submit" value="Reopen" name="reopen">
      {% endif %}
    </div>
  {% endif %}
{% endblock %}
{% extends "admin/change_form.html" %}

{% block after_related_objects %}
  {% if original.pk %}
    <div class="submit-row">
      <p class="deletelink-box">Manage this request</p>
      {% if original.requested %}
        <input type="submit" value="Approve" name="approve">
        <input type="submit" value="Deny" name="deny">
      {% endif %}
      {% if original.submitted %}
        <input type="submit" value="Accept" name="accept">
        <input type="submit" value="Return" name="return">
      {% endif %}
      {% if original.submitted or original.assigned %}
        <input type="submit" value="Cancel" name="cancel">
      {% endif %}
      {% if original.accepted %}
        <input type="submit" value="Complete" name="complete">
      {% endif %}
      {% if original.cancelled %}
        <input type="submit" value="Reopen" name="reopen">
      {% endif %}
    </div>
  {% endif %}
{% endblock %}

The original context variable refers to the object being created or changed. I checked for a primary key because I only wanted the extra buttons to show up on edits. The requested,submitted, etc. attributes are properties on my model that check a status field.

Handling the Extra Submit Options

To handle these extra submit options, I created a ModelForm subclass. On my ModelAdmin, I set form = MyAdminForm to point to my new ModelForm. On the form, I implemented the clean method to look for the workflow actions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def clean(self, *args, **kwargs):
    cleaned_data = super(MyAdminForm, self).clean(*args, **kwargs)
 
    if self.data.has_key('approve'):
        # Handle the approval logic here
        # ...
        # If there's a problem with the action, you can raise a
        # forms.ValidationError("Your message here.")
    elif self.data.has_key('deny'):
        # Handle the denial logic here
        # ...
 
    # Etc...
 
    return cleaned_data
def clean(self, *args, **kwargs):
    cleaned_data = super(MyAdminForm, self).clean(*args, **kwargs)

    if self.data.has_key('approve'):
        # Handle the approval logic here
        # ...
        # If there's a problem with the action, you can raise a
        # forms.ValidationError("Your message here.")
    elif self.data.has_key('deny'):
        # Handle the denial logic here
        # ...

    # Etc...

    return cleaned_data

In order to give the user effective feedback that their action was success, I wanted to customize the response message given to them after they click the custom button. To do that, I went back to my ModelAdmin subclass and overrode the response_change method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def response_change(self, request, obj, *args, **kwargs):
    if request.POST.has_key("approve"):
        msg = 'The %(name)s "%(obj)s" was approved.'  % {
            'name': obj._meta.verbose_name, 'obj': obj
        }
        self.message_user(request, msg)
        return HttpResponseRedirect("../")
    if request.POST.has_key("deny"):
        msg = 'The %(name)s "%(obj)s" was denied.' % {
            'name': obj._meta.verbose_name, 'obj': obj
        }
        self.message_user(request, msg)
        return HttpResponseRedirect("../")
 
    # Etc...
 
    if request.POST.has_key("reopen"):
        msg = 'The %(name)s "%(obj)s" was reopened. ' % {
            'name: obj._meta.verbose_name, 'obj': obj
        }
        msg += 'You may manage it below.'
        self.message_user(request, msg)
        if request.REQUEST.has_key('_popup'):
            return HttpResponseRedirect(request.path + "?_popup=1")
        else:
            return HttpResponseRedirect(request.path)
 
    return super(MyModelAdmin, self).response_change(
        request, obj, *args, **kwargs
    )
def response_change(self, request, obj, *args, **kwargs):
    if request.POST.has_key("approve"):
        msg = 'The %(name)s "%(obj)s" was approved.'  % {
            'name': obj._meta.verbose_name, 'obj': obj
        }
        self.message_user(request, msg)
        return HttpResponseRedirect("../")
    if request.POST.has_key("deny"):
        msg = 'The %(name)s "%(obj)s" was denied.' % {
            'name': obj._meta.verbose_name, 'obj': obj
        }
        self.message_user(request, msg)
        return HttpResponseRedirect("../")

    # Etc...

    if request.POST.has_key("reopen"):
        msg = 'The %(name)s "%(obj)s" was reopened. ' % {
            'name: obj._meta.verbose_name, 'obj': obj
        }
        msg += 'You may manage it below.'
        self.message_user(request, msg)
        if request.REQUEST.has_key('_popup'):
            return HttpResponseRedirect(request.path + "?_popup=1")
        else:
            return HttpResponseRedirect(request.path)

    return super(MyModelAdmin, self).response_change(
        request, obj, *args, **kwargs
    )

Adding Modules to the App Index

In order to create a dashboard for the workflow-based app, I wanted to add some modules to the app index that would act as a queue for items in a certain status. I decided to handle this through template tags:

1
2
3
4
@register.inclusion_tag('admin/myapp/_recent_requests.html')
def recent_requests():
    return {'requests':
            MyModel.pending.order_by('-requested')}
@register.inclusion_tag('admin/myapp/_recent_requests.html')
def recent_requests():
    return {'requests':
            MyModel.pending.order_by('-requested')}

I added an admin/myapp/app_index.html template that looked like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{% extends "admin/app_index.html" %}
 
{% load myapp_admin_tags %}
 
{% block sidebar %}
  <div id="content-related">
    <div class="module">
      <h2>Shortcuts</h2>
      <ul>
        <li><a href="{% url admin:myapp_mymodel_changelist %}?ot=desc&is_closed__exact=0&o=3">
          Open requests
        </a></li>
        <!-- Etc... -->
      </ul>
      {% recent_submissions %}
    </div>
  </div>
  <div id="content-secondary">
    {% recent_requests %}
  </div>
{% endblock %}
{% extends "admin/app_index.html" %}

{% load myapp_admin_tags %}

{% block sidebar %}
  <div id="content-related">
    <div class="module">
      <h2>Shortcuts</h2>
      <ul>
        <li><a href="{% url admin:myapp_mymodel_changelist %}?ot=desc&is_closed__exact=0&o=3">
          Open requests
        </a></li>
        <!-- Etc... -->
      </ul>
      {% recent_submissions %}
    </div>
  </div>
  <div id="content-secondary">
    {% recent_requests %}
  </div>
{% endblock %}

Creating a Site-Specific Admin

On one project with several sub-sites, I needed to create a site-specific admin so that administrators of one site could use the Django admin without interfering with content from the other sites. To do this, I created a secondary admin site.

Adding an Additional Admin Site

I wanted users of the site-specific admin to be a member of a specific group. With this in mind, I created a subclass of AdminSite with an overridden has_permission method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from django.contrib.admin.sites import AdminSite
 
class LocalAdminSite(AdminSite):
    def has_permission(self, request):
        """
        Checks to make sure that the user is authenticated and is either a
        staff member or a member of the 'Content editor' group.
        """
        from django.contrib.auth.models import Group
        ce_group = Group.objects.get(name='Content editor')
        return request.user.is_authenticated() and (
            request.user.is_staff or ce_group in request.user.groups.all()
        )
 
local_admin = LocalAdminSite(name='local')
from django.contrib.admin.sites import AdminSite

class LocalAdminSite(AdminSite):
    def has_permission(self, request):
        """
        Checks to make sure that the user is authenticated and is either a
        staff member or a member of the 'Content editor' group.
        """
        from django.contrib.auth.models import Group
        ce_group = Group.objects.get(name='Content editor')
        return request.user.is_authenticated() and (
            request.user.is_staff or ce_group in request.user.groups.all()
        )

local_admin = LocalAdminSite(name='local')

I created an app within my project called admin and added the above code to admin/sites.py. Then, to use my site, I could simply do this:

1
from myproject.admin.sites import local_admin
from myproject.admin.sites import local_admin
Follow on Google+ Updated on February 6, 2014 at
Post your comment

All the fields must be filled out before posting.