Fix weekly recurrence crash: null fields in serialized rule

model_dump() includes None values for optional RecurrenceRule fields.
When serialized to JSON, these become explicit nulls (e.g. "weekday": null).
The recurrence service then does int(None) which raises TypeError.

Fix: strip None values when serializing rule to JSON, and add defensive
None handling in recurrence service for all rule.get() calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-22 01:50:52 +08:00
parent f66ffba0ef
commit 1a707ff179
2 changed files with 8 additions and 7 deletions

View File

@ -192,8 +192,9 @@ async def create_event(
data["calendar_id"] = await _get_default_calendar_id(db)
# Serialize RecurrenceRule object to JSON string for DB storage
# Exclude None values so defaults in recurrence service work correctly
rule_obj = data.pop("recurrence_rule", None)
rule_json: Optional[str] = json.dumps(rule_obj) if rule_obj else None
rule_json: Optional[str] = json.dumps({k: v for k, v in rule_obj.items() if v is not None}) if rule_obj else None
if rule_json:
# Parent template: is_recurring=True, no parent_event_id
@ -282,7 +283,7 @@ async def update_event(
# Serialize RecurrenceRule → JSON string if present in update payload
rule_obj = update_data.pop("recurrence_rule", None)
if rule_obj is not None:
update_data["recurrence_rule"] = json.dumps(rule_obj) if rule_obj else None
update_data["recurrence_rule"] = json.dumps({k: v for k, v in rule_obj.items() if v is not None}) if rule_obj else None
start = update_data.get("start_datetime", event.start_datetime)
end_dt = update_data.get("end_datetime", event.end_datetime)

View File

@ -85,7 +85,7 @@ def generate_occurrences(
)
if rule_type == "every_n_days":
interval: int = int(rule.get("interval", 1))
interval: int = int(rule.get("interval") or 1)
if interval < 1:
interval = 1
current = parent_start + timedelta(days=interval)
@ -94,7 +94,7 @@ def generate_occurrences(
current += timedelta(days=interval)
elif rule_type == "weekly":
weekday: int = int(rule.get("weekday", parent_start.weekday()))
weekday: int = int(rule.get("weekday") if rule.get("weekday") is not None else parent_start.weekday())
# Start from the week after the parent
days_ahead = (weekday - parent_start.weekday()) % 7
if days_ahead == 0:
@ -105,8 +105,8 @@ def generate_occurrences(
current += timedelta(weeks=1)
elif rule_type == "monthly_nth_weekday":
week: int = int(rule.get("week", 1))
weekday = int(rule.get("weekday", parent_start.weekday()))
week: int = int(rule.get("week") or 1)
weekday = int(rule.get("weekday") if rule.get("weekday") is not None else parent_start.weekday())
# Advance month by month
year, month = parent_start.year, parent_start.month
# Move to the next month from the parent
@ -139,7 +139,7 @@ def generate_occurrences(
break
elif rule_type == "monthly_date":
day: int = int(rule.get("day", parent_start.day))
day: int = int(rule.get("day") or parent_start.day)
year, month = parent_start.year, parent_start.month
month += 1
if month > 12: