Project | Jul 1st, 2021
This is a simplified example of the backend portion of a drag-and-drop task list written in Python using Django and Graphene Django for GraphQL mutations.
Be sure to also check out the frontend example using react-beautiful-dnd and React Apollo.
I'm a big fan of working with React for the UI and Django for the backend logic and other pythonic activities. For the UI, there are multiple React drag-and-drop libraries from which to choose. These libraries are great for coding a slick UI, but you still may need a way to update your database. I won't go in to all the reasons I love Django in this post. But, if you don't know Django, I encourage you to check it out.
Python is simple to read and to understand, making it perfect for little classes like the task list re-arranger you'll find below in functions.py.
Three .py files
I've narrowed down what you need to the basics:
A models.py file to create the database table Task. In my model, each task has a position. If the task is marked "completed", its position is saved as 0 "on save" no matter what position is specified in the update or mutation. All completed tasks will have a position of 0.
A schema.py file to handle the incoming graphql mutation from the UI. After saving the update, it calls SortTask to arrange tasks in an organized sequence.
A functions.py file with a SortTask class. Conditional arguments guide the code along depending on the instance's "completed" status and what tasks are above or below it in the sequence. It also identifies and sorts out duplicate positions and removes any missing positions. The first "incomplete" task will always have position 1. Tasks will always be ordered in an uninterrupted sequence beginning with 1 up through the final active task.
Note that I don't call SortTask on save by using the post_save Signal. Calling SortTask on each save won't work because it saves in a couple spots in the code. If it saves mid-function and then calls the function on save, you'll (not likely) enjoy waiting around for eternity while the function keeps calling itself over and over when all you wanted to do was make dinner AFTER walking the dog rather than the other way around. If you're primarily using graphql in your application, calling SortTask in the mutation after save is a simple and workable solution to this issue. If you use the Django Admin, you'll also need to call it elsewhere in your code. Please let me know if you have a clever solution for slaying all saves at once.
If you want, you can return the full updated task list in the mutation. I do not do this because I prefer to update the list in the browser with JS rather than rely on the graphql query to do any of the UI updating.
Show me the code
python# models.py# ...your models...class Task(models.Model):title = models.CharField(max_length=140)completed = models.BooleanField(default=False)position = models.PositiveIntegerField(default=0)# Add more fields as neededdef __str__(self):return "%s: Position %s" % (self.title, self.position)def save(self, **kwargs):# If Task is being marked complete, set the completed_date# Completed tasks get position of zero and can be sorted by completed date and priorityif self.completed:self.completed_date = datetime.datetime.now()self.position = 0super(Task, self).save()
python# functions.pyfrom .models import Task# To be executed after a task is saved.class SortTask:def __init__(self, task_id):# Completed tasks get a position of 0 on save.# The second query retrieves all tasks that are not completed.self.task = Task.objects.get(id=task_id)self.active_tasks = Task.objects.filter(position__gt=0).order_by('position')# Find gaps in the order of positionsdef find_missing(self, positions):return [x for x in range(positions, positions[-1]+1)if x not in positions]# Find duplicate position numbersdef find_duplicates(self, positions):current_task = self.tasksetOfPositions = set()setOfPositions.add(current_task.position)for position in positions:if position in setOfPositions:return Trueelse:setOfPositions.add(position)return False# Find and return all tasks with a higher positiondef get_higher_tasks(self):current_task = self.taskactive_tasks = self.active_taskshigher_tasks = if current_task.position > 1:if active_tasks:higher_tasks = active_tasks.filter(position__lt=current_task.position)return(higher_tasks)# Find and return all tasks with the same or lower positiondef get_lower_tasks(self):current_task = self.taskactive_tasks = self.active_taskslower_tasks = if active_tasks:lower_tasks = active_tasks.filter(position__gte=current_task.position).exclude(id=current_task.id)return(lower_tasks)# If task is completed, reposition remaining tasksdef reposition_remaining_tasks(self):active_tasks = self.active_taskspositions = [task.position for task in active_tasks]missing = duplicates = if positions:missing = self.find_missing(positions=positions)duplicates = self.find_duplicates(positions=positions)updated_active_tasks = if positions != 1 or missing or duplicates:for index, item in enumerate(active_tasks):active_task_index = index + 1item.position = active_task_indexupdated_active_tasks.append(item)item.save()if updated_active_tasks:return updated_active_taskselse:return active_tasks# Call function to get higher tasks and reposition them as neededdef reposition_higher_tasks(self):higher_tasks = self.get_higher_tasks()positions = [task.position for task in higher_tasks]if positions:missing = self.find_missing(positions=positions)duplicates = self.find_duplicates(positions=positions)if positions != 1 or missing or duplicates:for index, item in enumerate(higher_tasks):higher_task_index = index + 1item.position = higher_task_indexitem.save()higher_tasks = self.get_higher_tasks()return higher_tasks# Call function to get lower tasks and reposition them as neededdef reposition_lower_tasks(self):lower_tasks = self.get_lower_tasks()current_task = self.taskexpected_position = current_task.position + 1positions = [task.position for task in lower_tasks]if positions:missing = self.find_missing(positions=positions)duplicates = self.find_duplicates(positions=positions)if not positions or positions != expected_position or missing or duplicates:for index, item in enumerate(lower_tasks):list_order_position = index + 1item.position = current_task.position + list_order_positionitem.save()lower_tasks = self.get_lower_tasks()return lower_tasks# Main function to insert task and arrange surrounding tasks as neededdef insert_task(self):current_task = self.taskif current_task.completed:self.reposition_remaining_tasks()return current_taskhigher_tasks = self.get_higher_tasks()if higher_tasks:higher_tasks = self.reposition_higher_tasks()updated_positions = [task.position for task in higher_tasks]lowest_higher_task_position = updated_positions[-1]if current_task.position != lowest_higher_task_position + 1:current_task.position = lowest_higher_task_position + 1current_task.save()lower_tasks = self.get_lower_tasks()if lower_tasks:lower_tasks = self.reposition_lower_tasks()return current_task
I hope you're able to use this code to knock a project off of your own todo list. If you haven't already, check out the ReactJS frontend code as well.
Discuss both frontend and backend posts together here at reddit.
Topics tagged in this post