From 77edc17732b3a15926a445b7082a6601673af633 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 15 Dec 2025 12:09:51 +0000 Subject: [PATCH] Refactor CID group view: enhance user display with improved layout, add help section, and implement user sorting functionality --- .../generic/cid_group_view_detail.html | 261 +++++++++--------- .../templates/generic/exam_users_edit.html | 36 +++ 2 files changed, 174 insertions(+), 123 deletions(-) diff --git a/generic/templates/generic/cid_group_view_detail.html b/generic/templates/generic/cid_group_view_detail.html index 2a470090..644a9c29 100755 --- a/generic/templates/generic/cid_group_view_detail.html +++ b/generic/templates/generic/cid_group_view_detail.html @@ -1,142 +1,157 @@ {% extends 'generic/cid_group_base.html' %} {% block content %} -

Group: {{group.name}}

- -
- Help -

This page shows an overview of the candidates and exams that are associated with the group.

- -

Once a candidate has been added to a group and the group has been added to an exam individual candidates can be allocated to the exam. Please note: simply adding the group to the exam will not automatically give the group candidates access to that exam, they will have to be added individually/as a group.

-
- -

Users

-

Count: {{users|length}}

- - {% if users %} - - - - {% if group_type == "cid" %} - - {% else %} - - {% endif %} - - {% for user in users %} - {% if group_type == "cid" %} - - {% else %} - - {% endif %} - {% endfor %} -
NameEmailCIDPasscode
User
{{user.name}}{{user.email}}{{user.cid}}{{user.passcode}}
{{user.username}}
- {% else %} - This group has no users. - - {% if group_type == "cid" %} - They can be created (or added) here - {% endif %} - {% endif %} - -
-
- Add users by email -
-
- {% csrf_token %} - - -
-
+
+
+
+
+

Group: {{ group.name }}

+
This page shows an overview of the candidates and exams associated with the group.
+
+
+
Users in group
+
{{ users|length }}
+
-
+ +
+
+ Help +
+ Once a candidate has been added to a group and the group has been added to an exam individual candidates can be allocated to the exam. Adding a group to an exam does not automatically give group members access — they must be added. +
+
+
+
-

Exams

- The group is currently associated with the following exams. This allows the users within the group to be easily added to the exam. It will not automatically give members of the group access to the exam. - {% if group_type == "cid" %} - Add more here. - {% else %} - Add more here. - {% endif %} -

It is possible to quickly add or remove all group candidates from an exam by clicking the buttons below. This will only affect the {{users|length}} candidates in this group. You will need to refresh the page to see updated candidate counts.

- {% with group.GetGroupExams as exam_map %} - {% for key, value in exam_map.items %} -

{{key}}

-
- {% for exam in value %} -
- {{exam}} - {% if group_type == "cid" %} - Edit candidates -
- - - - -
- {% else %} - Edit candidates -
- - - - -
- {% endif %} - - -
+
+
+

Users

+ {% if users %} +
    + {% for user in users %} +
  • {% if group_type == "cid" %} - (Cid candidate count: {{exam.valid_cid_users.count}}) +
    +
    +
    {{ user.name }}
    +
    {{ user.email }}
    +
    +
    +
    CID: {{ user.cid }}
    +
    Passcode: {{ user.passcode }}
    +
    +
    {% else %} - (User candidate count: {{exam.valid_user_users.count}}) +
    +
    {{ user.username }}
    +
    {{ user.email }}
    +
    {% endif %} -
+ + {% endfor %} + + {% else %} +

This group has no users. + {% if group_type == "cid" %} + You can create or add users here. + {% endif %} +

+ {% endif %} +
+
+ +
+
+

Add users by email

+
+ {% csrf_token %} +
+ +
+ +
+
+
+
+ +
+
+

Exams

