from django.shortcuts import render, get_object_or_404, redirect from django.urls import reverse from .models import RotaSchedule, Worker from .forms import WorkerForm, LeaveForm, RotaScheduleForm from .services import run_rota_async 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 def index(request): rotas = RotaSchedule.objects.all().order_by("-created_at") return render(request, "rota/index.html", {"rotas": rotas}) 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) return redirect(reverse("rota:rota_run_detail", args=(run.id,))) return render(request, "rota/rota_detail.html", {"rota": rota}) def rota_create(request): if request.method == "POST": form = RotaScheduleForm(request.POST) if form.is_valid(): rota = form.save() return redirect("rota:rota_detail", rota_id=rota.id) else: form = RotaScheduleForm() return render(request, "rota/rota_form.html", {"form": form}) def worker_create(request): if request.method == "POST": form = WorkerForm(request.POST) if form.is_valid(): worker = form.save() return redirect("/") else: form = WorkerForm() return render(request, "rota/worker_form.html", {"form": form}) def worker_detail(request, worker_id): worker = get_object_or_404(Worker, pk=worker_id) if request.method == "POST": form = LeaveForm(request.POST) if form.is_valid(): leave = form.save(commit=False) leave.worker = worker leave.save() return redirect("rota:worker_detail", worker_id=worker.id) else: form = LeaveForm() return render(request, "rota/worker_detail.html", {"worker": worker, "form": form}) @require_http_methods(["GET", "POST"]) def worker_edit(request, rota_id, worker_id): rota = get_object_or_404(RotaSchedule, pk=rota_id) worker = get_object_or_404(Worker, pk=worker_id) if request.method == "POST": form = WorkerForm(request.POST, instance=worker) if form.is_valid(): form.save() # Return updated worker list OOB and close modal worker_list_html = render_to_string("rota/partials/worker_list.html", {"rota": rota}, request=request) resp = ( f'
{worker_list_html}
' + '' ) response = HttpResponse(resp) response["HX-Trigger"] = "closeModal" return response # invalid: return form modal with errors modal_html = render_to_string( "rota/partials/worker_form_modal.html", {"form": form, "form_action": request.path, "rota": rota}, request=request, ) return HttpResponse(modal_html) # GET: populate form and return modal form = WorkerForm(instance=worker) modal_html = render_to_string( "rota/partials/worker_form_modal.html", {"form": form, "form_action": request.path, "rota": rota}, request=request, ) return HttpResponse(modal_html) @require_http_methods(["GET", "POST"]) def worker_delete(request, rota_id, worker_id): rota = get_object_or_404(RotaSchedule, pk=rota_id) worker = get_object_or_404(Worker, pk=worker_id) if request.method == "POST": # remove assignment between worker and rota try: from .models import Assignment Assignment.objects.filter(worker=worker, rota=rota).delete() except Exception: try: rota.workers.remove(worker) except Exception: return HttpResponseBadRequest("Unable to remove worker from rota") rota = get_object_or_404(RotaSchedule, pk=rota_id) worker_list_html = render_to_string("rota/partials/worker_list.html", {"rota": rota}, request=request) resp = ( f'
{worker_list_html}
' + '' ) response = HttpResponse(resp) response["HX-Trigger"] = "closeModal" return response # GET: confirmation modal modal_html = render_to_string( "rota/partials/worker_confirm_delete.html", {"rota": rota, "worker": worker}, request=request, ) return HttpResponse(modal_html) 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}) def rota_run_export_html(request, run_id): from .models import RotaRun run = get_object_or_404(RotaRun, pk=run_id) return render(request, "rota/rota_run_export.html", {"run": run}) def rota_run_export_download(request, run_id): from .models import RotaRun run = get_object_or_404(RotaRun, pk=run_id) html = render_to_string("rota/rota_run_export.html", {"run": run}, request=request) response = HttpResponse(html, content_type="text/html; charset=utf-8") filename = f"rota_run_{run.id}_export.html" response["Content-Disposition"] = f'attachment; filename="{filename}"' return response def rota_export_builder(request, rota_id): """Build a `RotaBuilder` for the given `RotaSchedule` and return its HTML export. Query parameters: - solve=1 : attempt to solve the model (may require solver binaries and can be slow) - solver= : solver name passed to `build_and_solve` """ rota = get_object_or_404(RotaSchedule, pk=rota_id) try: rb = rota.to_rota_builder() except Exception as exc: return HttpResponseBadRequest(f"Unable to construct RotaBuilder: {exc}") solve = request.GET.get("solve") == "1" solver = request.GET.get("solver", "appsi_highs") try: # Build model (and optionally solve) - keep defaults safe rb.build_and_solve(solve=solve, solver=solver) except Exception as exc: # Return error message in the page so it's visible in browser content = f"

