From c39c34a0d033d8f9a9477872db7c8c6b266ff0e2 Mon Sep 17 00:00:00 2001 From: Vadim Kazakov Date: Mon, 29 Jun 2026 21:56:57 +0300 Subject: [PATCH] feat: add distro and table selection to excel report page --- src/imgtests/reporting/excel_export.py | 18 ++---- src/imgtests/web/static/js/excel_reports.js | 23 +++++++- .../tests_interface/excel_reports.html | 24 +++++++- src/imgtests/web/tests_interface/views.py | 59 ++++++++++++++++--- 4 files changed, 99 insertions(+), 25 deletions(-) diff --git a/src/imgtests/reporting/excel_export.py b/src/imgtests/reporting/excel_export.py index 16b71072..1bca4f57 100644 --- a/src/imgtests/reporting/excel_export.py +++ b/src/imgtests/reporting/excel_export.py @@ -238,7 +238,7 @@ def configuration_id_from_os( ) -> int | str: distribution_name = distribution_name_from_os(os_name) - if not distribution_name: + if not distribution_name or distribution_name not in distributions: return "" return int(distributions[distribution_name]["configuration_id"]) @@ -268,23 +268,13 @@ def build_distribution_records( msg = f"Unsupported distributions: {joined_distributions}. Available: {valid_names}." raise ValueError(msg) - missing_distributions = [ - distribution_name - for distribution_name in DISTRIBUTION_DESCRIPTIONS - if distribution_name not in ids - ] - - if missing_distributions: - joined_distributions = ", ".join(missing_distributions) - msg = f"Missing configuration ids for distributions: {joined_distributions}." - raise ValueError(msg) - return { distribution_name: DistributionRecord( configuration_id=ids[distribution_name], - distribution_description=distribution_description, + distribution_description=DISTRIBUTION_DESCRIPTIONS[distribution_name], ) - for distribution_name, distribution_description in DISTRIBUTION_DESCRIPTIONS.items() + for distribution_name in ids + if distribution_name in DISTRIBUTION_DESCRIPTIONS } diff --git a/src/imgtests/web/static/js/excel_reports.js b/src/imgtests/web/static/js/excel_reports.js index 8e70bda8..cd7f0237 100644 --- a/src/imgtests/web/static/js/excel_reports.js +++ b/src/imgtests/web/static/js/excel_reports.js @@ -15,8 +15,26 @@ function getCookie(name) { return cookieValue; } +function getCheckedValues(selector) { + return Array.from(document.querySelectorAll(selector)) + .filter((cb) => cb.checked) + .map((cb) => cb.value); +} + document.getElementById("exportBtn").addEventListener("click", function () { const btn = this; + const tables = getCheckedValues(".table-checkbox"); + const distributions = getCheckedValues(".distro-checkbox"); + + if (tables.length === 0) { + alert("Please select at least one table."); + return; + } + if (distributions.length === 0) { + alert("Please select at least one distribution."); + return; + } + btn.disabled = true; btn.textContent = "Generating..."; @@ -26,6 +44,7 @@ document.getElementById("exportBtn").addEventListener("click", function () { "X-CSRFToken": getCookie("csrftoken"), "Content-Type": "application/json", }, + body: JSON.stringify({ tables: tables, distributions: distributions }), }) .then((response) => response.json()) .then((data) => { @@ -35,12 +54,12 @@ document.getElementById("exportBtn").addEventListener("click", function () { } else { alert("Error: " + data.error); btn.disabled = false; - btn.textContent = "Export to Excel"; + btn.textContent = "Generate New Excel Report"; } }) .catch((error) => { alert("Error: " + error); btn.disabled = false; - btn.textContent = "Export to Excel"; + btn.textContent = "Generate New Excel Report"; }); }); diff --git a/src/imgtests/web/tests_interface/templates/tests_interface/excel_reports.html b/src/imgtests/web/tests_interface/templates/tests_interface/excel_reports.html index 7ee7d0de..24943364 100644 --- a/src/imgtests/web/tests_interface/templates/tests_interface/excel_reports.html +++ b/src/imgtests/web/tests_interface/templates/tests_interface/excel_reports.html @@ -11,7 +11,29 @@

Excel Reports

-
+
+
+
+ Distributions: + {% for key, desc in distributions.items %} + + {% endfor %} +
+
+ Tables: + + +
+
diff --git a/src/imgtests/web/tests_interface/views.py b/src/imgtests/web/tests_interface/views.py index 1e86b445..d4fb96e8 100644 --- a/src/imgtests/web/tests_interface/views.py +++ b/src/imgtests/web/tests_interface/views.py @@ -2,7 +2,7 @@ import logging import re from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from django.core.management import call_command from django.http import FileResponse, Http404, HttpRequest, JsonResponse @@ -13,11 +13,12 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods from pydantic_core._pydantic_core import ValidationError -from sqlalchemy import create_engine +from sqlalchemy import Engine, create_engine, text -from imgtests.constant import CONFIG_DIR, EXCEL_REPORTS_DIR, REPORTS_DIR +from imgtests.constant import CONFIG_DIR, DISTRIBUTION_DESCRIPTIONS, EXCEL_REPORTS_DIR, REPORTS_DIR from imgtests.database.database import ImgtestsDatabase, PostgresCreds -from imgtests.reporting.excel_export import export_database_to_excel +from imgtests.reporting.cli import EXPORT_TABLES +from imgtests.reporting.excel_export import distribution_name_from_os, export_database_to_excel from imgtests.reporting.html_report import ReportGenerator from imgtests.runner import get_test_name from imgtests.suites.map import ALL_SUITES @@ -375,9 +376,21 @@ def __safe_relative_path(*parts: str) -> Path: @csrf_exempt @require_http_methods(["POST"]) -def api_export_excel(request: HttpRequest) -> JsonResponse: # noqa: ARG001 +def api_export_excel(request: HttpRequest) -> JsonResponse: + try: + body = json.loads(request.body) if request.body else {} + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid request body"}, status=400) + + tables = body.get("tables", list(EXPORT_TABLES)) + selected_distributions = body.get("distributions") + + if not tables: + return JsonResponse({"error": "At least one table must be selected"}, status=400) + timestamp = timezone.now().strftime("%Y%m%d_%H%M%S") output_path = EXCEL_REPORTS_DIR / f"export_{timestamp}.xlsx" + try: db_creds = PostgresCreds() except ValidationError: @@ -391,10 +404,12 @@ def api_export_excel(request: HttpRequest) -> JsonResponse: # noqa: ARG001 try: engine = create_engine(db_url) + configuration_ids = _resolve_configuration_ids(engine, selected_distributions) export_database_to_excel( engine=engine, output_path=output_path, - configuration_ids={"poky": 1, "suse": 2}, + configuration_ids=configuration_ids, + tables=tables, ) except Exception as err: # noqa: BLE001 return JsonResponse({"error": f"Export failed: {err}"}, status=500) @@ -410,10 +425,37 @@ def api_export_excel(request: HttpRequest) -> JsonResponse: # noqa: ARG001 ) +def _resolve_configuration_ids( + engine: Engine, + selected_distributions: list[str] | None = None, +) -> dict[str, int]: + + with engine.connect() as conn: + result = conn.execute(text("SELECT config_id, os FROM configuration")) + configs = result.all() + + all_distros: dict[str, int] = {} + for config_id, os_name in configs: + distro = distribution_name_from_os(os_name) + if distro and distro not in all_distros: + all_distros[distro] = config_id + + if selected_distributions is not None: + missing = [d for d in selected_distributions if d not in all_distros] + if missing: + msg = f"No configuration found for: {', '.join(missing)}." + raise ValueError(msg) + return {d: all_distros[d] for d in selected_distributions if d in all_distros} + + return all_distros + + def excel_report_list(request: HttpRequest) -> HttpResponse: + context: dict[str, Any] = {"distributions": DISTRIBUTION_DESCRIPTIONS} reports: list[dict[str, str | float]] = [] if not EXCEL_REPORTS_DIR.exists(): - return render(request, "tests_interface/excel_reports.html", {"reports": reports}) + context["reports"] = reports + return render(request, "tests_interface/excel_reports.html", context) for file_path in sorted(EXCEL_REPORTS_DIR.glob("*.xlsx"), reverse=True): stat = file_path.stat() @@ -425,7 +467,8 @@ def excel_report_list(request: HttpRequest) -> HttpResponse: }, ) - return render(request, "tests_interface/excel_reports.html", {"reports": reports}) + context["reports"] = reports + return render(request, "tests_interface/excel_reports.html", context) def download_excel_report(request: HttpRequest, filename: str) -> FileResponse: # noqa: ARG001