Internal
Public Access
Compare commits
2
Commits
2026.8.31
...
2026.8.31.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a23600a486 | ||
|
|
70dcc1f001 |
@@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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'], [])
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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);">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user