Graphene Django backend to go with a JavaScript UI Drag and Drop Task List

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 needed
def __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 priority
if self.completed:
self.completed_date = datetime.datetime.now()
self.position = 0
super(Task, self).save()
python
# functions.py
from .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 positions
def find_missing(self, positions):
return [x for x in range(positions[0], positions[-1]+1)
if x not in positions]
# Find duplicate position numbers
def find_duplicates(self, positions):
current_task = self.task
setOfPositions = set()
setOfPositions.add(current_task.position)
for position in positions:
if position in setOfPositions:
return True
else:
setOfPositions.add(position)
return False
# Find and return all tasks with a higher position
def get_higher_tasks(self):
current_task = self.task
active_tasks = self.active_tasks
higher_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 position
def get_lower_tasks(self):
current_task = self.task
active_tasks = self.active_tasks
lower_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 tasks
def reposition_remaining_tasks(self):
active_tasks = self.active_tasks
positions = [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[0] != 1 or missing or duplicates:
for index, item in enumerate(active_tasks):
active_task_index = index + 1
item.position = active_task_index
updated_active_tasks.append(item)
item.save()
if updated_active_tasks:
return updated_active_tasks
else:
return active_tasks
# Call function to get higher tasks and reposition them as needed
def 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[0] != 1 or missing or duplicates:
for index, item in enumerate(higher_tasks):
higher_task_index = index + 1
item.position = higher_task_index
item.save()
higher_tasks = self.get_higher_tasks()
return higher_tasks
# Call function to get lower tasks and reposition them as needed
def reposition_lower_tasks(self):
lower_tasks = self.get_lower_tasks()
current_task = self.task
expected_position = current_task.position + 1
positions = [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[0] != expected_position or missing or duplicates:
for index, item in enumerate(lower_tasks):
list_order_position = index + 1
item.position = current_task.position + list_order_position
item.save()
lower_tasks = self.get_lower_tasks()
return lower_tasks
# Main function to insert task and arrange surrounding tasks as needed
def insert_task(self):
current_task = self.task
if current_task.completed:
self.reposition_remaining_tasks()
return current_task
higher_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 + 1
current_task.save()
lower_tasks = self.get_lower_tasks()
if lower_tasks:
lower_tasks = self.reposition_lower_tasks()
return current_task
python
# schema.py
import graphene
from graphene import ObjectType, String, Field, Boolean
from graphene_django import DjangoObjectType
from .models import Task
from .functions import SortTask
# If you are new to Graphene Django, you will need to go through the proper setup first.
# Graphene Django setup is not covered in this tutorial
class TaskType(DjangoObjectType):
class Meta:
model = Task
fields = '__all__'
# The react UI example in the other post uses the "tasks" query
class Query(graphene.ObjectType):
tasks = graphene.List(TaskType)
# Mutation for re-positioning and completing tasks
class PositionTask(graphene.Mutation):
task = graphene.Field(TaskType)
class Arguments:
task_id = graphene.Int(required=True)
position = graphene.Int(required=True)
completed = graphene.Boolean()
def mutate(self, info, task_id, position, completed=None):
task = Task.objects.get(id=task_id)
task.position = position
task.completed = completed
task.save()
# After save, we call the SortTask class and pass in the task_id to sort/arrange
# all the incomplete tasks according to the updated task's new position. If you
# have lots of tasks or a production app, you probably want to cue this up and
# perform it asynchronously with something like Celery instead of waiting for
# the function to complete before returning the mutation. But, doing it
# synchronously is fast enough for our demonstration purposes with a small app
# and a small number of tasks to position. Calling SortTask asynchronously won't
# interfere with the UI update because the frontend repositions the tasks
# with javascript. So, you don't need to return the list here.
sort_task = SortTask(task_id=task_id)
insert_task = sort_task.insert_task()
return PositionTask(task=task)

Conclusion

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.

Tags

Topics tagged in this post


Profile picture

Written by brian kerr
"Existentialism Is a Cyborgism"
About Guide Projects || Topics || Glossary || Reddit || Github