diff --git a/generic/templates/generic/trainees.html b/generic/templates/generic/trainees.html index 67471066..86253770 100644 --- a/generic/templates/generic/trainees.html +++ b/generic/templates/generic/trainees.html @@ -11,6 +11,7 @@ {% endfor %} (Add trainee) (Bulk create) + (Bulk update)
Paste the spreadsheet rows below. Headings like "Year 1 - Group 1" are used to set the grade (Year N -> STN). Rows should contain "Name
| Raw | +Name | +Grade | +Matched user | +Supervisor text | +Matched 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 %} | +
Updated: {{ report.updated }}; Skipped (no user matched): {{ report.skipped }}
+{% endif %} + +{% endblock %} diff --git a/generic/views.py b/generic/views.py index 427a037c..389b8974 100644 --- a/generic/views.py +++ b/generic/views.py @@ -4254,6 +4254,121 @@ def trainees(request, grade: None | str = None): return render(request, "generic/trainees.html", context) +@user_is_cid_user_manager +def trainees_bulk_update_from_spreadsheet(request): + """Bulk update trainees based on pasted spreadsheet-like data. + + The view works in two stages: + - 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 + + preview = [] + report = None + + if request.method == "POST": + action = request.POST.get("action", "preview") + data = request.POST.get("data", "") + + lines = [l.strip() for l in data.splitlines()] + + current_grade = None + + for line in lines: + if not line: + continue + + # detect grade headings like 'Year 1 - Group 1' -> ST1 + m = re.search(r"Year\s*(\d+)", line, re.IGNORECASE) + if m: + num = m.group(1) + current_grade = f"ST{num}" + continue + + # split columns by tab or two+ spaces + parts = re.split(r"\t| {2,}", line) + if len(parts) < 2: + # maybe single-space separated; try splitting on tab or single tab + parts = [p.strip() for p in line.split("\t") if p.strip()] + + if not parts: + continue + + name_part = parts[0].strip() + supervisor_part = parts[1].strip() if len(parts) > 1 else "" + + # normalize name (remove parenthetical annotations) + norm_name = re.sub(r"\s*\([^)]*\)", "", name_part).strip() + # split into tokens; assume first token is first name, last token is last name + tokens = [t for t in re.split(r"\s+", norm_name) if t] + first = tokens[0] if tokens else "" + last = tokens[-1] if tokens else "" + + matched_user = None + if first and last: + 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() + + matched_supervisor = None + if supervisor_part: + sup_qs = Supervisor.objects.filter(name__icontains=supervisor_part) + if sup_qs.exists(): + matched_supervisor = sup_qs.first() + + preview.append( + { + "raw": line, + "name": name_part, + "norm_name": norm_name, + "first": first, + "last": last, + "supervisor_text": supervisor_part, + "matched_user": matched_user, + "matched_supervisor": matched_supervisor, + "grade": current_grade, + } + ) + + if action == "apply": + updated = 0 + skipped = 0 + for row in preview: + user = row.get("matched_user") + if not user: + skipped += 1 + continue + + profile = user.userprofile + + # apply grade if present + grade_name = row.get("grade") + if grade_name: + grade_obj, _ = UserGrades.objects.get_or_create(name=grade_name) + profile.grade = grade_obj + + # apply supervisor if matched + sup = row.get("matched_supervisor") + if sup: + profile.supervisor = sup + + profile.save() + updated += 1 + + report = {"updated": updated, "skipped": skipped} + + return render( + request, + "generic/trainees_bulk_update.html", + {"preview": preview, "report": report}, + ) + + def create_trainee(request, context=None): return create_user(request, context, trainee=True) diff --git a/rad/urls.py b/rad/urls.py index 8a522fc9..3f892463 100644 --- a/rad/urls.py +++ b/rad/urls.py @@ -74,6 +74,9 @@ urlpatterns = [ path( "accounts/trainees/