Add websites page to admin sidebar

This commit is contained in:
dwindown
2026-04-02 17:43:40 +07:00
parent b4ebdc9c4f
commit dcc427cebd

View File

@@ -15,6 +15,7 @@ from typing import Any
import aioredis import aioredis
from fastapi import APIRouter, Depends, Form, Request from fastapi import APIRouter, Depends, Form, Request
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import HTMLResponse, RedirectResponse from starlette.responses import HTMLResponse, RedirectResponse
from starlette.status import HTTP_303_SEE_OTHER, HTTP_401_UNAUTHORIZED from starlette.status import HTTP_303_SEE_OTHER, HTTP_401_UNAUTHORIZED
@@ -172,6 +173,7 @@ def _render_admin_page(title: str, page_title: str, body: str) -> HTMLResponse:
<aside class="sidebar"> <aside class="sidebar">
<h1>IRT Bank Soal Admin</h1> <h1>IRT Bank Soal Admin</h1>
<a href="/admin/dashboard">Dashboard</a> <a href="/admin/dashboard">Dashboard</a>
<a href="/admin/websites">Websites</a>
<a href="/admin/calibration-status">Calibration Status</a> <a href="/admin/calibration-status">Calibration Status</a>
<a href="/admin/item-statistics">Item Statistics</a> <a href="/admin/item-statistics">Item Statistics</a>
<a href="/admin/session-overview">Session Overview</a> <a href="/admin/session-overview">Session Overview</a>
@@ -209,6 +211,34 @@ def _truncate(text: str | None, max_length: int = 120) -> str:
return f"{text[: max_length - 3]}..." return f"{text[: max_length - 3]}..."
def _websites_form_body(
websites: list[Website],
error: str | None = None,
success: str | None = None,
site_name: str = "",
site_url: str = "",
) -> str:
error_html = f'<div class="error">{escape(error)}</div>' if error else ""
success_html = f'<div class="success">{escape(success)}</div>' if success else ""
rows = [[website.id, website.site_name, website.site_url] for website in websites]
websites_table = _table(["ID", "Name", "URL"], rows)
return f"""
<p class="muted">Register websites here so imports and tryout references can be tied to a known source site.</p>
{success_html}
{error_html}
<form method="post" action="/admin/websites" autocomplete="off">
<label for="site_name">Website Name</label>
<input id="site_name" name="site_name" type="text" value="{escape(site_name)}" placeholder="Sejoli Demo Site">
<label for="site_url">Website URL</label>
<input id="site_url" name="site_url" type="url" value="{escape(site_url)}" placeholder="https://example.com">
<button type="submit">Add Website</button>
</form>
<h3 style="margin-top:24px">Registered Websites</h3>
<p class="muted">Use the website ID when importing read-only tryout snapshots.</p>
{websites_table}
"""
async def _basis_items_for_playground(db: AsyncSession, limit: int = 20) -> list[Item]: async def _basis_items_for_playground(db: AsyncSession, limit: int = 20) -> list[Item]:
result = await db.execute( result = await db.execute(
select(Item) select(Item)
@@ -459,6 +489,79 @@ async def dashboard_view(request: Request, db: AsyncSession = Depends(get_db)):
return _render_admin_page("IRT Bank Soal Admin", "Dashboard", body) return _render_admin_page("IRT Bank Soal Admin", "Dashboard", body)
@router.get("/websites", include_in_schema=False)
async def websites_view(request: Request, db: AsyncSession = Depends(get_db)):
admin = await _current_admin(request)
if not admin:
return _login_redirect()
result = await db.execute(select(Website).order_by(Website.id.asc()))
websites = list(result.scalars().all())
body = _websites_form_body(websites)
return _render_admin_page("Websites", "Websites", body)
@router.post("/websites", include_in_schema=False)
async def websites_submit(
request: Request,
db: AsyncSession = Depends(get_db),
site_name: str = Form(...),
site_url: str = Form(...),
):
admin = await _current_admin(request)
if not admin:
return _login_redirect()
normalized_name = site_name.strip()
normalized_url = site_url.strip().rstrip("/")
if not normalized_name:
result = await db.execute(select(Website).order_by(Website.id.asc()))
websites = list(result.scalars().all())
body = _websites_form_body(
websites,
error="Website name is required.",
site_name=site_name,
site_url=site_url,
)
return _render_admin_page("Websites", "Websites", body)
if not normalized_url.startswith(("http://", "https://")):
result = await db.execute(select(Website).order_by(Website.id.asc()))
websites = list(result.scalars().all())
body = _websites_form_body(
websites,
error="Website URL must start with http:// or https://.",
site_name=site_name,
site_url=site_url,
)
return _render_admin_page("Websites", "Websites", body)
website = Website(site_name=normalized_name, site_url=normalized_url)
db.add(website)
try:
await db.commit()
except IntegrityError:
await db.rollback()
result = await db.execute(select(Website).order_by(Website.id.asc()))
websites = list(result.scalars().all())
body = _websites_form_body(
websites,
error="Website URL already exists.",
site_name=site_name,
site_url=site_url,
)
return _render_admin_page("Websites", "Websites", body)
result = await db.execute(select(Website).order_by(Website.id.asc()))
websites = list(result.scalars().all())
body = _websites_form_body(
websites,
success=f"Website added successfully with ID {website.id}.",
)
return _render_admin_page("Websites", "Websites", body)
@router.get("/calibration-status", include_in_schema=False) @router.get("/calibration-status", include_in_schema=False)
async def calibration_status_view(request: Request, db: AsyncSession = Depends(get_db)): async def calibration_status_view(request: Request, db: AsyncSession = Depends(get_db)):
admin = await _current_admin(request) admin = await _current_admin(request)