Split webui into Flask blueprints and data domain modules
- Split app.py (66 routes) into 3 blueprints: pages (public), api (JSON), admin (@admin_required) - Split data.py (4,360 LOC) into 7 domain modules: drafts, authors, ratings, gaps, analysis, search, proposals - Add data/__init__.py re-exporting all public functions for backward compatibility - Add custom 404/500 error pages matching dark theme - Add request timing logging via before_request/after_request hooks - Refactor app.py into create_app() factory pattern - All 106 tests pass, all 66 routes preserved Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
206
src/webui/blueprints/pages.py
Normal file
206
src/webui/blueprints/pages.py
Normal file
@@ -0,0 +1,206 @@
|
||||
"""Public page routes (no admin required)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import Blueprint, render_template, request, abort, g
|
||||
|
||||
from webui.data import (
|
||||
get_db,
|
||||
get_overview_stats,
|
||||
get_category_counts,
|
||||
get_drafts_page,
|
||||
get_draft_detail,
|
||||
get_rating_distributions,
|
||||
get_timeline_data,
|
||||
get_timeline_animation_data,
|
||||
get_ideas_by_type,
|
||||
get_top_authors,
|
||||
get_org_data,
|
||||
get_category_radar_data,
|
||||
get_score_histogram,
|
||||
get_author_network_full,
|
||||
get_cross_org_data,
|
||||
get_citation_graph,
|
||||
get_idea_clusters,
|
||||
get_category_summary,
|
||||
global_search,
|
||||
get_architecture,
|
||||
get_ask_search,
|
||||
get_citation_influence,
|
||||
get_bcp_analysis,
|
||||
)
|
||||
|
||||
pages_bp = Blueprint("pages", __name__)
|
||||
|
||||
|
||||
def db():
|
||||
if "db" not in g:
|
||||
g.db = get_db()
|
||||
return g.db
|
||||
|
||||
|
||||
@pages_bp.route("/")
|
||||
def overview():
|
||||
stats = get_overview_stats(db())
|
||||
categories = get_category_counts(db())
|
||||
timeline = get_timeline_data(db())
|
||||
scores = get_score_histogram(db())
|
||||
radar = get_category_radar_data(db())
|
||||
return render_template(
|
||||
"overview.html",
|
||||
stats=stats,
|
||||
categories=categories,
|
||||
timeline=timeline,
|
||||
scores=scores,
|
||||
radar=radar,
|
||||
)
|
||||
|
||||
|
||||
@pages_bp.route("/drafts")
|
||||
def drafts():
|
||||
page = request.args.get("page", 1, type=int)
|
||||
search = request.args.get("q", "")
|
||||
category = request.args.get("cat", "")
|
||||
source = request.args.get("source", "")
|
||||
min_score = request.args.get("min_score", 0.0, type=float)
|
||||
sort = request.args.get("sort", "score")
|
||||
sort_dir = request.args.get("dir", "desc")
|
||||
|
||||
result = get_drafts_page(
|
||||
db(),
|
||||
page=page,
|
||||
search=search,
|
||||
category=category,
|
||||
min_score=min_score,
|
||||
sort=sort,
|
||||
sort_dir=sort_dir,
|
||||
source=source,
|
||||
)
|
||||
categories = get_category_counts(db())
|
||||
cat_summary = get_category_summary(db(), category) if category else None
|
||||
return render_template(
|
||||
"drafts.html",
|
||||
result=result,
|
||||
categories=categories,
|
||||
cat_summary=cat_summary,
|
||||
search=search,
|
||||
current_cat=category,
|
||||
current_source=source,
|
||||
min_score=min_score,
|
||||
sort=sort,
|
||||
sort_dir=sort_dir,
|
||||
)
|
||||
|
||||
|
||||
@pages_bp.route("/drafts/<string:name>")
|
||||
def draft_detail(name: str):
|
||||
database = db()
|
||||
detail = get_draft_detail(database, name)
|
||||
if not detail:
|
||||
abort(404)
|
||||
# Build set of draft ref IDs that exist in our DB for internal linking
|
||||
ref_draft_ids = [r["id"] for r in detail.get("refs", []) if r["type"] == "draft"]
|
||||
known_drafts = set()
|
||||
if ref_draft_ids:
|
||||
placeholders = ",".join("?" * len(ref_draft_ids))
|
||||
rows = database.conn.execute(
|
||||
f"SELECT name FROM drafts WHERE name IN ({placeholders})", ref_draft_ids
|
||||
).fetchall()
|
||||
known_drafts = {r["name"] for r in rows}
|
||||
return render_template("draft_detail.html", draft=detail, known_drafts=known_drafts)
|
||||
|
||||
|
||||
@pages_bp.route("/ideas")
|
||||
def ideas():
|
||||
data = get_ideas_by_type(db())
|
||||
return render_template("ideas.html", data=data)
|
||||
|
||||
|
||||
@pages_bp.route("/ratings")
|
||||
def ratings():
|
||||
distributions = get_rating_distributions(db())
|
||||
radar = get_category_radar_data(db())
|
||||
return render_template(
|
||||
"ratings.html",
|
||||
dist=distributions,
|
||||
radar=radar,
|
||||
)
|
||||
|
||||
|
||||
@pages_bp.route("/timeline")
|
||||
def timeline_animation():
|
||||
data = get_timeline_animation_data(db())
|
||||
return render_template("timeline.html", animation=data)
|
||||
|
||||
|
||||
@pages_bp.route("/idea-clusters")
|
||||
def idea_clusters():
|
||||
data = get_idea_clusters(db())
|
||||
return render_template("idea_clusters.html", clusters=data)
|
||||
|
||||
|
||||
@pages_bp.route("/architecture")
|
||||
def architecture():
|
||||
data = get_architecture(db())
|
||||
return render_template("architecture.html", arch=data)
|
||||
|
||||
|
||||
@pages_bp.route("/authors")
|
||||
def authors():
|
||||
top = get_top_authors(db(), limit=50)
|
||||
orgs = get_org_data(db(), limit=20)
|
||||
network = get_author_network_full(db())
|
||||
cross_org = get_cross_org_data(db(), limit=20)
|
||||
return render_template(
|
||||
"authors.html",
|
||||
authors=top,
|
||||
orgs=orgs,
|
||||
orgs_data=orgs,
|
||||
network=network,
|
||||
cross_org=cross_org,
|
||||
)
|
||||
|
||||
|
||||
@pages_bp.route("/citations")
|
||||
def citations():
|
||||
from webui.auth import is_admin as check_admin
|
||||
graph = get_citation_graph(db())
|
||||
influence = get_citation_influence(db()) if check_admin() else None
|
||||
bcp = get_bcp_analysis(db()) if check_admin() else None
|
||||
return render_template("citations.html", graph=graph, influence=influence, bcp=bcp)
|
||||
|
||||
|
||||
@pages_bp.route("/about")
|
||||
def about():
|
||||
from ietf_analyzer.config import Config
|
||||
cfg = Config.load()
|
||||
stats = get_overview_stats(db())
|
||||
return render_template("about.html", stats=stats, search_keywords=cfg.search_keywords,
|
||||
fetch_since=cfg.fetch_since)
|
||||
|
||||
|
||||
@pages_bp.route("/impressum")
|
||||
def impressum():
|
||||
return render_template("impressum.html")
|
||||
|
||||
|
||||
@pages_bp.route("/datenschutz")
|
||||
def datenschutz():
|
||||
return render_template("datenschutz.html")
|
||||
|
||||
|
||||
@pages_bp.route("/search")
|
||||
def search():
|
||||
q = request.args.get("q", "").strip()
|
||||
results = global_search(db(), q) if q else {"drafts": [], "ideas": [], "authors": [], "gaps": []}
|
||||
total = sum(len(v) for v in results.values())
|
||||
return render_template("search_results.html", query=q, results=results, total=total)
|
||||
|
||||
|
||||
@pages_bp.route("/ask")
|
||||
def ask_page():
|
||||
question = request.args.get("q", "")
|
||||
result = None
|
||||
if question:
|
||||
top_k = request.args.get("top", 5, type=int)
|
||||
result = get_ask_search(db(), question, top_k=top_k)
|
||||
return render_template("ask.html", question=question, result=result)
|
||||
Reference in New Issue
Block a user