- 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>
207 lines
5.6 KiB
Python
207 lines
5.6 KiB
Python
"""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)
|