Add worker import functionality with CSV upload and text input options

This commit is contained in:
Ross
2025-12-17 18:04:53 +00:00
parent dadf035627
commit f4ab0be347
4 changed files with 156 additions and 1 deletions
+110
View File
@@ -376,6 +376,116 @@ def worker_create(request):
return render(request, "rota/worker_form.html", {"form": form})
@require_http_methods(["GET", "POST"])
def worker_import(request, rota_id=None):
"""HTMX endpoint: import workers from CSV or pasted text.
Accepts a file upload named 'file' (CSV) or a textarea 'workers_text'.
CSV columns supported: name,email,site,grade,fte (headers optional).
When `rota_id` is provided, the created workers will be assigned to
that rota.
"""
is_hx = request.headers.get("HX-Request", "false").lower() == "true"
rota = None
if rota_id is not None:
try:
rota = get_object_or_404(RotaSchedule, pk=rota_id)
except Exception:
rota = None
if request.method == "GET":
# render modal partial
return render(request, "rota/partials/worker_import_modal.html", {"rota": rota})
# POST: parse uploaded file or textarea
created = []
errors = []
assign_to_rota = request.POST.get("assign_to_rota") == "1"
import csv
try:
upload = request.FILES.get("file")
if upload:
# decode bytes to text
try:
text = upload.read().decode("utf-8")
except Exception:
text = upload.read().decode("latin-1")
else:
text = request.POST.get("workers_text", "")
# Normalize lines: if simple newline-separated names/emails, make a CSV
reader = csv.DictReader(text.splitlines())
rows = list(reader)
if not rows:
# Try parsing as simple CSV without headers
reader2 = csv.reader([line for line in text.splitlines() if line.strip()])
for r in reader2:
# Map positional columns: name,email,site,grade,fte
while len(r) < 5:
r.append("")
rows.append({"name": r[0].strip(), "email": r[1].strip(), "site": r[2].strip(), "grade": r[3].strip(), "fte": r[4].strip()})
from .models import Assignment
for i, row in enumerate(rows):
try:
# normalize keys lower-case
if not isinstance(row, dict):
continue
r = {k.strip().lower(): (v if v is not None else "") for k, v in row.items()}
name = r.get("name") or r.get("full_name") or r.get("username")
if not name or str(name).strip() == "":
errors.append(f"Row {i+1}: missing name")
continue
email = r.get("email") or ""
site = r.get("site") or ""
try:
grade = int(r.get("grade")) if r.get("grade") not in (None, "") else None
except Exception:
grade = None
try:
fte_raw = r.get("fte")
if fte_raw in (None, ""):
fte = 1.0
else:
fte = float(fte_raw)
except Exception:
fte = 1.0
w = Worker.objects.create(name=name.strip(), email=email.strip(), site=site.strip(), grade=grade, fte=fte)
created.append(w)
if assign_to_rota and rota is not None:
try:
Assignment.objects.create(worker=w, rota=rota)
except Exception:
try:
rota.workers.add(w)
except Exception:
pass
except Exception as exc:
errors.append(f"Row {i+1}: {exc}")
except Exception as exc:
errors.append(str(exc))
# If HTMX, return updated worker list partial and close modal
if is_hx and rota is not None:
worker_list_html = render_to_string(
"rota/partials/worker_list.html",
{"rota": rota},
request=request,
)
# Return both new worker list and a trigger to close modal
resp = f'<div id="worker-list" hx-swap-oob="true">{worker_list_html}</div>'
response = HttpResponse(resp)
response["HX-Trigger"] = "closeModal"
return response
# Non-HTMX: redirect back to rota detail with message (flash messages not added here)
return redirect("rota:rota_detail", rota_id=rota.id if rota is not None else None)
def worker_detail(request, worker_id):
worker = get_object_or_404(Worker, pk=worker_id)
if request.method == "POST":