First product
This commit is contained in:
parent
778da9e2fc
commit
4433d7d2c4
157 changed files with 23975 additions and 0 deletions
212
app/web/routers/scholars.py
Normal file
212
app/web/routers/scholars.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
|
||||
from fastapi.responses import HTMLResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.session import get_db_session
|
||||
from app.services import scholars as scholar_service
|
||||
from app.theme import resolve_theme
|
||||
from app.web import common
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _format_dt(value: datetime | None) -> str:
|
||||
if value is None:
|
||||
return "-"
|
||||
return value.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||
|
||||
|
||||
async def _render_scholars_page(
|
||||
request: Request,
|
||||
*,
|
||||
db_session: AsyncSession,
|
||||
current_user,
|
||||
theme_name: str,
|
||||
notice: str | None = None,
|
||||
page_error: str | None = None,
|
||||
status_code: int = status.HTTP_200_OK,
|
||||
form_scholar_id: str = "",
|
||||
form_display_name: str = "",
|
||||
) -> HTMLResponse:
|
||||
scholars = await scholar_service.list_scholars_for_user(
|
||||
db_session,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
scholar_items = []
|
||||
for scholar in scholars:
|
||||
scholar_items.append(
|
||||
{
|
||||
"id": scholar.id,
|
||||
"scholar_id": scholar.scholar_id,
|
||||
"display_name": scholar.display_name or "Unnamed",
|
||||
"is_enabled": scholar.is_enabled,
|
||||
"baseline_completed": scholar.baseline_completed,
|
||||
"last_run_status": (
|
||||
scholar.last_run_status.value
|
||||
if scholar.last_run_status is not None
|
||||
else "never"
|
||||
),
|
||||
"last_run_dt": _format_dt(scholar.last_run_dt),
|
||||
}
|
||||
)
|
||||
context = common.build_template_context(
|
||||
request,
|
||||
page_title="Scholars",
|
||||
active_nav="scholars",
|
||||
theme_name=theme_name,
|
||||
session_user=common.to_session_user(current_user),
|
||||
notice=notice,
|
||||
page_error=page_error,
|
||||
)
|
||||
context["scholars"] = scholar_items
|
||||
context["form_scholar_id"] = form_scholar_id
|
||||
context["form_display_name"] = form_display_name
|
||||
return common.templates.TemplateResponse(
|
||||
request=request,
|
||||
name="scholars.html",
|
||||
context=context,
|
||||
status_code=status_code,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/scholars", response_class=HTMLResponse)
|
||||
async def scholars_page(
|
||||
request: Request,
|
||||
theme: str | None = None,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> HTMLResponse:
|
||||
current_user = await common.get_authenticated_user(request, db_session)
|
||||
if current_user is None:
|
||||
return common.redirect_to_login()
|
||||
return await _render_scholars_page(
|
||||
request,
|
||||
db_session=db_session,
|
||||
current_user=current_user,
|
||||
theme_name=resolve_theme(theme),
|
||||
notice=request.query_params.get("notice"),
|
||||
page_error=request.query_params.get("error"),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/scholars")
|
||||
async def create_scholar(
|
||||
request: Request,
|
||||
scholar_id: Annotated[str, Form()],
|
||||
display_name: Annotated[str, Form()] = "",
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> HTMLResponse:
|
||||
current_user = await common.get_authenticated_user(request, db_session)
|
||||
if current_user is None:
|
||||
return common.redirect_to_login()
|
||||
active_theme = resolve_theme(None)
|
||||
try:
|
||||
created_profile = await scholar_service.create_scholar_for_user(
|
||||
db_session,
|
||||
user_id=current_user.id,
|
||||
scholar_id=scholar_id,
|
||||
display_name=display_name,
|
||||
)
|
||||
except scholar_service.ScholarServiceError as exc:
|
||||
logger.info(
|
||||
"scholars.create_failed",
|
||||
extra={
|
||||
"event": "scholars.create_failed",
|
||||
"user_id": current_user.id,
|
||||
"scholar_id": scholar_id.strip(),
|
||||
},
|
||||
)
|
||||
return await _render_scholars_page(
|
||||
request,
|
||||
db_session=db_session,
|
||||
current_user=current_user,
|
||||
theme_name=active_theme,
|
||||
page_error=str(exc),
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
form_scholar_id=scholar_id,
|
||||
form_display_name=display_name,
|
||||
)
|
||||
label = created_profile.display_name or created_profile.scholar_id
|
||||
logger.info(
|
||||
"scholars.created",
|
||||
extra={
|
||||
"event": "scholars.created",
|
||||
"user_id": current_user.id,
|
||||
"scholar_profile_id": created_profile.id,
|
||||
},
|
||||
)
|
||||
return common.redirect_with_message("/scholars", notice=f"Scholar added: {label}")
|
||||
|
||||
|
||||
@router.post("/scholars/{scholar_profile_id}/toggle")
|
||||
async def toggle_scholar(
|
||||
request: Request,
|
||||
scholar_profile_id: int,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
current_user = await common.get_authenticated_user(request, db_session)
|
||||
if current_user is None:
|
||||
return common.redirect_to_login()
|
||||
|
||||
profile = await scholar_service.get_user_scholar_by_id(
|
||||
db_session,
|
||||
user_id=current_user.id,
|
||||
scholar_profile_id=scholar_profile_id,
|
||||
)
|
||||
if profile is None:
|
||||
raise HTTPException(status_code=404, detail="Scholar not found.")
|
||||
updated_profile = await scholar_service.toggle_scholar_enabled(
|
||||
db_session,
|
||||
profile=profile,
|
||||
)
|
||||
status_label = "enabled" if updated_profile.is_enabled else "disabled"
|
||||
logger.info(
|
||||
"scholars.toggled",
|
||||
extra={
|
||||
"event": "scholars.toggled",
|
||||
"user_id": current_user.id,
|
||||
"scholar_profile_id": updated_profile.id,
|
||||
"is_enabled": updated_profile.is_enabled,
|
||||
},
|
||||
)
|
||||
return common.redirect_with_message(
|
||||
"/scholars",
|
||||
notice=f"Scholar {status_label}: {updated_profile.scholar_id}",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/scholars/{scholar_profile_id}/delete")
|
||||
async def delete_scholar(
|
||||
request: Request,
|
||||
scholar_profile_id: int,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
current_user = await common.get_authenticated_user(request, db_session)
|
||||
if current_user is None:
|
||||
return common.redirect_to_login()
|
||||
|
||||
profile = await scholar_service.get_user_scholar_by_id(
|
||||
db_session,
|
||||
user_id=current_user.id,
|
||||
scholar_profile_id=scholar_profile_id,
|
||||
)
|
||||
if profile is None:
|
||||
raise HTTPException(status_code=404, detail="Scholar not found.")
|
||||
deleted_label = profile.display_name or profile.scholar_id
|
||||
await scholar_service.delete_scholar(db_session, profile=profile)
|
||||
logger.info(
|
||||
"scholars.deleted",
|
||||
extra={
|
||||
"event": "scholars.deleted",
|
||||
"user_id": current_user.id,
|
||||
"scholar_profile_id": scholar_profile_id,
|
||||
},
|
||||
)
|
||||
return common.redirect_with_message("/scholars", notice=f"Scholar removed: {deleted_label}")
|
||||
Loading…
Add table
Add a link
Reference in a new issue