2 Commits
Author SHA1 Message Date
Keith SmithandClaude Sonnet 5 a23600a486 Document custom recurrence RRULE patterns and fix stale field names
API.md now lists the supported custom RRULE patterns (every Wednesday,
every other Wednesday, every second Tuesday, every 15th, etc.) matching
the newly implemented dateutil.rrule evaluation. CLAUDE.md's Task model
reference also gets corrected: it listed recurrence_type/recurrence_interval
fields that never existed on the model.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 21:21:50 -06:00
Keith SmithandClaude Sonnet 5 70dcc1f001 Add custom recurrence patterns (day-of-week, nth-weekday, day-of-month)
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>
2026-08-31 18:12:15 -06:00
12 changed files with 353 additions and 62 deletions
+14 -2
View File
@@ -1050,11 +1050,23 @@ Use `page` query parameter to navigate: `?page=2`
- New instance has `status: pending` and calculated `due_date`
- Recurrence stops at `recurrence_end_date` if specified
**Custom RRULE Example:**
**Custom RRULE Patterns:**
`recurrence_rule` accepts a standard RFC5545 RRULE string, evaluated via `dateutil.rrule`. Currently supported: `FREQ=WEEKLY` (with `INTERVAL` and `BYDAY`) and `FREQ=MONTHLY` (with `INTERVAL` and either `BYMONTHDAY` or an ordinal-prefixed `BYDAY`). The web UI's Custom recurrence builder generates these same strings.
| Pattern | `recurrence_rule` |
|---|---|
| Every Wednesday | `FREQ=WEEKLY;INTERVAL=1;BYDAY=WE` |
| Every other Wednesday | `FREQ=WEEKLY;INTERVAL=2;BYDAY=WE` |
| Every Mon/Wed/Fri | `FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,WE,FR` |
| Every 15th of the month | `FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=15` |
| Every second Tuesday | `FREQ=MONTHLY;INTERVAL=1;BYDAY=2TU` |
| Last Friday of the month | `FREQ=MONTHLY;INTERVAL=1;BYDAY=-1FR` |
```json
{
"recurrence": "custom",
"recurrence_rule": "FREQ=WEEKLY;BYDAY=MO,WE,FR",
"recurrence_rule": "FREQ=MONTHLY;INTERVAL=1;BYDAY=2TU",
"recurrence_end_date": "2024-12-31"
}
```
+3 -3
View File
@@ -97,7 +97,7 @@ celery -A config beat -l info
**tasks/** - Core task management
- Hierarchical tasks with parent/subtask relationships
- Task status (pending, in_progress, completed, cancelled) and priority (low, medium, high, urgent)
- Recurrence patterns (daily, weekly, monthly, yearly, custom RRULE)
- Recurrence patterns (daily, weekly, biweekly, monthly, yearly, or custom RRULE for day-of-week/nth-weekday/day-of-month control)
- Time tracking with start/stop timer functionality
- Tag system with colors and icons for organization
- Task sharing with permission levels (viewer/editor)
@@ -234,7 +234,7 @@ Configured in `config/celery.py`:
- Hierarchy: `parent` (ForeignKey to self)
- Status: pending, in_progress, completed, cancelled
- Priority: low, medium, high, urgent
- Recurrence: `recurrence_type`, `recurrence_interval`, `recurrence_rule` (RRULE)
- Recurrence: `recurrence` (none/daily/weekly/biweekly/monthly/yearly/custom), `recurrence_rule` (RRULE string, used when `recurrence='custom'`, parsed via `dateutil.rrule`), `recurrence_end_date`
- Timing: `due_date`, `due_time`, `completed_at`, `created_at`, `updated_at`
- Soft delete: `is_deleted` (filters via `SoftDeleteManager`)
- Sync: `sync_id` (UUID for mobile sync)
@@ -362,7 +362,7 @@ make test # Run tests in container
### Common Development Patterns
**Creating Recurring Tasks:** When a recurring task is marked complete, the next recurrence is automatically created in `tasks/models.py` `create_next_recurrence()` method. Celery task `process_recurring_tasks()` runs as safety net.
**Creating Recurring Tasks:** When a recurring task is marked complete, the next recurrence is automatically created in `tasks/models.py` `create_next_recurrence()` method, which calls `calculate_next_due_date()`. For `recurrence='custom'`, the next date is computed by evaluating `recurrence_rule` (an RRULE string) via `dateutil.rrule`; the web UI's Custom recurrence builder (in `templates/tasks/_recurrence_fields.html` and `static/js/app.js`) generates these strings for day-of-week, nth-weekday, and day-of-month patterns. Celery task `process_recurring_tasks()` runs as safety net.
**Time Entry Workflow:**
1. `POST /api/tasks/<id>/start-timer/` creates TimeEntry with start_time
+1 -1
View File
@@ -9,7 +9,7 @@ A powerful Django-based task management system with time tracking, tag organizat
- **Priority Levels**: Low, Medium, High, and Urgent
- **Status Tracking**: Pending, In Progress, Completed, Cancelled
- **Due Dates & Times**: Set specific due dates and times for tasks
- **Recurrence**: Daily, Weekly, Bi-weekly, Monthly, Yearly, and Custom patterns
- **Recurrence**: Daily, Weekly, Bi-weekly, Monthly, Yearly, plus Custom RRULE patterns (e.g. every other Wednesday, every second Tuesday, every 15th of the month)
### Organization
- **Tag-Based System**: Organize tasks with multiple colored tags
+68
View File
@@ -162,6 +162,74 @@ function closeDetail() {
closeMobileMenus();
}
/* ============================================
Custom Recurrence Builder
============================================ */
function toggleCustomRecurrenceBuilder(selectEl) {
const form = selectEl.closest('form');
const builder = form.querySelector('.custom-recurrence-builder');
if (builder) {
builder.classList.toggle('hidden', selectEl.value !== 'custom');
}
}
function toggleCustomFreqPanel(radioEl) {
const builder = radioEl.closest('.custom-recurrence-builder');
const weeklyPanel = builder.querySelector('.custom-weekly-panel');
const monthlyPanel = builder.querySelector('.custom-monthly-panel');
weeklyPanel.classList.toggle('hidden', radioEl.value !== 'weekly');
monthlyPanel.classList.toggle('hidden', radioEl.value !== 'monthly');
}
function assembleCustomRecurrenceRule(formEl) {
const recurrenceSelect = formEl.querySelector('[name="recurrence"]');
const ruleInput = formEl.querySelector('[name="recurrence_rule"]');
if (!recurrenceSelect || !ruleInput) {
return true;
}
if (recurrenceSelect.value !== 'custom') {
ruleInput.value = '';
return true;
}
const builder = formEl.querySelector('.custom-recurrence-builder');
if (!builder) {
return true;
}
const freqMode = builder.querySelector('.custom-freq-mode:checked');
const freq = freqMode ? freqMode.value : 'weekly';
if (freq === 'weekly') {
const panel = builder.querySelector('.custom-weekly-panel');
const interval = parseInt(panel.querySelector('.custom-interval').value, 10) || 1;
const days = Array.from(panel.querySelectorAll('.custom-weekday:checked')).map(cb => cb.value);
if (days.length === 0) {
ruleInput.value = '';
return true;
}
ruleInput.value = `FREQ=WEEKLY;INTERVAL=${interval};BYDAY=${days.join(',')}`;
} else {
const panel = builder.querySelector('.custom-monthly-panel');
const interval = parseInt(panel.querySelector('.custom-interval').value, 10) || 1;
const monthlyMode = panel.querySelector('.custom-monthly-mode:checked');
const mode = monthlyMode ? monthlyMode.value : 'day';
if (mode === 'day') {
const day = parseInt(panel.querySelector('.custom-bymonthday').value, 10) || 1;
ruleInput.value = `FREQ=MONTHLY;INTERVAL=${interval};BYMONTHDAY=${day}`;
} else {
const ordinal = panel.querySelector('.custom-nth-ordinal').value;
const weekday = panel.querySelector('.custom-nth-weekday').value;
ruleInput.value = `FREQ=MONTHLY;INTERVAL=${interval};BYDAY=${ordinal}${weekday}`;
}
}
return true;
}
/* ============================================
Mobile Navigation
============================================ */
+70 -3
View File
@@ -213,9 +213,17 @@ class Task(models.Model):
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)
from dateutil.rrule import rrulestr
from datetime import datetime, time as dt_time
dtstart = datetime.combine(base_date, dt_time.min)
try:
next_occurrence = rrulestr(self.recurrence_rule, dtstart=dtstart).after(dtstart, inc=False)
except Exception:
return None
if not next_occurrence:
return None
next_date = next_occurrence.date()
else:
return None
@@ -225,6 +233,65 @@ class Task(models.Model):
return next_date
@property
def parsed_custom_recurrence(self):
"""
Decompose recurrence_rule (an RRULE string) into simple fields for
prepopulating the custom recurrence builder UI. Never raises; returns
safe defaults for a blank or malformed rule.
"""
result = {
'freq': None,
'interval': 1,
'byweekday': [],
'monthly_mode': None,
'bymonthday': None,
'nth_ordinal': None,
'nth_weekday': None,
}
if not self.recurrence_rule:
return result
import re
params = {}
for part in self.recurrence_rule.split(';'):
if '=' in part:
key, value = part.split('=', 1)
params[key.strip().upper()] = value.strip()
freq = params.get('FREQ', '').upper()
if freq not in ('WEEKLY', 'MONTHLY'):
return result
result['freq'] = freq.lower()
try:
result['interval'] = int(params.get('INTERVAL', '1'))
except ValueError:
result['interval'] = 1
byday = params.get('BYDAY', '')
if freq == 'WEEKLY':
if byday:
result['byweekday'] = [d.strip() for d in byday.split(',') if d.strip()]
elif freq == 'MONTHLY':
bymonthday = params.get('BYMONTHDAY')
if bymonthday:
try:
result['bymonthday'] = int(bymonthday)
result['monthly_mode'] = 'day'
except ValueError:
pass
elif byday:
match = re.match(r'^(-?\d+)([A-Z]{2})$', byday)
if match:
result['nth_ordinal'] = int(match.group(1))
result['nth_weekday'] = match.group(2)
result['monthly_mode'] = 'nth'
return result
def create_next_recurrence(self):
"""
Create the next instance of this recurring task.
+85 -1
View File
@@ -1,3 +1,87 @@
from datetime import date
from django.contrib.auth import get_user_model
from django.test import TestCase
# Create your tests here.
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'], [])
+3
View File
@@ -479,6 +479,8 @@ class TaskDetailView(View):
else:
task.recurrence = 'none'
task.recurrence_rule = request.POST.get('recurrence_rule', '') if task.recurrence == 'custom' else ''
# Create next recurrence if task is being marked as completed
if old_status != 'completed' and task.status == 'completed' and task.recurrence != 'none':
task.create_next_recurrence()
@@ -541,6 +543,7 @@ class TaskCreateView(View):
due_date=request.POST.get('due_date') or None,
due_time=request.POST.get('due_time') or None,
recurrence=recurrence,
recurrence_rule=request.POST.get('recurrence_rule', '') if recurrence == 'custom' else '',
)
tag_ids = request.POST.getlist('tags')
+1 -1
View File
@@ -140,7 +140,7 @@
{% endif %}
{% endblock %}
<script src="{% static 'js/app.js' %}?v=3"></script>
<script src="{% static 'js/app.js' %}?v=4"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
+102
View File
@@ -0,0 +1,102 @@
<!-- Recurrence -->
{% with parsed=task.parsed_custom_recurrence %}
<div class="form-group">
<label class="form-label" for="{{ id_prefix }}recurrence">Recurrence</label>
<select class="form-select" id="{{ id_prefix }}recurrence" name="recurrence" onchange="toggleCustomRecurrenceBuilder(this)">
<option value="none" {% if not task or task.recurrence == 'none' %}selected{% endif %}>None</option>
<option value="daily" {% if task.recurrence == 'daily' %}selected{% endif %}>Daily</option>
<option value="weekly" {% if task.recurrence == 'weekly' %}selected{% endif %}>Weekly</option>
<option value="biweekly" {% if task.recurrence == 'biweekly' %}selected{% endif %}>Bi-weekly</option>
<option value="monthly" {% if task.recurrence == 'monthly' %}selected{% endif %}>Monthly</option>
<option value="yearly" {% if task.recurrence == 'yearly' %}selected{% endif %}>Yearly</option>
<option value="custom" {% if task.recurrence == 'custom' %}selected{% endif %}>Custom</option>
</select>
</div>
<input type="hidden" name="recurrence_rule" value="{{ task.recurrence_rule|default:'' }}">
<div class="custom-recurrence-builder form-group {% if not task or task.recurrence != 'custom' %}hidden{% endif %}" style="background: var(--bg); border-radius: var(--radius-sm); padding: var(--space-md);">
<div class="form-group">
<label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer; margin-bottom: var(--space-xs);">
<input type="radio" class="custom-freq-mode" value="weekly" onchange="toggleCustomFreqPanel(this)" {% if parsed.freq != 'monthly' %}checked{% endif %}>
Weekly
</label>
<label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer;">
<input type="radio" class="custom-freq-mode" value="monthly" onchange="toggleCustomFreqPanel(this)" {% if parsed.freq == 'monthly' %}checked{% endif %}>
Monthly
</label>
</div>
<!-- Weekly panel -->
<div class="custom-weekly-panel {% if parsed.freq == 'monthly' %}hidden{% endif %}">
<div class="form-group">
<label class="form-label">Every
<input type="number" class="custom-interval" min="1" value="{{ parsed.interval|default:1 }}" style="width: 60px;">
week(s) on:
</label>
<div style="display: flex; flex-wrap: wrap; gap: var(--space-sm);">
<label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer;">
<input type="checkbox" class="custom-weekday" value="SU" {% if "SU" in parsed.byweekday %}checked{% endif %}> Su
</label>
<label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer;">
<input type="checkbox" class="custom-weekday" value="MO" {% if "MO" in parsed.byweekday %}checked{% endif %}> Mo
</label>
<label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer;">
<input type="checkbox" class="custom-weekday" value="TU" {% if "TU" in parsed.byweekday %}checked{% endif %}> Tu
</label>
<label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer;">
<input type="checkbox" class="custom-weekday" value="WE" {% if "WE" in parsed.byweekday %}checked{% endif %}> We
</label>
<label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer;">
<input type="checkbox" class="custom-weekday" value="TH" {% if "TH" in parsed.byweekday %}checked{% endif %}> Th
</label>
<label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer;">
<input type="checkbox" class="custom-weekday" value="FR" {% if "FR" in parsed.byweekday %}checked{% endif %}> Fr
</label>
<label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer;">
<input type="checkbox" class="custom-weekday" value="SA" {% if "SA" in parsed.byweekday %}checked{% endif %}> Sa
</label>
</div>
</div>
</div>
<!-- Monthly panel -->
<div class="custom-monthly-panel {% if parsed.freq != 'monthly' %}hidden{% endif %}">
<div class="form-group">
<label class="form-label">Every
<input type="number" class="custom-interval" min="1" value="{{ parsed.interval|default:1 }}" style="width: 60px;">
month(s):
</label>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer; margin-bottom: var(--space-xs);">
<input type="radio" class="custom-monthly-mode" value="day" {% if parsed.monthly_mode != 'nth' %}checked{% endif %}>
On day
<input type="number" class="custom-bymonthday" min="1" max="31" value="{{ parsed.bymonthday|default:1 }}" style="width: 60px;">
of the month
</label>
<label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer;">
<input type="radio" class="custom-monthly-mode" value="nth" {% if parsed.monthly_mode == 'nth' %}checked{% endif %}>
On the
<select class="custom-nth-ordinal form-select" style="width: auto;">
<option value="1" {% if parsed.nth_ordinal == 1 %}selected{% endif %}>First</option>
<option value="2" {% if parsed.nth_ordinal == 2 %}selected{% endif %}>Second</option>
<option value="3" {% if parsed.nth_ordinal == 3 %}selected{% endif %}>Third</option>
<option value="4" {% if parsed.nth_ordinal == 4 %}selected{% endif %}>Fourth</option>
<option value="-1" {% if parsed.nth_ordinal == -1 %}selected{% endif %}>Last</option>
</select>
<select class="custom-nth-weekday form-select" style="width: auto;">
<option value="SU" {% if parsed.nth_weekday == "SU" %}selected{% endif %}>Sunday</option>
<option value="MO" {% if parsed.nth_weekday == "MO" %}selected{% endif %}>Monday</option>
<option value="TU" {% if parsed.nth_weekday == "TU" %}selected{% endif %}>Tuesday</option>
<option value="WE" {% if parsed.nth_weekday == "WE" %}selected{% endif %}>Wednesday</option>
<option value="TH" {% if parsed.nth_weekday == "TH" %}selected{% endif %}>Thursday</option>
<option value="FR" {% if parsed.nth_weekday == "FR" %}selected{% endif %}>Friday</option>
<option value="SA" {% if parsed.nth_weekday == "SA" %}selected{% endif %}>Saturday</option>
</select>
</label>
</div>
</div>
</div>
{% endwith %}
+2 -13
View File
@@ -8,7 +8,7 @@
</button>
</div>
<form method="post" action="{% url 'task-detail' task.id %}" id="task-detail-form">
<form method="post" action="{% url 'task-detail' task.id %}" id="task-detail-form" onsubmit="assembleCustomRecurrenceRule(this)">
{% csrf_token %}
<input type="hidden" name="next" value="{% url 'dashboard' %}?selected={{ task.id }}{% if request.GET.filter %}&filter={{ request.GET.filter }}{% endif %}{% if request.GET.tag %}&tag={{ request.GET.tag }}{% endif %}">
@@ -75,18 +75,7 @@
</div>
{% endif %}
<!-- Recurrence -->
<div class="form-group">
<label class="form-label" for="detail-recurrence">Recurrence</label>
<select class="form-select" id="detail-recurrence" name="recurrence">
<option value="none" {% if task.recurrence == 'none' %}selected{% endif %}>None</option>
<option value="daily" {% if task.recurrence == 'daily' %}selected{% endif %}>Daily</option>
<option value="weekly" {% if task.recurrence == 'weekly' %}selected{% endif %}>Weekly</option>
<option value="biweekly" {% if task.recurrence == 'biweekly' %}selected{% endif %}>Bi-weekly</option>
<option value="monthly" {% if task.recurrence == 'monthly' %}selected{% endif %}>Monthly</option>
<option value="yearly" {% if task.recurrence == 'yearly' %}selected{% endif %}>Yearly</option>
</select>
</div>
{% include 'tasks/_recurrence_fields.html' with id_prefix='detail-' task=task %}
<!-- Save Button (moved inside form, before subtasks) -->
<div class="detail-actions" style="margin-bottom: var(--space-lg);">
+2 -12
View File
@@ -10,7 +10,7 @@
</div>
<div style="background: var(--surface); border-radius: var(--radius-md); padding: var(--space-xl); max-width: 600px;">
<form method="post">
<form method="post" onsubmit="assembleCustomRecurrenceRule(this)">
{% csrf_token %}
<div class="form-group">
@@ -55,17 +55,7 @@
</div>
</div>
<div class="form-group">
<label class="form-label" for="recurrence">Recurrence</label>
<select class="form-select" id="recurrence" name="recurrence">
<option value="none" selected>None</option>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="biweekly">Bi-weekly</option>
<option value="monthly">Monthly</option>
<option value="yearly">Yearly</option>
</select>
</div>
{% include 'tasks/_recurrence_fields.html' with id_prefix='' %}
{% if tags %}
<div class="form-group">
+2 -26
View File
@@ -19,7 +19,7 @@
<div style="display: grid; grid-template-columns: 1fr 350px; gap: var(--space-lg);">
<!-- Main Form -->
<div style="background: var(--surface); border-radius: var(--radius-md); padding: var(--space-xl);">
<form method="post">
<form method="post" onsubmit="assembleCustomRecurrenceRule(this)">
{% csrf_token %}
<div class="form-group">
@@ -81,31 +81,7 @@
</div>
{% endif %}
<div class="form-group">
<label class="form-label" for="recurrence">Recurrence</label>
<select class="form-select" id="recurrence" name="recurrence">
<option value="none" {% if task.recurrence == 'none' %}selected{% endif %}>None</option>
<option value="daily" {% if task.recurrence == 'daily' %}selected{% endif %}>Daily</option>
<option value="weekly" {% if task.recurrence == 'weekly' %}selected{% endif %}>Weekly</option>
<option value="biweekly" {% if task.recurrence == 'biweekly' %}selected{% endif %}>Bi-weekly</option>
<option value="monthly" {% if task.recurrence == 'monthly' %}selected{% endif %}>Monthly</option>
<option value="yearly" {% if task.recurrence == 'yearly' %}selected{% endif %}>Yearly</option>
</select>
</div>
{% if tags %}
<div class="form-group">
<label class="form-label">Tags</label>
<div style="display: flex; flex-wrap: wrap; gap: var(--space-sm);">
{% for tag in tags %}
<label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer;">
<input type="checkbox" name="tags" value="{{ tag.id }}" {% if tag in task.tags.all %}checked{% endif %}>
<span style="color: {{ tag.color }}">{{ tag.name }}</span>
</label>
{% endfor %}
</div>
</div>
{% endif %}
{% include 'tasks/_recurrence_fields.html' with id_prefix='' task=task %}
<button type="submit" class="btn btn-primary">Save Changes</button>
</form>