This commit is contained in:
Ross
2023-09-02 10:24:17 +01:00
parent 06f1e7852d
commit f6d7bd594a
9 changed files with 349 additions and 43 deletions
@@ -9,7 +9,7 @@
<ol type="a">
{% for q, a, score, correct_answer in ans %}
<li class="question-part">
<span class="question-text">{{q|safe}}:</span> {% if exam.publish_results %}<span class="correct-answer">Correct answer: {{correct_answer}}</span>{% endif %}
<span class="question-text">{{q|safe}}:</span> {% if exam.publish_results %}<span class="correct-answer">Correct answer: {{correct_answer|safe}}</span>{% endif %}
<br />
<span class="submitted-user-answer">{{a}}</span>
{% if exam.publish_results %}
@@ -9,11 +9,17 @@
<h2>Exam: {{exam.name}}</h2>
{% comment %} {% if exam.publish_results %}
<div class="alert alert-primary" role="alert">
Exam is in review mode. Add question feedback <a href="#" onclick="return window.create_popup_window('{% url 'feedback_create' question_type='physics' pk=question.pk %}')">here</a>.
{% if exam.publish_results %}
<div class="alert alert-primary review-mode-alert" role="alert">
Exam is in review mode. Score are available
{% if request.user.is_authenticated %}
<a href="{% url 'physics:exam_scores_user' pk=exam.id %}">here</a>
{% else %}
<a href="{% url 'physics:exam_scores_cid_user' pk=exam.id cid=cid passcode=passcode %}">here</a>
{% endif %}
</div>
{% endif %} {% endcomment %}
{% endif %}
<div><p>Questions</p></div>
<div class="overview-text">{{answer_count}} out of {{exam_length}} questions answered. Click to go to question.</div>
+61 -22
View File
@@ -73,7 +73,12 @@ class ExamTester:
self.exam = exam = create_exam(db, ExamViews)
cidgroup1: CidUserGroup = CidUserGroup.objects.get(name="Group1")
assert exam in cidgroup1.physics_cid_user_groups.all()
match app_name:
case "physics":
assert exam in cidgroup1.physics_cid_user_groups.all()
case "sbas":
assert exam in cidgroup1.sba_cid_user_groups.all()
cid_user_1001 = CidUser.objects.get(cid=1001)
cid_user_1000 = CidUser.objects.get(cid=1000)
@@ -200,11 +205,16 @@ class ExamTester:
== question.stem
)
assert cid_take_soup.find("label", {"for": "id_a"}).string == question.a
assert cid_take_soup.find("label", {"for": "id_b"}).string == question.b
assert cid_take_soup.find("label", {"for": "id_c"}).string == question.c
assert cid_take_soup.find("label", {"for": "id_d"}).string == question.d
assert cid_take_soup.find("label", {"for": "id_e"}).string == question.e
match self.app_name:
case "physics":
assert cid_take_soup.find("label", {"for": "id_a"}).string == question.a
assert cid_take_soup.find("label", {"for": "id_b"}).string == question.b
assert cid_take_soup.find("label", {"for": "id_c"}).string == question.c
assert cid_take_soup.find("label", {"for": "id_d"}).string == question.d
assert cid_take_soup.find("label", {"for": "id_e"}).string == question.e
case "sbas":
assert cid_take_soup.find("ul", {"class": "sba-answer-list"}) is not None
assert len(cid_take_soup.find("ul", {"class": "sba-answer-list"}).find_all("li")) == 5
# As we haven't submitted any answers they should all be blank
assert (
@@ -379,17 +389,27 @@ class ExamTester:
alert = cid_exam_scores_soup.find("div", {"class": "alert"})
assert "Results are not currently published." in str(alert)
# Check that we are showing enough questions / answers
answer_lis = cid_exam_scores_soup.find_all("li", {"class": "question-part"})
assert len(answer_lis) == 5 * len(self.generated_questions)
submitted_answers = cid_exam_scores_soup.find_all(
"span", {"class": "submitted-user-answer"}
)
assert len(submitted_answers) == 5 * len(self.generated_questions)
for ans in submitted_answers:
assert ans.text.strip() == "True"
# Check that we are showing enough questions / answers
match self.app_name:
case "physics":
answer_lis = cid_exam_scores_soup.find_all("li", {"class": "question-part"})
assert len(answer_lis) == 5 * len(self.generated_questions)
assert len(submitted_answers) == 5 * len(self.generated_questions)
for ans in submitted_answers:
assert ans.text.strip() == "True"
case "sbas":
answer_lis = cid_exam_scores_soup.find_all("li", {"class": "user-answer-li"})
assert len(answer_lis) == len(self.generated_questions)
assert len(submitted_answers) == len(self.generated_questions)
for ans in submitted_answers:
assert ans.text.strip() == "a"
# Check no correct answers are shown
assert not cid_exam_scores_soup.find_all("span", {"class": "correct-answer"})
@@ -401,20 +421,39 @@ class ExamTester:
cid_exam_scores_res = self.client.get(exam_scores_url)
cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
answer_lis = cid_exam_scores_soup.find_all("li", {"class": "question-part"})
assert len(answer_lis) == 5 * len(self.generated_questions)
submitted_answers = cid_exam_scores_soup.find_all(
"span", {"class": "submitted-user-answer"}
)
assert len(submitted_answers) == 5 * len(self.generated_questions)
match self.app_name:
case "physics":
answer_lis = cid_exam_scores_soup.find_all("li", {"class": "question-part"})
assert len(answer_lis) == 5 * len(self.generated_questions)
assert len(submitted_answers) == 5 * len(self.generated_questions)
for ans in submitted_answers[:-5]:
assert ans.text.strip() == "True"
# The new question will not have any answers
for ans in submitted_answers[-5:]:
assert ans.text.strip() == ""
case "sbas":
answer_lis = cid_exam_scores_soup.find_all("li", {"class": "user-answer-li"})
assert len(answer_lis) == len(self.generated_questions)
assert len(submitted_answers) == len(self.generated_questions)
print(submitted_answers)
for ans in submitted_answers[:3]:
print("test1", ans)
assert ans.text.strip() == "a"
# The new question will not have any answers
for ans in submitted_answers[3:]:
print("test2", ans)
assert ans.text.strip() == ""
for ans in submitted_answers[:-5]:
assert ans.text.strip() == "True"
# The new question will not have any answers
for ans in submitted_answers[-5:]:
assert ans.text.strip() == ""
# Publish the results!
self.exam.publish_results = True
+22 -7
View File
@@ -5,14 +5,29 @@
{% include 'user_score_header.html' %}
<ul>
{% for question, a, score, correct_answer, chosen_answer in answers_and_marks %}
<li class="user-answer-li"><a href="{% url 'sbas:exam_take' exam.pk forloop.counter0 cid passcode %}">Question
{{forloop.counter}}</a> - {{ question.stem |safe}}</li>
<span>Correct answer: {{correct_answer|safe}} <br />Chosen answer: {{a}} {{chosen_answer|safe}}
{% if exam.publish_results or view_all_results %}
<span class="answer-{{score}}">(Score: {{score}})</span>
<li class="user-answer-li">
{% if request.user.is_authenticated %}
<a href="{% url 'sbas:exam_take_user' pk=exam.id sk=forloop.counter0 %}">
Question {{forloop.counter}}
</a>
{% else %}
<a href="{% url 'sbas:exam_take' exam.pk forloop.counter0 cid passcode %}">
Question {{forloop.counter}}
</a>
{% endif %}
</span>
- {{ question.stem |safe}}
{% if exam.publish_results %}
<span class="correct-answer">Correct answer: {{correct_answer|safe}}</span> <br />
{% endif %}
<span class="submitted-user-answer">{{a}}</span>
Chosen answer: {{chosen_answer|safe}}
{% if exam.publish_results or view_all_results %}
<span class="answer-{{score}}">(Score: {{score}})</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% include 'user_scores_footer.html' %}
+5 -3
View File
@@ -1,11 +1,13 @@
{% extends 'base.html' %}
{% block content %}
<span id="user-id">
{% if request.user.is_authenticated %}
User: {{request.user}}
{% else %}
CID: {{cid}}
{% endif %}
</span>
<div id="timeup">
Allocated time over.
</div>
@@ -29,15 +31,15 @@
</div>
</div>
<div class="no-select">
<h2>{{exam.name}}: Question [{{pos|add:1}}/{{exam_length}}]</h2>
<h2>{{exam.name}}: Question [<span id="question-number">{{pos|add:1}}</span>/<span id="exam-length">{{exam_length}}</span>]</h2>
{% if exam.publish_results %}
<div class="alert alert-primary" role="alert">
<div class="alert alert-primary review-mode-alert" role="alert">
Exam is in review mode. Add question feedback <a href="#" onclick="return window.create_popup_window('{% url 'feedback_create' question_type='sbas' pk=question.pk %}')">here</a>.
</div>
{% endif %}
<div>
<p>{{question.stem|safe}}</p>
<p><span id="question-stem">{{question.stem|safe}}</span></p>
</div>
<ul class="sba-answer-list">
<li data-ans="a">{{question.a_answer|safe}}</li>
+26 -5
View File
@@ -1,19 +1,40 @@
{% extends 'base.html' %}
{% block content %}
CID: {{cid}}
{% if request.user.is_authenticated %}
User: {{request.user}}
{% else %}
CID: {{cid}}
{% endif %}
<h2>Exam: {{exam.name}}</h2>
{% if exam.publish_results %}
<div class="alert alert-primary review-mode-alert" role="alert">
Exam is in review mode. Score are available
{% if request.user.is_authenticated %}
<a href="{% url 'sbas:exam_scores_user' pk=exam.id %}">here</a>
{% else %}
<a href="{% url 'sbas:exam_scores_cid_user' pk=exam.id cid=cid passcode=passcode %}">here</a>
{% endif %}
</div>
{% endif %}
<div><p>Questions</p></div>
{{answer_count}} out of {{exam_length}} questions answered. Click to go to question.
<div class="overview-text">{{answer_count}} out of {{exam_length}} questions answered. Click to go to question.</div>
<div class="sba-finish-list">
{% for question, answer in question_answer_tuples %}
<a href="{% url 'sbas:exam_take' pk=exam.id sk=forloop.counter0 cid=cid passcode=passcode %}"><button {% if not answer %}class="unanswered"{% endif %}>{{forloop.counter}}: {{answer.answer}}</button></a>
{% if request.user.is_authenticated %}
<a href="{% url 'sbas:exam_take_user' pk=exam.id sk=forloop.counter0 %}"><button {% if not answer %}class="unanswered"{% endif %}>{{forloop.counter}}: {{answer.answer}}</button></a>
{% else %}
<a href="{% url 'sbas:exam_take' pk=exam.id sk=forloop.counter0 cid=cid passcode=passcode %}"><button {% if not answer %}class="unanswered"{% endif %}>{{forloop.counter}}: {{answer.answer}}</button></a>
{% endif %}
{% endfor %}
</div>
Start time: {{cid_user_exam.start_time}}
Start time: {{cid_user_exam.start_time}}<br/>
Last change time: {{cid_user_exam.end_time}}
{% endblock %}
+217
View File
@@ -0,0 +1,217 @@
from http import HTTPStatus
import json
import os
from re import A
from django.conf import settings
import pytest
import tempfile
# from django.contrib.auth.models import User
from django.urls import reverse
from rich.pretty import pprint
from bs4 import BeautifulSoup
from sbas.views import GenericExamViews as SbaExamViews
from sbas.models import Category, UserAnswer, Exam, Question
from generic.models import CidUser, CidUserGroup, UserUserGroup
from rad.test_helpers import AssertNotFound, ExamTester, create_cid_user_and_groups, create_exam
from django.contrib.auth.models import User
APP_NAME = "sbas"
def create_category(db):
category = Category.objects.create(category="test category")
category.save()
category = Category.objects.create(category="test category 2")
category.save()
def create_question(exam, answer=None):
# Just assign random stuff
if answer is None:
best_answer = "a"
else:
best_answer = answer
question: Question = Question.objects.create(
stem="test question stem",
a_answer="question a text",
a_feedback="question a feedback",
b_answer="question b text",
b_feedback="question b feedback",
c_answer="question c text",
c_feedback="question c feedback",
d_answer="question d text",
d_feedback="question d feedback",
e_answer="question e text",
e_feedback="question e feedback",
best_answer=best_answer,
)
question.exams.set([exam])
return question
def test_exams(db, client):
create_category(db)
exam_tester = ExamTester(db, client, APP_NAME, SbaExamViews)
exam_tester.create_questions(create_question)
users_tested = 1
# for cid_user in [cid_user_1001, cid_user_1000, user1, user2]:
for cid_user in exam_tester.all_users:
exam_tester.load_user(cid_user)
exam = exam_tester.exam
exam.active = False
exam.save()
exam_tester.check_active_exams(0)
exam.active = True
exam.publish_results = False
exam.save()
exam_tester.check_onsite_start_page()
#check_onsite_start_page
exam_tester.check_take_page()
exam_tester.check_take_overview(answered=0, total=3)
# Loop through questions again to test submissions
# All physics questions have 5 parts (and require a csrf token)
questions_len = len(exam_tester.generated_questions) - 1
for n, question in enumerate(exam_tester.generated_questions):
if exam_tester.testing_cid_user:
question_url = reverse(
f"{APP_NAME}:exam_take",
kwargs={
"pk": exam.pk,
"sk": n,
"cid": exam_tester.current_user.cid,
"passcode": exam_tester.current_user.passcode,
},
)
else:
question_url = reverse(
f"{APP_NAME}:exam_take_user",
kwargs={
"pk": exam.pk,
"sk": n,
},
)
cid_take_res = client.get(question_url)
if cid_user in exam_tester.invalid_users:
assert cid_take_res.status_code == HTTPStatus.NOT_FOUND
continue
else:
assert cid_take_res.status_code == HTTPStatus.OK
cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser")
# Extract csrftoken
csrftoken = cid_take_soup.find(
"input", attrs={"name": "csrfmiddlewaretoken"}
)["value"]
# Send answers (all TRUE)
post_json = {
"csrfmiddlewaretoken": csrftoken,
"answer": "a",
"next": "",
}
res = client.post(question_url, data=post_json)
next_button = cid_take_soup.find("button", attrs={"name": "next"})
# Our final question will trigger a bad request (it shouldn't show a next button)
# We currently do still save the form though....
if questions_len == n:
assert next_button == None
assert res.status_code == HTTPStatus.BAD_REQUEST
else:
assert next_button
assert res.status_code == HTTPStatus.FOUND
overview_button = cid_take_soup.find("button", attrs={"name": "finish"})
assert overview_button
previous_button = cid_take_soup.find("button", attrs={"name": "previous"})
if n > 0:
assert previous_button
else:
assert not previous_button
# Filter answer to the question we just submitted to
cid_user_answers = question.cid_user_answers.all()
assert len(cid_user_answers) == users_tested
if exam_tester.testing_cid_user:
cid_user_answer = cid_user_answers.filter(cid=exam_tester.current_user.cid)[0]
else:
cid_user_answer = cid_user_answers.filter(user=exam_tester.current_user)[0]
assert all(cid_user_answer.get_answer())
# Recheck the overview page (now that we have submitted answers)
exam_tester.check_take_overview(3, 3)
exam_tester.check_cid_scores_page()
exam_tester.check_cid_scores_user_page()
# The rest of the tests are for valid users
if exam_tester.current_user in exam_tester.invalid_users:
continue
# answer the final question
extra_question = exam_tester.generated_questions[-1]
if exam_tester.testing_cid_user:
cid_user_answer = UserAnswer(
question=extra_question,
a=True,
b=False,
c=True,
d=False,
e=True,
cid=cid_user.cid,
exam=exam,
)
else:
cid_user_answer = UserAnswer(
question=extra_question,
a=True,
b=False,
c=True,
d=False,
e=True,
user=exam_tester.current_user,
exam=exam,
)
cid_user_answer.save()
exam_tester.check_cid_scores_user_page2()
users_tested = users_tested + 1
+6
View File
@@ -43,6 +43,12 @@ urlpatterns = [
views.exam_take_overview,
name="exam_take_overview",
),
path(
"exam/<int:pk>/finish",
views.exam_take_overview,
name="exam_take_overview_user",
),
path(
"exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"
),
+1 -1
View File
@@ -99,7 +99,7 @@ def active_exams(request):
return render(request, "sbas/available_exam_list.html", {"exams": active_exams})
def exam_scores_cid_user(request, pk, cid, passcode):
def exam_scores_cid_user(request, pk, cid=None, passcode=None):
exam = get_object_or_404(Exam, pk=pk)
exam.check_user_can_review(cid, passcode, request)