321 lines
10 KiB
Python
321 lines
10 KiB
Python
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'<div id="worker-list" hx-swap-oob="true">{worker_list_html}</div>'
|
|
+ '<div id="modal" hx-swap-oob="true"></div>'
|
|
)
|
|
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'<div id="worker-list" hx-swap-oob="true">{worker_list_html}</div>'
|
|
+ '<div id="modal" hx-swap-oob="true"></div>'
|
|
)
|
|
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=<name> : 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"<html><body><h1>Error running builder</h1><pre>{exc}</pre></body></html>"
|
|
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'<div id="shift-list" hx-swap-oob="true">{shift_list_html}</div>'
|
|
+ '<div id="shift-form" hx-swap-oob="true"></div>'
|
|
+ '<div id="modal" hx-swap-oob="true"></div>'
|
|
)
|
|
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'<div id="shift-list" hx-swap-oob="true">{shift_list_html}</div>'
|
|
+ '<div id="shift-form" hx-swap-oob="true"></div>'
|
|
+ '<div id="modal" hx-swap-oob="true"></div>'
|
|
)
|
|
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'<div id="shift-list" hx-swap-oob="true">{shift_list_html}</div>'
|
|
+ '<div id="shift-form" hx-swap-oob="true"></div>'
|
|
+ '<div id="modal" hx-swap-oob="true"></div>'
|
|
)
|
|
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)
|
|
|