Add functionality to create personal exams from filtered questions and update exam overview with edit option
This commit is contained in:
@@ -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 %}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user