Implement HTMX support for CID and User candidates lists, and add submitted candidates summary
This commit is contained in:
@@ -19,37 +19,31 @@
|
||||
<input id="cid-filter" class="form-control form-control-sm" placeholder="Filter CID candidates by CID, name, email or passcode">
|
||||
</div>
|
||||
<div id="htmx-info"></div>
|
||||
{% if cid_users %}
|
||||
<div id="cid-list" hx-get="{% url exam.app_name|add:':exam_cids_cid_list' exam.id %}" hx-trigger="load" hx-swap="innerHTML">
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for cid in cid_users %}
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex align-items-start justify-content-between">
|
||||
<div>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-clock-history text-muted me-2" title="View user exam history"
|
||||
hx-get="{% url exam.app_name|add:':exam_user_status_cid' exam.id cid.cid %}"
|
||||
hx-target=".modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#history-modal"
|
||||
_="on htmx:afterOnLoad put {{cid.cid}} into #history-user.innerHTML "
|
||||
></i>
|
||||
<a class="fw-bold me-2 cid-link" href="{% url 'generic:update_cid' cid.pk %}">{{ cid.cid }}</a>
|
||||
<span class="text-muted">[{{ cid.passcode }}]</span>
|
||||
<a class="ms-3" title="View CID exam details" hx-get="{% url 'generic:cid_user_exam_partial' exam.app_name exam.id cid.cid %}" hx-target=".modal-content" data-bs-toggle="modal" data-bs-target="#history-modal"><i class="bi bi-box-arrow-up-right"></i></a>
|
||||
</div>
|
||||
<div class="small text-muted mt-1">{{ cid.name }}</div>
|
||||
<div class="small">Email: {{ cid.email }}</div>
|
||||
{% if cid.notes %}
|
||||
<div class="small text-muted mt-1 cid-notes" title="{{ cid.notes|escape }}" data-bs-toggle="tooltip" data-bs-placement="top">Notes: {{ cid.notes|truncatechars:120 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex align-items-center placeholder-glow">
|
||||
<span class="placeholder col-2 me-2"></span>
|
||||
<span class="placeholder col-4 me-2"></span>
|
||||
<span class="placeholder col-3"></span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex align-items-center placeholder-glow">
|
||||
<span class="placeholder col-2 me-2"></span>
|
||||
<span class="placeholder col-4 me-2"></span>
|
||||
<span class="placeholder col-3"></span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex align-items-center placeholder-glow">
|
||||
<span class="placeholder col-2 me-2"></span>
|
||||
<span class="placeholder col-4 me-2"></span>
|
||||
<span class="placeholder col-3"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-muted">This exam has no CID candidates. Add some with the edit link above.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,49 +55,37 @@
|
||||
<div class="mb-2">
|
||||
<input id="user-filter" class="form-control form-control-sm" placeholder="Filter User candidates by username or email">
|
||||
</div>
|
||||
{% if user_users %}
|
||||
<div id="user-list" hx-get="{% url exam.app_name|add:':exam_cids_user_list' exam.id %}" hx-trigger="load" hx-swap="innerHTML">
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for user in user_users %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-clock-history text-muted me-2" title="View user exam history"
|
||||
hx-get="{% url exam.app_name|add:':exam_user_status_user' exam.id user.id %}"
|
||||
hx-target=".modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#history-modal"
|
||||
_="on htmx:afterOnLoad put {{cid.cid}} into #history-user.innerHTML "
|
||||
></i>
|
||||
{% if request.user.is_superuser %}
|
||||
<a class="me-2" href='{% url "account_profile" user.username %}'><i class="bi bi-link"></i></a>
|
||||
{% endif %}
|
||||
<span class="fw-bold">{{ user.username }}</span>
|
||||
</div>
|
||||
<div class="small">Email: {{ user.email }}</div>
|
||||
</div>
|
||||
<div class="small text-muted"> </div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex align-items-center placeholder-glow">
|
||||
<span class="placeholder col-3 me-2"></span>
|
||||
<span class="placeholder col-4"></span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex align-items-center placeholder-glow">
|
||||
<span class="placeholder col-3 me-2"></span>
|
||||
<span class="placeholder col-4"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-muted">This exam has no User candidates. Add some with the edit link above.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if user_exam_data %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Submitted candidates</h5>
|
||||
<p class="small text-muted">The following candidate submissions have been received. Candidates with submissions are highlighted above.</p>
|
||||
<ul id="submitted-candidates" class="list-unstyled small">
|
||||
{% for user_data in user_exam_data %}
|
||||
<li data-cid="{{user_data.cid_user}}" data-user="{{user_data.user_user}}">CID: {{user_data.cid_user}} / User: {{user_data.user_user}} / Start: {{user_data.start_time}} / End: {{user_data.end_time}}</li>
|
||||
{% endfor %}
|
||||
{% if has_submissions %}
|
||||
<div id="submitted-list" hx-get="{% url exam.app_name|add:':exam_cids_submitted_list' exam.id %}" hx-trigger="load" hx-swap="innerHTML">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="placeholder-glow">
|
||||
<h5 class="card-title placeholder col-4"></h5>
|
||||
<p class="placeholder col-8"></p>
|
||||
<ul class="list-unstyled small">
|
||||
<li class="placeholder col-6 mb-2"></li>
|
||||
<li class="placeholder col-6 mb-2"></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,23 +117,9 @@
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
// Highlight users that have submitted answers
|
||||
document.querySelectorAll('#submitted-candidates li').forEach(function(el){
|
||||
try{
|
||||
const cid = el.dataset.cid;
|
||||
const user = el.dataset.user;
|
||||
if(cid && cid !== 'None'){
|
||||
// highlight matching cid element(s)
|
||||
document.querySelectorAll(`.list-group-item`).forEach(function(li){ if(li.querySelector('.cid-link') && li.querySelector('.cid-link').textContent.trim() === cid.toString()) li.classList.add('submitted'); });
|
||||
}
|
||||
if(user && user !== 'None'){
|
||||
document.querySelectorAll('.list-group-item').forEach(function(li){ if(li.textContent.includes(user)) li.classList.add('submitted'); });
|
||||
}
|
||||
}catch(e){ console.error(e); }
|
||||
});
|
||||
(function(){
|
||||
// Utilities to run both on initial load and after HTMX swaps
|
||||
|
||||
// Filtering helpers
|
||||
function normalizeText(s){ return (s||'').toString().toLowerCase(); }
|
||||
|
||||
function filterList(inputElem, listSelector){
|
||||
@@ -166,27 +134,60 @@
|
||||
});
|
||||
}
|
||||
|
||||
const cidFilter = document.getElementById('cid-filter');
|
||||
if(cidFilter){
|
||||
cidFilter.addEventListener('input', function(){
|
||||
// target CID list items only (left column)
|
||||
filterList(cidFilter, '.col-lg-6:first-of-type .list-group-item');
|
||||
function initTooltips(){
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(el){
|
||||
try{ new bootstrap.Tooltip(el); }catch(e){/* ignore if bootstrap not available */}
|
||||
});
|
||||
}
|
||||
|
||||
// enable Bootstrap tooltips for notes
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(el){
|
||||
try{ new bootstrap.Tooltip(el); }catch(e){/* ignore if bootstrap not available */}
|
||||
function highlightSubmitted(){
|
||||
document.querySelectorAll('#submitted-candidates li').forEach(function(el){
|
||||
try{
|
||||
const cid = el.dataset.cid;
|
||||
const user = el.dataset.user;
|
||||
if(cid && cid !== 'None'){
|
||||
document.querySelectorAll(`.list-group-item`).forEach(function(li){
|
||||
try{
|
||||
const link = li.querySelector('.cid-link');
|
||||
if(link && link.textContent.trim() === cid.toString()) li.classList.add('submitted');
|
||||
}catch(e){}
|
||||
});
|
||||
}
|
||||
if(user && user !== 'None'){
|
||||
document.querySelectorAll('.list-group-item').forEach(function(li){ if(li.textContent.includes(user)) li.classList.add('submitted'); });
|
||||
}
|
||||
}catch(e){ console.error(e); }
|
||||
});
|
||||
}
|
||||
|
||||
// Wire filters
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
const cidFilter = document.getElementById('cid-filter');
|
||||
if(cidFilter){ cidFilter.addEventListener('input', function(){ filterList(cidFilter, '.col-lg-6:first-of-type .list-group-item'); }); }
|
||||
|
||||
const userFilter = document.getElementById('user-filter');
|
||||
if(userFilter){ userFilter.addEventListener('input', function(){ filterList(userFilter, '.col-lg-6:nth-of-type(2) .list-group-item, .col-lg-6:nth-of-type(2) .list-group-item.d-flex'); }); }
|
||||
|
||||
// initial runs (may be no partials yet)
|
||||
initTooltips();
|
||||
highlightSubmitted();
|
||||
});
|
||||
|
||||
const userFilter = document.getElementById('user-filter');
|
||||
if(userFilter){
|
||||
userFilter.addEventListener('input', function(){
|
||||
// target user list items only (right column)
|
||||
filterList(userFilter, '.col-lg-6:nth-of-type(2) .list-group-item, .col-lg-6:nth-of-type(2) .list-group-item.d-flex');
|
||||
});
|
||||
}
|
||||
});
|
||||
// After HTMX swaps, re-run highlight and tooltips when our partials load
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt){
|
||||
try{
|
||||
const target = evt.detail && evt.detail.target ? evt.detail.target : null;
|
||||
if(!target) return;
|
||||
|
||||
const id = target.id || (target.closest && target.closest('[id]') ? target.closest('[id]').id : null);
|
||||
if(id === 'cid-list' || id === 'user-list' || id === 'submitted-list'){
|
||||
initTooltips();
|
||||
// submitted-list may have loaded now; re-run highlighting
|
||||
highlightSubmitted();
|
||||
}
|
||||
}catch(e){ console.error(e); }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{% comment %} Partial: list of CID candidates for HTMX swap {% endcomment %}
|
||||
{% if cid_users %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for cid in cid_users %}
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex align-items-start justify-content-between">
|
||||
<div>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-clock-history text-muted me-2" title="View user exam history"
|
||||
hx-get="{% url exam.app_name|add:':exam_user_status_cid' exam.id cid.cid %}"
|
||||
hx-target=".modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#history-modal"
|
||||
></i>
|
||||
<a class="fw-bold me-2 cid-link" href="{% url 'generic:update_cid' cid.pk %}">{{ cid.cid }}</a>
|
||||
<span class="text-muted">[{{ cid.passcode }}]</span>
|
||||
<a class="ms-3" title="View CID exam details" hx-get="{% url 'generic:cid_user_exam_partial' exam.app_name exam.id cid.cid %}" hx-target=".modal-content" data-bs-toggle="modal" data-bs-target="#history-modal"><i class="bi bi-box-arrow-up-right"></i></a>
|
||||
</div>
|
||||
<div class="small text-muted mt-1">{{ cid.name }}</div>
|
||||
<div class="small">Email: {{ cid.email }}</div>
|
||||
{% if cid.notes %}
|
||||
<div class="small text-muted mt-1 cid-notes" title="{{ cid.notes|escape }}" data-bs-toggle="tooltip" data-bs-placement="top">Notes: {{ cid.notes|truncatechars:120 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-muted">This exam has no CID candidates.</p>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,16 @@
|
||||
{% comment %} Partial: submitted candidates fragment for HTMX swap {% endcomment %}
|
||||
{% if user_exam_data %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Submitted candidates</h5>
|
||||
<p class="small text-muted">The following candidate submissions have been received. Candidates with submissions are highlighted above.</p>
|
||||
<ul id="submitted-candidates" class="list-unstyled small">
|
||||
{% for user_data in user_exam_data %}
|
||||
<li data-cid="{{user_data.cid_user}}" data-user="{{user_data.user_user}}">CID: {{user_data.cid_user}} / User: {{user_data.user_user}} / Start: {{user_data.start_time}} / End: {{user_data.end_time}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="small text-muted">No submissions yet.</p>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% comment %} Partial: list of User candidates for HTMX swap {% endcomment %}
|
||||
{% if user_users %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for user in user_users %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-clock-history text-muted me-2" title="View user exam history"
|
||||
hx-get="{% url exam.app_name|add:':exam_user_status_user' exam.id user.id %}"
|
||||
hx-target=".modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#history-modal"
|
||||
></i>
|
||||
{% if request.user.is_superuser %}
|
||||
<a class="me-2" href='{% url "account_profile" user.username %}'><i class="bi bi-link"></i></a>
|
||||
{% endif %}
|
||||
<span class="fw-bold">{{ user.username }}</span>
|
||||
</div>
|
||||
<div class="small">Email: {{ user.email }}</div>
|
||||
</div>
|
||||
<div class="small text-muted"> </div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-muted">This exam has no User candidates.</p>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user