Add enhanced models, scheduled jobs, per-locale templates (Phase 7b)
Some checks failed
Validate / Hassfest (push) Has been cancelled

Backend changes for full blueprint feature parity:

Database models:
- MessageTemplate: add body_ru (locale variant), event_type field
- AlbumTracker: add track_images, track_videos, notify_favorites_only,
  include_people, include_asset_details, max_assets_to_show,
  assets_order_by, assets_order fields
- New ScheduledJob model: supports periodic_summary, scheduled_assets,
  and memory (On This Day) notification modes with full config
  (times, interval, album_mode, limit, filters, sorting)

API:
- New /api/scheduled/* CRUD endpoints for scheduled jobs
- Tracker create/update accept all new filtering fields
- Tracker response includes new fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 15:48:01 +03:00
parent 2aa9b8939d
commit 89cb2bbb70
7 changed files with 221 additions and 0 deletions

View File

@@ -0,0 +1,152 @@
"""Scheduled notification job API routes."""
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
from ..auth.dependencies import get_current_user
from ..database.engine import get_session
from ..database.models import AlbumTracker, ScheduledJob, User
router = APIRouter(prefix="/api/scheduled", tags=["scheduled"])
class ScheduledJobCreate(BaseModel):
tracker_id: int
job_type: str # periodic_summary, scheduled_assets, memory
enabled: bool = True
times: str = "09:00"
interval_days: int = 1
start_date: str = "2025-01-01"
album_mode: str = "per_album"
limit: int = 10
favorite_only: bool = False
asset_type: str = "all"
min_rating: int = 0
order_by: str = "random"
order: str = "descending"
min_date: str | None = None
max_date: str | None = None
message_template: str = ""
class ScheduledJobUpdate(BaseModel):
enabled: bool | None = None
times: str | None = None
interval_days: int | None = None
album_mode: str | None = None
limit: int | None = None
favorite_only: bool | None = None
asset_type: str | None = None
min_rating: int | None = None
order_by: str | None = None
order: str | None = None
min_date: str | None = None
max_date: str | None = None
message_template: str | None = None
@router.get("")
async def list_jobs(
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
"""List all scheduled jobs for the current user's trackers."""
trackers = await session.exec(
select(AlbumTracker).where(AlbumTracker.user_id == user.id)
)
tracker_ids = [t.id for t in trackers.all()]
if not tracker_ids:
return []
result = await session.exec(
select(ScheduledJob).where(ScheduledJob.tracker_id.in_(tracker_ids))
)
return [_job_response(j) for j in result.all()]
@router.post("", status_code=status.HTTP_201_CREATED)
async def create_job(
body: ScheduledJobCreate,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
"""Create a scheduled notification job."""
# Verify tracker ownership
tracker = await session.get(AlbumTracker, body.tracker_id)
if not tracker or tracker.user_id != user.id:
raise HTTPException(status_code=404, detail="Tracker not found")
if body.job_type not in ("periodic_summary", "scheduled_assets", "memory"):
raise HTTPException(status_code=400, detail="Invalid job_type")
job = ScheduledJob(**body.model_dump())
session.add(job)
await session.commit()
await session.refresh(job)
return _job_response(job)
@router.put("/{job_id}")
async def update_job(
job_id: int,
body: ScheduledJobUpdate,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
"""Update a scheduled job."""
job = await _get_user_job(session, job_id, user.id)
for field, value in body.model_dump(exclude_unset=True).items():
setattr(job, field, value)
session.add(job)
await session.commit()
await session.refresh(job)
return _job_response(job)
@router.delete("/{job_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_job(
job_id: int,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
"""Delete a scheduled job."""
job = await _get_user_job(session, job_id, user.id)
await session.delete(job)
await session.commit()
def _job_response(j: ScheduledJob) -> dict:
return {
"id": j.id,
"tracker_id": j.tracker_id,
"job_type": j.job_type,
"enabled": j.enabled,
"times": j.times,
"interval_days": j.interval_days,
"start_date": j.start_date,
"album_mode": j.album_mode,
"limit": j.limit,
"favorite_only": j.favorite_only,
"asset_type": j.asset_type,
"min_rating": j.min_rating,
"order_by": j.order_by,
"order": j.order,
"min_date": j.min_date,
"max_date": j.max_date,
"message_template": j.message_template,
"created_at": j.created_at.isoformat(),
}
async def _get_user_job(
session: AsyncSession, job_id: int, user_id: int
) -> ScheduledJob:
job = await session.get(ScheduledJob, job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
tracker = await session.get(AlbumTracker, job.tracker_id)
if not tracker or tracker.user_id != user_id:
raise HTTPException(status_code=404, detail="Job not found")
return job

View File

@@ -23,6 +23,14 @@ class TrackerCreate(BaseModel):
enabled: bool = True
quiet_hours_start: str | None = None
quiet_hours_end: str | None = None
track_images: bool = True
track_videos: bool = True
notify_favorites_only: bool = False
include_people: bool = True
include_asset_details: bool = False
max_assets_to_show: int = 5
assets_order_by: str = "none"
assets_order: str = "descending"
class TrackerUpdate(BaseModel):
@@ -35,6 +43,14 @@ class TrackerUpdate(BaseModel):
enabled: bool | None = None
quiet_hours_start: str | None = None
quiet_hours_end: str | None = None
track_images: bool | None = None
track_videos: bool | None = None
notify_favorites_only: bool | None = None
include_people: bool | None = None
include_asset_details: bool | None = None
max_assets_to_show: int | None = None
assets_order_by: str | None = None
assets_order: str | None = None
@router.get("")
@@ -174,6 +190,14 @@ def _tracker_response(t: AlbumTracker) -> dict:
"enabled": t.enabled,
"quiet_hours_start": t.quiet_hours_start,
"quiet_hours_end": t.quiet_hours_end,
"track_images": t.track_images,
"track_videos": t.track_videos,
"notify_favorites_only": t.notify_favorites_only,
"include_people": t.include_people,
"include_asset_details": t.include_asset_details,
"max_assets_to_show": t.max_assets_to_show,
"assets_order_by": t.assets_order_by,
"assets_order": t.assets_order,
"created_at": t.created_at.isoformat(),
}