Add websites page to admin sidebar
This commit is contained in:
103
app/admin_web.py
103
app/admin_web.py
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user