I was looking for a way to manually order content items, and I posted a message in the Django-users group with a proposed solution. I never heard back as to whether there was a better way to do what I was looking for, so I went ahead and built it out.

Explanation

I created a new app named utils, and in that app are two simple models, OrderedList and OrderedItem.

OrderedList represents a list of ordered items, and can be edited via the admin interface (via a custom template). OrderedItem is simply a generic relation, which allows OrderedLists to order any type of model.

In order to use an OrderedList, one simply needs to install the models and templates, then add two lines of code to each model he would would like to order:

from utils.models import OrderedItem
and
order = generic.GenericRelation(OrderedItem)

The Code

The code consists of a model, a view and a couple of templates.

The model

I put my model into an app named utils. If you download the ZIP file, this is how the files will be laid out, but you should be able to install the model into another app, if you so choose.


# utils.models.py
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

class OrderedList(models.Model):
    name = models.CharField(max_length=200, unique=True)
    content_type = models.ForeignKey(ContentType)
    number_of_items = models.PositiveIntegerField(help_text="How many items will be managed by this page?")

    class Admin:
        pass

    def __unicode__(self):
        return self.name

class OrderedItem(models.Model):
    position = models.PositiveIntegerField()
    ordered_list = models.ForeignKey(OrderedList)
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = generic.GenericForeignKey('content_type', 'object_id')

    class Meta:
        unique_together = (('position', 'ordered_list', 'content_type'))

The view

This view is used to generate the custom admin template.


# utils.admin_views.py
from mysite.utils.models import OrderedList,OrderedItem
from django import template
from django.template import RequestContext
from django.shortcuts import render_to_response
from django.views.decorators.cache import never_cache
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.admin.views.main import ChangeList
from django.http import Http404, HttpResponseRedirect
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied
from django.db import models
from django.db.models import get_model
from django import newforms as forms  

def order(request, app_label, model_name, ol_id):

    # The Page object being reordered
    ol = OrderedList.objects.get(id=ol_id)
    # List of items, as currently ordered
    ordered_items = OrderedItem.objects.order_by('position').filter(ordered_list=ol)


    # Create lists of items to be selected via ChoiceFields, with a blank choice on top
    choices = [(0,'(None Selected)')]

    for item in get_model(ol.content_type.app_label, ol.content_type.model).objects.all():
        # Be sure the model you're ordering has a __str__ method defined, because
        # that is what will be used in the ChoiceField
        choices.append((item.id,item))
     
    # Must be a tuple    
    choices = (tuple(choices))


    # Create form, and add fields based on "number of items in page model"
    class OrderForm(forms.Form):
        def __init__(self, *args, **kwargs):
                super(OrderForm, self).__init__(*args, **kwargs)               
                for i in range(1, ol.number_of_items+1):
                    k1 = 'position_%d' % i
                    self.fields[k1] = forms.ChoiceField(required=False, choices=choices)

    # Populate fields with saved values, if there are any
    form_values = {}

    for item in range(1, ol.number_of_items+1):
        try:
            form_values['position_%d' % item] = ordered_items.filter(position=item)[0].content_object.id
        except:
            form_values['position_%d' % item] = 0
            
    order_form = OrderForm(form_values)
    
    
    # Make sure model exists, and user has permission
    model = models.get_model(app_label, model_name)
    if model is None:
        raise Http404("App %r, model %r, not found" % (app_label, model_name))
    if not request.user.has_perm(app_label + '.' + model._meta.get_change_permission()):
        raise PermissionDenied
    try:
        cl = ChangeList(request, model)
    except IncorrectLookupParameters:
        # Wacky lookup parameters were given, so redirect to the main
        # changelist page, without parameters, and pass an 'invalid=1'
        # parameter via the query string. If wacky parameters were given and
        # the 'invalid=1' parameter was already in the query string, something
        # is screwed up with the database, so display an error page.
        if ERROR_FLAG in request.GET.keys():
            return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
        return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')

    c = template.RequestContext(request, {
        "ol" : ol,
        'order_form' : order_form,
    })
    c.update({'has_add_permission': c['perms'][app_label][cl.opts.get_add_permission()]})
    
    if request.method == 'POST':
        form = OrderForm(request.POST)
        if form.is_valid():
            for i in range(1, ol.number_of_items+1):
                k1 = 'position_%d' % i
                if (form.data[k1] != '0'):
                    content_object = get_model(ol.content_type.app_label, ol.content_type.model).objects.get(id=form.data[k1])
                    try:
                        matching_ordered_item = OrderedItem.objects.get(position=i, ordered_list=ol)
                        ordered_item = OrderedItem(position=i, ordered_list=ol, content_object=content_object)
                        ordered_item.id = matching_ordered_item.id
                    except OrderedItem.DoesNotExist:
                        ordered_item = OrderedItem(position=i, ordered_list=ol, content_object=content_object)
                    ordered_item.save()
                    #assert False
                else:
                    try:
                        # changing from selected item to none
                        matching_ordered_item = OrderedItem.objects.get(position=i, ordered_list=ol)
                        matching_ordered_item.delete()
                    except:
                        # already none selected
                        pass
                        
            request.user.message_set.create(message=('Your changes have been saved.'))
            return HttpResponseRedirect(request.path)
        else:
            form = OrderForm()
        
    return render_to_response(
        "admin/%s/%s/order.html" % (app_label, model_name.lower()),
        context_instance=c
    ) 
    
