diff --git a/djangorota/djangorota/templates/rota/rota_run_detail.html b/djangorota/djangorota/templates/rota/rota_run_detail.html index b1167b6..24cfbc8 100644 --- a/djangorota/djangorota/templates/rota/rota_run_detail.html +++ b/djangorota/djangorota/templates/rota/rota_run_detail.html @@ -20,8 +20,16 @@
{{ run.result }}
+ {{ result_pretty|default:run.result }}
+ {% if run.export_html %} View Stored Export diff --git a/djangorota/rota/services.py b/djangorota/rota/services.py index 3753c8d..5f6e42b 100644 --- a/djangorota/rota/services.py +++ b/djangorota/rota/services.py @@ -1,6 +1,7 @@ import threading import traceback -from typing import Optional +import io +import contextlib from django.utils import timezone @@ -34,7 +35,27 @@ def _run_rota_job(run_id: int, use_external_generator: bool = False, generate_bu try: from project_rota_hook import generate_rota_for_schedule # type: ignore - result = generate_rota_for_schedule(rota) + # Capture any stdout/stderr produced by the external generator + out_buf = io.StringIO() + err_buf = io.StringIO() + with contextlib.redirect_stdout(out_buf), contextlib.redirect_stderr(err_buf): + result = generate_rota_for_schedule(rota) + # Attach captured output to the result for debugging + try: + result = result or {} + result["generator_stdout"] = out_buf.getvalue() + result["generator_stderr"] = err_buf.getvalue() + except Exception: + pass + # Also persist captured generator output into the run.log so + # it's visible from the run detail page when executed in the + # background thread. + try: + captured = "\n[GENERATOR STDOUT]\n" + (result.get("generator_stdout") or "") + "\n[GENERATOR STDERR]\n" + (result.get("generator_stderr") or "") + run.log = (run.log or "") + captured + run.save(update_fields=["log"]) + except Exception: + pass except Exception: tb = traceback.format_exc() run.mark_failed(message=tb) @@ -51,25 +72,54 @@ def _run_rota_job(run_id: int, use_external_generator: bool = False, generate_bu # result. This allows the web UI to present the generated HTML for a # run without requiring a separate on-demand build. if generate_builder: + # Defensive: if no workers are assigned to the rota, skip the + # builder step and return a clear message rather than raising an + # exception from deep inside the builder. try: - rb = rota.to_rota_builder() - # build the model and optionally solve - rb.build_and_solve(solve=builder_solve) - html = rb.get_worker_timetable_html(include_html_tag=True) - # store under result so it's accessible from UI - result["builder_html"] = html - # Persist the HTML on the RotaRun record for easy retrieval + worker_count = rota.workers.count() + except Exception: + worker_count = 0 + + if worker_count == 0: + result["builder_error"] = "No workers assigned to rota; add workers before running the builder." + else: try: - run.export_html = html - run.save(update_fields=["export_html"]) - except Exception: - # best-effort: if saving fails, include a log entry - run.log = (run.log or "") + "\nFailed to persist export_html" - except Exception as exc: - # include failure information in run.log and result - tb = traceback.format_exc() - run.log = (run.log or "") + "\n" + tb - result["builder_error"] = str(exc) + # Capture stdout/stderr from the builder so debugging info + # is available in the RotaRun result and log. + out_buf = io.StringIO() + err_buf = io.StringIO() + with contextlib.redirect_stdout(out_buf), contextlib.redirect_stderr(err_buf): + rb = rota.to_rota_builder() + # build the model and optionally solve + rb.build_and_solve(solve=builder_solve) + html = rb.get_worker_timetable_html(include_html_tag=True) + + # store under result so it's accessible from UI + result["builder_html"] = html + result["builder_stdout"] = out_buf.getvalue() + result["builder_stderr"] = err_buf.getvalue() + + # Persist the HTML on the RotaRun record for easy retrieval + try: + run.export_html = html + run.save(update_fields=["export_html"]) + except Exception: + # best-effort: if saving fails, include a log entry + run.log = (run.log or "") + "\nFailed to persist export_html" + # Append captured output to run.log for convenient viewing + try: + captured = "\n[BUILDER STDOUT]\n" + result.get("builder_stdout", "") + "\n[BUILDER STDERR]\n" + result.get("builder_stderr", "") + run.log = (run.log or "") + captured + # persist the appended log so it is visible to users + # when the run finishes + run.save(update_fields=["log"]) + except Exception: + pass + except Exception as exc: + # include failure information in run.log and result + tb = traceback.format_exc() + run.log = (run.log or "") + "\n" + tb + result["builder_error"] = str(exc) run.mark_finished(result=result) diff --git a/djangorota/rota/views.py b/djangorota/rota/views.py index bd8a768..3e42319 100644 --- a/djangorota/rota/views.py +++ b/djangorota/rota/views.py @@ -422,7 +422,24 @@ def rota_run_detail(request, run_id): from .models import RotaRun run = get_object_or_404(RotaRun, pk=run_id) - return render(request, "rota/rota_run_detail.html", {"run": run}) + # Prepare a pretty-printed JSON representation of the result for easier + # debugging in templates. Also expose any builder HTML present in the + # persisted `export_html` or embedded in the `result` under + # `builder_html` so it can be viewed inline. + result_pretty = None + builder_html = None + try: + if run.result is not None: + import json as _json + result_pretty = _json.dumps(run.result, indent=2, default=str) + # prefer persisted export_html if present + builder_html = getattr(run, "export_html", None) or (run.result.get("builder_html") if isinstance(run.result, dict) else None) + else: + builder_html = getattr(run, "export_html", None) + except Exception: + result_pretty = str(run.result) + + return render(request, "rota/rota_run_detail.html", {"run": run, "result_pretty": result_pretty, "builder_html": builder_html}) def rota_run_export_html(request, run_id):