Implement self-review functionality with enhanced UI and dynamic scoring options

This commit is contained in:
Ross
2026-04-27 11:07:09 +01:00
parent e82b353ad9
commit b864f3d3fd
5 changed files with 401 additions and 36 deletions
@@ -214,9 +214,23 @@
{% if collection.self_review %}
<div>
<p>
<a href="{% url 'atlas:add_self_review' cid_user_exam.id case.id %}"><button type="button">Add self review</button></a>
</p>
<div class="self-review-actions mb-3">
<div class="btn-group btn-group-sm" role="group" aria-label="Self review actions">
<a class="btn btn-primary" href="{% url 'atlas:add_self_review' cid_user_exam.id case.id %}">
<i class="bi bi-card-checklist"></i> Add Self Review
</a>
<button type="button"
class="btn btn-outline-info"
onclick="openSelfReviewPopup('{% url 'atlas:add_self_review' cid_user_exam.id case.id %}')">
Popup
</button>
<button type="button"
class="btn btn-outline-secondary"
onclick="openSelfReviewSidebar('{% url 'atlas:add_self_review' cid_user_exam.id case.id %}')">
Sidebar
</button>
</div>
</div>
{% if self_review %}
<h4>Self Feedback</h4>
@@ -263,9 +277,23 @@
{% if question_completed %}
<div>
{% if collection.self_review %}
<p>
<a target="_blank" title="Add self review, this will open in a new page / tab" href="{% url 'atlas:add_self_review' cid_user_exam.id case.id %}"><button type="button">Add self review</button></a>
</p>
<div class="self-review-actions mb-3">
<div class="btn-group btn-group-sm" role="group" aria-label="Self review actions">
<a class="btn btn-primary" href="{% url 'atlas:add_self_review' cid_user_exam.id case.id %}">
<i class="bi bi-card-checklist"></i> Add Self Review
</a>
<button type="button"
class="btn btn-outline-info"
onclick="openSelfReviewPopup('{% url 'atlas:add_self_review' cid_user_exam.id case.id %}')">
Popup
</button>
<button type="button"
class="btn btn-outline-secondary"
onclick="openSelfReviewSidebar('{% url 'atlas:add_self_review' cid_user_exam.id case.id %}')">
Sidebar
</button>
</div>
</div>
{% if self_review %}
<h4>Self Feedback</h4>
@@ -306,6 +334,17 @@
<button type="submit" id="goto-button" value="test" name="goto" class="hide">goto</button>
</div>
</form>
<aside id="selfReviewSidebar" class="self-review-sidebar" aria-live="polite" aria-label="Self review sidebar">
<div class="self-review-sidebar-header">
<h5 class="mb-0" id="selfReviewSidebarLabel">Self Review</h5>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="closeSelfReviewSidebar()">Close</button>
</div>
<div id="selfReviewSidebarBody" class="self-review-sidebar-body">
<div class="p-3 text-muted small">Choose Sidebar to load the self-review form here.</div>
</div>
</aside>
<style>
label {
vertical-align: top;
@@ -376,6 +415,44 @@
background-color: darkblue;
}
.self-review-actions .btn {
white-space: nowrap;
}
.self-review-sidebar {
position: fixed;
top: 0;
right: 0;
width: min(760px, 94vw);
height: 100vh;
background: var(--bs-body-bg);
border-left: 1px solid var(--bs-border-color);
box-shadow: -8px 0 18px rgba(0, 0, 0, 0.35);
z-index: 1050;
transform: translateX(100%);
transition: transform 0.2s ease-in-out;
display: flex;
flex-direction: column;
}
.self-review-sidebar.open {
transform: translateX(0);
}
.self-review-sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--bs-border-color);
background: var(--bs-secondary-bg);
}
.self-review-sidebar-body {
overflow: auto;
flex: 1 1 auto;
}
</style>
{% endblock %}
@@ -590,6 +667,89 @@
}
});
window.openSelfReviewPopup = function (url) {
var features = 'width=980,height=840,resizable=yes,scrollbars=yes';
window.open(url, 'selfReviewPopup', features);
};
window.selfReviewCurrentEmbedUrl = null;
window.bindSelfReviewEmbedForm = function () {
var root = document.getElementById('selfReviewSidebarBody');
if (!root) return;
var form = root.querySelector('form.self-review-embed-form');
if (!form) return;
if (form.dataset.bound === '1') return;
form.dataset.bound = '1';
form.addEventListener('submit', async function (event) {
event.preventDefault();
var formData = new FormData(form);
var csrftokenInput = form.querySelector('input[name="csrfmiddlewaretoken"]');
var csrftoken = csrftokenInput ? csrftokenInput.value : '';
try {
var resp = await fetch(window.selfReviewCurrentEmbedUrl || window.location.href, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrftoken,
},
credentials: 'same-origin',
});
if (resp.redirected) {
root.innerHTML = '<div class="p-3"><div class="alert alert-success mb-0">Self review saved. You can continue reviewing this case.</div></div>';
setTimeout(function () { window.closeSelfReviewSidebar(); }, 700);
return;
}
var html = await resp.text();
root.innerHTML = html;
window.bindSelfReviewEmbedForm();
} catch (e) {
var msg = root.querySelector('.self-review-embed-message');
if (msg) {
msg.innerHTML = '<div class="alert alert-danger py-2">Unable to save self review. Please try again.</div>';
}
}
});
};
window.closeSelfReviewSidebar = function () {
var sidebar = document.getElementById('selfReviewSidebar');
if (sidebar) {
sidebar.classList.remove('open');
}
};
window.openSelfReviewSidebar = async function (url) {
var sidebar = document.getElementById('selfReviewSidebar');
var body = document.getElementById('selfReviewSidebarBody');
if (!sidebar || !body) {
window.location.href = url;
return;
}
var embedUrl = url + (url.indexOf('?') === -1 ? '?embed=1' : '&embed=1');
window.selfReviewCurrentEmbedUrl = embedUrl;
body.innerHTML = '<div class="p-3 text-muted small">Loading self-review form...</div>';
sidebar.classList.add('open');
try {
var resp = await fetch(embedUrl, { credentials: 'same-origin' });
var html = await resp.text();
body.innerHTML = html;
window.bindSelfReviewEmbedForm();
} catch (e) {
body.innerHTML = '<div class="p-3"><div class="alert alert-danger py-2 mb-0">Could not load self-review sidebar content. Use Popup as fallback.</div></div>';
}
};
</script>
{% endblock js %}
@@ -0,0 +1,83 @@
{% load crispy_forms_tags %}
<div class="p-3">
<div class="card border-secondary shadow-sm mb-2">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h6 mb-0">Self Review</h2>
<span class="badge bg-primary-subtle text-primary-emphasis">{{ user_exam.exam }}</span>
</div>
<div class="card-body">
<p class="text-muted small mb-2">
Record a quick reflection while reviewing this case.
</p>
<div class="small text-muted mb-3">
Case: <strong>{{ case }}</strong>
</div>
<form method="post" class="self-review-embed-form" action="">
{% csrf_token %}
{{ form.non_field_errors }}
{{ form.user_exam }}
{{ form.case }}
<div class="mb-3">
{{ form.comments|as_crispy_field }}
</div>
<div class="mb-3">
<label class="form-label fw-semibold d-block">{{ form.findings.label }}</label>
<div class="btn-group" role="group" aria-label="Findings self rating">
{% for value, label in form.fields.findings.choices %}
<input
type="radio"
class="btn-check"
name="{{ form.findings.html_name }}"
id="id_embed_findings_{{ forloop.counter0 }}"
value="{{ value }}"
{% if form.findings.value|stringformat:'s' == value|stringformat:'s' %}checked{% endif %}
>
<label class="btn btn-outline-info" for="id_embed_findings_{{ forloop.counter0 }}">{{ label }}</label>
{% endfor %}
</div>
{% if form.findings.help_text %}
<div class="form-text">{{ form.findings.help_text }}</div>
{% endif %}
{% if form.findings.errors %}
<div class="text-danger small mt-1">{{ form.findings.errors|striptags }}</div>
{% endif %}
</div>
<div class="mb-4">
<label class="form-label fw-semibold d-block">{{ form.interpretation.label }}</label>
<div class="btn-group" role="group" aria-label="Interpretation self rating">
{% for value, label in form.fields.interpretation.choices %}
<input
type="radio"
class="btn-check"
name="{{ form.interpretation.html_name }}"
id="id_embed_interpretation_{{ forloop.counter0 }}"
value="{{ value }}"
{% if form.interpretation.value|stringformat:'s' == value|stringformat:'s' %}checked{% endif %}
>
<label class="btn btn-outline-warning" for="id_embed_interpretation_{{ forloop.counter0 }}">{{ label }}</label>
{% endfor %}
</div>
{% if form.interpretation.help_text %}
<div class="form-text">{{ form.interpretation.help_text }}</div>
{% endif %}
{% if form.interpretation.errors %}
<div class="text-danger small mt-1">{{ form.interpretation.errors|striptags }}</div>
{% endif %}
</div>
<div class="d-flex gap-2 flex-wrap">
<button type="submit" class="btn btn-primary btn-sm">Save Self Review</button>
{% if review %}
<a href="{% url 'atlas:self_review_delete' user_exam.id case.id review.id %}" class="btn btn-outline-danger btn-sm">Delete</a>
{% endif %}
</div>
</form>
<div class="self-review-embed-message mt-2"></div>
</div>
</div>
</div>
+80 -30
View File
@@ -1,38 +1,88 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<h2>Add Self Review</h2>
<div class="alert alert-info" role="alert">
Adding self feedback for {{user_exam.exam}}/{{case}}
</div>
This form allows you to record your own feedback for the question / case. This can take the form of a note or a ranking (1-5) of your findings and interpretation.
<form class="highlight" action="" method="post">
{% csrf_token %}
{{ form.non_field_errors }}
<div class="container px-0" style="max-width: 920px;">
<div class="card border-secondary shadow-sm mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h5 mb-0">Self Review</h2>
<span class="badge bg-primary-subtle text-primary-emphasis">{{ user_exam.exam }}</span>
</div>
<div class="card-body">
<p class="text-muted mb-3">
Reflect on this case and score your own performance. These scores are personal and intended to support learning.
</p>
<div class="small text-muted mb-3">
Case: <strong>{{ case }}</strong>
</div>
{{ form }}
{% comment %} <div class="fieldWrapper">
{{ form.content_type.errors }}
{{ form.content_type }}
<form method="post" class="needs-validation" novalidate>
{% csrf_token %}
{{ form.non_field_errors }}
{{ form.user_exam }}
{{ form.case }}
<div class="mb-3">
{{ form.comments|as_crispy_field }}
</div>
<div class="mb-3">
<label class="form-label fw-semibold d-block">{{ form.findings.label }}</label>
<div class="btn-group" role="group" aria-label="Findings self rating">
{% for value, label in form.fields.findings.choices %}
<input
type="radio"
class="btn-check"
name="{{ form.findings.html_name }}"
id="id_findings_{{ forloop.counter0 }}"
value="{{ value }}"
{% if form.findings.value|stringformat:'s' == value|stringformat:'s' %}checked{% endif %}
>
<label class="btn btn-outline-info" for="id_findings_{{ forloop.counter0 }}">{{ label }}</label>
{% endfor %}
</div>
{% if form.findings.help_text %}
<div class="form-text">{{ form.findings.help_text }}</div>
{% endif %}
{% if form.findings.errors %}
<div class="text-danger small mt-1">{{ form.findings.errors|striptags }}</div>
{% endif %}
</div>
<div class="mb-4">
<label class="form-label fw-semibold d-block">{{ form.interpretation.label }}</label>
<div class="btn-group" role="group" aria-label="Interpretation self rating">
{% for value, label in form.fields.interpretation.choices %}
<input
type="radio"
class="btn-check"
name="{{ form.interpretation.html_name }}"
id="id_interpretation_{{ forloop.counter0 }}"
value="{{ value }}"
{% if form.interpretation.value|stringformat:'s' == value|stringformat:'s' %}checked{% endif %}
>
<label class="btn btn-outline-warning" for="id_interpretation_{{ forloop.counter0 }}">{{ label }}</label>
{% endfor %}
</div>
{% if form.interpretation.help_text %}
<div class="form-text">{{ form.interpretation.help_text }}</div>
{% endif %}
{% if form.interpretation.errors %}
<div class="text-danger small mt-1">{{ form.interpretation.errors|striptags }}</div>
{% endif %}
</div>
<div class="d-flex gap-2 flex-wrap">
<button type="submit" class="btn btn-primary">Save Self Review</button>
<a href="{{ case.get_absolute_url }}" class="btn btn-outline-secondary">Open Case</a>
{% if review %}
<a href="{% url 'atlas:self_review_delete' user_exam.id case.id review.id %}" class="btn btn-outline-danger">Delete Review</a>
{% endif %}
</div>
</form>
</div>
</div>
<div class="fieldWrapper">
{{ form.object_id.errors }}
{{ form.object_id }}
</div>
<div class="fieldWrapper">
{{ form.note_type.errors }}
<label for="{{ form.note_type.id_for_label }}">Note type</label>
{{ form.note_type }}
</div>
<div class="fieldWrapper">
{{ form.note.errors }}
<label for="{{ form.note.id_for_label }}">Additional information</label><br/>
{{ form.note }}
</div> {% endcomment %}
<input type="submit" value="Submit">
</form>
{% if review %}<br/><a href="{% url 'atlas:self_review_delete' user_exam.id case.id review.id %}"><button>Delete review</button></a>{% endif %}
</div>
{% endblock %}