This commit is contained in:
Timothy Jaeryang Baek
2026-04-21 13:46:39 +09:00
parent a2875f13c6
commit 46d73c9dcd
3 changed files with 20 additions and 9 deletions
+2 -2
View File
@@ -163,7 +163,7 @@ async def create_new_automation(
):
await check_automations_permission(request, user)
try:
validate_rrule(form_data.data.rrule)
validate_rrule(form_data.data.rrule, tz=user.timezone)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -213,7 +213,7 @@ async def update_automation_by_id(
check_automation_access(automation, user)
try:
validate_rrule(form_data.data.rrule)
validate_rrule(form_data.data.rrule, tz=user.timezone)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
+2 -2
View File
@@ -2575,7 +2575,7 @@ async def create_automation(
# Validate the RRULE
try:
validate_rrule(rrule)
validate_rrule(rrule, tz=user.timezone)
except ValueError as e:
return json.dumps({'error': f'Invalid schedule: {e}'})
@@ -2656,7 +2656,7 @@ async def update_automation(
# Validate RRULE if changed
if rrule is not None:
try:
validate_rrule(new_rrule)
validate_rrule(new_rrule, tz=user.timezone if user else None)
except ValueError as e:
return json.dumps({'error': f'Invalid schedule: {e}'})
+16 -5
View File
@@ -61,13 +61,19 @@ def _parse_rule(s: str):
return rrulestr(s, ignoretz=True)
def validate_rrule(s: str) -> None:
"""Raise ValueError if the RRULE is malformed or exhausted."""
def validate_rrule(s: str, tz: str = None) -> None:
"""Raise ValueError if the RRULE is malformed or exhausted.
When *tz* is provided the "now" reference uses the user's local
clock so that near-future schedules are not incorrectly rejected
on servers whose system clock is ahead (e.g. UTC vs US timezones).
"""
try:
rule = _parse_rule(s)
except Exception as e:
raise ValueError(ERROR_MESSAGES.AUTOMATION_INVALID_RRULE(e))
if rule.after(datetime.now()) is None:
now = datetime.now(ZoneInfo(tz)).replace(tzinfo=None) if tz else datetime.now()
if rule.after(now) is None:
raise ValueError(ERROR_MESSAGES.AUTOMATION_NO_FUTURE_RUNS)
@@ -83,10 +89,15 @@ def next_run_ns(s: str, tz: str = None) -> Optional[int]:
def next_n_runs_ns(s: str, n: int = 5, tz: str = None) -> list[int]:
"""Compute next N occurrences for UI preview."""
"""Compute next N occurrences for UI preview.
Uses the user's timezone for the starting "now" so that the
preview matches the user's local clock (same as next_run_ns).
"""
rule = _parse_rule(s)
result = []
dt = datetime.now()
now = datetime.now(ZoneInfo(tz)).replace(tzinfo=None) if tz else datetime.now()
dt = now
for _ in range(n):
dt = rule.after(dt)
if not dt: