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" %}
<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}} /
Email: {{cid.email}} <button class="toggle-btn" data-pk="{{cid.pk}}" data-cid="{{cid.cid}}">Toggle</button>
</li>
<div class="card mb-2 bg-dark text-light border-0">
<div class="card-body py-2">
<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 %}
{% for cid in available_cid_users %}
<div class="d-flex align-items-center mb-2">
<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}} /
Email: {{cid.email}} <button class="toggle-btn" data-pk="{{cid.pk}}" data-cid="{{cid.cid}}">Toggle</button>
</li>
{% endfor %}
</ol>
{% comment %} Hide the toggle button if there are no users (to add or remove) {% endcomment %}
<p>{% if current_cid_users or available_cid_users %}
<button class="toggle-all-btn">Toggle all</button>
{% endif %}
<button class="add-all-btn">Add all</button>
<button class="remove-all-btn">Remove all</button>
</p>
<ul class="list-group list-group-flush" id="cid-users-list">
{% for cid in current_cid_users %}
<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">
<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-success ms-2 small">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 %}
{% 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>
$(document).ready(() => {
// Wire buttons: Add/Remove/Toggle
$(".remove-all-btn").click((el) => {
$(".current .toggle-btn").click();
$(".list-group-item.current .toggle-btn").each(function(){ $(this).click(); });
});
$(".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-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({
url: "{% url exam.app_name|add:':exam_json_edit' exam.pk %}",
data: {
csrfmiddlewaretoken: "{{csrf_token}}",
edit_cid_user: el.target.dataset.pk,
add: !parent.hasClass("current"),
edit_cid_user: btn.dataset.pk,
add: !parentLi.hasClass('current'),
},
type: "POST",
dataType: "json",
})
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) {
console.log("done", data);
if (data.status == "success") {
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`)
}
.done(function (data) {
if (data.status == "success") {
if (data.added) {
parentLi.addClass('current');
toastr.info(`User ${btn.dataset.cid} added to exam`)
} else {
toastr.error(data.status)
parentLi.removeClass('current');
toastr.info(`User ${btn.dataset.cid} removed from exam`)
}
})
.fail(function (data) {
console.log("fail", data);
toastr.error(data.responseJSON.status)
})
.always(function () {
})
} else {
toastr.error(data.status)
}
})
.fail(function (data) {
toastr.error((data && data.responseJSON && data.responseJSON.status) ? data.responseJSON.status : 'Request failed');
});
});
})
});
// 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>
<style>
.current {
color: lightgray;
}
.current::before {
content: "[ADDED TO EXAM] - "
}
button {
padding: 0;
}
<style>
/* 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>