.
This commit is contained in:
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -180,6 +180,10 @@ class RotaRun(models.Model):
|
|||||||
result = models.JSONField(null=True, blank=True)
|
result = models.JSONField(null=True, blank=True)
|
||||||
log = models.TextField(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):
|
def mark_running(self):
|
||||||
self.status = self.STATUS_RUNNING
|
self.status = self.STATUS_RUNNING
|
||||||
self.started_at = timezone.now()
|
self.started_at = timezone.now()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.utils import timezone
|
|||||||
from .models import RotaRun, RotaSchedule
|
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.
|
"""Background job that executes the scheduler and updates RotaRun.
|
||||||
|
|
||||||
If `use_external_generator` is True, callers should implement a project
|
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(),
|
"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)
|
run.mark_finished(result=result)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -59,7 +83,7 @@ def _run_rota_job(run_id: int, use_external_generator: bool = False):
|
|||||||
print(tb)
|
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.
|
"""Create a RotaRun and start background thread to run it.
|
||||||
|
|
||||||
Returns the created RotaRun instance.
|
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)
|
rota = RotaSchedule.objects.get(pk=rota_id)
|
||||||
run = RotaRun.objects.create(rota=rota)
|
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()
|
thread.start()
|
||||||
|
|
||||||
return run
|
return run
|
||||||
|
|||||||
@@ -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/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("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>/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"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from .forms import ShiftForm
|
|||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest
|
from django.http import HttpResponse, HttpResponseBadRequest
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
@@ -19,7 +20,9 @@ def rota_detail(request, rota_id):
|
|||||||
rota = get_object_or_404(RotaSchedule, pk=rota_id)
|
rota = get_object_or_404(RotaSchedule, pk=rota_id)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
# trigger a background run
|
# 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 redirect(reverse("rota:rota_run_detail", args=(run.id,)))
|
||||||
|
|
||||||
return render(request, "rota/rota_detail.html", {"rota": rota})
|
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")
|
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"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def shift_add(request, rota_id):
|
def shift_add(request, rota_id):
|
||||||
"""HTMX endpoint to render a shift form or accept submission to append a shift to the rota."""
|
"""HTMX endpoint to render a shift form or accept submission to append a shift to the rota."""
|
||||||
|
|||||||
Reference in New Issue
Block a user