feat(api): session router — list, detail, patch endpoints

GET /bands/{id}/sessions — list with recording counts, newest first
GET /bands/{id}/sessions/{sid} — session detail with flat song list
PATCH /bands/{id}/sessions/{sid} — admin: update label/notes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Steffen Schuhmann
2026-03-29 13:38:27 +02:00
parent f930bb061c
commit 1e53ddf8eb
3 changed files with 96 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ from rehearsalhub.routers import (
bands_router, bands_router,
internal_router, internal_router,
members_router, members_router,
sessions_router,
songs_router, songs_router,
versions_router, versions_router,
ws_router, ws_router,
@@ -51,6 +52,7 @@ def create_app() -> FastAPI:
prefix = "/api/v1" prefix = "/api/v1"
app.include_router(auth_router, prefix=prefix) app.include_router(auth_router, prefix=prefix)
app.include_router(bands_router, prefix=prefix) app.include_router(bands_router, prefix=prefix)
app.include_router(sessions_router, prefix=prefix)
app.include_router(songs_router, prefix=prefix) app.include_router(songs_router, prefix=prefix)
app.include_router(versions_router, prefix=prefix) app.include_router(versions_router, prefix=prefix)
app.include_router(annotations_router, prefix=prefix) app.include_router(annotations_router, prefix=prefix)

View File

@@ -3,6 +3,7 @@ from rehearsalhub.routers.auth import router as auth_router
from rehearsalhub.routers.bands import router as bands_router from rehearsalhub.routers.bands import router as bands_router
from rehearsalhub.routers.internal import router as internal_router from rehearsalhub.routers.internal import router as internal_router
from rehearsalhub.routers.members import router as members_router from rehearsalhub.routers.members import router as members_router
from rehearsalhub.routers.sessions import router as sessions_router
from rehearsalhub.routers.songs import router as songs_router from rehearsalhub.routers.songs import router as songs_router
from rehearsalhub.routers.versions import router as versions_router from rehearsalhub.routers.versions import router as versions_router
from rehearsalhub.routers.ws import router as ws_router from rehearsalhub.routers.ws import router as ws_router
@@ -12,6 +13,7 @@ __all__ = [
"bands_router", "bands_router",
"internal_router", "internal_router",
"members_router", "members_router",
"sessions_router",
"songs_router", "songs_router",
"versions_router", "versions_router",
"annotations_router", "annotations_router",

View File

@@ -0,0 +1,92 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from rehearsalhub.db.engine import get_session
from rehearsalhub.db.models import Member
from rehearsalhub.dependencies import get_current_member
from rehearsalhub.repositories.band import BandRepository
from rehearsalhub.repositories.rehearsal_session import RehearsalSessionRepository
from rehearsalhub.schemas.rehearsal_session import (
RehearsalSessionDetail,
RehearsalSessionRead,
RehearsalSessionUpdate,
)
from rehearsalhub.schemas.song import SongRead
from rehearsalhub.services.band import BandService
router = APIRouter(prefix="/bands", tags=["sessions"])
@router.get("/{band_id}/sessions", response_model=list[RehearsalSessionRead])
async def list_sessions(
band_id: uuid.UUID,
session: AsyncSession = Depends(get_session),
current_member: Member = Depends(get_current_member),
):
svc = BandService(session)
try:
await svc.assert_membership(band_id, current_member.id)
except PermissionError:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a member")
repo = RehearsalSessionRepository(session)
rows = await repo.list_for_band(band_id)
return [
RehearsalSessionRead.model_validate(s, update={"recording_count": count})
for s, count in rows
]
@router.get("/{band_id}/sessions/{session_id}", response_model=RehearsalSessionDetail)
async def get_session_detail(
band_id: uuid.UUID,
session_id: uuid.UUID,
session: AsyncSession = Depends(get_session),
current_member: Member = Depends(get_current_member),
):
svc = BandService(session)
try:
await svc.assert_membership(band_id, current_member.id)
except PermissionError:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a member")
repo = RehearsalSessionRepository(session)
rehearsal = await repo.get_with_songs(session_id)
if rehearsal is None or rehearsal.band_id != band_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Session not found")
songs = [
SongRead.model_validate(s, update={"version_count": len(s.versions)})
for s in rehearsal.songs
]
return RehearsalSessionDetail.model_validate(
rehearsal,
update={"recording_count": len(songs), "songs": songs},
)
@router.patch("/{band_id}/sessions/{session_id}", response_model=RehearsalSessionRead)
async def update_session(
band_id: uuid.UUID,
session_id: uuid.UUID,
data: RehearsalSessionUpdate,
session: AsyncSession = Depends(get_session),
current_member: Member = Depends(get_current_member),
):
band_repo = BandRepository(session)
role = await band_repo.get_member_role(band_id, current_member.id)
if role != "admin":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin only")
repo = RehearsalSessionRepository(session)
rehearsal = await repo.get_by_id(session_id)
if rehearsal is None or rehearsal.band_id != band_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Session not found")
updates = {k: v for k, v in data.model_dump().items() if v is not None}
if updates:
rehearsal = await repo.update(rehearsal, **updates)
return RehearsalSessionRead.model_validate(rehearsal, update={"recording_count": 0})