Add bulk update functionality for trainees from spreadsheet input
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user