Internal
Public Access
Recurring tasks were locked to fixed daily/weekly/biweekly/monthly/yearly intervals. The `custom` recurrence type and `recurrence_rule` field already existed in the model and API docs, but RRULE evaluation was a TODO stub that silently fell back to weekly, and no UI exposed the option. Implements real RRULE parsing via dateutil.rrule, and adds a builder UI (day-of-week checkboxes for weekly, day-of-month or Nth-weekday for monthly) so users can express patterns like "every other Wednesday" or "every second Tuesday" without hand-writing RRULE strings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
88 lines
3.5 KiB
Python
88 lines
3.5 KiB
Python
from datetime import date
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.test import TestCase
|
|
|
|
from tasks.models import Task
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class CustomRecurrenceTests(TestCase):
|
|
"""Tests for Task.calculate_next_due_date() with custom RRULE patterns."""
|
|
|
|
def setUp(self):
|
|
self.user = User.objects.create_user(
|
|
username='testuser',
|
|
email='testuser@example.com',
|
|
password='testpass123',
|
|
)
|
|
|
|
def make_task(self, **kwargs):
|
|
defaults = {
|
|
'user': self.user,
|
|
'title': 'Test task',
|
|
'recurrence': 'custom',
|
|
}
|
|
defaults.update(kwargs)
|
|
return Task.objects.create(**defaults)
|
|
|
|
def test_every_wednesday(self):
|
|
# 2026-01-07 is a Wednesday
|
|
task = self.make_task(due_date=date(2026, 1, 7), recurrence_rule='FREQ=WEEKLY;BYDAY=WE')
|
|
self.assertEqual(task.calculate_next_due_date(), date(2026, 1, 14))
|
|
|
|
def test_every_other_wednesday(self):
|
|
task = self.make_task(due_date=date(2026, 1, 7), recurrence_rule='FREQ=WEEKLY;INTERVAL=2;BYDAY=WE')
|
|
self.assertEqual(task.calculate_next_due_date(), date(2026, 1, 21))
|
|
|
|
def test_every_second_tuesday(self):
|
|
# 2026-01-13 is the second Tuesday of January 2026
|
|
task = self.make_task(due_date=date(2026, 1, 13), recurrence_rule='FREQ=MONTHLY;BYDAY=2TU')
|
|
self.assertEqual(task.calculate_next_due_date(), date(2026, 2, 10))
|
|
|
|
def test_every_15th(self):
|
|
task = self.make_task(due_date=date(2026, 1, 15), recurrence_rule='FREQ=MONTHLY;BYMONTHDAY=15')
|
|
self.assertEqual(task.calculate_next_due_date(), date(2026, 2, 15))
|
|
|
|
def test_respects_recurrence_end_date(self):
|
|
task = self.make_task(
|
|
due_date=date(2026, 1, 7),
|
|
recurrence_rule='FREQ=WEEKLY;BYDAY=WE',
|
|
recurrence_end_date=date(2026, 1, 10),
|
|
)
|
|
self.assertIsNone(task.calculate_next_due_date())
|
|
|
|
def test_malformed_rule_returns_none(self):
|
|
task = self.make_task(due_date=date(2026, 1, 7), recurrence_rule='not a valid rrule')
|
|
self.assertIsNone(task.calculate_next_due_date())
|
|
|
|
def test_create_next_recurrence(self):
|
|
task = self.make_task(due_date=date(2026, 1, 15), recurrence_rule='FREQ=MONTHLY;BYMONTHDAY=15')
|
|
new_task = task.create_next_recurrence()
|
|
self.assertIsNotNone(new_task)
|
|
self.assertEqual(new_task.due_date, date(2026, 2, 15))
|
|
self.assertEqual(new_task.recurrence_rule, 'FREQ=MONTHLY;BYMONTHDAY=15')
|
|
self.assertEqual(new_task.status, 'pending')
|
|
|
|
def test_parsed_custom_recurrence_weekly(self):
|
|
task = self.make_task(recurrence_rule='FREQ=WEEKLY;INTERVAL=2;BYDAY=WE')
|
|
parsed = task.parsed_custom_recurrence
|
|
self.assertEqual(parsed['freq'], 'weekly')
|
|
self.assertEqual(parsed['interval'], 2)
|
|
self.assertEqual(parsed['byweekday'], ['WE'])
|
|
|
|
def test_parsed_custom_recurrence_monthly_nth(self):
|
|
task = self.make_task(recurrence_rule='FREQ=MONTHLY;BYDAY=2TU')
|
|
parsed = task.parsed_custom_recurrence
|
|
self.assertEqual(parsed['freq'], 'monthly')
|
|
self.assertEqual(parsed['monthly_mode'], 'nth')
|
|
self.assertEqual(parsed['nth_ordinal'], 2)
|
|
self.assertEqual(parsed['nth_weekday'], 'TU')
|
|
|
|
def test_parsed_custom_recurrence_blank(self):
|
|
task = self.make_task(recurrence_rule='')
|
|
parsed = task.parsed_custom_recurrence
|
|
self.assertIsNone(parsed['freq'])
|
|
self.assertEqual(parsed['byweekday'], [])
|