From 4dc07b1860a03237232787c0bef854a73ca2546f Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 27 Oct 2025 14:03:21 +0000 Subject: [PATCH] Refactor trainees bulk update logic to improve user matching criteria and enhance clarity in code comments --- .../generic/trainees_bulk_update.html | 82 +++++++++---------- generic/views.py | 21 ++++- 2 files changed, 59 insertions(+), 44 deletions(-) diff --git a/generic/templates/generic/trainees_bulk_update.html b/generic/templates/generic/trainees_bulk_update.html index b43fdf36..109a97d0 100644 --- a/generic/templates/generic/trainees_bulk_update.html +++ b/generic/templates/generic/trainees_bulk_update.html @@ -1,50 +1,50 @@ {% 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 %} diff --git a/generic/views.py b/generic/views.py index 389b8974..edba078d 100644 --- a/generic/views.py +++ b/generic/views.py @@ -4262,7 +4262,6 @@ def trainees_bulk_update_from_spreadsheet(request): - Preview: parse the pasted text and show rows with any matched user / supervisor - Apply: apply grade and supervisor updates for matched users """ - logger.debug("Trainees bulk update called") import re @@ -4309,11 +4308,27 @@ def trainees_bulk_update_from_spreadsheet(request): matched_user = None if first and last: + # Prefer an exact first+last match qs = User.objects.filter(first_name__iexact=first, last_name__iexact=last) - if not qs.exists(): - qs = User.objects.filter(last_name__iexact=last) if qs.exists(): matched_user = qs.first() + else: + # Fallback: only match by last name when the last-name query returns + # a single candidate AND the candidate's first name reasonably + # matches the provided first token. This avoids ambiguous matches + # (e.g. Maria vs Stephanie both 'Bailey'). We accept a match when + # the candidate first name startswith the first 3 chars of the + # provided first token, or contains it as a substring (case-ins). + candidates = User.objects.filter(last_name__iexact=last) + if candidates.count() == 1: + candidate = candidates.first() + cand_first = (candidate.first_name or "").strip() + if cand_first: + # take at most 3 chars for prefix comparison + prefix = first[:3].lower() + cand_first_low = cand_first.lower() + if cand_first_low.startswith(prefix) or first.lower() in cand_first_low: + matched_user = candidate matched_supervisor = None if supervisor_part: