Add functionality to create personal exams from filtered questions and update exam overview with edit option

This commit is contained in:
Ross
2025-10-27 12:39:44 +00:00
parent 473efbc8f4
commit 3f56af4881
4 changed files with 157 additions and 0 deletions
@@ -12,6 +12,21 @@
{% include "exam_clock.html" %}
<h2>Exam: {{exam}}</h2>
{% if can_edit %}
<div class="card mb-3">
<div class="card-body d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center">
<div class="mb-2 mb-md-0">
{% include "generic/partials/exams/exam_status.html#publish-results" %}
</div>
<div>
<a href="{% url 'sbas:exam_update' pk=exam.id %}" class="btn btn-outline-primary btn-sm">
Edit exam
</a>
</div>
</div>
</div>
{% endif %}
{% if exam.publish_results %}
<div class="alert alert-primary review-mode-alert" role="alert">
Exam is in review mode. Score are available
@@ -59,4 +59,37 @@
})();
</script>
<hr/>
<div class="mt-3">
<h5>Create your personal exam</h5>
<p>Generate a personal SBA exam from the filtered question bank. Optionally limit the number of questions.</p>
<form id="create-exam-form" method="post" action="{% url 'sbas:exam_create_from_filters' %}">
{% csrf_token %}
<input type="hidden" name="category" id="create-category" />
<input type="hidden" name="status" id="create-status" />
<div class="mb-2">
<label for="create-count" class="form-label">Number of questions (0 = all)</label>
<input type="number" name="count" id="create-count" class="form-control" min="0" value="0" />
</div>
<button id="create-exam-btn" type="button" class="btn btn-success">Create my exam</button>
</form>
</div>
<script>
(function(){
const createBtn = document.getElementById('create-exam-btn');
createBtn.addEventListener('click', function(){
const categoryEl = document.getElementById('id_category');
const statusEl = document.getElementById('id_status');
const createCat = document.getElementById('create-category');
const createStatus = document.getElementById('create-status');
if (categoryEl) createCat.value = categoryEl.value || '';
if (statusEl) createStatus.value = statusEl.value || '';
document.getElementById('create-exam-form').submit();
});
})();
</script>
{% endblock %}
+5
View File
@@ -99,6 +99,11 @@ urlpatterns = [
views.import_llm_confirm,
name="import_llm_confirm",
),
path(
"exam/create_from_filters/",
views.create_personal_exam,
name="exam_create_from_filters",
),
#path(
# "exam/<int:pk>/scores/<int:cid>/<str:passcode>/",
# views.exam_scores_cid_user,
+104
View File
@@ -29,6 +29,10 @@ from django.http import Http404, HttpResponseBadRequest, JsonResponse
from django.http import HttpResponseRedirect, HttpResponse
from .models import Question, Category, Exam, UserAnswer
from django.contrib.contenttypes.models import ContentType
from generic.models import QuestionReview
from django.http import HttpResponseBadRequest
import random
from django.views.decorators.http import require_http_methods
from django.db import transaction
import re
@@ -149,6 +153,8 @@ def exam_take_overview(request, pk, cid=None, passcode=None):
exam.check_user_can_take(cid, passcode, request.user)
can_edit = exam.check_user_can_edit(request.user)
questions = exam.get_questions()
if cid is not None:
@@ -182,6 +188,7 @@ def exam_take_overview(request, pk, cid=None, passcode=None):
"exam_length": len(questions),
"passcode": passcode,
"cid_user_exam": cid_user_exam,
"can_edit": can_edit,
},
)
@@ -377,6 +384,103 @@ class UserAnswerView(LoginRequiredMixin, DetailView):
model = UserAnswer
@login_required
def create_personal_exam(request):
"""Create a personal SBA exam from question filters and redirect to start page.
POST parameters expected:
- category (optional)
- status (optional)
- count (optional): number of questions to include (0 or missing = all)
"""
if request.method != "POST":
return HttpResponseBadRequest("POST required")
category = request.POST.get("category") or None
status = request.POST.get("status") or "ANY"
try:
count = int(request.POST.get("count") or 0)
if count < 0:
count = 0
except Exception:
count = 0
qs = Question.objects.all().order_by("pk")
if category:
try:
qs = qs.filter(category__id=int(category))
except Exception:
pass
# Permission filter: non-checkers see open_access or their own questions
try:
if not request.user.groups.filter(name="sbas_checker").exists():
from django.db.models import Q
qs = qs.filter(Q(open_access=True) | Q(author__id=request.user.id))
except Exception:
pass
matches = []
for question in qs:
try:
if question.authors_only and not (
request.user.is_superuser or request.user in question.author.all()
):
continue
except Exception:
pass
ct = ContentType.objects.get_for_model(question)
latest = (
QuestionReview.objects.filter(content_type=ct, object_id=question.pk)
.order_by("-created_on").first()
)
match = False
if status == "ANY":
match = True
elif status == "UNREVIEWED":
match = latest is None
else:
if latest is not None and latest.status == status:
match = True
if match:
matches.append(question)
if not matches:
return HttpResponseBadRequest("No matching questions found")
# Optionally randomize and limit
if count and count > 0 and count < len(matches):
random.shuffle(matches)
matches = matches[:count]
# Create the Exam
name = f"{request.user.get_full_name() or request.user.username} - Personal SBA Exam"
exam = Exam(name=name, active=True, candidates_only=True, exam_mode=True)
exam.save()
# Add author and allow this user to take the exam
try:
exam.author.add(request.user)
except Exception:
pass
try:
exam.valid_user_users.add(request.user)
except Exception:
pass
# Add questions
for q in matches:
exam.exam_questions.add(q)
exam.order_questions()
return redirect("sbas:exam_start", pk=exam.pk)
class UserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView):
model = UserAnswer
table_class = UserAnswerTable