+
The group is associated with these exams. Adding the group to an exam helps add members quickly but does not grant automatic access.
+
+ {% if group_type == "cid" %} + Add more here. + {% else %} + Add more here. + {% endif %} +
+ + {% with group.GetGroupExams as exam_map %} + {% for key, value in exam_map.items %} +
{{ key }}
+
+ {% for exam in value %} +
+
+ {{ exam }} +
{{ exam }}
+
+ +
+ {% if group_type == "cid" %} + Edit candidates + {% else %} + Edit candidates + {% endif %} +
+ +
+
+ {% if group_type == "cid" %} +
+ + + + +
+ {% else %} +
+ + + + +
+ {% endif %} +
+
+ +
+ {% if group_type == "cid" %} + (Cid candidate count: {{ exam.valid_cid_users.count }}) + {% else %} + (User candidate count: {{ exam.valid_user_users.count }}) + {% endif %} +
+
+ {% endfor %}
{% endfor %} -
- {% endfor %} - {% endwith %} + {% endwith %} +
+
{% endblock %} {% block js %} {% endblock %} \ No newline at end of file diff --git a/generic/templates/generic/exam_users_edit.html b/generic/templates/generic/exam_users_edit.html index 7e12645d..a3cff93c 100644 --- a/generic/templates/generic/exam_users_edit.html +++ b/generic/templates/generic/exam_users_edit.html @@ -23,6 +23,15 @@
+
+
Sort by:
+
+ + + +
+
+ {% include 'generic/partials/user_user_list.html' with exam=exam current_user_users=current_user_users available_user_users=available_user_users %} @@ -72,6 +81,33 @@ // Delegated click handler for per-item toggle buttons (HTMX preferred) document.body.addEventListener('click', function(e){ const btn = e.target.closest && e.target.closest('.toggle-btn'); if(!btn) return; const form = btn.closest('form'); if(!form) return; e.preventDefault(); setLoading(btn, true); if(window.htmx && typeof htmx.submit === 'function'){ htmx.submit(form); return; } (async function(){ try{ const url = form.action || form.getAttribute('hx-post'); const fd = new FormData(form); const csrf = document.querySelector('input[name="csrfmiddlewaretoken"]')?.value; const headers = { 'HX-Request': 'true' }; if(csrf) headers['X-CSRFToken'] = csrf; const resp = await fetch(url, { method: 'POST', body: fd, headers: headers, credentials: 'same-origin' }); const text = await resp.text(); try{ const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); const newLi = doc.querySelector('li'); const oldLi = form.closest('li'); if(newLi && oldLi){ oldLi.replaceWith(newLi); } const trigger = resp.headers.get('HX-Trigger'); if (trigger){ try{ const detail = JSON.parse(trigger); for(const k in detail){ document.body.dispatchEvent(new CustomEvent(k, { detail: detail[k] })); } }catch(e){} } }catch(err){ console.error('toggle parse/swap failed', err); } }catch(err){ console.error('toggle request failed', err); } finally{ setLoading(btn, false); } })(); }); + // Sorting for user list (scoped to this card) + (function(){ + const sortGroup = document.querySelector('.user-sort-group'); + if(!sortGroup) return; + let userSort = { key: 'cid', dir: 'asc' }; + function updateUserCarets(){ + const buttons = sortGroup.querySelectorAll('.sort-btn'); + buttons.forEach(function(b){ const key = b.dataset.key; const caret = b.querySelector('.sort-caret'); if(caret) caret.textContent = (userSort.key === key) ? (userSort.dir === 'asc' ? ' ▲' : ' ▼') : ''; }); + } + function getUserValue(li, key){ + if(!li) return ''; + if(key === 'cid') return (li.dataset.username || '').toString().toLowerCase(); + return (li.dataset[key] || '').toString().toLowerCase(); + } + function sortUserList(key){ + const ul = document.getElementById('user-users-list'); if(!ul) return; + const items = Array.from(ul.children); + const dir = (userSort.key === key && userSort.dir === 'asc') ? 'desc' : 'asc'; + items.sort(function(a,b){ const va = getUserValue(a,key); const vb = getUserValue(b,key); if(va < vb) return dir === 'asc' ? -1 : 1; if(va > vb) return dir === 'asc' ? 1 : -1; return 0; }); + items.forEach(function(it){ ul.appendChild(it); }); + userSort = { key: key, dir: dir }; + updateUserCarets(); + } + sortGroup.querySelectorAll('.sort-btn').forEach(function(b){ b.addEventListener('click', function(){ sortUserList(b.dataset.key); }); }); + updateUserCarets(); + })(); + });