Internal
Public Access
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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4d88d10382
commit
70dcc1f001
@@ -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