working 3 tier approach
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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"])
|
||||
@@ -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)
|
||||
@@ -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}
|
||||
Reference in New Issue
Block a user