New programming screen

This commit is contained in:
kfj001 committed 2026-06-21 21:55:55 +00:00
1 parent 96d0fef792
commit e627fe3637
24 files changed
+1297 -122

No files matched your search

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