Refactor CID users display: enhance layout with card structure, add sorting functionality, and improve toggle button interactions

This commit is contained in:
Ross
2025-12-15 11:21:44 +00:00
parent 4db1f2ca1a
commit 7a4cf3627d
+129 -70
View File
@@ -1,97 +1,156 @@
{% include "generic/exam_candidate_headers.html" %} {% include "generic/exam_candidate_headers.html" %}
<h3>CID Users</h3>
{% if groups %}
Users from the following groups are available: {% for group in groups %}<a href='{{group.get_absolute_url}}'>{{group}}</a>, {% endfor %} .
{% else %}
This exam is not associated with any user groups. <a href="{% url exam.app_name|add:':exam_groups_edit' exam.pk %}">Edit and add a group</a> to enable user management.
{% endif %}
<ol>
{% for cid in current_cid_users %}
<li class="current"><a href="{% url 'generic:update_cid' cid.pk %}">{{cid.cid}}</a> [{{cid.passcode}}] {{cid.name}} / <div class="card mb-2 bg-dark text-light border-0">
Email: {{cid.email}} <button class="toggle-btn" data-pk="{{cid.pk}}" data-cid="{{cid.cid}}">Toggle</button> <div class="card-body py-2">
</li> <div class="d-flex justify-content-between align-items-start mb-2">
<div>
<h5 class="card-title mb-0">CID Users</h5>
{% if groups %}
<div class="small text-muted mt-1">Users from the following groups are available:
{% for group in groups %}
<a href='{{group.get_absolute_url}}' class="badge bg-light text-dark border ms-1">{{group}}</a>
{% endfor %}
</div>
{% else %}
<div class="small text-muted mt-1">This exam is not associated with any user groups. <a href="{% url exam.app_name|add:':exam_groups_edit' exam.pk %}">Edit and add a group</a> to enable user management.</div>
{% endif %}
</div>
<div class="btn-group">
{% if current_cid_users or available_cid_users %}
<button class="btn btn-sm btn-outline-light toggle-all-btn">Toggle all</button>
{% endif %}
<button class="btn btn-sm btn-outline-success add-all-btn">Add all</button>
<button class="btn btn-sm btn-outline-danger remove-all-btn">Remove all</button>
</div>
</div>
{% endfor %} <div class="d-flex align-items-center mb-2">
{% for cid in available_cid_users %} <div class="me-auto small text-muted">Sort by:</div>
<div class="btn-group btn-group-sm" role="group" aria-label="Sort">
<button type="button" class="btn btn-sm btn-outline-light sort-btn" data-key="cid">CID <span class="sort-caret"></span></button>
<button type="button" class="btn btn-sm btn-outline-light sort-btn" data-key="name">Name <span class="sort-caret"></span></button>
<button type="button" class="btn btn-sm btn-outline-light sort-btn" data-key="email">Email <span class="sort-caret"></span></button>
</div>
</div>
<li><a href="{% url 'generic:update_cid' cid.pk %}">{{cid.cid}}</a> [{{cid.passcode}}] {{cid.name}} / <ul class="list-group list-group-flush" id="cid-users-list">
Email: {{cid.email}} <button class="toggle-btn" data-pk="{{cid.pk}}" data-cid="{{cid.cid}}">Toggle</button> {% for cid in current_cid_users %}
</li> <li class="list-group-item d-flex justify-content-between align-items-center current py-1 px-2 bg-transparent border-secondary" data-pk="{{cid.pk}}" data-cid="{{cid.cid}}" data-name="{{cid.name|escape}}" data-email="{{cid.email|escape}}">
<div class="me-2">
{% endfor %} <div class="d-flex align-items-center">
</ol> <div class="fw-bold small mb-0">{{cid.cid}} <span class="text-muted">[{{cid.passcode}}]</span></div>
<span class="badge bg-success ms-2 small">Added</span>
{% comment %} Hide the toggle button if there are no users (to add or remove) {% endcomment %} </div>
<p>{% if current_cid_users or available_cid_users %} <div class="small text-muted">{{cid.name}} &middot; {{cid.email}}</div>
<button class="toggle-all-btn">Toggle all</button> </div>
{% endif %} <div class="btn-group ms-2">
<button class="add-all-btn">Add all</button> <a href="{% url 'generic:update_cid' cid.pk %}" class="btn btn-sm btn-outline-light">Edit</a>
<button class="remove-all-btn">Remove all</button> <button class="btn btn-sm btn-outline-light toggle-btn" data-pk="{{cid.pk}}" data-cid="{{cid.cid}}">Toggle</button>
</div>
</p> </li>
{% endfor %}
{% for cid in available_cid_users %}
<li class="list-group-item d-flex justify-content-between align-items-center py-1 px-2 bg-transparent border-secondary" data-pk="{{cid.pk}}" data-cid="{{cid.cid}}" data-name="{{cid.name|escape}}" data-email="{{cid.email|escape}}">
<div class="me-2">
<div class="d-flex align-items-center">
<div class="fw-bold small mb-0">{{cid.cid}} <span class="text-muted">[{{cid.passcode}}]</span></div>
<span class="badge bg-secondary text-dark ms-2 small">Not added</span>
</div>
<div class="small text-muted">{{cid.name}} &middot; {{cid.email}}</div>
</div>
<div class="btn-group ms-2">
<a href="{% url 'generic:update_cid' cid.pk %}" class="btn btn-sm btn-outline-light">Edit</a>
<button class="btn btn-sm btn-outline-light toggle-btn" data-pk="{{cid.pk}}" data-cid="{{cid.cid}}">Toggle</button>
</div>
</li>
{% endfor %}
</ul>
</div>
</div>
<script> <script>
$(document).ready(() => { $(document).ready(() => {
// Wire buttons: Add/Remove/Toggle
$(".remove-all-btn").click((el) => { $(".remove-all-btn").click((el) => {
$(".current .toggle-btn").click(); $(".list-group-item.current .toggle-btn").each(function(){ $(this).click(); });
}); });
$(".add-all-btn").click((el) => { $(".add-all-btn").click((el) => {
$(":not(.current) .toggle-btn").click(); $(".list-group-item:not(.current) .toggle-btn").each(function(){ $(this).click(); });
}); });
$(".toggle-all-btn").click((el) => { $(".toggle-all-btn").click((el) => {
$(".toggle-btn").click(); $(".toggle-btn").each(function(){ $(this).click(); });
}); });
$(".toggle-btn").click((el) => {
let parent = $(el.target).parent() $(document).on('click', '.toggle-btn', function(ev){
const btn = ev.currentTarget;
const parentLi = $(btn).closest('li');
$.ajax({ $.ajax({
url: "{% url exam.app_name|add:':exam_json_edit' exam.pk %}", url: "{% url exam.app_name|add:':exam_json_edit' exam.pk %}",
data: { data: {
csrfmiddlewaretoken: "{{csrf_token}}", csrfmiddlewaretoken: "{{csrf_token}}",
edit_cid_user: el.target.dataset.pk, edit_cid_user: btn.dataset.pk,
add: !parent.hasClass("current"), add: !parentLi.hasClass('current'),
}, },
type: "POST", type: "POST",
dataType: "json", dataType: "json",
}) })
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly .done(function (data) {
.done(function (data) { if (data.status == "success") {
console.log("done", data); if (data.added) {
parentLi.addClass('current');
if (data.status == "success") { toastr.info(`User ${btn.dataset.cid} added to exam`)
if (data.added) {
parent.addClass("current");
toastr.info(`User ${el.target.dataset.cid} added to exam`)
} else {
parent.removeClass("current");
toastr.info(`User ${el.target.dataset.cid} removed from exam`)
}
} else { } else {
toastr.error(data.status) parentLi.removeClass('current');
toastr.info(`User ${btn.dataset.cid} removed from exam`)
} }
}) } else {
.fail(function (data) { toastr.error(data.status)
console.log("fail", data); }
toastr.error(data.responseJSON.status) })
.fail(function (data) {
}) toastr.error((data && data.responseJSON && data.responseJSON.status) ? data.responseJSON.status : 'Request failed');
.always(function () { });
})
}); });
});
}) // Sorting
let currentSort = { key: 'cid', dir: 'asc' };
function updateCarets() {
$('.sort-btn').each(function(){
const key = $(this).data('key');
const caret = $(this).find('.sort-caret');
caret.text('');
if (currentSort.key === key) caret.text(currentSort.dir === 'asc' ? ' ▲' : ' ▼');
});
}
function sortList(key) {
const $ul = $('#cid-users-list');
const items = $ul.children('li').get();
const dir = (currentSort.key === key && currentSort.dir === 'asc') ? 'desc' : 'asc';
items.sort(function(a,b){
const va = ($(a).data(key) || '').toString().toLowerCase();
const vb = ($(b).data(key) || '').toString().toLowerCase();
if (va < vb) return dir === 'asc' ? -1 : 1;
if (va > vb) return dir === 'asc' ? 1 : -1;
return 0;
});
// re-append
for (let i=0;i<items.length;i++) $ul.append(items[i]);
currentSort = { key: key, dir: dir };
updateCarets();
}
$('.sort-btn').on('click', function(){ sortList($(this).data('key')); });
// initialize caret
updateCarets();
</script> </script>
<style>
.current {
color: lightgray;
}
.current::before {
content: "[ADDED TO EXAM] - "
}
button { <style>
padding: 0; /* Compact, dark-mode adjustments */
} .card.bg-dark { background-color: #111 !important; }
.card-body { padding: .5rem !important; }
.list-group-item { padding: .35rem .5rem; font-size: .88rem; border-left: 0; border-right: 0; }
.list-group-item.current { background: rgba(255,255,255,0.03); border-left: 4px solid rgba(13,110,253,0.25); }
.list-group-item .small { display:block; color: #bfc6cf; }
.btn-group .btn { padding: .25rem .45rem; font-size: .8rem; }
.btn-outline-light { color: #e9ecef; border-color: rgba(233,236,239,0.12); }
.btn-outline-light:hover { color:#fff; background: rgba(233,236,239,0.06); }
</style> </style>