This commit is contained in:
Ross
2025-12-09 12:16:59 +00:00
parent c63d4d1585
commit e6943ccfc7
5 changed files with 76 additions and 4 deletions
@@ -0,0 +1,21 @@
"""Add export_html field to RotaRun.
Generated by assistant.
"""
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("rota", "0002_rotaschedule_options_rotaschedule_shifts"),
]
operations = [
migrations.AddField(
model_name="rotarun",
name="export_html",
field=models.TextField(blank=True, null=True),
),
]
+4
View File
@@ -180,6 +180,10 @@ class RotaRun(models.Model):
result = models.JSONField(null=True, blank=True)
log = models.TextField(blank=True)
# Optional HTML export produced by a run (stored inline for convenience).
# May be large; keep nullable to avoid breaking existing runs.
export_html = models.TextField(null=True, blank=True)
def mark_running(self):
self.status = self.STATUS_RUNNING
self.started_at = timezone.now()
+31 -3
View File
@@ -7,7 +7,7 @@ from django.utils import timezone
from .models import RotaRun, RotaSchedule
def _run_rota_job(run_id: int, use_external_generator: bool = False):
def _run_rota_job(run_id: int, use_external_generator: bool = False, generate_builder: bool = False, builder_solve: bool = False):
"""Background job that executes the scheduler and updates RotaRun.
If `use_external_generator` is True, callers should implement a project
@@ -47,6 +47,30 @@ def _run_rota_job(run_id: int, use_external_generator: bool = False):
"generated_at": timezone.now().isoformat(),
}
# Optionally build the RotaBuilder and attach generated HTML to the
# result. This allows the web UI to present the generated HTML for a
# run without requiring a separate on-demand build.
if generate_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
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)
run.mark_finished(result=result)
except Exception:
@@ -59,7 +83,7 @@ def _run_rota_job(run_id: int, use_external_generator: bool = False):
print(tb)
def run_rota_async(rota_id: int, use_external_generator: bool = False) -> RotaRun:
def run_rota_async(rota_id: int, use_external_generator: bool = False, generate_builder: bool = False, builder_solve: bool = False) -> RotaRun:
"""Create a RotaRun and start background thread to run it.
Returns the created RotaRun instance.
@@ -68,7 +92,11 @@ def run_rota_async(rota_id: int, use_external_generator: bool = False) -> RotaRu
rota = RotaSchedule.objects.get(pk=rota_id)
run = RotaRun.objects.create(rota=rota)
thread = threading.Thread(target=_run_rota_job, args=(run.id, use_external_generator), daemon=True)
thread = threading.Thread(
target=_run_rota_job,
args=(run.id, use_external_generator, generate_builder, builder_solve),
daemon=True,
)
thread.start()
return run
+1
View File
@@ -19,4 +19,5 @@ urlpatterns = [
path("run/<int:run_id>/export/html/", views.rota_run_export_html, name="rota_run_export_html"),
path("run/<int:run_id>/export/download/", views.rota_run_export_download, name="rota_run_export_download"),
path("rota/<int:rota_id>/export/builder/", views.rota_export_builder, name="rota_export_builder"),
path("rota/<int:rota_id>/runs/clear/", views.rota_runs_clear, name="rota_runs_clear"),
]
+19 -1
View File
@@ -8,6 +8,7 @@ from .forms import ShiftForm
from django.views.decorators.http import require_http_methods
from django.template.loader import render_to_string
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.http import require_POST
def index(request):
@@ -19,7 +20,9 @@ def rota_detail(request, rota_id):
rota = get_object_or_404(RotaSchedule, pk=rota_id)
if request.method == "POST":
# trigger a background run
run = run_rota_async(rota.id)
generate_builder = request.POST.get("generate_builder") == "1"
builder_solve = request.POST.get("builder_solve") == "1"
run = run_rota_async(rota.id, generate_builder=generate_builder, builder_solve=builder_solve)
return redirect(reverse("rota:rota_run_detail", args=(run.id,)))
return render(request, "rota/rota_detail.html", {"rota": rota})
@@ -253,6 +256,21 @@ def rota_export_builder(request, rota_id):
return HttpResponse(html, content_type="text/html; charset=utf-8")
@require_POST
def rota_runs_clear(request, rota_id):
"""Delete all RotaRun records for the given rota and redirect back.
This is a POST-only endpoint; the template uses an inline form with a
JavaScript confirmation to avoid accidental removal.
"""
rota = get_object_or_404(RotaSchedule, pk=rota_id)
# Delete all runs for this rota
from .models import RotaRun
RotaRun.objects.filter(rota=rota).delete()
return redirect("rota:rota_detail", rota_id=rota.id)
@require_http_methods(["GET", "POST"])
def shift_add(request, rota_id):
"""HTMX endpoint to render a shift form or accept submission to append a shift to the rota."""