data driven about us screen

This commit is contained in:
2026-06-21 05:24:40 +00:00
parent f52625b37b
commit 1b537b3dfd
34 changed files with 2119 additions and 44 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+79
View File
@@ -0,0 +1,79 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth import get_current_admin_user
from app.database import get_session
from app.models import CommunityHighlight
from app.schemas import CommunityHighlightCreate, CommunityHighlightUpdate, CommunityHighlightResponse
from app.user_models import User
router = APIRouter()
@router.get("/", response_model=list[CommunityHighlightResponse])
async def list_community_highlights(
active: Optional[bool] = Query(None, description="Filter by active status"),
session: AsyncSession = Depends(get_session),
):
query = select(CommunityHighlight).order_by(CommunityHighlight.display_order)
if active is not None:
query = query.where(CommunityHighlight.active == active)
result = await session.execute(query)
return list(result.scalars().all())
@router.get("/{highlight_id}", response_model=CommunityHighlightResponse)
async def get_community_highlight(highlight_id: int, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(CommunityHighlight).where(CommunityHighlight.id == highlight_id))
highlight = result.scalar_one_or_none()
if not highlight:
return {"error": "Community highlight not found"}
return highlight
@router.post("/", response_model=CommunityHighlightResponse, status_code=201)
async def create_community_highlight(
payload: CommunityHighlightCreate,
session: AsyncSession = Depends(get_session),
_: User = Depends(get_current_admin_user),
):
highlight = CommunityHighlight(**payload.model_dump(), active=True)
session.add(highlight)
await session.commit()
await session.refresh(highlight)
return highlight
@router.put("/{highlight_id}", response_model=CommunityHighlightResponse)
async def update_community_highlight(
highlight_id: int,
payload: CommunityHighlightUpdate,
session: AsyncSession = Depends(get_session),
_: User = Depends(get_current_admin_user),
):
result = await session.execute(select(CommunityHighlight).where(CommunityHighlight.id == highlight_id))
highlight = result.scalar_one_or_none()
if not highlight:
return {"error": "Community highlight not found"}
for key, value in payload.model_dump(exclude_unset=True).items():
setattr(highlight, key, value)
await session.commit()
await session.refresh(highlight)
return highlight
@router.delete("/{highlight_id}", status_code=204)
async def delete_community_highlight(
highlight_id: int,
session: AsyncSession = Depends(get_session),
_: User = Depends(get_current_admin_user),
):
result = await session.execute(select(CommunityHighlight).where(CommunityHighlight.id == highlight_id))
highlight = result.scalar_one_or_none()
if not highlight:
return {"error": "Community highlight not found"}
await session.delete(highlight)
await session.commit()
+79
View File
@@ -0,0 +1,79 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth import get_current_admin_user
from app.database import get_session
from app.models import HistoryEntry
from app.schemas import HistoryEntryCreate, HistoryEntryUpdate, HistoryEntryResponse
from app.user_models import User
router = APIRouter()
@router.get("/", response_model=list[HistoryEntryResponse])
async def list_history_entries(
active: Optional[bool] = Query(None, description="Filter by active status"),
session: AsyncSession = Depends(get_session),
):
query = select(HistoryEntry).order_by(HistoryEntry.display_order)
if active is not None:
query = query.where(HistoryEntry.active == active)
result = await session.execute(query)
return list(result.scalars().all())
@router.get("/{entry_id}", response_model=HistoryEntryResponse)
async def get_history_entry(entry_id: int, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(HistoryEntry).where(HistoryEntry.id == entry_id))
entry = result.scalar_one_or_none()
if not entry:
return {"error": "History entry not found"}
return entry
@router.post("/", response_model=HistoryEntryResponse, status_code=201)
async def create_history_entry(
payload: HistoryEntryCreate,
session: AsyncSession = Depends(get_session),
_: User = Depends(get_current_admin_user),
):
entry = HistoryEntry(**payload.model_dump(), active=True)
session.add(entry)
await session.commit()
await session.refresh(entry)
return entry
@router.put("/{entry_id}", response_model=HistoryEntryResponse)
async def update_history_entry(
entry_id: int,
payload: HistoryEntryUpdate,
session: AsyncSession = Depends(get_session),
_: User = Depends(get_current_admin_user),
):
result = await session.execute(select(HistoryEntry).where(HistoryEntry.id == entry_id))
entry = result.scalar_one_or_none()
if not entry:
return {"error": "History entry not found"}
for key, value in payload.model_dump(exclude_unset=True).items():
setattr(entry, key, value)
await session.commit()
await session.refresh(entry)
return entry
@router.delete("/{entry_id}", status_code=204)
async def delete_history_entry(
entry_id: int,
session: AsyncSession = Depends(get_session),
_: User = Depends(get_current_admin_user),
):
result = await session.execute(select(HistoryEntry).where(HistoryEntry.id == entry_id))
entry = result.scalar_one_or_none()
if not entry:
return {"error": "History entry not found"}
await session.delete(entry)
await session.commit()
+79
View File
@@ -0,0 +1,79 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth import get_current_admin_user
from app.database import get_session
from app.models import TeamMember
from app.schemas import TeamMemberCreate, TeamMemberUpdate, TeamMemberResponse
from app.user_models import User
router = APIRouter()
@router.get("/", response_model=list[TeamMemberResponse])
async def list_team_members(
active: Optional[bool] = Query(None, description="Filter by active status"),
session: AsyncSession = Depends(get_session),
):
query = select(TeamMember).order_by(TeamMember.display_order)
if active is not None:
query = query.where(TeamMember.active == active)
result = await session.execute(query)
return list(result.scalars().all())
@router.get("/{member_id}", response_model=TeamMemberResponse)
async def get_team_member(member_id: int, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(TeamMember).where(TeamMember.id == member_id))
member = result.scalar_one_or_none()
if not member:
return {"error": "Team member not found"}
return member
@router.post("/", response_model=TeamMemberResponse, status_code=201)
async def create_team_member(
payload: TeamMemberCreate,
session: AsyncSession = Depends(get_session),
_: User = Depends(get_current_admin_user),
):
member = TeamMember(**payload.model_dump(), active=True)
session.add(member)
await session.commit()
await session.refresh(member)
return member
@router.put("/{member_id}", response_model=TeamMemberResponse)
async def update_team_member(
member_id: int,
payload: TeamMemberUpdate,
session: AsyncSession = Depends(get_session),
_: User = Depends(get_current_admin_user),
):
result = await session.execute(select(TeamMember).where(TeamMember.id == member_id))
member = result.scalar_one_or_none()
if not member:
return {"error": "Team member not found"}
for key, value in payload.model_dump(exclude_unset=True).items():
setattr(member, key, value)
await session.commit()
await session.refresh(member)
return member
@router.delete("/{member_id}", status_code=204)
async def delete_team_member(
member_id: int,
session: AsyncSession = Depends(get_session),
_: User = Depends(get_current_admin_user),
):
result = await session.execute(select(TeamMember).where(TeamMember.id == member_id))
member = result.scalar_one_or_none()
if not member:
return {"error": "Team member not found"}
await session.delete(member)
await session.commit()
+5 -2
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
from app.models import Base, Program, StationConfig, HistoryEntry, TeamMember, CommunityHighlight
from app.user_models import User
from app.api import programs, events, tiers, auth, station_config
from app.api import programs, events, tiers, auth, station_config, history, team, community
async def _ensure_station_config(session):
@@ -96,6 +96,9 @@ 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"])
app.include_router(station_config.router, prefix="/api/station-config", tags=["station-config"])
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"])
# Serve uploaded files (dev only — Nginx handles this in production)
if settings.ENV != "production":
+34
View File
@@ -80,3 +80,37 @@ class StationConfig(Base):
phone = Column(String(30), nullable=False, default="(970) 555-0198")
email = Column(String(100), nullable=False, default="hello@kmountainflower.org")
website = Column(String(200), nullable=False, default="kmountainflower.org")
class HistoryEntry(Base):
__tablename__ = "history_entries"
id = Column(Integer, primary_key=True, autoincrement=True)
year = Column(Integer, nullable=False)
title = Column(String(200), nullable=False)
body = Column(Text, nullable=False)
display_order = Column(Integer, nullable=False)
active = Column(Boolean, default=True)
class TeamMember(Base):
__tablename__ = "team_members"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False)
title = Column(String(150), nullable=False)
bio = Column(Text, nullable=True)
photo_url = Column(String(500), nullable=True)
display_order = Column(Integer, nullable=False)
active = Column(Boolean, default=True)
class CommunityHighlight(Base):
__tablename__ = "community_highlights"
id = Column(Integer, primary_key=True, autoincrement=True)
icon = Column(String(10), nullable=False)
title = Column(String(200), nullable=False)
description = Column(Text, nullable=False)
display_order = Column(Integer, nullable=False)
active = Column(Boolean, default=True)
+87
View File
@@ -156,3 +156,90 @@ class StationConfigUpdate(BaseModel):
phone: Optional[str] = None
email: Optional[str] = None
website: Optional[str] = None
# ── HistoryEntry ──────────────────────────────────────────
class HistoryEntryCreate(BaseModel):
year: int
title: str
body: str
display_order: int
class HistoryEntryUpdate(BaseModel):
year: Optional[int] = None
title: Optional[str] = None
body: Optional[str] = None
display_order: Optional[int] = None
active: Optional[bool] = None
class HistoryEntryResponse(BaseModel):
id: int
year: int
title: str
body: str
display_order: int
active: bool
model_config = {"from_attributes": True}
# ── TeamMember ─────────────────────────────────────────────
class TeamMemberCreate(BaseModel):
name: str
title: str
bio: Optional[str] = None
photo_url: Optional[str] = None
display_order: int
class TeamMemberUpdate(BaseModel):
name: Optional[str] = None
title: Optional[str] = None
bio: Optional[str] = None
photo_url: Optional[str] = None
display_order: Optional[int] = None
active: Optional[bool] = None
class TeamMemberResponse(BaseModel):
id: int
name: str
title: str
bio: Optional[str]
photo_url: Optional[str]
display_order: int
active: bool
model_config = {"from_attributes": True}
# ── CommunityHighlight ─────────────────────────────────────
class CommunityHighlightCreate(BaseModel):
icon: str
title: str
description: str
display_order: int
class CommunityHighlightUpdate(BaseModel):
icon: Optional[str] = None
title: Optional[str] = None
description: Optional[str] = None
display_order: Optional[int] = None
active: Optional[bool] = None
class CommunityHighlightResponse(BaseModel):
id: int
icon: str
title: str
description: str
display_order: int
active: bool
model_config = {"from_attributes": True}