Add grade filter to user search widget and enhance logging for bulk add operations
This commit is contained in:
+39
-4
@@ -593,14 +593,29 @@ class UserUserGroupForm(ModelForm):
|
|||||||
|
|
||||||
url = reverse("generic:user_search_widget")
|
url = reverse("generic:user_search_widget")
|
||||||
|
|
||||||
|
# Build a small grade filter select
|
||||||
|
grades_options = '<option value="">All grades</option>'
|
||||||
|
try:
|
||||||
|
from generic.models import UserGrades
|
||||||
|
grades_qs = UserGrades.objects.all()
|
||||||
|
for g in grades_qs:
|
||||||
|
grades_options += f'<option value="{g.pk}">{g.name}</option>'
|
||||||
|
except Exception:
|
||||||
|
grades_options = '<option value="">All grades</option>'
|
||||||
|
|
||||||
html = f"""
|
html = f"""
|
||||||
<div class="user-search-widget">
|
<div class="user-search-widget">
|
||||||
<select name="{self.name}" id="{field_id}" multiple class="d-none">
|
<select name="{self.name}" id="{field_id}" multiple class="d-none">
|
||||||
{options_html}
|
{options_html}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
<input type="search" id="{search_id}" class="form-control form-control-sm" placeholder="Search users by name, username or email" />
|
<input type="search" id="{search_id}" class="form-control form-control-sm" placeholder="Search users by name, username or email" />
|
||||||
|
<select id="grade_filter_{self.name}" class="form-select form-select-sm" style="max-width:180px">
|
||||||
|
{grades_options}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div id="{results_id}" class="mt-2"></div>
|
<div id="{results_id}" class="mt-2"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -617,14 +632,17 @@ class UserUserGroupForm(ModelForm):
|
|||||||
const results = document.getElementById('{results_id}');
|
const results = document.getElementById('{results_id}');
|
||||||
const select = document.getElementById('{field_id}');
|
const select = document.getElementById('{field_id}');
|
||||||
const selectedList = document.getElementById('{selected_list_id}');
|
const selectedList = document.getElementById('{selected_list_id}');
|
||||||
|
const gradeSelect = document.getElementById('grade_filter_{self.name}');
|
||||||
|
|
||||||
let timeout = null;
|
let timeout = null;
|
||||||
function fetchResults(q) {{
|
function fetchResults(q) {{
|
||||||
|
const grade = gradeSelect ? gradeSelect.value : '';
|
||||||
if (!q || q.trim().length === 0) {{
|
if (!q || q.trim().length === 0) {{
|
||||||
results.innerHTML = '';
|
results.innerHTML = '';
|
||||||
return;
|
return;
|
||||||
}}
|
}}
|
||||||
fetch('{url}?field=' + encodeURIComponent('{self.name}') + '&q=' + encodeURIComponent(q))
|
const url = '{url}?field=' + encodeURIComponent('{self.name}') + '&q=' + encodeURIComponent(q) + (grade ? '&grade=' + encodeURIComponent(grade) : '');
|
||||||
|
fetch(url)
|
||||||
.then(r => r.text())
|
.then(r => r.text())
|
||||||
.then(html => {{ results.innerHTML = html; }});
|
.then(html => {{ results.innerHTML = html; }});
|
||||||
}}
|
}}
|
||||||
@@ -634,6 +652,12 @@ class UserUserGroupForm(ModelForm):
|
|||||||
timeout = setTimeout(() => fetchResults(search.value), 300);
|
timeout = setTimeout(() => fetchResults(search.value), 300);
|
||||||
}});
|
}});
|
||||||
|
|
||||||
|
gradeSelect && gradeSelect.addEventListener('change', function(e) {{
|
||||||
|
// re-run search with same query when grade changes
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => fetchResults(search.value), 50);
|
||||||
|
}});
|
||||||
|
|
||||||
window.addUserToField = function(fieldName, id, text) {{
|
window.addUserToField = function(fieldName, id, text) {{
|
||||||
const sel = document.getElementById('id_' + fieldName);
|
const sel = document.getElementById('id_' + fieldName);
|
||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
@@ -660,10 +684,21 @@ class UserUserGroupForm(ModelForm):
|
|||||||
const li = document.getElementById('selected_user_' + fieldName + '_' + id);
|
const li = document.getElementById('selected_user_' + fieldName + '_' + id);
|
||||||
if (li && li.parentNode) li.parentNode.removeChild(li);
|
if (li && li.parentNode) li.parentNode.removeChild(li);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
window.addAllFromResults = function(fieldName) {{
|
||||||
|
const list = document.getElementById('user_search_results_list_' + fieldName);
|
||||||
|
if (!list) return;
|
||||||
|
const items = list.querySelectorAll('li[data-user-id]');
|
||||||
|
items.forEach(function(it) {{
|
||||||
|
const id = it.getAttribute('data-user-id');
|
||||||
|
const text = it.getAttribute('data-user-text') || it.textContent.trim();
|
||||||
|
addUserToField(fieldName, id, text);
|
||||||
|
}});
|
||||||
|
}}
|
||||||
}})();
|
}})();
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return mark_safe(html)
|
return mark_safe(html)
|
||||||
|
|
||||||
|
|||||||
@@ -1967,6 +1967,13 @@ class ExamCollection(models.Model, AuthorMixin):
|
|||||||
if UserUserGroup is None:
|
if UserUserGroup is None:
|
||||||
raise RuntimeError("UserUserGroup model not available")
|
raise RuntimeError("UserUserGroup model not available")
|
||||||
|
|
||||||
|
# Debug: log what we received
|
||||||
|
try:
|
||||||
|
group_pks = [g.pk for g in groups] if groups is not None else []
|
||||||
|
except Exception:
|
||||||
|
group_pks = []
|
||||||
|
logger.debug("bulk_add_user_user_groups called on collection=%s with groups=%s exam_types=%s", self.pk, group_pks, exam_types)
|
||||||
|
|
||||||
# Normalize groups to a list of model instances
|
# Normalize groups to a list of model instances
|
||||||
if groups is None:
|
if groups is None:
|
||||||
return 0
|
return 0
|
||||||
@@ -1994,9 +2001,21 @@ class ExamCollection(models.Model, AuthorMixin):
|
|||||||
for exam in exams:
|
for exam in exams:
|
||||||
# Each exam model exposes a `user_user_groups` ManyToManyField
|
# Each exam model exposes a `user_user_groups` ManyToManyField
|
||||||
try:
|
try:
|
||||||
|
before_pks = set(exam.user_user_groups.values_list('pk', flat=True))
|
||||||
exam.user_user_groups.add(*groups_qs)
|
exam.user_user_groups.add(*groups_qs)
|
||||||
# Ensure any downstream signals / caches see the change
|
# Ensure any downstream signals / caches see the change
|
||||||
exam.save()
|
exam.save()
|
||||||
|
after_pks = set(exam.user_user_groups.values_list('pk', flat=True))
|
||||||
|
# log if expected group pks are not present after add
|
||||||
|
try:
|
||||||
|
expected_pks = {g.pk for g in groups_qs}
|
||||||
|
except Exception:
|
||||||
|
expected_pks = set()
|
||||||
|
if not expected_pks.issubset(after_pks):
|
||||||
|
logger.warning(
|
||||||
|
"After add, exam %s missing expected user_user_groups; before=%s after=%s expected=%s",
|
||||||
|
getattr(exam, 'pk', exam), sorted(before_pks), sorted(after_pks), sorted(expected_pks),
|
||||||
|
)
|
||||||
updated += 1
|
updated += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Failed to add user_user_groups to exam %s: %s", getattr(exam, 'pk', exam), e)
|
logger.exception("Failed to add user_user_groups to exam %s: %s", getattr(exam, 'pk', exam), e)
|
||||||
@@ -2011,6 +2030,13 @@ class ExamCollection(models.Model, AuthorMixin):
|
|||||||
if CidUserGroup is None:
|
if CidUserGroup is None:
|
||||||
raise RuntimeError("CidUserGroup model not available")
|
raise RuntimeError("CidUserGroup model not available")
|
||||||
|
|
||||||
|
# Debug: log what we received
|
||||||
|
try:
|
||||||
|
group_pks = [g.pk for g in groups] if groups is not None else []
|
||||||
|
except Exception:
|
||||||
|
group_pks = []
|
||||||
|
logger.debug("bulk_add_cid_user_groups called on collection=%s with groups=%s exam_types=%s", self.pk, group_pks, exam_types)
|
||||||
|
|
||||||
if groups is None:
|
if groups is None:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -2033,8 +2059,19 @@ class ExamCollection(models.Model, AuthorMixin):
|
|||||||
for exam in exams:
|
for exam in exams:
|
||||||
# Each exam model exposes a `cid_user_groups` ManyToManyField
|
# Each exam model exposes a `cid_user_groups` ManyToManyField
|
||||||
try:
|
try:
|
||||||
|
before_pks = set(exam.cid_user_groups.values_list('pk', flat=True))
|
||||||
exam.cid_user_groups.add(*groups_qs)
|
exam.cid_user_groups.add(*groups_qs)
|
||||||
exam.save()
|
exam.save()
|
||||||
|
after_pks = set(exam.cid_user_groups.values_list('pk', flat=True))
|
||||||
|
try:
|
||||||
|
expected_pks = {g.pk for g in groups_qs}
|
||||||
|
except Exception:
|
||||||
|
expected_pks = set()
|
||||||
|
if not expected_pks.issubset(after_pks):
|
||||||
|
logger.warning(
|
||||||
|
"After add, exam %s missing expected cid_user_groups; before=%s after=%s expected=%s",
|
||||||
|
getattr(exam, 'pk', exam), sorted(before_pks), sorted(after_pks), sorted(expected_pks),
|
||||||
|
)
|
||||||
updated += 1
|
updated += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Failed to add cid_user_groups to exam %s: %s", getattr(exam, 'pk', exam), e)
|
logger.exception("Failed to add cid_user_groups to exam %s: %s", getattr(exam, 'pk', exam), e)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% if results %}
|
{% if results %}
|
||||||
<ul class="list-unstyled mb-0">
|
<ul class="list-unstyled mb-0" id="user_search_results_list_{{ field }}">
|
||||||
{% for item in results %}
|
{% for item in results %}
|
||||||
<li class="py-1 d-flex justify-content-between align-items-center">
|
<li class="py-1 d-flex justify-content-between align-items-center" data-user-id="{{ item.id }}" data-user-text="{{ item.text|escapejs }}">
|
||||||
<div class="flex-grow-1">{{ item.text|escape }}</div>
|
<div class="flex-grow-1">{{ item.text|escape }}</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" class="btn btn-sm btn-primary" onclick="addUserToField('{{ field }}', {{ item.id }}, '{{ item.text|escapejs }}')">Add</button>
|
<button type="button" class="btn btn-sm btn-primary" onclick="addUserToField('{{ field }}', {{ item.id }}, '{{ item.text|escapejs }}')">Add</button>
|
||||||
@@ -9,6 +9,11 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% if results %}
|
||||||
|
<div class="mt-2 text-end">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-success" onclick="addAllFromResults('{{ field }}')">Add all results</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div><em>No users found</em></div>
|
<div><em>No users found</em></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
+28
-1
@@ -408,6 +408,23 @@ def exam_collection_bulk_add_groups(request, collection_id):
|
|||||||
else:
|
else:
|
||||||
exam_types = None
|
exam_types = None
|
||||||
|
|
||||||
|
# Debug: log what was submitted so we can trace why groups may not be applied
|
||||||
|
try:
|
||||||
|
cid_pks = [g.pk for g in cid_groups] if cid_groups is not None else []
|
||||||
|
except Exception:
|
||||||
|
cid_pks = []
|
||||||
|
try:
|
||||||
|
user_pks = [g.pk for g in user_groups] if user_groups is not None else []
|
||||||
|
except Exception:
|
||||||
|
user_pks = []
|
||||||
|
logger.debug(
|
||||||
|
"ExamCollection bulk-add POST: collection=%s cid_groups=%s user_groups=%s exam_types=%s",
|
||||||
|
collection.pk,
|
||||||
|
cid_pks,
|
||||||
|
user_pks,
|
||||||
|
exam_types,
|
||||||
|
)
|
||||||
|
|
||||||
total_user = 0
|
total_user = 0
|
||||||
total_cid = 0
|
total_cid = 0
|
||||||
try:
|
try:
|
||||||
@@ -5930,7 +5947,17 @@ def user_search_widget(request):
|
|||||||
| Q(last_name__icontains=q)
|
| Q(last_name__icontains=q)
|
||||||
| Q(email__icontains=q)
|
| Q(email__icontains=q)
|
||||||
| Q(username__icontains=q)
|
| Q(username__icontains=q)
|
||||||
).order_by("last_name", "first_name")[:10]
|
)
|
||||||
|
|
||||||
|
# optional grade filter (UserProfile.grade FK)
|
||||||
|
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]
|
||||||
|
|
||||||
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]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user