- Migration 007: parent_event_id (self-ref FK CASCADE), is_recurring, original_start columns on calendar_events - CalendarEvent model: three new Mapped[] columns for recurrence tracking - RecurrenceRule Pydantic model: typed schema for every_n_days, weekly, monthly_nth_weekday, monthly_date - CalendarEventCreate/Update: accept structured RecurrenceRule (router serializes to JSON string for DB) - CalendarEventUpdate: edit_scope field (this | this_and_future) - CalendarEventResponse: exposes parent_event_id, is_recurring, original_start - recurrence.py service: generates unsaved child ORM objects from parent rule up to 365-day horizon - GET /: excludes parent template rows (children are displayed instead) - POST /: creates parent + bulk children when recurrence_rule provided - PUT /: scope=this detaches occurrence; scope=this_and_future deletes future siblings and regenerates - DELETE /: scope=this deletes one; scope=this_and_future deletes future siblings; no scope deletes all (CASCADE) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
70 lines
2.3 KiB
Python
70 lines
2.3 KiB
Python
from pydantic import BaseModel, ConfigDict
|
|
from datetime import datetime
|
|
from typing import Literal, Optional
|
|
|
|
|
|
class RecurrenceRule(BaseModel):
|
|
"""Structured recurrence rule — serialized to/from JSON string in the DB column."""
|
|
type: Literal["every_n_days", "weekly", "monthly_nth_weekday", "monthly_date"]
|
|
# every_n_days
|
|
interval: Optional[int] = None
|
|
# weekly / monthly_nth_weekday
|
|
weekday: Optional[int] = None # 0=Mon … 6=Sun
|
|
# monthly_nth_weekday
|
|
week: Optional[int] = None # 1-4
|
|
# monthly_date
|
|
day: Optional[int] = None # 1-31
|
|
|
|
|
|
class CalendarEventCreate(BaseModel):
|
|
title: str
|
|
description: Optional[str] = None
|
|
start_datetime: datetime
|
|
end_datetime: datetime
|
|
all_day: bool = False
|
|
color: Optional[str] = None
|
|
location_id: Optional[int] = None
|
|
recurrence_rule: Optional[RecurrenceRule] = None # structured; router serializes to JSON string
|
|
is_starred: bool = False
|
|
calendar_id: Optional[int] = None # If None, server assigns default calendar
|
|
|
|
|
|
class CalendarEventUpdate(BaseModel):
|
|
title: Optional[str] = None
|
|
description: Optional[str] = None
|
|
start_datetime: Optional[datetime] = None
|
|
end_datetime: Optional[datetime] = None
|
|
all_day: Optional[bool] = None
|
|
color: Optional[str] = None
|
|
location_id: Optional[int] = None
|
|
recurrence_rule: Optional[RecurrenceRule] = None # structured; router serializes to JSON string
|
|
is_starred: Optional[bool] = None
|
|
calendar_id: Optional[int] = None
|
|
# Controls which occurrences an edit applies to; absent = non-recurring or whole-series
|
|
edit_scope: Optional[Literal["this", "this_and_future"]] = None
|
|
|
|
|
|
class CalendarEventResponse(BaseModel):
|
|
id: int
|
|
title: str
|
|
description: Optional[str]
|
|
start_datetime: datetime
|
|
end_datetime: datetime
|
|
all_day: bool
|
|
color: Optional[str]
|
|
location_id: Optional[int]
|
|
recurrence_rule: Optional[str] # raw JSON string from DB; frontend parses
|
|
is_starred: bool
|
|
calendar_id: int
|
|
calendar_name: str
|
|
calendar_color: str
|
|
is_virtual: bool = False
|
|
# Recurrence fields
|
|
parent_event_id: Optional[int] = None
|
|
is_recurring: bool = False
|
|
original_start: Optional[datetime] = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|