Say I wanted to have a project, and one-to-many with to-do items, and wanted to re-order the to-do items arbitrarily?
In the past, I\'ve added a numbered order field, a
I've run into this so many times that I've settled on managing these dynamically in the BL or UI, and then just persisting the ordering to a purpose-built column once the user is happy. SQL is just intentially designed not to handle orderings, and it always fights back.
I have had this problem with two projects I've worked on in the last little while. For my example solution I have a "Form" that has many "Variables" assigned to it and the order of the variables on the form needs to be sortable. So I have implemented the following:
models.py
class Form(models.Model):
FormName = models.CharField(verbose_name="Form Name:", max_length=40)
VariableOrder = models.CommaSeparatedIntegerField(default="[]", editable=False)
def __unicode__(self):
return "%s" % (self.FormName)
class Variable(models.Model):
FormID = models.ForeignKey(Form, default=0, editable=False, related_name="Variable")
VarName = models.CharField(max_length=32, verbose_name="Name of variable in the database:")
def __unicode__(self):
return "%s" % self.VarName
The key from above is the VariableOrder CommaSeparatedIntegerField is where we are going to store the order of the Variables on the Form, and we are going to be using it as a python list, which is why the default is [].
For the template I render my Variables in an that we are going to make drag and drop sortable (the list elements I actually use have a ton more CSS related styling and information about the Variable).
<ul id="sortable">
{% for Variable in VarList %}
<li id="{{ Variable.id }}">{{ Variable }}</li>
{% endfor %}
</ul>
Now we are going to make the list drag and drop for the changing of order. For this to work you need to have the AJAX CSRF snippet from Django site in the head
$(function() {
$("#sortable" ).sortable({
placeholder: "ui-state-highlight",
update: function(event, ui){
$.ajax({
type:"POST",
url:"{% url builder.views.variableorder %}",
data: {Order: JSON.stringify($('#sortable').sortable('toArray')) },
success: function(data){
// Do stuff here - I don't do anything.
}
});
}
});
$( "#sortable" ).disableSelection();
});
The important part above is that "update" calls the function every time there is a position change of any of the variables, which sends the AJAX. toArray on sortable along with the JSON stringify gets us sending the top to bottom id's of each variable, which is used by the view as follows. Note: I keep the active Form object as a session variable, but in another case you would just need to call the Form object you were wanting to change the order of.
def variableorder(request):
if request.is_ajax():
Order = request.POST['Order']
updateOrder = request.session['FormID']
updateOrder.VariableOrder = newOrder
updateOrder.save()
request.session['FormID'] = Form.objects.get(id=updateOrder.id)
return HttpResponse("Order changed.")
else:
pass
The key of all of this is that you can use this CommaSeparatedIntegerField as a list by evaluating the string. For example:
Adding a Variable:
aForm = Form.objects.get(id=1)
currentOrder = aForm.VariableOrder
currentOrder = eval(currentOrder)
newVar = Variable(stuff in here)
newVar.save()
currentOrder.append(newVar.id)
aForm.VariableOrder = currentOrder
aForm.save()
Removing a Variable:
aForm = Form.objects.get(id=1)
currentOrder = aForm.VariableOrder
currentOrder = eval(currentOrder)
# Variable ID that we want to delete = 3
currentOrder.remove(3)
aForm.VariableOrder = currentOrder
aForm.save()
Rendering the Variables in Order:
aForm = Form.objects.get(id=1)
currentOrder = aForm.VariableOrder
currentOrder = eval(currentOrder)
VarList = []
for i in currentOrder:
VarList.append(Variable.objects.get(id=i))
This is a rough first draft of what I am going to use, but it is working well for me. The obvious first improvement being the evaluation to python list being a method in the class. eg.
def getVarOrder(self):
return eval(self.VariableOrder)
and then just call Form.getVarOrder() when want to manipulate the list. In any case hopefully this helps out.
JD
"added a numbered order field" - good.
"update all the items with their new order numbers" - avoidable.
Use numbers with gaps.
Floating point. That way, someone can insert "1.1" between 1 and 2. I find that this works nicely, as most people can understand how the sequencing works. And you don't have to worry too much about how much space to leave -- there's lots and lots of space between each number.
On the initial load, number the articles by the 100 or 1000 or something with space between each one. In this case, you have to guess how many digits to leave for reordering.
A comma-separated position. Initially, they're all (1,0), (2,0), (3,0), etc. But when you want to rearrange things, you might have to introduce (2,1) and (2,2) that go after (2,0) but before (3.0).
This looks kind of complicated, but some people like this kind of complexity. It's essentially the same as floating-point, except the single number is replace by a (whole-number, implicit-fraction) tuple. And this extends to handle hierarchies.
I hate this problem ... and I run into it all the time.
For my most recent Django site we had a Newsletter which contained N Articles and, of course, order was important. I assigned the default order as ascending Article.id, but this failed if Articles were entered in something other than "correct" order.
On the Newsletter change_form.html page I added a little bit of jQuery magic using the Interface plugin (http://interface.eyecon.ro/). I show the titles of the associated Articles and the user can drag them around as they like. There is an onChange handler that recomputes the Article.id's in article_order field.
Enjoy,
Peter
For app=content, model=Newsletter, the following is in templates/admin/content/newslettter/change_form.html
{% extends 'admin/change_form.html' %}
{% block form_top %}{% endblock %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="/media/js/jquery.js"></script>
<script type="text/javascript" src="/media/js/interface.js"></script>
<script>
$(document).ready(
function () {
$('ol.articles').Sortable(
{
accept : 'sortableitem',
helperclass : 'sorthelper',
activeclass : 'sortableactive',
hoverclass : 'sortablehover',
opacity: 0.8,
fx: 200,
axis: 'vertically',
opacity: 0.4,
revert: true,
trim: 'art_',
onchange:
function(list){
var arts = list[0].o[list[0].id];
var vals = new Array();
var a;
for (a in arts) {
vals[a] = arts[a].replace(/article./, '');
}
$('#id_article_order').attr('value', vals.join(','));
}
});
}
);
</script>
{% endblock %}
{% block after_related_objects %}
{% if original.articles %}
<style>
.sortableitem {
cursor:move;
width: 300px;
list-style-type: none;
}
</style>
<h4>Associated Articles</h4>
<ol class="articles" id="article_list">
{% for art in original.articles %}
<li id="article.{{art.id}}" class="sortableitem">{{art.title}}</li>
{% endfor %}
</ol>
{% endif %}
{% endblock %}
This is a late answer to the question, but I just wanted to chime in and point out that B-Trees are a great data structure for this sort of thing, especially if your access patterns don't require you to retrieve the entire list at once.
http://en.wikipedia.org/wiki/B-tree