Add user answer viewing and compaction
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load help_tags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container anatomy my-3">
|
<div class="container anatomy my-3">
|
||||||
@@ -52,6 +53,15 @@
|
|||||||
{% if request.user.is_staff %}
|
{% if request.user.is_staff %}
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary ms-2 admin-edit-button" data-qn="{{ forloop.counter0 }}">Edit</button>
|
<button type="button" class="btn btn-sm btn-outline-secondary ms-2 admin-edit-button" data-qn="{{ forloop.counter0 }}">Edit</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if request.user.is_staff or view_all_results or request.user in exam.author.all %}
|
||||||
|
{% with q=questions|get_item:forloop.counter0 %}
|
||||||
|
{% with ua=user_answers_map|get_item:q.pk %}
|
||||||
|
{% if ua %}
|
||||||
|
<a href="{% url 'generic:view_answer_history' app_name='anatomy' model_name='UserAnswer' pk=ua.pk %}" class="btn btn-sm btn-outline-info ms-2">History</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -2592,6 +2592,7 @@ class BaseReportAnswer(models.Model):
|
|||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
class CidReportAnswer(BaseReportAnswer):
|
class CidReportAnswer(BaseReportAnswer):
|
||||||
cid = models.BigIntegerField(
|
cid = models.BigIntegerField(
|
||||||
blank=False,
|
blank=False,
|
||||||
@@ -2599,6 +2600,7 @@ class CidReportAnswer(BaseReportAnswer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
class UserReportAnswer(BaseReportAnswer):
|
class UserReportAnswer(BaseReportAnswer):
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load help_tags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
|
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
|
||||||
@@ -98,6 +99,19 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
{% if request.user.is_staff or request.user.is_superuser or request.user in collection.author.all %}
|
||||||
|
{% if answer %}
|
||||||
|
{% if answer.user %}
|
||||||
|
<a href="{% url 'generic:view_answer_history' app_name='atlas' model_name='UserReportAnswer' pk=answer.pk %}" class="btn btn-sm btn-outline-info">
|
||||||
|
History
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'generic:view_answer_history' app_name='atlas' model_name='CidReportAnswer' pk=answer.pk %}" class="btn btn-sm btn-outline-info">
|
||||||
|
History
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
<a class="btn btn-sm btn-primary"
|
<a class="btn btn-sm btn-primary"
|
||||||
href="{% if cid is not None %}{% url 'atlas:collection_case_view_take' pk=collection.id case_number=forloop.counter0 cid=cid passcode=passcode %}{% else %}{% url 'atlas:collection_case_view_take_user' pk=collection.id case_number=forloop.counter0 %}{% endif %}">
|
href="{% if cid is not None %}{% url 'atlas:collection_case_view_take' pk=collection.id case_number=forloop.counter0 cid=cid passcode=passcode %}{% else %}{% url 'atlas:collection_case_view_take_user' pk=collection.id case_number=forloop.counter0 %}{% endif %}">
|
||||||
Open Case
|
Open Case
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
{% extends 'generic/base.html' %}
|
||||||
|
{% load help_tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-lg-10">
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
||||||
|
<h3 class="card-title mb-0">Answer Revision History</h3>
|
||||||
|
<span class="badge bg-secondary fs-6">{{ history|length }} version{{ history|pluralize }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>App/Model:</strong> <span class="text-primary">{{ app_name }}.{{ model_name }}</span><br>
|
||||||
|
<strong>Answer ID:</strong> <span class="text-primary">{{ answer.pk }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 text-md-end">
|
||||||
|
{% if exam %}
|
||||||
|
<strong>Exam:</strong> <span class="text-info">{{ exam }}</span>
|
||||||
|
{% elif collection %}
|
||||||
|
<strong>Case Collection:</strong> <span class="text-info">{{ collection }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover align-middle">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 80px;">Rev ID</th>
|
||||||
|
<th style="width: 180px;">Date / Time</th>
|
||||||
|
<th style="width: 150px;">Editor</th>
|
||||||
|
<th>Answer Value</th>
|
||||||
|
<th style="width: 100px;">Score</th>
|
||||||
|
<th>Comment</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for rev in history %}
|
||||||
|
<tr>
|
||||||
|
<td><code>#{{ rev.revision_id }}</code></td>
|
||||||
|
<td>{{ rev.date }}</td>
|
||||||
|
<td>
|
||||||
|
{% if rev.user %}
|
||||||
|
<span class="badge bg-light text-dark">{{ rev.user.username }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted italic">System / Anon</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<pre class="mb-0 bg-light text-dark p-2 rounded" style="white-space: pre-wrap; font-size: 0.85rem;">{{ rev.answer_text }}</pre>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if rev.score is not None and rev.score != "" %}
|
||||||
|
<span class="badge bg-primary">{{ rev.score }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td><small class="text-muted">{{ rev.comment|default:"-" }}</small></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Compacting tools panel -->
|
||||||
|
<div class="card mt-4 border-warning">
|
||||||
|
<div class="card-header bg-warning-subtle text-warning-emphasis fw-bold">
|
||||||
|
<i class="bi bi-scissors me-1"></i> Compact History Settings
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted small">
|
||||||
|
Compacting history deletes all older versions and their associated revision logs, retaining only the final answer database state. This is permanent and cannot be undone.
|
||||||
|
</p>
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
<form method="post" style="display:inline;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="compact_single">
|
||||||
|
<button type="submit" class="btn btn-warning btn-sm" onclick="return confirm('Are you sure you want to delete all historical edits for this individual answer?');">
|
||||||
|
Compact Single Answer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if exam or collection %}
|
||||||
|
<form method="post" style="display:inline;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="compact_bulk">
|
||||||
|
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Are you sure you want to delete all historical revisions for ALL candidate answers in this entire exam/collection?');">
|
||||||
|
Compact Entire {% if exam %}Exam{% else %}Collection{% endif %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.history.back();">Back</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<h4 class="mb-3">Answers Imported Successfully</h4>
|
<h4 class="mb-3">Answers Imported Successfully</h4>
|
||||||
|
|
||||||
<div class="bg-light p-3 rounded mb-4 text-start">
|
<div class="bg-dark p-3 rounded mb-4 text-start">
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-6 text-muted">Exam:</div>
|
<div class="col-6 text-muted">Exam:</div>
|
||||||
<div class="col-6 fw-bold">{{ eid_str }}</div>
|
<div class="col-6 fw-bold">{{ eid_str }}</div>
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ from django.template import Node, TemplateSyntaxError, Variable
|
|||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def get_item(obj, key):
|
||||||
|
try:
|
||||||
|
return obj[key]
|
||||||
|
except (KeyError, TypeError, IndexError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def update_query_param(context, key, value):
|
def update_query_param(context, key, value):
|
||||||
"""Return a querystring with a dynamic key updated.
|
"""Return a querystring with a dynamic key updated.
|
||||||
|
|||||||
@@ -244,3 +244,50 @@ def test_import_exam_answers_post_success(cid_manager_client, exam_data, logged_
|
|||||||
assert answers.count() == 1
|
assert answers.count() == 1
|
||||||
assert answers.first().answer == "Imported Test Answer"
|
assert answers.first().answer == "Imported Test Answer"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_answer_history_and_compacting(cid_manager_client, exam_data, logged_in_user):
|
||||||
|
exam, question = exam_data
|
||||||
|
from anatomy.models import UserAnswer as AnatomyUserAnswer
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
# 1. Create an answer within a revision context
|
||||||
|
with reversion.create_revision():
|
||||||
|
ans = AnatomyUserAnswer.objects.create(
|
||||||
|
exam=exam,
|
||||||
|
question=question,
|
||||||
|
user=logged_in_user,
|
||||||
|
answer="Version 1",
|
||||||
|
)
|
||||||
|
reversion.set_comment("Initial answer")
|
||||||
|
|
||||||
|
# 2. Modify the answer within a revision context
|
||||||
|
with reversion.create_revision():
|
||||||
|
ans.answer = "Version 2"
|
||||||
|
ans.save()
|
||||||
|
reversion.set_comment("Correction")
|
||||||
|
|
||||||
|
url = reverse("generic:view_answer_history", kwargs={
|
||||||
|
"app_name": "anatomy",
|
||||||
|
"model_name": "UserAnswer",
|
||||||
|
"pk": ans.pk,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check GET revision list
|
||||||
|
response = cid_manager_client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"Version 1" in response.content
|
||||||
|
assert b"Version 2" in response.content
|
||||||
|
assert b"Initial answer" in response.content
|
||||||
|
|
||||||
|
# Test Compact Single action
|
||||||
|
response_compact = cid_manager_client.post(url, {"action": "compact_single"})
|
||||||
|
assert response_compact.status_code == 302 # redirect
|
||||||
|
|
||||||
|
# Verify only 1 version remains
|
||||||
|
from reversion.models import Version
|
||||||
|
versions = Version.objects.get_for_object(ans)
|
||||||
|
assert versions.count() == 1
|
||||||
|
assert versions.first().field_dict["answer"] == "Version 2"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path("cids/manage/", views.CidUserView.as_view(), name="manage_cids"),
|
path("cids/manage/", views.CidUserView.as_view(), name="manage_cids"),
|
||||||
path("cids/manage/import-answers/", views.import_exam_answers, name="import_exam_answers"),
|
path("cids/manage/import-answers/", views.import_exam_answers, name="import_exam_answers"),
|
||||||
|
path("answer-history/<str:app_name>/<str:model_name>/<int:pk>/", views.view_answer_history, name="view_answer_history"),
|
||||||
path("cids/manage/exams", views.CidUserExamView.as_view(), name="manage_cid_exams"),
|
path("cids/manage/exams", views.CidUserExamView.as_view(), name="manage_cid_exams"),
|
||||||
path("cids/group/", views.cid_group_view, name="cid_group_view"),
|
path("cids/group/", views.cid_group_view, name="cid_group_view"),
|
||||||
path("cids/group/all", views.cid_group_view_all, name="cid_group_view_all"),
|
path("cids/group/all", views.cid_group_view_all, name="cid_group_view_all"),
|
||||||
|
|||||||
@@ -3897,6 +3897,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
"total_score": total_score,
|
"total_score": total_score,
|
||||||
"max_score": max_score,
|
"max_score": max_score,
|
||||||
"answers_and_marks": answers_and_marks,
|
"answers_and_marks": answers_and_marks,
|
||||||
|
"user_answers_map": user_answers_map,
|
||||||
"view_all_results": view_all_results,
|
"view_all_results": view_all_results,
|
||||||
"supervisor": supervisor,
|
"supervisor": supervisor,
|
||||||
#"cid_user_exam": cid_user_exam,
|
#"cid_user_exam": cid_user_exam,
|
||||||
@@ -5238,6 +5239,135 @@ def import_exam_answers(request):
|
|||||||
del request.session["pending_import_data"]
|
del request.session["pending_import_data"]
|
||||||
return render(request, "generic/import_exam_answers.html")
|
return render(request, "generic/import_exam_answers.html")
|
||||||
|
|
||||||
|
|
||||||
|
def compact_history_for_object(obj):
|
||||||
|
"""
|
||||||
|
Helper function to delete all historical revisions for a registered object,
|
||||||
|
leaving only the most recent version.
|
||||||
|
"""
|
||||||
|
from reversion.models import Version
|
||||||
|
versions = Version.objects.get_for_object(obj).order_by("-revision__date_created")
|
||||||
|
count = versions.count()
|
||||||
|
if count > 1:
|
||||||
|
# Keep the latest version, delete older versions and their revisions
|
||||||
|
recent_version = versions[0]
|
||||||
|
old_versions = list(versions[1:])
|
||||||
|
for v in old_versions:
|
||||||
|
revision = v.revision
|
||||||
|
v.delete()
|
||||||
|
if not revision.version_set.exists():
|
||||||
|
revision.delete()
|
||||||
|
return count - 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def view_answer_history(request, app_name, model_name, pk):
|
||||||
|
"""
|
||||||
|
View to display the revision history for a user/candidate answer,
|
||||||
|
and compact history (individual and bulk).
|
||||||
|
"""
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.apps import apps
|
||||||
|
from reversion.models import Version
|
||||||
|
|
||||||
|
try:
|
||||||
|
Model = apps.get_model(app_name, model_name)
|
||||||
|
except LookupError:
|
||||||
|
raise Http404("Model not found")
|
||||||
|
|
||||||
|
answer = get_object_or_404(Model, pk=pk)
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
can_view = (
|
||||||
|
request.user.is_staff
|
||||||
|
or request.user.is_superuser
|
||||||
|
or request.user.groups.filter(name__in=[
|
||||||
|
"cid_user_manager", "anatomy_checker", "rapid_checker", "long_checker"
|
||||||
|
]).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
exam = getattr(answer, "exam", None)
|
||||||
|
collection = None
|
||||||
|
if hasattr(answer, "question") and hasattr(answer.question, "collection"):
|
||||||
|
collection = answer.question.collection
|
||||||
|
|
||||||
|
if not can_view:
|
||||||
|
if exam and hasattr(exam, "author") and request.user in exam.author.all():
|
||||||
|
can_view = True
|
||||||
|
if collection and hasattr(collection, "author") and request.user in collection.author.all():
|
||||||
|
can_view = True
|
||||||
|
|
||||||
|
if not can_view:
|
||||||
|
raise PermissionDenied("You do not have permission to view this answer's history")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
action = request.POST.get("action")
|
||||||
|
if action == "compact_single":
|
||||||
|
compacted = compact_history_for_object(answer)
|
||||||
|
messages.success(request, f"Compacted single answer history. Removed {compacted} old revisions.")
|
||||||
|
return HttpResponseRedirect(request.path)
|
||||||
|
|
||||||
|
elif action == "compact_bulk":
|
||||||
|
compacted_total = 0
|
||||||
|
if exam:
|
||||||
|
answers_qs = Model.objects.filter(exam=exam)
|
||||||
|
for ans in answers_qs:
|
||||||
|
compacted_total += compact_history_for_object(ans)
|
||||||
|
messages.success(request, f"Bulk compacted history for all answers in Exam '{exam}'. Removed {compacted_total} old revisions.")
|
||||||
|
elif collection:
|
||||||
|
from atlas.models import CidReportAnswer, UserReportAnswer
|
||||||
|
cids_qs = CidReportAnswer.objects.filter(question__collection=collection)
|
||||||
|
users_qs = UserReportAnswer.objects.filter(question__collection=collection)
|
||||||
|
for ans in cids_qs:
|
||||||
|
compacted_total += compact_history_for_object(ans)
|
||||||
|
for ans in users_qs:
|
||||||
|
compacted_total += compact_history_for_object(ans)
|
||||||
|
messages.success(request, f"Bulk compacted history for all answers in Case Collection '{collection}'. Removed {compacted_total} old revisions.")
|
||||||
|
return HttpResponseRedirect(request.path)
|
||||||
|
|
||||||
|
# GET request
|
||||||
|
versions = Version.objects.get_for_object(answer).select_related("revision__user").order_by("-revision__date_created")
|
||||||
|
|
||||||
|
history_data = []
|
||||||
|
for v in versions:
|
||||||
|
fields = v.field_dict
|
||||||
|
|
||||||
|
# Format answer contents nicely based on model/app
|
||||||
|
ans_text = fields.get("answer", "")
|
||||||
|
if not ans_text:
|
||||||
|
if model_name.lower() == "useranswer" and app_name.lower() == "longs":
|
||||||
|
ans_text = (
|
||||||
|
f"Observations: {fields.get('answer_observations', '')}\n"
|
||||||
|
f"Interpretation: {fields.get('answer_interpretation', '')}\n"
|
||||||
|
f"Diagnosis: {fields.get('answer_principle_diagnosis', '')}\n"
|
||||||
|
f"Differential: {fields.get('answer_differential_diagnosis', '')}\n"
|
||||||
|
f"Management: {fields.get('answer_management', '')}"
|
||||||
|
)
|
||||||
|
elif model_name.lower() == "useranswer" and app_name.lower() == "physics":
|
||||||
|
ans_text = f"A: {fields.get('a')}, B: {fields.get('b')}, C: {fields.get('c')}, D: {fields.get('d')}, E: {fields.get('e')}"
|
||||||
|
|
||||||
|
history_data.append({
|
||||||
|
"revision_id": v.revision_id,
|
||||||
|
"date": v.revision.date_created,
|
||||||
|
"user": v.revision.user,
|
||||||
|
"answer_text": ans_text,
|
||||||
|
"score": fields.get("score", ""),
|
||||||
|
"comment": v.revision.comment,
|
||||||
|
})
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"answer": answer,
|
||||||
|
"history": history_data,
|
||||||
|
"exam": exam,
|
||||||
|
"collection": collection,
|
||||||
|
"app_name": app_name,
|
||||||
|
"model_name": model_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "generic/answer_history.html", context)
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def user_group_add_by_email_user(request, group_id):
|
def user_group_add_by_email_user(request, group_id):
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load help_tags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="question-display-block">
|
<div class="question-display-block">
|
||||||
@@ -23,6 +24,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
" data-question-number="{{forloop.counter}}"><b>Question {{forloop.counter}}</b> <span class="view-question-link-longs"
|
" data-question-number="{{forloop.counter}}"><b>Question {{forloop.counter}}</b> <span class="view-question-link-longs"
|
||||||
data-qn={{forloop.counter0}}>View</span>
|
data-qn={{forloop.counter0}}>View</span>
|
||||||
|
{% if request.user.is_staff or view_all_results or request.user in exam.author.all %}
|
||||||
|
{% with q=questions|get_item:forloop.counter0 %}
|
||||||
|
{% with ua=user_answers_map|get_item:q.pk %}
|
||||||
|
{% if ua %}
|
||||||
|
<a href="{% url 'generic:view_answer_history' app_name='longs' model_name='UserAnswer' pk=ua.pk %}" class="btn btn-sm btn-outline-info ms-2" style="font-size: 11px; padding: 2px 6px; vertical-align: middle;">History</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
<ul class="">
|
<ul class="">
|
||||||
<li class="Observations" data-qidn="1">Observations</br>
|
<li class="Observations" data-qidn="1">Observations</br>
|
||||||
<pre>{{ans.0}}</pre>
|
<pre>{{ans.0}}</pre>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load help_tags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container physics my-3">
|
<div class="container physics my-3">
|
||||||
@@ -28,6 +29,13 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'physics:exam_take' exam.pk forloop.counter0 cid passcode %}" class="text-decoration-none text-body">{{ question.stem|safe }}</a>
|
<a href="{% url 'physics:exam_take' exam.pk forloop.counter0 cid passcode %}" class="text-decoration-none text-body">{{ question.stem|safe }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if request.user.is_staff or view_all_results or request.user in exam.author.all %}
|
||||||
|
{% with ua=user_answers_map|get_item:question.pk %}
|
||||||
|
{% if ua %}
|
||||||
|
<a href="{% url 'generic:view_answer_history' app_name='physics' model_name='UserAnswer' pk=ua.pk %}" class="btn btn-sm btn-outline-info ms-2" style="font-size: 11px; padding: 2px 6px; vertical-align: middle;">History</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load help_tags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="question-display-block">
|
<div class="question-display-block">
|
||||||
@@ -21,6 +22,15 @@
|
|||||||
{% for ans, score, correct_answer in answers_and_marks %}
|
{% for ans, score, correct_answer in answers_and_marks %}
|
||||||
<li class="user-answer-li" data-question-number="{{forloop.counter}}">Question {{forloop.counter}} - Correct answer: {% if exam.publish_results %}<span class="correct-answer">{{correct_answer}}</span>{% endif %}
|
<li class="user-answer-li" data-question-number="{{forloop.counter}}">Question {{forloop.counter}} - Correct answer: {% if exam.publish_results %}<span class="correct-answer">{{correct_answer}}</span>{% endif %}
|
||||||
<span class="view-question-link" data-qn={{forloop.counter0}}>View</span>
|
<span class="view-question-link" data-qn={{forloop.counter0}}>View</span>
|
||||||
|
{% if request.user.is_staff or view_all_results or request.user in exam.author.all %}
|
||||||
|
{% with q=questions|get_item:forloop.counter0 %}
|
||||||
|
{% with ua=user_answers_map|get_item:q.pk %}
|
||||||
|
{% if ua %}
|
||||||
|
<a href="{% url 'generic:view_answer_history' app_name='rapids' model_name='UserAnswer' pk=ua.pk %}" class="btn btn-sm btn-outline-info ms-2" style="font-size: 11px; padding: 2px 6px; vertical-align: middle;">History</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
<br/>
|
<br/>
|
||||||
<span class="user-answer-score user-answer-score-{{score}}">
|
<span class="user-answer-score user-answer-score-{{score}}">
|
||||||
<span class="submitted-user-answer">
|
<span class="submitted-user-answer">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load help_tags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="sbas">
|
<div class="sbas">
|
||||||
@@ -25,6 +26,13 @@
|
|||||||
{% if exam.publish_results or view_all_results %}
|
{% if exam.publish_results or view_all_results %}
|
||||||
<span class="answer-{{score}}">(Score: {{score}})</span>
|
<span class="answer-{{score}}">(Score: {{score}})</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if request.user.is_staff or view_all_results or request.user in exam.author.all %}
|
||||||
|
{% with ua=user_answers_map|get_item:question.pk %}
|
||||||
|
{% if ua %}
|
||||||
|
<a href="{% url 'generic:view_answer_history' app_name='sbas' model_name='UserAnswer' pk=ua.pk %}" class="btn btn-sm btn-outline-info ms-2" style="font-size: 11px; padding: 2px 6px; vertical-align: middle;">History</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from rad.settings import REMOTE_URL
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
import dicognito
|
import dicognito
|
||||||
import json
|
import json
|
||||||
|
import reversion
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
from generic.models import (
|
from generic.models import (
|
||||||
@@ -590,6 +591,7 @@ class Exam(ExamBase):
|
|||||||
return reverse("shorts:exam_overview", kwargs={"pk": self.pk})
|
return reverse("shorts:exam_overview", kwargs={"pk": self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
class UserAnswer(UserAnswerBase):
|
class UserAnswer(UserAnswerBase):
|
||||||
"""User answers by candidate"""
|
"""User answers by candidate"""
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load help_tags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="question-display-block card mb-4" style="background-color: #111; color: #fff;">
|
<div class="question-display-block card mb-4" style="background-color: #111; color: #fff;">
|
||||||
@@ -24,6 +25,15 @@
|
|||||||
{% for ans, score, correct_answer, feedback in answers_and_marks %}
|
{% for ans, score, correct_answer, feedback in answers_and_marks %}
|
||||||
<li class="user-answer-li" data-question-number="{{forloop.counter}}">Question {{forloop.counter}} - Correct answer: {% if exam.publish_results %}<span class="correct-answer">{{correct_answer}}</span>{% endif %}
|
<li class="user-answer-li" data-question-number="{{forloop.counter}}">Question {{forloop.counter}} - Correct answer: {% if exam.publish_results %}<span class="correct-answer">{{correct_answer}}</span>{% endif %}
|
||||||
<span class="view-question-link" data-qn={{forloop.counter0}}>View</span>
|
<span class="view-question-link" data-qn={{forloop.counter0}}>View</span>
|
||||||
|
{% if request.user.is_staff or view_all_results or request.user in exam.author.all %}
|
||||||
|
{% with q=questions|get_item:forloop.counter0 %}
|
||||||
|
{% with ua=user_answers_map|get_item:q.pk %}
|
||||||
|
{% if ua %}
|
||||||
|
<a href="{% url 'generic:view_answer_history' app_name='shorts' model_name='UserAnswer' pk=ua.pk %}" class="btn btn-sm btn-outline-info ms-2" style="font-size: 11px; padding: 2px 6px; vertical-align: middle;">History</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
<br/>
|
<br/>
|
||||||
<span class="user-answer-score user-answer-score-{{score}}">
|
<span class="user-answer-score user-answer-score-{{score}}">
|
||||||
<span class="submitted-user-answer">
|
<span class="submitted-user-answer">
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
Import exam answers should be accesible as its own page from the admin submenu. it should have a preview mode that shows what is to be imported (and what it will overwrite / update). it should also give some feedback about the imported answers (so we know what has been imported.).
|
User answer compaction should only be available to admin users and on an exam wide basis.
|
||||||
|
|
||||||
Add manual instructions to rts about how to fully reset the app if the reset parameter fails.
|
|
||||||
Reference in New Issue
Block a user