.
This commit is contained in:
@@ -20,8 +20,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h2 class="subtitle">Result</h2>
|
<h2 class="subtitle">Result (raw)</h2>
|
||||||
<pre>{{ run.result }}</pre>
|
<pre>{{ result_pretty|default:run.result }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if builder_html %}
|
||||||
|
<div class="box">
|
||||||
|
<h2 class="subtitle">Builder HTML</h2>
|
||||||
|
<div style="border:1px solid #eee; padding:0.5rem; overflow:auto">{{ builder_html|safe }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<p class="mt-4">
|
<p class="mt-4">
|
||||||
{% if run.export_html %}
|
{% if run.export_html %}
|
||||||
<a class="button is-link" href="{% url 'rota:rota_run_export_html' run.id %}" target="_blank">View Stored Export</a>
|
<a class="button is-link" href="{% url 'rota:rota_run_export_html' run.id %}" target="_blank">View Stored Export</a>
|
||||||
|
|||||||
+69
-19
@@ -1,6 +1,7 @@
|
|||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Optional
|
import io
|
||||||
|
import contextlib
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@@ -34,7 +35,27 @@ def _run_rota_job(run_id: int, use_external_generator: bool = False, generate_bu
|
|||||||
try:
|
try:
|
||||||
from project_rota_hook import generate_rota_for_schedule # type: ignore
|
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:
|
except Exception:
|
||||||
tb = traceback.format_exc()
|
tb = traceback.format_exc()
|
||||||
run.mark_failed(message=tb)
|
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
|
# result. This allows the web UI to present the generated HTML for a
|
||||||
# run without requiring a separate on-demand build.
|
# run without requiring a separate on-demand build.
|
||||||
if generate_builder:
|
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:
|
try:
|
||||||
rb = rota.to_rota_builder()
|
worker_count = rota.workers.count()
|
||||||
# build the model and optionally solve
|
except Exception:
|
||||||
rb.build_and_solve(solve=builder_solve)
|
worker_count = 0
|
||||||
html = rb.get_worker_timetable_html(include_html_tag=True)
|
|
||||||
# store under result so it's accessible from UI
|
if worker_count == 0:
|
||||||
result["builder_html"] = html
|
result["builder_error"] = "No workers assigned to rota; add workers before running the builder."
|
||||||
# Persist the HTML on the RotaRun record for easy retrieval
|
else:
|
||||||
try:
|
try:
|
||||||
run.export_html = html
|
# Capture stdout/stderr from the builder so debugging info
|
||||||
run.save(update_fields=["export_html"])
|
# is available in the RotaRun result and log.
|
||||||
except Exception:
|
out_buf = io.StringIO()
|
||||||
# best-effort: if saving fails, include a log entry
|
err_buf = io.StringIO()
|
||||||
run.log = (run.log or "") + "\nFailed to persist export_html"
|
with contextlib.redirect_stdout(out_buf), contextlib.redirect_stderr(err_buf):
|
||||||
except Exception as exc:
|
rb = rota.to_rota_builder()
|
||||||
# include failure information in run.log and result
|
# build the model and optionally solve
|
||||||
tb = traceback.format_exc()
|
rb.build_and_solve(solve=builder_solve)
|
||||||
run.log = (run.log or "") + "\n" + tb
|
html = rb.get_worker_timetable_html(include_html_tag=True)
|
||||||
result["builder_error"] = str(exc)
|
|
||||||
|
# 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)
|
run.mark_finished(result=result)
|
||||||
|
|
||||||
|
|||||||
@@ -422,7 +422,24 @@ def rota_run_detail(request, run_id):
|
|||||||
from .models import RotaRun
|
from .models import RotaRun
|
||||||
|
|
||||||
run = get_object_or_404(RotaRun, pk=run_id)
|
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):
|
def rota_run_export_html(request, run_id):
|
||||||
|
|||||||
Reference in New Issue
Block a user