Error running builder

{exc}
" return HttpResponse(content, status=500, content_type="text/html") html = rb.get_worker_timetable_html(include_html_tag=True) return HttpResponse(html, content_type="text/html; charset=utf-8") @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.""" rota = get_object_or_404(RotaSchedule, pk=rota_id) if request.method == "POST": form = ShiftForm(request.POST) if form.is_valid(): data = form.cleaned_data shift_dict = { "sites": data["sites"], "name": data["name"], "length": float(data["length"]), "days": data["days"], "workers_required": int(data["workers_required"]), "assign_as_block": bool(data["assign_as_block"]), } current = rota.shifts or [] current.append(shift_dict) rota.shifts = current rota.save() # Return out-of-band swap: update shift-list and clear modal shift_list_html = render_to_string("rota/partials/shift_list.html", {"rota": rota}, request=request) # wrap shift list for OOB swap and clear form/modal containers resp = ( f'
{shift_list_html}
' + '
' + '' ) response = HttpResponse(resp) response["HX-Trigger"] = "closeModal" return response # invalid: return modal with form and errors modal_html = render_to_string("rota/partials/shift_form_modal.html", {"form": form, "rota": rota, "form_action": request.path}, request=request) return HttpResponse(modal_html) else: form = ShiftForm() modal_html = render_to_string("rota/partials/shift_form_modal.html", {"form": form, "rota": rota, "form_action": request.path}, request=request) return HttpResponse(modal_html) @require_http_methods(["GET", "POST"]) def shift_edit(request, rota_id, idx): rota = get_object_or_404(RotaSchedule, pk=rota_id) try: shift = rota.shifts[idx] except Exception: return HttpResponseBadRequest("Shift not found") if request.method == "POST": form = ShiftForm(request.POST) if form.is_valid(): data = form.cleaned_data shift_dict = { "sites": data["sites"], "name": data["name"], "length": float(data["length"]), "days": data["days"], "workers_required": int(data["workers_required"]), "assign_as_block": bool(data["assign_as_block"]), } current = rota.shifts or [] current[idx] = shift_dict rota.shifts = current rota.save() shift_list_html = render_to_string("rota/partials/shift_list.html", {"rota": rota}, request=request) resp = ( f'
{shift_list_html}
' + '
' + '' ) response = HttpResponse(resp) response["HX-Trigger"] = "closeModal" return response modal_html = render_to_string("rota/partials/shift_form_modal.html", {"form": form, "rota": rota, "form_action": request.path}, request=request) return HttpResponse(modal_html) # GET: populate form with existing shift initial = { "name": shift.get("name"), "sites": ",".join(shift.get("sites") or []), "length": shift.get("length"), "days": shift.get("days"), "workers_required": shift.get("workers_required"), "assign_as_block": shift.get("assign_as_block", False), } form = ShiftForm(initial=initial) modal_html = render_to_string("rota/partials/shift_form_modal.html", {"form": form, "rota": rota, "form_action": request.path}, request=request) return HttpResponse(modal_html) @require_http_methods(["GET", "POST"]) def shift_delete(request, rota_id, idx): rota = get_object_or_404(RotaSchedule, pk=rota_id) try: shift = rota.shifts[idx] except Exception: return HttpResponseBadRequest("Shift not found") if request.method == "POST": current = rota.shifts or [] try: current.pop(idx) except Exception: return HttpResponseBadRequest("Invalid index") rota.shifts = current rota.save() shift_list_html = render_to_string("rota/partials/shift_list.html", {"rota": rota}, request=request) resp = ( f'
{shift_list_html}
' + '
' + '' ) response = HttpResponse(resp) response["HX-Trigger"] = "closeModal" return response # GET: confirmation modal modal_html = render_to_string("rota/partials/shift_confirm_delete.html", {"rota": rota, "idx": idx, "shift": shift}, request=request) return HttpResponse(modal_html)