Refactor templates and views for improved structure and maintainability
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
<h2>Edit Exam Collection {{object.name}}</h2>
|
|
||||||
<div class="card mb-4">
|
<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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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
@@ -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('"', '"') 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) {{
|
||||||
|
|||||||
Reference in New Issue
Block a user