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" %}
|
{% include "exam_clock.html" %}
|
||||||
<h2>Exam: {{exam}}</h2>
|
<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 %}
|
{% if exam.publish_results %}
|
||||||
<div class="alert alert-primary review-mode-alert" role="alert">
|
<div class="alert alert-primary review-mode-alert" role="alert">
|
||||||
Exam is in review mode. Score are available
|
Exam is in review mode. Score are available
|
||||||
|
|||||||
@@ -59,4 +59,37 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</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 %}
|
{% endblock %}
|
||||||
@@ -99,6 +99,11 @@ urlpatterns = [
|
|||||||
views.import_llm_confirm,
|
views.import_llm_confirm,
|
||||||
name="import_llm_confirm",
|
name="import_llm_confirm",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"exam/create_from_filters/",
|
||||||
|
views.create_personal_exam,
|
||||||
|
name="exam_create_from_filters",
|
||||||
|
),
|
||||||
#path(
|
#path(
|
||||||
# "exam/<int:pk>/scores/<int:cid>/<str:passcode>/",
|
# "exam/<int:pk>/scores/<int:cid>/<str:passcode>/",
|
||||||
# views.exam_scores_cid_user,
|
# views.exam_scores_cid_user,
|
||||||
|
|||||||
+104
@@ -29,6 +29,10 @@ from django.http import Http404, HttpResponseBadRequest, JsonResponse
|
|||||||
from django.http import HttpResponseRedirect, HttpResponse
|
from django.http import HttpResponseRedirect, HttpResponse
|
||||||
|
|
||||||
from .models import Question, Category, Exam, UserAnswer
|
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.views.decorators.http import require_http_methods
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
import re
|
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)
|
exam.check_user_can_take(cid, passcode, request.user)
|
||||||
|
|
||||||
|
can_edit = exam.check_user_can_edit(request.user)
|
||||||
|
|
||||||
questions = exam.get_questions()
|
questions = exam.get_questions()
|
||||||
|
|
||||||
if cid is not None:
|
if cid is not None:
|
||||||
@@ -182,6 +188,7 @@ def exam_take_overview(request, pk, cid=None, passcode=None):
|
|||||||
"exam_length": len(questions),
|
"exam_length": len(questions),
|
||||||
"passcode": passcode,
|
"passcode": passcode,
|
||||||
"cid_user_exam": cid_user_exam,
|
"cid_user_exam": cid_user_exam,
|
||||||
|
"can_edit": can_edit,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -377,6 +384,103 @@ class UserAnswerView(LoginRequiredMixin, DetailView):
|
|||||||
model = UserAnswer
|
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):
|
class UserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView):
|
||||||
model = UserAnswer
|
model = UserAnswer
|
||||||
table_class = UserAnswerTable
|
table_class = UserAnswerTable
|
||||||
|
|||||||
Reference in New Issue
Block a user