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.
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 OrderedItemand
order = generic.GenericRelation(OrderedItem)
The code consists of a model, a view and a couple of templates.
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'))
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))
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'),
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 %}
You will also need to add
myproject.utilsto
INSTALLED_APPSin settings.py. You will also need to add
from utils.models import OrderedItemand
order = generic.GenericRelation(OrderedItem)to any model you would like to order (e.g. blog Entry).
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.



Comments
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.)
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!