working 3 tier approach

This commit is contained in:
kfj001 committed 2026-06-11 22:34:55 -07:00
1 parent 4482a30793
commit e5fef8b181
47 files changed
+1288 -200

No files matched your search

View File
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+70
View File
@@ -0,0 +1,70 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_session
from app.models import Event
from app.schemas import EventCreate, EventUpdate, EventResponse
router = APIRouter()
@router.get("/", response_model=list[EventResponse])
async def list_events(
active: Optional[bool] = Query(None, description="Filter by active status"),
session: AsyncSession = Depends(get_session),
):
query = select(Event).order_by(Event.date)
if active is not None:
query = query.where(Event.active == active)
result = await session.execute(query)
return list(result.scalars().all())
@router.get("/{event_id}", response_model=EventResponse)
async def get_event(event_id: int, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Event).where(Event.id == event_id))
event = result.scalar_one_or_none()
if not event:
return {"error": "Event not found"}
return event
@router.post("/", response_model=EventResponse, status_code=201)
async def create_event(
payload: EventCreate, session: AsyncSession = Depends(get_session)
):
event = Event(**payload.model_dump(), active=True)
session.add(event)
await session.commit()
await session.refresh(event)
return event
@router.put("/{event_id}", response_model=EventResponse)
async def update_event(
event_id: int,
payload: EventUpdate,
session: AsyncSession = Depends(get_session),
):
result = await session.execute(select(Event).where(Event.id == event_id))
event = result.scalar_one_or_none()
if not event:
return {"error": "Event not found"}
for key, value in payload.model_dump(exclude_unset=True).items():
setattr(event, key, value)
await session.commit()
await session.refresh(event)
return event
@router.delete("/{event_id}", status_code=204)
async def delete_event(event_id: int, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Event).where(Event.id == event_id))
event = result.scalar_one_or_none()
if not event:
return {"error": "Event not found"}
await session.delete(event)
await session.commit()
+75
View File
@@ -0,0 +1,75 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_session
from app.models import Program
from app.schemas import ProgramCreate, ProgramUpdate, ProgramResponse
router = APIRouter()
def _day_filter(day: int):
"""Return the SQLAlchemy filter for a given day_of_week."""
return Program.day_of_week == day
@router.get("/", response_model=list[ProgramResponse])
async def list_programs(
day: Optional[int] = Query(None, description="Filter by day_of_week (1=Mon … 7=Sun)"),
session: AsyncSession = Depends(get_session),
):
query = select(Program).order_by(Program.day_of_week, Program.time)
if day is not None:
query = query.filter(_day_filter(day))
result = await session.execute(query)
return list(result.scalars().all())
@router.get("/{program_id}", response_model=ProgramResponse)
async def get_program(program_id: int, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Program).where(Program.id == program_id))
program = result.scalar_one_or_none()
if not program:
return {"error": "Program not found"}
return program
@router.post("/", response_model=ProgramResponse, status_code=201)
async def create_program(
payload: ProgramCreate, session: AsyncSession = Depends(get_session)
):
program = Program(**payload.model_dump())
session.add(program)
await session.commit()
await session.refresh(program)
return program
@router.put("/{program_id}", response_model=ProgramResponse)
async def update_program(
program_id: int,
payload: ProgramUpdate,
session: AsyncSession = Depends(get_session),
):
result = await session.execute(select(Program).where(Program.id == program_id))
program = result.scalar_one_or_none()
if not program:
return {"error": "Program not found"}
for key, value in payload.model_dump(exclude_unset=True).items():
setattr(program, key, value)
await session.commit()
await session.refresh(program)
return program
@router.delete("/{program_id}", status_code=204)
async def delete_program(program_id: int, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Program).where(Program.id == program_id))
program = result.scalar_one_or_none()
if not program:
return {"error": "Program not found"}
await session.delete(program)
await session.commit()
+66
View File
@@ -0,0 +1,66 @@
from typing import Optional
from fastapi import APIRouter, Depends
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_session
from app.models import DonationTier
from app.schemas import TierCreate, TierUpdate, TierResponse
router = APIRouter()
@router.get("/", response_model=list[TierResponse])
async def list_tiers(session: AsyncSession = Depends(get_session)):
result = await session.execute(
select(DonationTier).order_by(DonationTier.display_order)
)
return list(result.scalars().all())
@router.get("/{tier_id}", response_model=TierResponse)
async def get_tier(tier_id: int, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(DonationTier).where(DonationTier.id == tier_id))
tier = result.scalar_one_or_none()
if not tier:
return {"error": "Tier not found"}
return tier
@router.post("/", response_model=TierResponse, status_code=201)
async def create_tier(
payload: TierCreate, session: AsyncSession = Depends(get_session)
):
tier = DonationTier(**payload.model_dump())
session.add(tier)
await session.commit()
await session.refresh(tier)
return tier
@router.put("/{tier_id}", response_model=TierResponse)
async def update_tier(
tier_id: int,
payload: TierUpdate,
session: AsyncSession = Depends(get_session),
):
result = await session.execute(select(DonationTier).where(DonationTier.id == tier_id))
tier = result.scalar_one_or_none()
if not tier:
return {"error": "Tier not found"}
for key, value in payload.model_dump(exclude_unset=True).items():
setattr(tier, key, value)
await session.commit()
await session.refresh(tier)
return tier
@router.delete("/{tier_id}", status_code=204)
async def delete_tier(tier_id: int, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(DonationTier).where(DonationTier.id == tier_id))
tier = result.scalar_one_or_none()
if not tier:
return {"error": "Tier not found"}
await session.delete(tier)
await session.commit()
+11
View File
@@ -0,0 +1,11 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain"
CORS_ORIGINS: list[str] = ["http://localhost:4200", "http://localhost:3000"]
model_config = {"env_prefix": "KMTN_"}
settings = Settings()
+11
View File
@@ -0,0 +1,11 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.config import settings
engine = create_async_engine(settings.DATABASE_URL, echo=False)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def get_session() -> AsyncSession:
async with async_session() as session:
yield session
+38
View File
@@ -0,0 +1,38 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.config import settings
from app.database import engine
from app.models import Base
from app.api import programs, events, tiers
@asynccontextmanager
async def lifespan(app: FastAPI):
# Create tables on startup (seed.py handles data seeding)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
app = FastAPI(
title="KMountain Flower Radio API",
description="Backend API for the KMountain Flower Radio Station website.",
version="1.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Register routers
app.include_router(programs.router, prefix="/api/programs", tags=["programs"])
app.include_router(events.router, prefix="/api/events", tags=["events"])
app.include_router(tiers.router, prefix="/api/tiers", tags=["tiers"])
+51
View File
@@ -0,0 +1,51 @@
from sqlalchemy import (
Column,
Integer,
String,
Text,
Numeric,
Boolean,
Date,
ARRAY,
)
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
class Program(Base):
__tablename__ = "programs"
id = Column(Integer, primary_key=True, autoincrement=True)
day_of_week = Column(Integer, nullable=False) # 1=Mon … 7=Sun
day_label = Column(String(10), nullable=False) # "Monday", …
time = Column(String(10), nullable=False) # "6:00 AM"
title = Column(String(200), nullable=False)
host = Column(String(100), nullable=False)
genre = Column(String(80), nullable=False)
class Event(Base):
__tablename__ = "events"
id = Column(Integer, primary_key=True, autoincrement=True)
date = Column(Date, nullable=False)
title = Column(String(200), nullable=False)
description = Column(Text, nullable=False)
location = Column(String(200), nullable=False)
icon = Column(String(10), nullable=False)
rsvp_url = Column(String(500), nullable=True)
active = Column(Boolean, default=True)
class DonationTier(Base):
__tablename__ = "donation_tiers"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(50), unique=True, nullable=False)
amount = Column(Numeric(10, 2), nullable=False)
icon = Column(String(10), nullable=False)
benefits = Column(ARRAY(Text), nullable=False)
display_order = Column(Integer, nullable=False)
+99
View File
@@ -0,0 +1,99 @@
from datetime import date
from typing import Optional
from pydantic import BaseModel, field_validator
# ── Program ──────────────────────────────────────────────
class ProgramCreate(BaseModel):
day_of_week: int
day_label: str
time: str
title: str
host: str
genre: str
class ProgramUpdate(BaseModel):
day_of_week: Optional[int] = None
day_label: Optional[str] = None
time: Optional[str] = None
title: Optional[str] = None
host: Optional[str] = None
genre: Optional[str] = None
class ProgramResponse(BaseModel):
id: int
day_of_week: int
day_label: str
time: str
title: str
host: str
genre: str
model_config = {"from_attributes": True}
# ── Event ────────────────────────────────────────────────
class EventCreate(BaseModel):
date: date
title: str
description: str
location: str
icon: str
rsvp_url: Optional[str] = None
class EventUpdate(BaseModel):
date: Optional[date] = None
title: Optional[str] = None
description: Optional[str] = None
location: Optional[str] = None
icon: Optional[str] = None
rsvp_url: Optional[str] = None
active: Optional[bool] = None
class EventResponse(BaseModel):
id: int
date: date
title: str
description: str
location: str
icon: str
rsvp_url: Optional[str]
active: bool
model_config = {"from_attributes": True}
# ── DonationTier ─────────────────────────────────────────
class TierCreate(BaseModel):
name: str
amount: float
icon: str
benefits: list[str]
display_order: int
class TierUpdate(BaseModel):
name: Optional[str] = None
amount: Optional[float] = None
icon: Optional[str] = None
benefits: Optional[list[str]] = None
display_order: Optional[int] = None
class TierResponse(BaseModel):
id: int
name: str
amount: float
icon: str
benefits: list[str]
display_order: int
model_config = {"from_attributes": True}