Fix task edit crashing after due_date/due_time save (regression from reminders)

The web edit/create/quick-add views and the sync endpoint assigned
due_date/due_time/reminder_at straight from request data as raw strings.
Django tolerates this at save() (types get coerced deep in the SQL
layer), but the in-memory instance keeps the raw strings afterward.
Task.reschedule_reminders(), added for per-task reminders and run from
save() on that same instance, was the first code to actually operate on
those fields before a page reload and crashed with a TypeError - which
the service worker's 5xx-treated-as-offline fallback then masked as a
misleading "you're offline" screen instead of a real error.

Fixed by parsing with Django's parse_date/parse_time/parse_datetime at
the point these values are read from the request/payload in
tasks/views.py and sync/views.py, rather than trusting raw strings.

5 new regression tests exercise the actual views/endpoint with raw
date/time strings and assert proper types land on the model.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Keith Smith
2026-09-05 10:00:45 -06:00
co-authored by Claude Sonnet 5
parent 5359bf243a
commit a27fcc4c69
4 changed files with 117 additions and 12 deletions
+15 -7
View File
@@ -8,7 +8,7 @@ import logging
import uuid
from datetime import datetime
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from django.utils.dateparse import parse_date, parse_datetime, parse_time
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes, throttle_classes
from rest_framework.permissions import IsAuthenticated
@@ -289,6 +289,14 @@ def process_time_entry_changes(user, entry_changes, last_sync_at):
return conflicts
def _parsed(data, key, default, parser):
"""Get data[key], parsing it if it's still a raw string (e.g. from JSON), else default if absent."""
if key not in data:
return default
value = data[key]
return parser(value) if isinstance(value, str) else value
def update_task_from_data(task, data):
"""Update a task from sync data."""
# Track old status to detect completion
@@ -298,9 +306,9 @@ def update_task_from_data(task, data):
task.description = data.get('description', task.description)
task.status = data.get('status', task.status)
task.priority = data.get('priority', task.priority)
task.due_date = data.get('due_date', task.due_date)
task.due_time = data.get('due_time', task.due_time)
task.reminder_at = data.get('reminder_at', task.reminder_at)
task.due_date = _parsed(data, 'due_date', task.due_date, parse_date)
task.due_time = _parsed(data, 'due_time', task.due_time, parse_time)
task.reminder_at = _parsed(data, 'reminder_at', task.reminder_at, parse_datetime)
task.recurrence = data.get('recurrence', task.recurrence)
task.recurrence_rule = data.get('recurrence_rule', task.recurrence_rule)
task.sort_order = data.get('sort_order', task.sort_order)
@@ -341,9 +349,9 @@ def create_task_from_data(user, data):
description=data.get('description', ''),
status=data.get('status', 'pending'),
priority=data.get('priority', 'medium'),
due_date=data.get('due_date'),
due_time=data.get('due_time'),
reminder_at=data.get('reminder_at'),
due_date=_parsed(data, 'due_date', None, parse_date),
due_time=_parsed(data, 'due_time', None, parse_time),
reminder_at=_parsed(data, 'reminder_at', None, parse_datetime),
recurrence=data.get('recurrence', 'none'),
recurrence_rule=data.get('recurrence_rule', ''),
sort_order=data.get('sort_order', 0),