Add user search functionality for manual trainee matching in bulk update
This commit is contained in:
@@ -33,7 +33,21 @@
|
|||||||
<td><pre style="margin:0">{{ r.raw }}</pre></td>
|
<td><pre style="margin:0">{{ r.raw }}</pre></td>
|
||||||
<td>{{ r.norm_name }}</td>
|
<td>{{ r.norm_name }}</td>
|
||||||
<td>{{ r.grade }}</td>
|
<td>{{ r.grade }}</td>
|
||||||
<td>{% if r.matched_user %}<a href="{% url 'account_profile' r.matched_user.username %}">{{ r.matched_user.first_name }} {{ r.matched_user.last_name }}</a>{% else %}<span style="color:red">No match</span>{% endif %}</td>
|
<td>
|
||||||
|
{% if r.matched_user %}
|
||||||
|
<a href="{% url 'account_profile' r.matched_user.username %}">{{ r.matched_user.first_name }} {{ r.matched_user.last_name }}</a>
|
||||||
|
{% else %}
|
||||||
|
<div>
|
||||||
|
<span style="color:red">No match</span>
|
||||||
|
<div>
|
||||||
|
<input type="text" id="user-search-input-{{ r.row_index }}" placeholder="Search users..." style="width:220px" />
|
||||||
|
<button type="button" onclick="searchUsers({{ r.row_index }})">Search</button>
|
||||||
|
</div>
|
||||||
|
<div id="user-results-{{ r.row_index }}"></div>
|
||||||
|
<input type="hidden" name="manual_user_{{ r.row_index }}" id="manual_user_{{ r.row_index }}" />
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>{{ r.supervisor_text }}</td>
|
<td>{{ r.supervisor_text }}</td>
|
||||||
<td>{% if r.matched_supervisor %}<a href="{% url 'generic:supervisor_detail' r.matched_supervisor.pk %}">{{ r.matched_supervisor.name }}</a>{% else %}<span style="color:red">No match</span>{% endif %}</td>
|
<td>{% if r.matched_supervisor %}<a href="{% url 'generic:supervisor_detail' r.matched_supervisor.pk %}">{{ r.matched_supervisor.name }}</a>{% else %}<span style="color:red">No match</span>{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -48,3 +62,56 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
<script>
|
||||||
|
function searchUsers(rowIndex) {
|
||||||
|
const input = document.getElementById('user-search-input-' + rowIndex);
|
||||||
|
const q = input ? input.value.trim() : '';
|
||||||
|
const resultsDiv = document.getElementById('user-results-' + rowIndex);
|
||||||
|
const hiddenInput = document.getElementById('manual_user_' + rowIndex);
|
||||||
|
resultsDiv.innerHTML = 'Searching...';
|
||||||
|
fetch("{% url 'generic:user_search' %}?q=" + encodeURIComponent(q))
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
resultsDiv.innerHTML = '';
|
||||||
|
if (!data || !data.results || data.results.length === 0) {
|
||||||
|
resultsDiv.innerHTML = '<div><em>No users found</em></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ul = document.createElement('ul');
|
||||||
|
ul.style.listStyle = 'none';
|
||||||
|
ul.style.padding = '0';
|
||||||
|
data.results.forEach(function(item){
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.style.margin = '4px 0';
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.textContent = 'Select';
|
||||||
|
btn.style.marginLeft = '8px';
|
||||||
|
btn.onclick = function(){
|
||||||
|
hiddenInput.value = item.id;
|
||||||
|
resultsDiv.innerHTML = '<div>Selected: ' + escapeHtml(item.text) + '</div>';
|
||||||
|
};
|
||||||
|
li.innerHTML = '<strong>' + escapeHtml(item.text) + '</strong>';
|
||||||
|
li.appendChild(btn);
|
||||||
|
ul.appendChild(li);
|
||||||
|
});
|
||||||
|
resultsDiv.appendChild(ul);
|
||||||
|
})
|
||||||
|
.catch(function(err){
|
||||||
|
resultsDiv.innerHTML = '<div style="color:red">Error searching users</div>';
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(unsafe) {
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/\"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ urlpatterns = [
|
|||||||
views.SupervisorAutocomplete.as_view(model=Supervisor),
|
views.SupervisorAutocomplete.as_view(model=Supervisor),
|
||||||
name="supervisor-autocomplete",
|
name="supervisor-autocomplete",
|
||||||
),
|
),
|
||||||
|
path("user-search/", views.user_search, name="user_search"),
|
||||||
path(
|
path(
|
||||||
"cids/manage/<int:pk>/update", views.CidUserUpdate.as_view(), name="update_cid"
|
"cids/manage/<int:pk>/update", views.CidUserUpdate.as_view(), name="update_cid"
|
||||||
),
|
),
|
||||||
|
|||||||
+33
-3
@@ -3068,12 +3068,11 @@ class GenericViewBase:
|
|||||||
Accepts POST or GET parameters:
|
Accepts POST or GET parameters:
|
||||||
- category: integer primary key of category (optional)
|
- category: integer primary key of category (optional)
|
||||||
- status: one of the status codes (AC, OD, ER, RJ, IP), or 'UNREVIEWED' or 'ANY'
|
- status: one of the status codes (AC, OD, ER, RJ, IP), or 'UNREVIEWED' or 'ANY'
|
||||||
If not supplied, the default behavior is to return only unreviewed questions.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Read filters
|
# Read filters
|
||||||
category = request.POST.get("category") or request.GET.get("category")
|
category = request.POST.get("category") or request.GET.get("category")
|
||||||
status = request.POST.get("status") or request.GET.get("status") or "UNREVIEWED"
|
status = request.POST.get("status") or request.GET.get("status") or "ANY"
|
||||||
# How many matching questions to skip (useful for Skip button). Expected integer >= 0.
|
# How many matching questions to skip (useful for Skip button). Expected integer >= 0.
|
||||||
try:
|
try:
|
||||||
skip = int(request.POST.get("skip") or request.GET.get("skip") or 0)
|
skip = int(request.POST.get("skip") or request.GET.get("skip") or 0)
|
||||||
@@ -4277,7 +4276,7 @@ def trainees_bulk_update_from_spreadsheet(request):
|
|||||||
|
|
||||||
current_grade = None
|
current_grade = None
|
||||||
|
|
||||||
for line in lines:
|
for idx, line in enumerate(lines):
|
||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -4340,6 +4339,7 @@ def trainees_bulk_update_from_spreadsheet(request):
|
|||||||
preview.append(
|
preview.append(
|
||||||
{
|
{
|
||||||
"raw": line,
|
"raw": line,
|
||||||
|
"row_index": idx,
|
||||||
"name": name_part,
|
"name": name_part,
|
||||||
"norm_name": norm_name,
|
"norm_name": norm_name,
|
||||||
"first": first,
|
"first": first,
|
||||||
@@ -4355,7 +4355,21 @@ def trainees_bulk_update_from_spreadsheet(request):
|
|||||||
updated = 0
|
updated = 0
|
||||||
skipped = 0
|
skipped = 0
|
||||||
for row in preview:
|
for row in preview:
|
||||||
|
# Check for a manual override from the submitted form
|
||||||
|
manual_key = f"manual_user_{row.get('row_index')}"
|
||||||
|
manual_val = request.POST.get(manual_key)
|
||||||
|
|
||||||
|
user = None
|
||||||
|
if manual_val:
|
||||||
|
try:
|
||||||
|
user = User.objects.get(pk=int(manual_val))
|
||||||
|
except Exception:
|
||||||
|
user = None
|
||||||
|
|
||||||
|
# Fall back to automatic match
|
||||||
|
if not user:
|
||||||
user = row.get("matched_user")
|
user = row.get("matched_user")
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
@@ -4385,6 +4399,22 @@ def trainees_bulk_update_from_spreadsheet(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@user_is_cid_user_manager
|
||||||
|
def user_search(request):
|
||||||
|
"""Simple JSON endpoint to search users by name/email for manual matching."""
|
||||||
|
q = request.GET.get("q", "").strip()
|
||||||
|
results = []
|
||||||
|
if q:
|
||||||
|
qs = User.objects.filter(
|
||||||
|
Q(first_name__icontains=q) | Q(last_name__icontains=q) | Q(email__icontains=q)
|
||||||
|
).order_by("last_name", "first_name")[:50]
|
||||||
|
|
||||||
|
for u in qs:
|
||||||
|
results.append({"id": u.pk, "text": f"{u.first_name} {u.last_name} ({u.email})"})
|
||||||
|
|
||||||
|
return JsonResponse({"results": results})
|
||||||
|
|
||||||
|
|
||||||
def create_trainee(request, context=None):
|
def create_trainee(request, context=None):
|
||||||
return create_user(request, context, trainee=True)
|
return create_user(request, context, trainee=True)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user