diff --git a/djangorota/djangorota/asgi.py b/djangorota/djangorota/asgi.py index 62283fd..3b70766 100644 --- a/djangorota/djangorota/asgi.py +++ b/djangorota/djangorota/asgi.py @@ -8,6 +8,13 @@ https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ """ import os +import sys +import pathlib + +# Ensure repo root is importable when running via ASGI +repo_root = pathlib.Path(__file__).resolve().parents[2] +if str(repo_root) not in sys.path: + sys.path.insert(0, str(repo_root)) from django.core.asgi import get_asgi_application diff --git a/djangorota/djangorota/wsgi.py b/djangorota/djangorota/wsgi.py index cabeddc..bc464b4 100644 --- a/djangorota/djangorota/wsgi.py +++ b/djangorota/djangorota/wsgi.py @@ -8,6 +8,13 @@ https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ """ import os +import sys +import pathlib + +# Ensure repo root is importable when running via WSGI +repo_root = pathlib.Path(__file__).resolve().parents[2] +if str(repo_root) not in sys.path: + sys.path.insert(0, str(repo_root)) from django.core.wsgi import get_wsgi_application diff --git a/djangorota/manage.py b/djangorota/manage.py index 0414ee1..fc8be85 100755 --- a/djangorota/manage.py +++ b/djangorota/manage.py @@ -2,6 +2,14 @@ """Django's command-line utility for administrative tasks.""" import os import sys +import pathlib + +# Ensure the repository root is on sys.path so top-level modules (e.g. +# `rota_generator` or `rota`) are importable when running Django from +# the `djangorota/` directory. +repo_root = pathlib.Path(__file__).resolve().parents[1] +if str(repo_root) not in sys.path: + sys.path.insert(0, str(repo_root)) def main(): diff --git a/djangorota/rota/models.py b/djangorota/rota/models.py index c21ca2e..3f634e8 100644 --- a/djangorota/rota/models.py +++ b/djangorota/rota/models.py @@ -32,10 +32,26 @@ class RotaSchedule(models.Model): Returns the RotaBuilder instance. """ - try: - from rota_generator.shifts import RotaBuilder, SingleShift - except Exception: - raise RuntimeError("Unable to import rota_generator.shifts; ensure the rota package is importable") + # Try importing the project's rota generator implementation. Some setups + # provide the implementation as `rota_generator.shifts` (script/package), + # while others provide it as `rota.shifts` (library folder). Try both + # so the Django app works in either layout. + RotaBuilder = None + SingleShift = None + import importlib + for mod_name in ("rota_generator.shifts", "rota.shifts"): + try: + mod = importlib.import_module(mod_name) + RotaBuilder = getattr(mod, "RotaBuilder") + SingleShift = getattr(mod, "SingleShift") + break + except Exception: + continue + if RotaBuilder is None or SingleShift is None: + raise RuntimeError( + "Unable to import rota generator (tried 'rota_generator.shifts' and 'rota.shifts'); " + "ensure the rota package is importable and contains RotaBuilder/SingleShift" + ) # compute weeks_to_rota (integer number of weeks) delta = self.end_date - self.start_date diff --git a/djangorota/rota/views.py b/djangorota/rota/views.py index 04e7073..0b44763 100644 --- a/djangorota/rota/views.py +++ b/djangorota/rota/views.py @@ -38,14 +38,76 @@ def rota_create(request): def worker_create(request): + # Support both full-page and HTMX modal flows. If the request comes from + # HTMX (header `HX-Request: true`) we return the modal partial so it can + # be injected into the `#modal` container. On successful HTMX POST we + # return an out-of-band swap to refresh the worker list and clear the + # modal. + is_hx = request.headers.get("HX-Request", "false").lower() == "true" + if request.method == "POST": form = WorkerForm(request.POST) if form.is_valid(): worker = form.save() + if is_hx: + # Return updated worker list and close modal + # If a rota context was supplied via querystring, we do not need it + # here because the partial will re-render from DB. + # Render worker list OOB and clear modal container + # We need a rota to render the list — try to use `rota_id` query param + # rota_id may be provided as a querystring or as a hidden form field. + rota_id = request.GET.get("rota_id") or request.POST.get("rota_id") + rota = None + if rota_id: + try: + rota = get_object_or_404(RotaSchedule, pk=int(rota_id)) + except Exception: + rota = None + + # If we created a worker and a rota_id was provided, create Assignment + if rota is not None: + from .models import Assignment + try: + Assignment.objects.create(worker=worker, rota=rota) + except Exception: + # best-effort: try using the M2M add as fallback + try: + rota.workers.add(worker) + except Exception: + pass + + if rota is not None: + 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 + # If we couldn't render the worker list (no rota), just close modal + response = HttpResponse('') + response["HX-Trigger"] = "closeModal" + return response + # Non-HTMX POST -> redirect return redirect("/") else: form = WorkerForm() + # HTMX GET: return modal partial + if is_hx: + modal_html = render_to_string( + "rota/partials/worker_form_modal.html", + {"form": form, "form_action": request.get_full_path()}, + request=request, + ) + return HttpResponse(modal_html) + + # Non-HTMX GET: full page return render(request, "rota/worker_form.html", {"form": form}) @@ -86,7 +148,7 @@ def worker_edit(request, rota_id, worker_id): # 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}, + {"form": form, "form_action": request.get_full_path(), "rota": rota}, request=request, ) return HttpResponse(modal_html)