From 668007b89955aad51b4e939e6525c62915208ec0 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 27 Oct 2025 14:32:03 +0000 Subject: [PATCH] Implement user search functionality with HTMX support for manual user selection in bulk updates --- .../generic/partials/user_search_results.html | 12 ++ .../generic/trainees_bulk_update.html | 184 ++++++++---------- generic/views.py | 10 +- 3 files changed, 101 insertions(+), 105 deletions(-) create mode 100644 generic/templates/generic/partials/user_search_results.html diff --git a/generic/templates/generic/partials/user_search_results.html b/generic/templates/generic/partials/user_search_results.html new file mode 100644 index 00000000..4ce73f20 --- /dev/null +++ b/generic/templates/generic/partials/user_search_results.html @@ -0,0 +1,12 @@ +{% if results %} + +{% else %} +
No users found
+{% endif %} diff --git a/generic/templates/generic/trainees_bulk_update.html b/generic/templates/generic/trainees_bulk_update.html index 10b828ce..60d2bd8e 100644 --- a/generic/templates/generic/trainees_bulk_update.html +++ b/generic/templates/generic/trainees_bulk_update.html @@ -1,117 +1,93 @@ {% extends 'generic/base.html' %} {% block content %} -

Bulk update trainees (paste spreadsheet text)

+

Bulk update trainees (paste spreadsheet text)

-

Paste the spreadsheet rows below. Headings like "Year 1 - Group 1" are used to set the grade (Year N -> STN). Rows should contain "Name Supervisor".

+

Paste the spreadsheet rows below. Headings like "Year 1 - Group 1" are used to set the grade (Year N -> STN). Rows should contain "Name Supervisor".

-
- {% csrf_token %} - -
- - -
-
+
+ {% csrf_token %} + +
+ + +
+
- {% if preview %} -

Preview ({{ preview|length }} rows)

- - - - - - - - - - - - - {% for r in preview %} - - - - - - - - - {% endfor %} - -
RawNameGradeMatched userSupervisor textMatched supervisor
{{ r.raw }}
{{ r.norm_name }}{{ r.grade }} - {% if r.matched_user %} - {{ r.matched_user.first_name }} {{ r.matched_user.last_name }} - {% else %} -
- No match -
- - -
-
- -
- {% endif %} -
{{ r.supervisor_text }}{% if r.matched_supervisor %}{{ r.matched_supervisor.name }}{% else %}No match{% endif %}
- {% endif %} +{% if preview %} +

Preview ({{ preview|length }} rows)

+ + + + + + + + + + + + + {% for r in preview %} + + + + + + + + + {% endfor %} + +
RawNameGradeMatched userSupervisor textMatched supervisor
{{ r.raw }}
{{ r.norm_name }}{{ r.grade }} + {% if r.matched_user %} + {{ r.matched_user.first_name }} {{ r.matched_user.last_name }} + {% else %} +
+ No match +
+
+ + + +
+
+
+ +
+ {% endif %} +
{{ r.supervisor_text }}{% if r.matched_supervisor %}{{ r.matched_supervisor.name }}{% else %}No match{% endif %}
+{% endif %} - {% if report %} -

Apply report

-

Updated: {{ report.updated }}; Skipped (no user matched): {{ report.skipped }}

- {% endif %} +{% if report %} +

Apply report

+

Updated: {{ report.updated }}; Skipped (no user matched): {{ report.skipped }}

+{% endif %} {% endblock %} {% block extra_scripts %} - +function escapeHtml(unsafe) { + if (!unsafe) return ''; + return String(unsafe) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\"/g, """) + .replace(/'/g, "'"); +} + {% endblock %} diff --git a/generic/views.py b/generic/views.py index e3512665..4efa8cc3 100644 --- a/generic/views.py +++ b/generic/views.py @@ -4412,7 +4412,15 @@ def user_search(request): for u in qs: results.append({"id": u.pk, "text": f"{u.first_name} {u.last_name} ({u.email})"}) - return JsonResponse({"results": results}) + # If this is an HTMX request (or a row param was provided) render an HTML fragment + # that can be swapped into the preview row's results container. Otherwise return JSON. + row = request.GET.get("row") + if request.headers.get("HX-Request") == "true" or row is not None: + # Render a small partial that lists results with Select buttons that call + # the setManualUser(row, id, text) function in the parent page. + return render(request, "generic/partials/user_search_results.html", {"results": results, "row": int(row) if row is not None else None}) + + return JsonResponse({"results": results}) def create_trainee(request, context=None):