From d7afab15e1204ef74370700fcfc426795c990296 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Dec 2025 12:31:58 +0000 Subject: [PATCH] Refactor templates and views for improved structure and maintainability --- atlas/templates/atlas/index.html | 3 +- .../generic/examcollection_form.html | 213 +++++++++++------- .../partials/user_search_widget_results.html | 4 +- generic/views.py | 27 +++ generic/widgets.py | 78 ++++++- 5 files changed, 242 insertions(+), 83 deletions(-) diff --git a/atlas/templates/atlas/index.html b/atlas/templates/atlas/index.html index 4942e73f..6ca62ada 100644 --- a/atlas/templates/atlas/index.html +++ b/atlas/templates/atlas/index.html @@ -21,7 +21,7 @@
diff --git a/generic/templates/generic/examcollection_form.html b/generic/templates/generic/examcollection_form.html index 0977d16e..4aa845bc 100755 --- a/generic/templates/generic/examcollection_form.html +++ b/generic/templates/generic/examcollection_form.html @@ -7,95 +7,152 @@ {% endblock %} {% block js %} -{{form.media}} + {{form.media}} - + {% endblock %} {% block content %} +
+
+

Edit Exam Collection {{ object.name }}

-

Edit Exam Collection {{object.name}}

-
-
-

Edit Exam Collection {{ object.name }}

+
+ {% csrf_token %} + {% if form.non_field_errors %} +
{{ form.non_field_errors }}
+ {% endif %} - - {% csrf_token %} - {% if form.non_field_errors %} -
{{ form.non_field_errors }}
- {% endif %} - - - -
- {{ form|crispy }} -
- - + #examcollection-form .mb-3:nth-child(odd) { + background: rgba(255,255,255,0.015); + } + #examcollection-form .form-section-divider { + height: 1px; + background: rgba(255,255,255,0.04); + margin: .5rem 0 1rem 0; + border-radius: 2px; + } + #examcollection-form .form-section-title { + font-size: .95rem; + margin-bottom: .5rem; + font-weight: 600; + color: var(--bs-light); + } + -
- - Back to collections - -
-
+
+
+
+
+ + {{ form.name }} + {% if form.name.errors %} +
{{ form.name.errors }}
+ {% endif %} +
+
+ +
+
+ + {{ form.date }} + {% if form.date.errors %} +
{{ form.date.errors }}
+ {% endif %} +
+
+ +
+
+ {{ form.archive }} + +
+
+
+ +
+ +
+
+
Authors
+
+ {{ form.author }} + {% if form.author.errors %} +
{{ form.author.errors }}
+ {% endif %} +
+
+
+ +
+ {% for label, field_name, bound in exam_groups %} +
+
+
+
{{ label }}
+
+ {{ bound }} + {% if bound.errors %} +
{{ bound.errors }}
+ {% endif %} +
+
+
+
+ {% endfor %} +
+
+ + + +
+ + Back to collections + +
+ +
-
- + (function () { + 'use strict' + var forms = document.querySelectorAll('.needs-validation') + Array.prototype.slice.call(forms).forEach(function (form) { + form.addEventListener('submit', function (event) { + if (!form.checkValidity()) { + event.preventDefault() + event.stopPropagation() + } + form.classList.add('was-validated') + }, false) + }) + })() + {% endblock %} \ No newline at end of file diff --git a/generic/templates/generic/partials/user_search_widget_results.html b/generic/templates/generic/partials/user_search_widget_results.html index 898cd9f7..fbc3d1ca 100644 --- a/generic/templates/generic/partials/user_search_widget_results.html +++ b/generic/templates/generic/partials/user_search_widget_results.html @@ -1,7 +1,7 @@ {% if results %}
    {% for item in results %} -
  • +
  • {{ item.text|escape }} {% if item.grade %} @@ -9,7 +9,7 @@ {% endif %}
    - +
  • {% endfor %} diff --git a/generic/views.py b/generic/views.py index 0cb2f976..aa27ac74 100644 --- a/generic/views.py +++ b/generic/views.py @@ -5490,11 +5490,38 @@ class ExamCollectionEdit(UpdateView, AuthorRequiredMixin): model = ExamCollection 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): model = ExamCollection 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): model = ExamCollection diff --git a/generic/widgets.py b/generic/widgets.py index ee2d7eba..84d3153f 100644 --- a/generic/widgets.py +++ b/generic/widgets.py @@ -33,7 +33,7 @@ class UserSearchSelectMultipleWidget: options_html = "" selected_html = "" for u in users: - options_html += f'' + # determine grade_text first, then include data-user-grade attribute on option grade_text = "" try: up = getattr(u, "userprofile", None) @@ -42,6 +42,8 @@ class UserSearchSelectMultipleWidget: grade_text = getattr(g, "name", str(g)) if g is not None else "" except Exception: grade_text = "" + safe_grade_attr = (grade_text.replace('"', '"') if grade_text else '') + options_html += f'' # Build the selected item HTML incrementally to avoid accidental # mixing of boolean values into string concatenation (was causing @@ -147,6 +149,16 @@ class UserSearchSelectMultipleWidget: 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) {{ // re-run search with same query when grade changes clearTimeout(timeout); @@ -172,7 +184,16 @@ class UserSearchSelectMultipleWidget: if (!btn) return; const id = btn.getAttribute('data-user-id'); 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); }}); @@ -226,6 +247,49 @@ class UserSearchSelectMultipleWidget: addUserToField(fieldName, id, text, grade); }}); }} + + // Initialize visible selected-list from any pre-existing
@@ -481,6 +545,16 @@ class ExamSearchSelectMultipleWidget: 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 if (helpBtn && helpPanel) {{ helpBtn.addEventListener('click', function(e) {{