Building a Powerful Task Management Workflow in the Django Admin
The Django admin is renowned for its rapid development capabilities, providing a ready-to-use interface for managing your application's data. However, with a few customizations, you can transform it from a simple data entry tool into a powerful and dynamic task management system. This post will walk you through setting up the models and customizing the admin interface to create an efficient workflow for handling sprints, epics, and tasks.
The Foundation: Your Django Models
First, let's define the core components of our task management system. We'll have Theme
, GoalType
, Epic
, Sprint
, Task
, and ScheduledTask
models. These models establish the relationships between high-level goals and day-to-day tasks.
Here is the Python code for the models:
```python
import datetime
from django.db import models
def generate_sprint_choices():
"""
Generates a list of tuples for sprint choices.
Format: ('YY-WNN', 'YY-WNN')
Covers years from 2024 to 2030 to be safe.
"""
choices = []
# Expanded the year range to avoid validation errors for future dates
for year in range(2024, 2031):
last_week = datetime.date(year, 12, 28).isocalendar()[1]
for week in range(1, last_week + 1):
sprint_string = f"{str(year)[-2:]}-W{week:02}"
choices.append((sprint_string, sprint_string))
return choices
def get_next_week_sprint_id():
"""
Calculates the sprint ID for the next ISO week.
Returns: 'YY-WNN' string.
"""
today = datetime.date.today()
# Simplified logic for finding the start of the next week
next_week_start = today + datetime.timedelta(days=(7 - today.weekday()))
next_week_iso = next_week_start.isocalendar()
next_year_short = str(next_week_iso[0])[-2:]
next_week_num = next_week_iso[1]
return f"{next_year_short}-W{next_week_num:02}"
def get_next_nine_sprint_choices():
"""
Generates choices for the current week and the next 9 weeks.
Format: ('YY-WNN', 'YY-WNN')
"""
choices = []
current_date = datetime.date.today()
for i in range(10): # Current week + next 9 weeks
iso_year, iso_week, _ = current_date.isocalendar()
sprint_string = f"{str(iso_year)[-2:]}-W{iso_week:02}"
choices.append((sprint_string, sprint_string))
current_date += datetime.timedelta(weeks=1)
return choices
class Sprint(models.Model):
"""
A model to represent a development sprint.
"""
sprint_id = models.CharField(
max_length=7,
choices=generate_sprint_choices(),
default=get_next_week_sprint_id,
verbose_name="Sprint Week",
)
description = models.TextField(blank=True, null=True)
start_date = models.DateField(blank=True, null=True)
end_date = models.DateField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["sprint_id"]
def __str__(self):
return self.sprint_id
class Epic(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
theme = models.ForeignKey(
"Theme", on_delete=models.CASCADE, related_name="epics", blank=True, null=True
)
start_date = models.DateField(blank=True, null=True)
end_date = models.DateField(blank=True, null=True, default=datetime.date(2026, 12, 31))
minutes_expected = models.IntegerField(default=0)
def __str__(self):
return self.name
class Task(models.Model):
class Status(models.TextChoices):
SCRATCHPAD = "SCRATCHPAD", "Scratchpad"
PLANNING = "PLANNING", "Planning"
BACKLOG = "BACKLOG", "Backlog"
IN_REVIEW = "IN_REVIEW", "In Review"
TODO = "TODO", "To Do"
IN_PROGRESS = "IN_PROGRESS", "In Progress"
DONE = "DONE", "Done"
title = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
# The 'choices' attribute is removed here to avoid the static list issue.
# The available choices should be handled in the form.
sprint = models.ForeignKey(
Sprint,
on_delete=models.CASCADE,
related_name="tasks",
blank=True,
null=True,
)
sprint_char = models.CharField(
max_length=7,
choices=generate_sprint_choices(),
default=get_next_week_sprint_id,
verbose_name="Sprint Week",
)
status = models.CharField(
max_length=20, choices=Status.choices, default=Status.TODO
)
frequency = models.CharField(
max_length=20,
choices=[
("DAILY", "Daily"),
("WEEKLY", "Weekly"),
("MONTHLY", "Monthly"),
("YEARLY", "Yearly"),
("ONCE", "Once"),
],
default="ONCE",
)
epic = models.ForeignKey(
Epic, on_delete=models.CASCADE, related_name="tasks", blank=True, null=True
)
minutes_expected = models.IntegerField(default=0)
minutes_spent = models.IntegerField(default=0)
due_date = models.DateField(blank=True, null=True)
start_date = models.DateField(blank=True, null=True)
priority = models.IntegerField(default=0)
links = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "sprint":
# Here you can dynamically filter the choices for the sprint field
# For example, showing only recent and upcoming sprints
kwargs["queryset"] = Sprint.objects.filter(
# your filter logic here, e.g., by date
)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
class ScheduledTask(models.Model):
task = models.ForeignKey(
Task, on_delete=models.CASCADE, related_name="scheduled_tasks"
)
scheduled_time = models.DateTimeField()
is_completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
description = models.TextField(blank=True, null=True)
def __str__(self):
return (
f"{self.task.title} - {self.scheduled_time.strftime('%Y-%m-%d %H:%M:%S')}"
)
class Theme(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
class GoalType(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Type(models.TextChoices):
PERSONAL = "PERSONAL", "Personal"
PROFESSIONAL = "PROFESSIONAL", "Professional"
HEALTH = "HEALTH", "Health"
FINANCIAL = "FINANCIAL", "Financial"
RELATIONSHIP = "RELATIONSHIP", "Relationship"
type = models.CharField(max_length=20, choices=Type.choices, default=Type.PERSONAL)
class GoalDurationType(models.TextChoices):
SHORT_TERM = "SHORT_TERM", "Short Term"
MEDIUM_TERM = "MEDIUM_TERM", "Medium Term"
LONG_TERM = "LONG_TERM", "Long Term"
VERY_SHORT_TERM = "VERY_SHORT_TERM", "Very Short Term"
goal_duration_type = models.CharField(
help_text="* Long-term Goals: ...",
max_length=20,
choices=GoalDurationType.choices,
default=GoalDurationType.SHORT_TERM,
)
```
Customizing the Admin Interface
With the models in place, the next step is to customize the Django admin to create a user-friendly workflow. We'll focus on the Task
model's admin view as it's the central point of interaction.
In your app's admin.py
file, you can define a ModelAdmin
class for your Task
model:
```python
from django.contrib import admin
from .models import Task, Sprint, Epic, Theme, GoalType, ScheduledTask
@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
list_display = ('title', 'status', 'sprint_char', 'epic', 'priority', 'due_date')
list_filter = ('status', 'sprint_char', 'epic', 'priority')
search_fields = ('title', 'description', 'epic__name')
ordering = ('priority', 'due_date')
```
Key ModelAdmin
Options for Workflow Enhancement:
-
list_display
: This tuple determines which fields are shown on the main list view for tasks. Displaying key information like the status, sprint, epic, priority, and due date provides an at-a-glance overview of all tasks. -
list_filter
: This creates a sidebar that allows for easy filtering of tasks. Users can quickly narrow down the task list by their status (e.g., "To Do", "In Progress"), the assigned sprint, the associated epic, or their priority. -
search_fields
: This adds a search bar that enables full-text search across the specified fields. In this case, users can search for tasks by their title, description, or the name of the epic they belong to. -
ordering
: This sets the default sorting of the task list. Ordering by priority and then by due date ensures that the most critical and urgent tasks appear at the top.
By registering your other models (Sprint
, Epic
, Theme
, GoalType
, and ScheduledTask
) with the admin site, you provide a complete interface for managing all aspects of your project.
Creating an Effective Workflow
The combination of these models and admin customizations facilitates a clear and efficient task management workflow:
-
High-Level Planning: Start by defining
Themes
andGoalTypes
to set the broad objectives of your projects. -
Breaking Down Goals: Create
Epics
that break down these larger themes into more manageable, high-level features or initiatives. -
Sprint Planning: Define your
Sprints
with specific start and end dates. -
Task Creation: Create individual
Tasks
and associate them with anEpic
and aSprint
. Assign astatus
,priority
, anddue_date
. -
Execution and Tracking: As work progresses, update the
status
of eachTask
. The filters in the admin view make it easy to see what's "In Progress," what's "Done," and what's next in the "To Do" list. -
Scheduling: For tasks that need to occur at a specific time, you can create
ScheduledTask
entries.
This setup allows your team to move seamlessly from high-level strategic planning to detailed daily execution, all within the convenience of the Django admin interface.