Refactor templates and views for improved structure and maintainability

This commit is contained in:
Ross
2025-12-08 12:31:58 +00:00
parent ad5e7aeed8
commit d7afab15e1
5 changed files with 242 additions and 83 deletions
+2 -1
View File
@@ -21,7 +21,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">My cases / collections</h5> <h5 class="card-title">My cases / collections</h5>
<p>View my <a href="{% url 'atlas:case_view' %}?author={{ request.user.id }}">cases</a>.</p> <p>View my <a href="{% url 'atlas:case_view' %}?author={{ request.user.id }}">cases</a> or <a href="{% url 'atlas:collection_view' %}?author={{ request.user.id }}">my collections</a>.</p>
<p> <p>
<a href="{% url 'atlas:user_uploads' %}" class="btn btn-outline-secondary"> <a href="{% url 'atlas:user_uploads' %}" class="btn btn-outline-secondary">
<i class="bi bi-cloud-arrow-up me-1"></i> Uploads awaiting import <i class="bi bi-cloud-arrow-up me-1"></i> Uploads awaiting import
@@ -38,6 +38,7 @@
<ul class="list-unstyled mb-0"> <ul class="list-unstyled mb-0">
<li><a href="{% url 'atlas:user_collections' %}">Collections to view / take</a></li> <li><a href="{% url 'atlas:user_collections' %}">Collections to view / take</a></li>
<li><a href="{% url 'atlas:case_view' %}?author={{ request.user.id }}">My cases</a></li> <li><a href="{% url 'atlas:case_view' %}?author={{ request.user.id }}">My cases</a></li>
<li><a href="{% url 'atlas:collection_view' %}?author={{ request.user.id }}">My collections</a></li>
<li><a href="{% url 'atlas:user_uploads' %}">Uploads awaiting import</a></li> <li><a href="{% url 'atlas:user_uploads' %}">Uploads awaiting import</a></li>
</ul> </ul>
</div> </div>
@@ -7,18 +7,16 @@
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>--> <!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
{{form.media}} {{form.media}}
<script type="text/javascript"> <script type="text/javascript">
</script> </script>
<!-- {{ form.media }} --> <!-- {{ form.media }} -->
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="card mb-4">
<h2>Edit Exam Collection {{object.name}}</h2>
<div class="card mb-4">
<div class="card-body"> <div class="card-body">
<h2 class="card-title">Edit Exam Collection <small class="text-muted">{{ object.name }}</small></h2> <h2 class="card-title">Edit Exam Collection <small class="text-muted">{{ object.name }}</small></h2>
@@ -55,7 +53,66 @@
</style> </style>
<div class="mb-3"> <div class="mb-3">
{{ form|crispy }} <div class="row gy-3">
<div class="col-md-6">
<div class="mb-2">
<label for="id_name" class="form-label requiredField">Name <span class="text-danger">*</span></label>
{{ form.name }}
{% if form.name.errors %}
<div class="invalid-feedback d-block">{{ form.name.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-3">
<div class="mb-2">
<label for="id_date" class="form-label">Date</label>
{{ form.date }}
{% if form.date.errors %}
<div class="invalid-feedback d-block">{{ form.date.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-3 d-flex align-items-center">
<div class="form-check mb-2">
{{ form.archive }}
<label for="id_archive" class="form-check-label ms-2">Archive</label>
</div>
</div>
</div>
<hr class="my-3" />
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title mb-2">Authors</h5>
<div>
{{ form.author }}
{% if form.author.errors %}
<div class="invalid-feedback d-block">{{ form.author.errors }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2 g-3">
{% for label, field_name, bound in exam_groups %}
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title small mb-2">{{ label }}</h5>
<div>
{{ bound }}
{% if bound.errors %}
<div class="invalid-feedback d-block">{{ bound.errors }}</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div> </div>
<script> <script>
@@ -81,8 +138,8 @@
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<script> <script>
// Simple client-side bootstrap validation // Simple client-side bootstrap validation
(function () { (function () {
'use strict' 'use strict'
@@ -97,5 +154,5 @@
}, false) }, false)
}) })
})() })()
</script> </script>
{% endblock %} {% endblock %}
@@ -1,7 +1,7 @@
{% if results %} {% if results %}
<ul class="list-unstyled mb-0" id="user_search_results_list_{{ field }}"> <ul class="list-unstyled mb-0" id="user_search_results_list_{{ field }}">
{% for item in results %} {% for item in results %}
<li class="py-1 d-flex justify-content-between align-items-center" data-user-id="{{ item.id }}" data-user-text="{{ item.text|escapejs }}" data-user-grade="{{ item.grade|default_if_none:''|escapejs }}"> <li class="py-1 d-flex justify-content-between align-items-center" data-user-id="{{ item.id }}" data-user-text="{{ item.text|escape }}" data-user-grade="{{ item.grade|default_if_none:''|escape }}">
<div class="flex-grow-1"> <div class="flex-grow-1">
{{ item.text|escape }} {{ item.text|escape }}
{% if item.grade %} {% if item.grade %}
@@ -9,7 +9,7 @@
{% endif %} {% endif %}
</div> </div>
<div> <div>
<button type="button" class="btn btn-sm btn-primary user-add-btn" data-user-id="{{ item.id }}" data-user-text="{{ item.text|escapejs }}" data-user-grade="{{ item.grade|default_if_none:''|escapejs }}">Add</button> <button type="button" class="btn btn-sm btn-primary user-add-btn" data-user-id="{{ item.id }}" data-user-text="{{ item.text|escape }}" data-user-grade="{{ item.grade|default_if_none:''|escape }}">Add</button>
</div> </div>
</li> </li>
{% endfor %} {% endfor %}
+27
View File
@@ -5490,11 +5490,38 @@ class ExamCollectionEdit(UpdateView, AuthorRequiredMixin):
model = ExamCollection model = ExamCollection
form_class = ExamCollectionForm form_class = ExamCollectionForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Ensure we have a bound form to extract bound fields
form = kwargs.get('form') or self.get_form()
groups = []
for label, field_name, model in getattr(self.form_class, 'GROUP_TYPES', []):
try:
bound = form[field_name]
except Exception:
bound = None
groups.append((label, field_name, bound))
context['exam_groups'] = groups
return context
class ExamCollectionCreate(CreateView, AuthorRequiredMixin): class ExamCollectionCreate(CreateView, AuthorRequiredMixin):
model = ExamCollection model = ExamCollection
form_class = ExamCollectionForm form_class = ExamCollectionForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
form = kwargs.get('form') or self.get_form()
groups = []
for label, field_name, model in getattr(self.form_class, 'GROUP_TYPES', []):
try:
bound = form[field_name]
except Exception:
bound = None
groups.append((label, field_name, bound))
context['exam_groups'] = groups
return context
class ExamCollectionClone(CreateView, AuthorRequiredMixin): class ExamCollectionClone(CreateView, AuthorRequiredMixin):
model = ExamCollection model = ExamCollection
+76 -2
View File
@@ -33,7 +33,7 @@ class UserSearchSelectMultipleWidget:
options_html = "" options_html = ""
selected_html = "" selected_html = ""
for u in users: for u in users:
options_html += f'<option value="{u.pk}" selected>{u.get_full_name() or u.username} - {u.email}</option>' # determine grade_text first, then include data-user-grade attribute on option
grade_text = "" grade_text = ""
try: try:
up = getattr(u, "userprofile", None) up = getattr(u, "userprofile", None)
@@ -42,6 +42,8 @@ class UserSearchSelectMultipleWidget:
grade_text = getattr(g, "name", str(g)) if g is not None else "" grade_text = getattr(g, "name", str(g)) if g is not None else ""
except Exception: except Exception:
grade_text = "" grade_text = ""
safe_grade_attr = (grade_text.replace('"', '&quot;') if grade_text else '')
options_html += f'<option value="{u.pk}" selected data-user-grade="{safe_grade_attr}">{u.get_full_name() or u.username} - {u.email}</option>'
# Build the selected item HTML incrementally to avoid accidental # Build the selected item HTML incrementally to avoid accidental
# mixing of boolean values into string concatenation (was causing # mixing of boolean values into string concatenation (was causing
@@ -147,6 +149,16 @@ class UserSearchSelectMultipleWidget:
timeout = setTimeout(() => fetchResults(search.value), 300); timeout = setTimeout(() => fetchResults(search.value), 300);
}}); }});
// Prevent Enter from submitting the surrounding form; run the search instead
search.addEventListener('keydown', function(e) {{
if (e.key === 'Enter' || e.keyCode === 13) {{
e.preventDefault();
e.stopPropagation();
clearTimeout(timeout);
fetchResults(search.value);
}}
}});
gradeSelect && gradeSelect.addEventListener('change', function(e) {{ gradeSelect && gradeSelect.addEventListener('change', function(e) {{
// re-run search with same query when grade changes // re-run search with same query when grade changes
clearTimeout(timeout); clearTimeout(timeout);
@@ -172,7 +184,16 @@ class UserSearchSelectMultipleWidget:
if (!btn) return; if (!btn) return;
const id = btn.getAttribute('data-user-id'); const id = btn.getAttribute('data-user-id');
const text = btn.getAttribute('data-user-text') || btn.textContent.trim(); const text = btn.getAttribute('data-user-text') || btn.textContent.trim();
const grade = btn.getAttribute('data-user-grade') || ''; // Try button attribute, then list-item dataset, then visible badge text
let grade = btn.getAttribute('data-user-grade') || '';
if (!grade) {{
const li = btn.closest('li[data-user-id]');
if (li && li.dataset && li.dataset.userGrade) grade = li.dataset.userGrade;
else {{
const badge = btn.closest('li') ? btn.closest('li').querySelector('.badge') : null;
if (badge) grade = badge.textContent.trim();
}}
}}
addUserToField(widgetFieldName, id, text, grade); addUserToField(widgetFieldName, id, text, grade);
}}); }});
@@ -226,6 +247,49 @@ class UserSearchSelectMultipleWidget:
addUserToField(fieldName, id, text, grade); addUserToField(fieldName, id, text, grade);
}}); }});
}} }}
// Initialize visible selected-list from any pre-existing <option selected> entries
(function initSelectedListFromOptions() {{
try {{
const sel = document.getElementById('{field_id}');
const listEl = document.getElementById('{selected_list_id}');
if (!sel || !listEl) return;
for (let i=0;i<sel.options.length;i++) {{
const opt = sel.options[i];
const id = opt.value;
if (!id) continue;
const liId = 'selected_user_' + '{self.name}' + '_' + id;
const existingLi = document.getElementById(liId);
const grade = opt.getAttribute('data-user-grade') || '';
if (existingLi) {{
// If server-rendered li exists but has no badge, add one from the option attribute
const hasBadge = existingLi.querySelector('.badge');
if (!hasBadge && grade) {{
const span = existingLi.querySelector('span');
if (span) {{
const badge = document.createElement('span');
badge.className = 'badge bg-secondary ms-2';
badge.textContent = grade;
span.appendChild(badge);
}}
}}
continue;
}}
// build li from option when not rendered server-side
const text = opt.textContent || opt.innerText || opt.text;
const li = document.createElement('li');
li.className = 'list-group-item d-flex justify-content-between align-items-center';
li.id = liId;
const span = document.createElement('span');
span.innerHTML = text + (grade ? ' <span class="badge bg-secondary ms-2">' + grade + '</span>' : '');
const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn btn-sm btn-outline-danger ms-2'; btn.innerText = 'Remove';
btn.addEventListener('click', function() {{ removeUserFromField('{self.name}', id); }});
li.appendChild(span); li.appendChild(btn); listEl.appendChild(li);
}}
}} catch (e) {{ /* ignore init errors */ }}
}})();
}})(); }})();
</script> </script>
</div> </div>
@@ -481,6 +545,16 @@ class ExamSearchSelectMultipleWidget:
timeout = setTimeout(() => fetchResults(search.value), 300); timeout = setTimeout(() => fetchResults(search.value), 300);
}}); }});
// Prevent Enter from submitting the surrounding form; run the search instead
search.addEventListener('keydown', function(e) {{
if (e.key === 'Enter' || e.keyCode === 13) {{
e.preventDefault();
e.stopPropagation();
clearTimeout(timeout);
fetchResults(search.value);
}}
}});
// Toggle help panel visibility // Toggle help panel visibility
if (helpBtn && helpPanel) {{ if (helpBtn && helpPanel) {{
helpBtn.addEventListener('click', function(e) {{ helpBtn.addEventListener('click', function(e) {{