Auto-populate activity log from timer events with edit and delete
- New GET /api/logs/timeline endpoint joins TimerEvent with block/subject/session data
- New PATCH and DELETE /api/logs/timeline/{id} endpoints for editing/removing events
- LogView redesigned as a chronological timeline grouped by date
- Edit inline: timer events support type + time correction; notes support text edit
- Delete works for both auto events and manual notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,16 +3,112 @@ from datetime import date
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.activity import ActivityLog
|
||||
from app.models.child import Child
|
||||
from app.models.session import DailySession, TimerEvent
|
||||
from app.models.schedule import ScheduleBlock
|
||||
from app.models.subject import Subject # noqa: F401
|
||||
from app.models.user import User
|
||||
from app.schemas.activity import ActivityLogCreate, ActivityLogOut, ActivityLogUpdate
|
||||
from app.schemas.activity import ActivityLogCreate, ActivityLogOut, ActivityLogUpdate, TimelineEventOut, TimelineEventUpdate
|
||||
|
||||
router = APIRouter(prefix="/api/logs", tags=["logs"])
|
||||
|
||||
|
||||
@router.get("/timeline", response_model=list[TimelineEventOut])
|
||||
async def get_timeline(
|
||||
child_id: int | None = None,
|
||||
log_date: date | None = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
query = (
|
||||
select(TimerEvent)
|
||||
.join(DailySession, TimerEvent.session_id == DailySession.id)
|
||||
.join(Child, DailySession.child_id == Child.id)
|
||||
.where(Child.user_id == current_user.id)
|
||||
.options(
|
||||
selectinload(TimerEvent.block).selectinload(ScheduleBlock.subject),
|
||||
selectinload(TimerEvent.session).selectinload(DailySession.child),
|
||||
)
|
||||
.order_by(TimerEvent.occurred_at.desc())
|
||||
)
|
||||
if child_id:
|
||||
query = query.where(DailySession.child_id == child_id)
|
||||
if log_date:
|
||||
query = query.where(DailySession.session_date == log_date)
|
||||
|
||||
result = await db.execute(query)
|
||||
events = result.scalars().all()
|
||||
|
||||
return [_to_timeline_out(e) for e in events]
|
||||
|
||||
|
||||
def _to_timeline_out(e: TimerEvent) -> TimelineEventOut:
|
||||
blk = e.block
|
||||
sub = blk.subject if blk else None
|
||||
return TimelineEventOut(
|
||||
id=e.id,
|
||||
event_type=e.event_type,
|
||||
occurred_at=e.occurred_at,
|
||||
session_date=e.session.session_date,
|
||||
child_id=e.session.child_id,
|
||||
child_name=e.session.child.name,
|
||||
block_label=(blk.label or sub.name) if blk and sub else (blk.label if blk else None),
|
||||
subject_name=sub.name if sub else None,
|
||||
subject_icon=sub.icon if sub else None,
|
||||
subject_color=sub.color if sub else None,
|
||||
)
|
||||
|
||||
|
||||
async def _get_timer_event(event_id: int, current_user: User, db: AsyncSession) -> TimerEvent:
|
||||
result = await db.execute(
|
||||
select(TimerEvent)
|
||||
.join(DailySession, TimerEvent.session_id == DailySession.id)
|
||||
.join(Child, DailySession.child_id == Child.id)
|
||||
.where(TimerEvent.id == event_id, Child.user_id == current_user.id)
|
||||
.options(
|
||||
selectinload(TimerEvent.block).selectinload(ScheduleBlock.subject),
|
||||
selectinload(TimerEvent.session).selectinload(DailySession.child),
|
||||
)
|
||||
)
|
||||
event = result.scalar_one_or_none()
|
||||
if not event:
|
||||
raise HTTPException(status_code=404, detail="Event not found")
|
||||
return event
|
||||
|
||||
|
||||
@router.patch("/timeline/{event_id}", response_model=TimelineEventOut)
|
||||
async def update_timeline_event(
|
||||
event_id: int,
|
||||
body: TimelineEventUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
event = await _get_timer_event(event_id, current_user, db)
|
||||
if body.event_type is not None:
|
||||
event.event_type = body.event_type
|
||||
if body.occurred_at is not None:
|
||||
event.occurred_at = body.occurred_at
|
||||
await db.commit()
|
||||
await db.refresh(event)
|
||||
event = await _get_timer_event(event_id, current_user, db)
|
||||
return _to_timeline_out(event)
|
||||
|
||||
|
||||
@router.delete("/timeline/{event_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_timeline_event(
|
||||
event_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
event = await _get_timer_event(event_id, current_user, db)
|
||||
await db.delete(event)
|
||||
await db.commit()
|
||||
|
||||
|
||||
@router.get("", response_model=list[ActivityLogOut])
|
||||
async def list_logs(
|
||||
child_id: int | None = None,
|
||||
|
||||
Reference in New Issue
Block a user