Implement recurring tasks functionality

Adds automatic creation of next task instance when recurring tasks are completed.

- Add calculate_next_due_date() and create_next_recurrence() methods to Task model
- Update task completion handlers in views and sync API to create next recurrence
- Add hourly Celery task to process any missed recurring tasks
- Support daily, weekly, biweekly, monthly, yearly recurrence patterns
- Respect recurrence_end_date limits

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Keith Smith
2026-01-04 10:00:34 -07:00
co-authored by Claude Sonnet 4.5
parent c51fd846a2
commit 1d6b07bfed
5 changed files with 145 additions and 0 deletions
+73
View File
@@ -174,6 +174,79 @@ class Task(models.Model):
secs = seconds % 60
return f"{hours}:{minutes:02d}:{secs:02d}"
def calculate_next_due_date(self, from_date=None):
"""
Calculate the next due date based on recurrence pattern.
Returns None if task doesn't recur or has reached end date.
"""
if self.recurrence == 'none':
return None
from datetime import timedelta
from dateutil.relativedelta import relativedelta
base_date = from_date or self.due_date
if not base_date:
from django.utils import timezone
base_date = timezone.now().date()
# Calculate next date based on recurrence type
if self.recurrence == 'daily':
next_date = base_date + timedelta(days=1)
elif self.recurrence == 'weekly':
next_date = base_date + timedelta(weeks=1)
elif self.recurrence == 'biweekly':
next_date = base_date + timedelta(weeks=2)
elif self.recurrence == 'monthly':
next_date = base_date + relativedelta(months=1)
elif self.recurrence == 'yearly':
next_date = base_date + relativedelta(years=1)
elif self.recurrence == 'custom' and self.recurrence_rule:
# TODO: Implement RRULE parsing for custom recurrence
# For now, default to weekly
next_date = base_date + timedelta(weeks=1)
else:
return None
# Check if we've passed the end date
if self.recurrence_end_date and next_date > self.recurrence_end_date:
return None
return next_date
def create_next_recurrence(self):
"""
Create the next instance of this recurring task.
Returns the new task or None if no recurrence should be created.
"""
if self.recurrence == 'none':
return None
next_due_date = self.calculate_next_due_date()
if not next_due_date:
return None
# Create new task with same properties
new_task = Task.objects.create(
user=self.user,
parent=self.parent,
title=self.title,
description=self.description,
status='pending',
priority=self.priority,
due_date=next_due_date,
due_time=self.due_time,
recurrence=self.recurrence,
recurrence_rule=self.recurrence_rule,
recurrence_end_date=self.recurrence_end_date,
sort_order=self.sort_order,
)
# Copy tags
new_task.tags.set(self.tags.all())
return new_task
class TimeEntry(models.Model):
"""