1

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?

A. L
  • 131
  • 2
  • 12

1 Answers1

1

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.

raphael
  • 2,469
  • 2
  • 7
  • 19
  • Hi raphael, thank you a lot for your input. It's really helpful. I will try it out tomorrow. For me it seems feasible to override the save method like you mentioned it and also to override the delete method. But after deleting instead of looping through the queryset also using a similar F expression to decrement the priority field of all following entries. – A. L Nov 16 '22 at 21:07
  • I hope it works for you, however, I did make edits about overriding the delete method, which I had a feeling was not going to be as easy. Good luck @A.Lang – raphael Nov 16 '22 at 21:34
  • Ah now I see. Thank you for pointing it out again. Didn't read the link carefully before. Sorry for that – A. L Nov 16 '22 at 21:50
  • thanks again. it worked like a charm. I added a hook like this: ```@django.dispatch.receiver(django.db.models.signals.post_delete, sender=TaskModel) def delete_TaskModel_hook(sender, instance, using, **kwargs): queryset = TaskModel.objects.filter(priority__gte=instance.priority) queryset.update(priority=django.db.models.F('priority') -1)``` – A. L Nov 17 '22 at 08:07
  • Excellent! Glad you got it to work. I'll have to look into using hooks myself. – raphael Nov 17 '22 at 12:08