Programming

1. views

from __future__ import annotations

import io
from pathlib import Path

import pandas as pd
from django.http import FileResponse, Http404, HttpResponse, JsonResponse
from django.shortcuts import render
from django.views.decorators.cache import cache_page
from django.views.decorators.http import require_GET

from lending_analytics import config as C
from lending_analytics.i18n import period_label, t

from . import bridge, charts
from .models import QualityCheck, ReportRun, Snapshot


def _spec(request) -> C.ReportSpec:
    lang = request.GET.get("lang", "ru")
    return C.ReportSpec(
        lang=lang if lang in ("ru", "en") else "ru",
        fx_mode=request.GET.get("fx", "nominal"),
        units=request.GET.get("units", "mlrd"),
    )


def _period(request) -> int | None:
    raw = request.GET.get("period")
    try:
        return int(raw) if raw else bridge.latest_period()
    except (TypeError, ValueError):
        return bridge.latest_period()


def _base_context(request):
    spec = _spec(request)
    period = _period(request)
    return spec, period, {
        "spec": spec,
        "brand": spec.brand,
        "lang": spec.lang,
        "period": period,
        "period_label": period_label(period, spec.lang) if period else "—",
        "periods": charts.period_choices(spec),
        "sections": [{"slug": s, "title": t(key, spec.lang)}
                     for s, (key, _) in charts.SECTIONS.items()],
        "t": {k: t(k, spec.lang) for k in
              ("report_title", "sec_overview", "sec_method", "filial", "exposure",
               "npl_ratio", "watch_ratio", "coverage_ratio", "n_loans", "methodology")},
    }


def dashboard(request):
    """
    Renders the shell and only the first section server-side.

    Every other section is fetched on demand by `section_partial`. Building
    fourteen Plotly figures on one request is what turns a fast dashboard into
    a four-second one, and most visitors never scroll past the first tab.
    """
    spec, period, ctx = _base_context(request)
    if period is None:
        return render(request, "portfolio/empty.html", ctx)

    marts = bridge.get_marts()
    ctx.update({
        "kpis": charts.kpis(marts, spec, period),
        "active": "overview",
        "charts": charts.build_section("overview", marts, spec),
        "narrative": charts.narrative(marts, spec).get("overview", []),
        "branches": charts.branch_table(marts, spec, period),
        "snapshots": Snapshot.objects.all()[:1],
        "quality": QualityCheck.objects.filter(run_period=period).exclude(status="pass"),
        "last_report": ReportRun.objects.filter(lang=spec.lang).first(),
    })
    return render(request, "portfolio/dashboard.html", ctx)


@require_GET
def section_partial(request, section: str):
    if section not in charts.SECTIONS:
        raise Http404(section)
    spec, period, ctx = _base_context(request)
    marts = bridge.get_marts()
    key_map = {"overview": "overview", "vintage": "vintage",
               "bridge": "bridge", "migration": "cure", "insurance": "insurance"}
    ctx.update({
        "active": section,
        "charts": charts.build_section(section, marts, spec),
        "narrative": charts.narrative(marts, spec).get(key_map.get(section, ""), []),
    })
    return render(request, "portfolio/_section.html", ctx)


@require_GET
def chart_partial(request, slug: str):
    """One chart, for htmx swaps or an iframe embed."""
    if slug not in charts.CHARTS:
        raise Http404(slug)
    spec = _spec(request)
    html = charts.build_chart(slug, bridge.get_marts(), spec)
    return HttpResponse(html)


@require_GET
def mart_api(request, name: str):
    """JSON for a mart, filtered. Useful for ad-hoc work and for Excel exports."""
    if name not in bridge.MART_NAMES:
        raise Http404(name)
    df = bridge.get_mart(name)
    for field in ("dim_name", "dim_value", "period"):
        val = request.GET.get(field)
        if val and field in df.columns:
            df = df[df[field].astype(str) == val]
    return JsonResponse({"mart": name, "version": bridge.mart_version(),
                         "rows": len(df),
                         "data": df.head(5000).to_dict("records")}, safe=False)


@require_GET
def mart_csv(request, name: str):
    if name not in bridge.MART_NAMES:
        raise Http404(name)
    buf = io.StringIO()
    bridge.get_mart(name).to_csv(buf, index=False)
    resp = HttpResponse(buf.getvalue().encode("utf-8-sig"), content_type="text/csv")
    resp["Content-Disposition"] = f'attachment; filename="{name}.csv"'
    return resp


@require_GET
def download_report(request, period: int, lang: str = "ru"):
    run = ReportRun.objects.filter(period=period, lang=lang).first()
    if not run or not run.html_path or not Path(run.html_path).exists():
        raise Http404("report not generated")
    return FileResponse(open(run.html_path, "rb"), content_type="text/html",
                        as_attachment=False, filename=Path(run.html_path).name)


@require_GET
def health(request):
    """What the pipeline thinks its own state is. Check this before a board meeting."""
    latest = Snapshot.objects.first()
    failed = QualityCheck.objects.filter(status="FAIL")
    return JsonResponse({
        "latest_period": bridge.latest_period(),
        "snapshots": Snapshot.objects.count(),
        "mart_version": bridge.mart_version(),
        "last_ingested": latest.ingested_at.isoformat() if latest else None,
        "failing_checks": list(failed.values_list("check_name", flat=True)),
        "ok": not failed.exists() and bridge.latest_period() is not None,
    })
Helpful? Dislike 0 Log in to react