How to insert an item into a Queryset which is sorted by a numeric field and increment the value of the field of all subsequent items

Question:

Let´s say, there is a Django model called TaskModel which has a field priority and we want to insert a new element and increment the existing element which has already the priority and increment also the priority of the following elements.

priority is just a numeric field without any special flags like unique or primary/foreign key

queryset = models.TaskModel.objects.filter().order_by('priority')

Can this be done in a smart way with some methods on the Queryset itself?

Asked By: A. Lang

||

Answers:

I believe you can do this by using Django’s F expressions and overriding the model’s save method. I guess you could instead override the model’s __init__ method as in this answer, but I think using the save method is best.

class TaskModel(models.Model):
    task = models.CharField(max_length=20)
    priority = models.IntegerField()
    
    # Override the save method so whenever a new TaskModel object is
    # added, this will be run.
    def save(self, *args, **kwargs):
    
        # First get all TaskModels with priority greater than, or
        # equal to the priority of the new task you are adding
        queryset = TaskModel.objects.filter(priority__gte=self.priority)

        # Use update with the F expression to increase the priorities
        # of all the tasks above the one you're adding
        queryset.update(priority=F('priority') + 1)

        # Finally, call the super method to call the model's
        # actual save() method
        super(TaskModel, self).save(*args, **kwargs)

    def __str__(self):
        return self.task

Keep in mind that this can create gaps in the priorities. For example, what if you create a task with priority 5, then delete it, then add another task with priority 5? I think the only way to handle that would be to loop through the queryset, perhaps with a function like the one below, in your view, and call it whenever a new task is created, or it’s priority modified:

# tasks would be the queryset of all tasks, i.e, TaskModels.objects.all()
def reorder_tasks(tasks):
    for i, task in enumerate(tasks):
        task.priority = i + 1
        task.save()

This method is not nearly as efficient, but it will not create the gaps. For this method, you would not change the TaskModel at all.

Or perhaps you can also override the delete method of the TaskModel as well, as shown in this answer, but I haven’t had a chance to test this yet.


EDIT

Short Version
I don’t know how to delete objects using a similar method to saving while keeping preventing priorities from having gaps. I would just use a loop as I have shown above.

Long version
I knew there was something different about deleting objects like this:

def delete(self, *args, **kwargs):
    queryset = TaskModel.objects.filter(priority__gt=self.priority)
    queryset.update(priority=F('priority') - 1)
    super(TaskModel, self).delete(*args, **kwargs)

This will work, in some situations.

According to the docs on delete():

Keep in mind that this [calling delete()] will, whenever possible, be executed purely in
SQL, and so the delete() methods of individual object instances will
not necessarily be called during the process. If you’ve provided a
custom delete() method on a model class and want to ensure that it is
called, you will need to “manually” delete instances of that model
(e.g., by iterating over a QuerySet and calling delete() on each
object individually) rather than using the bulk delete() method of a
QuerySet.

So if you delete() a TaskModel object using the admin panel, the custom delete written above will never even get called, and while it should work if deleting an instance, for example in your view, since it will try acting directly on the database, it will not show up in the python until you refresh the query:

tasks = TaskModel.objects.order_by('priority')
    
for t in tasks:
    print(t.task, t.priority)

tr = TaskModel.objects.get(task='three')
tr.delete()

# Here I need to call this AGAIN
tasks = TaskModel.objects.order_by('priority')

# BEFORE calling this
for t in tasks:
    print(t.task, t.priority)
    
# to see the effect

If you still want to do it, I again refer to this answer to see how to handle it.

Answered By: raphael
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.