From d8055a509c294c3e5ae2b6252d190ce95b7aba06 Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 12 Nov 2025 21:54:51 +0000 Subject: [PATCH] Enhance user search widget with wildcard support and field-specific queries; improve logging in bulk add groups --- generic/views.py | 91 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 13 deletions(-) diff --git a/generic/views.py b/generic/views.py index 980ffbb3..885d3691 100644 --- a/generic/views.py +++ b/generic/views.py @@ -389,13 +389,18 @@ def exam_collection_bulk_add_groups(request, collection_id): """ collection = get_object_or_404(ExamCollection, pk=collection_id) + logger.debug(f"exam_collection_bulk_add_groups called with method={request.method} by user={request.user.username}") + + if request.method == "GET": + logger.debug(f"exam_collection_bulk_add_groups GET request by user={request.user.username}") form = ExamCollectionBulkAddGroupsForm(user=request.user) return render(request, "generic/partials/examcollection_bulk_add_groups_form.html", {"form": form, "collection": collection}) # POST form = ExamCollectionBulkAddGroupsForm(request.POST, user=request.user) if not form.is_valid(): + logger.debug(f"exam_collection_bulk_add_groups form invalid: {form.errors}") return render(request, "generic/partials/examcollection_bulk_add_groups_form.html", {"form": form, "collection": collection}) cid_groups = form.cleaned_data.get("cid_user_groups") @@ -5937,31 +5942,91 @@ def user_search_widget(request): Returns a small HTML fragment listing up to 10 matching users. Each result will call `addUserToField(field, id, text)` when clicked. """ - q = (request.GET.get("q") or "").strip() + import re + + q_raw = (request.GET.get("q") or "").strip() field = request.GET.get("field") or request.GET.get("row") - if not q: + if not q_raw: return HttpResponse("") - users_qs = User.objects.filter( - Q(first_name__icontains=q) - | Q(last_name__icontains=q) - | Q(email__icontains=q) - | Q(username__icontains=q) - ) + # Support wildcard '*' (return many results) and field-scoped queries like 'name:smith' or 'email:foo' + q = q_raw - # optional grade filter (UserProfile.grade FK) - grade = request.GET.get('grade') + # If user explicitly asks for wildcard, return an unfiltered queryset (limited) + if q in ("*", "%"): + users_qs = User.objects.all() + else: + # field-specific search syntax: field:term + m = re.match(r"^(?P\w+):(?P.+)$", q) + if m: + f = m.group("field").lower() + term = m.group("term").strip() + if term in ("*", "%"): + users_qs = User.objects.all() + else: + if f in ("name", "fullname", "full_name"): + users_qs = User.objects.filter( + Q(first_name__icontains=term) | Q(last_name__icontains=term) + ) + elif f in ("first", "first_name"): + users_qs = User.objects.filter(first_name__icontains=term) + elif f in ("last", "last_name"): + users_qs = User.objects.filter(last_name__icontains=term) + elif f == "email": + users_qs = User.objects.filter(email__icontains=term) + elif f == "username": + users_qs = User.objects.filter(username__icontains=term) + elif f in ("id", "pk"): + try: + users_qs = User.objects.filter(pk=int(term)) + except Exception: + users_qs = User.objects.none() + elif f == "grade": + # allow grade name or id + try: + gid = int(term) + users_qs = User.objects.filter(userprofile__grade_id=gid) + except Exception: + users_qs = User.objects.filter(userprofile__grade__name__icontains=term) + else: + # unknown field: fallback to general search + users_qs = User.objects.filter( + Q(first_name__icontains=term) + | Q(last_name__icontains=term) + | Q(email__icontains=term) + | Q(username__icontains=term) + ) + else: + # default substring search (icontains) + users_qs = User.objects.filter( + Q(first_name__icontains=q) + | Q(last_name__icontains=q) + | Q(email__icontains=q) + | Q(username__icontains=q) + ) + + # optional grade filter (UserProfile.grade FK) still applies and will narrow results + grade = request.GET.get("grade") if grade: try: users_qs = users_qs.filter(userprofile__grade_id=int(grade)) except Exception: pass - users_qs = users_qs.order_by("last_name", "first_name")[:10] + # Limit results to avoid returning too many rows; wildcard returns up to 50 + limit = 50 if q in ("*", "%") or q_raw in ("*", "%") else 10 + users_qs = users_qs.order_by("last_name", "first_name")[:limit] - results = [{"id": u.pk, "text": f"{u.get_full_name() or u.username} - {u.email or ''}"} for u in users_qs] + results = [ + {"id": u.pk, "text": f"{u.get_full_name() or u.username} - {u.email or ''}"} + for u in users_qs + ] - return render(request, "generic/partials/user_search_widget_results.html", {"results": results, "field": field, "q": q}) + return render( + request, + "generic/partials/user_search_widget_results.html", + {"results": results, "field": field, "q": q_raw}, + ) @login_required