order = staff_member_required(never_cache(order))

URL Dispatcher

This line should be added to your root urls.py file, above the line enabling the admin interface.


 (r'^admin/(?P<app_label>\w+)/(?P<model_name>\w+)/order/(?P<ol_id>\d+)/$', 'myproject.utils.admin_views.order'),

Templates

The first template simply adds a link to the custom ordering template to the OrderedList admin screen.



{% extends "admin/change_form.html" %}

{% block form_top %}
  <p><a href="../order/{{ object_id }}">Order items for this OrderedList</a></p>
{% endblock %}

The second template is used to display the custom ordering screen.



{% extends "admin/base_site.html" %}
{% load i18n admin_modify adminmedia %}
{% block extrahead %}{{ block.submiter }}
<script type="text/javascript" src="../../../jsi18n/"></script>
{% for js in javascript_imports %}{% include_admin_script js %}{% endfor %}
{% endblock %}
{% block stylesheet %}{% admin_media_prefix %}css/forms.css{% endblock %}
{% block coltype %}colM{% endblock %}
{% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
     <a href="../../../../">{% trans "Home" %}</a> ›
     <a href="../../">Ordered Lists</a>
</div>
{% endblock %}

{% block title %}Change order of items{% endblock %}

{% block content %}<div id="content-main">
<h1>Change order for {{ ol.name }}</h1>
<br />
{% if has_add_permission %}
<form method="post" action="">
{{ order_form.as_p }}
<div class="submit-row">
    <input type="submit" value="Save and continue editing" class="default" />
</div>
</form>
{% else %}
<p>You do not have permission to makes these changes.</p>
{% endif %}

</div>

{% endblock %}

Configuration

You will also need to add

myproject.utils
to
INSTALLED_APPS
in settings.py. You will also need to add
from utils.models import OrderedItem
and
order = generic.GenericRelation(OrderedItem)
to any model you would like to order (e.g. blog Entry).

Usage

OrderedLists represent lists of content that you would like to order and display in a template. You will need to create a new ordered list via the admin interface. You will need to specify a unique name; select a model to order (all of your models should appear in a select field); and define how many items will appear in the list. Once you have created an OrderedList, you can order items by going to the OrderedList's page, and clicking the "Order items for this OrderedList" link at the top of the page. This will take you to the custom ordering template. You will see the number of select fields that you entered when creating the OrderedList, each with a list of items from the model you selected when creating the OrderedList. After you have ordered some items, you can use the OrderedList in your template. In your view, you'll simply pass the result of a QuerySet to your template. For example:

# views.py
def home(request):
    
    ordered_list = OrderedList.objects.get(name='home_left')
    ordered_items = OrderedItem.objects.order_by('position').filter(ordered_list=ordered_list)

    return render_to_response(
            "home.html",
            {'ordered_items' : ordered_items},
            RequestContext(request, {}),
        )

# home.html template
{% for item in ordered_items %}

    {{ item.content_object.headline }}
{% endfor %}
OrderedLists are filled with content_objects, each of which are of the type selected in the OrderedList's content type field. If you want to grab individual items, simply put the items into a list and use the list index from the template:
#views.py
ordered_list = OrderedList.objects.get(name='home_left')
    ordered_items = []
    for item in OrderedItem.objects.order_by('position').filter(ordered_list=ordered_list):
        ordered_items.append(item.content_object)

    return render_to_response(
            "home.html",
            {'ordered_items' : ordered_items},
            RequestContext(request, {}),
        )

#home.html
Item 1: {{ ordered_items.0.headline }}
Item 2: {{ ordered_items.1.headline }}
One caveat to note is that if a list has blank spaces (e.g. Positions 1 and 3 are filled, but 2 is empty), the OrderedList will not return a QuerySet with an empty space for position 2. This model expects positions to be filled sequentially.

Screenshots

OrderedList select
OrderedList edit
OrderedList order content

Download

OrderedList.zip

Comments

#1

David
April 21, 2008
9:13 p.m.

Brilliant. I thank you wholeheartedly.

P.S. In your “URL Dispatcher” section, the angle brackets naming the regex groups aren’t being escaped, so they aren’t showing up. (Can see them in the page source though, and in your OrderedList.zip file.)

 
#2

Bret
April 22, 2008
10:27 a.m.

Glad someone’s using it!

I fixed the escaping problem with the brackets. Thanks.

 

Post a comment

Markdown accepted here.
You can also post links to YouTube, Vimeo, etc., and your links will turn into embedded media!

Your e-mail address will not be shown publicly.