Add user answer viewing and compaction
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load help_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container anatomy my-3">
|
||||
@@ -52,6 +53,15 @@
|
||||
{% 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>
|
||||
{% 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>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -2592,6 +2592,7 @@ class BaseReportAnswer(models.Model):
|
||||
abstract = True
|
||||
|
||||
|
||||
@reversion.register
|
||||
class CidReportAnswer(BaseReportAnswer):
|
||||
cid = models.BigIntegerField(
|
||||
blank=False,
|
||||
@@ -2599,6 +2600,7 @@ class CidReportAnswer(BaseReportAnswer):
|
||||
)
|
||||
|
||||
|
||||
@reversion.register
|
||||
class UserReportAnswer(BaseReportAnswer):
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load help_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
|
||||
@@ -98,6 +99,19 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<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"
|
||||
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
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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="col-6 text-muted">Exam:</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.filter
|
||||
def get_item(obj, key):
|
||||
try:
|
||||
return obj[key]
|
||||
except (KeyError, TypeError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def update_query_param(context, key, value):
|
||||
"""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.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/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/group/", views.cid_group_view, name="cid_group_view"),
|
||||
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,
|
||||
"max_score": max_score,
|
||||
"answers_and_marks": answers_and_marks,
|
||||
"user_answers_map": user_answers_map,
|
||||
"view_all_results": view_all_results,
|
||||
"supervisor": supervisor,
|
||||
#"cid_user_exam": cid_user_exam,
|
||||
@@ -5238,6 +5239,135 @@ def import_exam_answers(request):
|
||||
del request.session["pending_import_data"]
|
||||
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
|
||||
def user_group_add_by_email_user(request, group_id):
|
||||
if request.htmx:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load help_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="question-display-block">
|
||||
@@ -23,6 +24,15 @@
|
||||
{% endif %}
|
||||
" data-question-number="{{forloop.counter}}"><b>Question {{forloop.counter}}</b> <span class="view-question-link-longs"
|
||||
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="">
|
||||
<li class="Observations" data-qidn="1">Observations</br>
|
||||
<pre>{{ans.0}}</pre>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load help_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container physics my-3">
|
||||
@@ -28,6 +29,13 @@
|
||||
{% else %}
|
||||
<a href="{% url 'physics:exam_take' exam.pk forloop.counter0 cid passcode %}" class="text-decoration-none text-body">{{ question.stem|safe }}</a>
|
||||
{% 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 class="mb-2">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load help_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="question-display-block">
|
||||
@@ -21,6 +22,15 @@
|
||||
{% 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 %}
|
||||
<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/>
|
||||
<span class="user-answer-score user-answer-score-{{score}}">
|
||||
<span class="submitted-user-answer">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load help_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="sbas">
|
||||
@@ -25,6 +26,13 @@
|
||||
{% if exam.publish_results or view_all_results %}
|
||||
<span class="answer-{{score}}">(Score: {{score}})</span>
|
||||
{% 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>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from rad.settings import REMOTE_URL
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import dicognito
|
||||
import json
|
||||
import reversion
|
||||
|
||||
# Create your models here.
|
||||
from generic.models import (
|
||||
@@ -590,6 +591,7 @@ class Exam(ExamBase):
|
||||
return reverse("shorts:exam_overview", kwargs={"pk": self.pk})
|
||||
|
||||
|
||||
@reversion.register
|
||||
class UserAnswer(UserAnswerBase):
|
||||
"""User answers by candidate"""
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load help_tags %}
|
||||
|
||||
{% block content %}
|
||||
<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 %}
|
||||
<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>
|
||||
{% 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/>
|
||||
<span class="user-answer-score user-answer-score-{{score}}">
|
||||
<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.).
|
||||
|
||||
Add manual instructions to rts about how to fully reset the app if the reset parameter fails.
|
||||
User answer compaction should only be available to admin users and on an exam wide basis.
|
||||
Reference in New Issue
Block a user