Refactor CID users display: enhance layout with card structure, add sorting functionality, and improve toggle button interactions
This commit is contained in:
@@ -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}} · {{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}} · {{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>
|
||||
Reference in New Issue
Block a user