New programming screen

This commit is contained in:
2026-06-21 21:55:55 +00:00
parent 96d0fef792
commit e627fe3637
24 changed files with 1297 additions and 122 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -1
View File
@@ -2,7 +2,7 @@ from fastapi import APIRouter
from app.config import settings
from app.database import get_session
from app.models import DonationTier, Event, Program, StationConfig
from app.models import DonationTier, Event, Program, StationConfig, Show, ShowSchedule
router = APIRouter()
@@ -17,6 +17,8 @@ async def reset_database():
from seed import seed as run_seed
async with async_session() as session:
await session.execute(ShowSchedule.__table__.delete())
await session.execute(Show.__table__.delete())
await session.execute(Program.__table__.delete())
await session.execute(Event.__table__.delete())
await session.execute(DonationTier.__table__.delete())
+118
View File
@@ -0,0 +1,118 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.auth import get_current_admin_user
from app.database import get_session
from app.models import Show, ShowSchedule
from app.schemas import ShowCreate, ShowUpdate, ShowResponse
from app.user_models import User
router = APIRouter()
@router.get("/", response_model=list[ShowResponse])
async def list_shows(session: AsyncSession = Depends(get_session)):
query = (
select(Show)
.where(Show.active == True)
.options(selectinload(Show.schedules))
.order_by(Show.display_order, Show.title)
)
result = await session.execute(query)
return list(result.scalars().all())
@router.get("/{show_id}", response_model=ShowResponse)
async def get_show(
show_id: int,
session: AsyncSession = Depends(get_session),
):
result = await session.execute(
select(Show)
.options(selectinload(Show.schedules))
.where(Show.id == show_id)
)
show = result.scalar_one_or_none()
if not show:
raise HTTPException(status_code=404, detail="Show not found")
return show
@router.post("/", response_model=ShowResponse, status_code=201)
async def create_show(
payload: ShowCreate,
session: AsyncSession = Depends(get_session),
_: User = Depends(get_current_admin_user),
):
data = payload.model_dump(exclude={"schedules"})
show = Show(**data)
for slot in payload.schedules:
show.schedules.append(ShowSchedule(**slot.model_dump()))
session.add(show)
await session.commit()
await session.refresh(show)
await session.refresh(show)
# Re-query with eager load to return schedules
result = await session.execute(
select(Show)
.options(selectinload(Show.schedules))
.where(Show.id == show.id)
)
return result.scalar_one()
@router.put("/{show_id}", response_model=ShowResponse)
async def update_show(
show_id: int,
payload: ShowUpdate,
session: AsyncSession = Depends(get_session),
_: User = Depends(get_current_admin_user),
):
result = await session.execute(
select(Show)
.options(selectinload(Show.schedules))
.where(Show.id == show_id)
)
show = result.scalar_one_or_none()
if not show:
raise HTTPException(status_code=404, detail="Show not found")
update_data = payload.model_dump(exclude_unset=True, exclude={"schedules"})
for key, value in update_data.items():
setattr(show, key, value)
if payload.schedules is not None:
# Replace all schedule slots
for sched in show.schedules:
await session.delete(sched)
for slot in payload.schedules:
session.add(ShowSchedule(show_id=show.id, **slot.model_dump()))
await session.commit()
await session.refresh(show)
# Re-query with eager load
result = await session.execute(
select(Show)
.options(selectinload(Show.schedules))
.where(Show.id == show.id)
)
return result.scalar_one()
@router.delete("/{show_id}", status_code=204)
async def delete_show(
show_id: int,
session: AsyncSession = Depends(get_session),
_: User = Depends(get_current_admin_user),
):
result = await session.execute(select(Show).where(Show.id == show_id))
show = result.scalar_one_or_none()
if not show:
raise HTTPException(status_code=404, detail="Show not found")
await session.delete(show)
await session.commit()
+4 -3
View File
@@ -8,9 +8,9 @@ from sqlalchemy import func, select
from app.config import settings
from app.database import async_session, engine
from app.models import Base, Program, StationConfig, HistoryEntry, TeamMember, CommunityHighlight
from app.models import Base, Program, Show, StationConfig, HistoryEntry, TeamMember, CommunityHighlight
from app.user_models import User
from app.api import programs, events, tiers, auth, station_config, history, team, community
from app.api import programs, events, tiers, auth, station_config, history, team, community, shows
async def _ensure_station_config(session):
@@ -36,7 +36,7 @@ async def lifespan(app: FastAPI):
# Auto-seed if database is empty
async with async_session() as session:
result = await session.execute(select(func.count(Program.id)))
result = await session.execute(select(func.count(Show.id)))
count = result.scalar()
if count == 0:
print(" → Database is empty — running seed...")
@@ -99,6 +99,7 @@ app.include_router(station_config.router, prefix="/api/station-config", tags=["s
app.include_router(history.router, prefix="/api/history", tags=["history"])
app.include_router(team.router, prefix="/api/team", tags=["team"])
app.include_router(community.router, prefix="/api/community", tags=["community"])
app.include_router(shows.router, prefix="/api/shows", tags=["shows"])
# Serve uploaded files (dev only — Nginx handles this in production)
if settings.ENV != "production":
+32 -1
View File
@@ -7,8 +7,10 @@ from sqlalchemy import (
Boolean,
Date,
JSON,
ForeignKey,
UniqueConstraint,
)
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import DeclarativeBase, relationship
class Base(DeclarativeBase):
@@ -27,6 +29,35 @@ class Program(Base):
genre = Column(String(80), nullable=False)
class Show(Base):
__tablename__ = "shows"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(200), nullable=False)
description = Column(Text, nullable=False)
host = Column(String(100), nullable=False)
genre = Column(String(80), nullable=False)
show_art_url = Column(String(500), nullable=True)
hero_image_url = Column(String(500), nullable=True)
display_order = Column(Integer, nullable=False, default=0)
active = Column(Boolean, default=True)
schedules = relationship("ShowSchedule", back_populates="show", cascade="all, delete-orphan")
class ShowSchedule(Base):
__tablename__ = "show_schedules"
id = Column(Integer, primary_key=True, autoincrement=True)
show_id = Column(Integer, ForeignKey("shows.id", ondelete="CASCADE"), nullable=False)
day_of_week = Column(Integer, nullable=False)
time = Column(String(10), nullable=False)
show = relationship("Show", back_populates="schedules")
__table_args__ = (
UniqueConstraint("show_id", "day_of_week", "time", name="uq_show_sched"),
)
class Event(Base):
__tablename__ = "events"
+61
View File
@@ -243,3 +243,64 @@ class CommunityHighlightResponse(BaseModel):
active: bool
model_config = {"from_attributes": True}
# ── ShowSchedule ──────────────────────────────────────────
class ShowScheduleResponse(BaseModel):
id: int
show_id: int
day_of_week: int
time: str
@computed_field
@property
def day_label(self) -> str:
return DAY_NAMES.get(self.day_of_week, "Unknown")
model_config = {"from_attributes": True}
class ShowScheduleCreate(BaseModel):
day_of_week: int
time: str
# ── Show ────────────────────────────────────────────────────
class ShowResponse(BaseModel):
id: int
title: str
description: str
host: str
genre: str
show_art_url: Optional[str]
hero_image_url: Optional[str]
display_order: int
active: bool
schedules: list[ShowScheduleResponse]
model_config = {"from_attributes": True}
class ShowCreate(BaseModel):
title: str
description: str
host: str
genre: str
show_art_url: Optional[str] = None
hero_image_url: Optional[str] = None
display_order: int = 0
schedules: list[ShowScheduleCreate]
class ShowUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
host: Optional[str] = None
genre: Optional[str] = None
show_art_url: Optional[str] = None
hero_image_url: Optional[str] = None
display_order: Optional[int] = None
active: Optional[bool] = None
schedules: Optional[list[ShowScheduleCreate]] = None