Files
penracourses/longs/tests/test_longs_exams.py

686 lines
22 KiB
Python

from collections import defaultdict
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 longs.views import GenericExamViews as LongsExamViews
from longs.models import (
AnswerMarks,
UserAnswer,
Exam,
Examination,
Long as Question,
LongSeries,
LongSeriesImage,
)
from generic.models import CidUser, CidUserGroup, UserUserGroup
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from rad.tests.test_helpers import AssertNotFound, create_cid_user_and_groups
APP_NAME = "longs"
def create_exam(db):
exam: Exam = LongsExamViews.Exam.objects.create(
name="Cid Exam", exam_mode=True, active=True
)
group1: CidUserGroup = CidUserGroup.objects.get(name="Group1")
usergroup1: UserUserGroup = UserUserGroup.objects.get(name="Group1")
exam.cid_user_groups.add(group1)
exam.valid_cid_users.add(*group1.ciduser_set.all())
exam.valid_user_users.add(*usergroup1.users.all())
assert exam in group1.longs_cid_user_groups.all()
assert exam
return exam
def create_examinations(db):
examination = Examination.objects.create(examination="test examination")
examination.save()
examination = Examination.objects.create(examination="test examination 2")
examination.save()
def create_long_series(question, image_number=5):
series = LongSeries(
description="Test series",
examination=Examination.objects.all().first(),
)
series.save()
for n in range(image_number):
create_long_series_image(series, n)
return series
def create_long_series_image(series, n=0):
image = tempfile.NamedTemporaryFile(
dir=settings.MEDIA_ROOT, suffix="{n}.jpg", delete=False
)
long_series_image = LongSeriesImage(image=image.name, series=series)
long_series_image.save()
def create_question(exam, answer=None, normal=False):
# Just assign random stuff
question: Question = Question.objects.create(
description="Test question",
history="Test history",
feedback="Test feedback",
model_observations="Model obs",
model_interpretation="Model interp",
model_principle_diagnosis="Model pd",
model_differential_diagnosis="Model dd",
model_management="Model management",
)
series = create_long_series(question=question)
question.series.add(series)
question.exams.set([exam])
return question
def test_exams(db, client):
create_cid_user_and_groups(db)
exam = create_exam(db)
# create_structures(db)
# create_question_types(db)
create_examinations(db)
question1 = create_question(exam)
generated_questions = [question1, create_question(exam), create_question(exam)]
active_exams = client.get(reverse(f"{APP_NAME}:active_exams"))
assert active_exams.status_code == 200
assert len(json.loads(active_exams.content)["exams"]) == 0
cid_user_1000 = CidUser.objects.get(cid=1000)
cid_user_1001 = CidUser.objects.get(cid=1001)
cid_user_2001 = CidUser.objects.get(cid=2001)
user1 = User.objects.get(username="user1")
# Test exam access urls
# Check that we can't access without logging in
assert client.get(exam.get_json_url()).status_code == 404
assert (
client.get(
exam.get_json_url(cid=cid_user_2001.cid, passcode=cid_user_2001.passcode)
).status_code
== 404
)
# Valid user but incorrect passcode
assert (
client.get(
exam.get_json_url(cid=cid_user_1001.cid, passcode=cid_user_2001.passcode)
).status_code
== 404
)
users_tested = 0
for cid_user in [cid_user_1001, cid_user_1000, user1]:
if isinstance(cid_user, CidUser):
testing_cid_user = True
else:
testing_cid_user = False
current_user = cid_user
client.force_login(cid_user)
exam.active = True
exam.publish_results = False
exam.save()
# Test active exams sections works as intended
if testing_cid_user:
active_exams_url = reverse(
f"active_exams_cid",
kwargs={"cid": cid_user.cid, "passcode": cid_user.passcode},
)
else:
active_exams_url = reverse(
f"active_exams",
)
active_exams_cid = client.get(active_exams_url)
assert active_exams_cid.status_code == 200
assert len(json.loads(active_exams_cid.content)["exams"]) == 1
if testing_cid_user:
no_active_exams_cid = client.get(
reverse(f"active_exams_cid", kwargs={"cid": 2001, "passcode": "EGFH"})
)
assert no_active_exams_cid.status_code == 401
assert json.loads(no_active_exams_cid.content)["status"] == "invalid"
invalid_passcode = client.get(
reverse(
f"active_exams_cid",
kwargs={"cid": cid_user.cid, "passcode": "AAAA"},
)
)
assert invalid_passcode.status_code == 401
assert json.loads(invalid_passcode.content)["status"] == "invalid"
exam_metadata = json.loads(active_exams_cid.content)["exams"][0]
assert exam_metadata["name"] == "Cid Exam"
assert exam_metadata["exam_active"] == True
assert exam_metadata["exam_mode"] == True
res = client.get(exam_metadata["url"])
assert res.status_code == 200
exam_json = json.loads(res.content)
# We can also check the unbased questions
res = client.get(exam_metadata["url"] + "/unbased")
assert res.status_code == 200
exam_json_unbased = json.loads(res.content)
# This show we have different types (int vs string)
assert (
set([str(i) for i in exam_json["exam_order"]])
== set(exam_json["questions"].keys())
== set(exam_json["question_requests"].keys())
== set([str(i) for i in exam_json_unbased["exam_order"]])
== set(exam_json_unbased["questions"].keys())
)
for q in exam_json["question_requests"]:
# check redirects to the json file work
# TODO: sort out testing of the saved JSON files
try:
question_json_res = client.get(
reverse(
f"longs:exam_question_json_cid",
kwargs={
"pk": exam.pk,
"sk": q,
"cid": cid_user.cid,
"passcode": cid_user.passcode,
},
),
follow=True,
)
except AttributeError: # UserUser has no cid attribute
question_json_res = client.get(
reverse(
f"longs:exam_question_json",
kwargs={
"pk": exam.pk,
"sk": q,
},
),
follow=True,
)
# assert question_json_res.status_code == 200
assert len(question_json_res.redirect_chain) == 2
assert question_json_res.redirect_chain[0][1] == 302
assert question_json_res.redirect_chain[1][1] == 302
assert question_json_res.redirect_chain[1][0].endswith(f"{q}.json")
if testing_cid_user:
question_json_unbased_res = client.get(
reverse(
f"longs:exam_question_json_unbased_cid",
kwargs={
"pk": exam.pk,
"sk": q,
"cid": cid_user.cid,
"passcode": cid_user.passcode,
},
),
follow=True,
)
else:
test = reverse(
f"longs:exam_question_json_unbased",
kwargs={
"pk": exam.pk,
"sk": q,
},
)
question_json_unbased_res = client.get(
reverse(
f"longs:exam_question_json_unbased",
kwargs={
"pk": exam.pk,
"sk": q,
},
),
follow=True,
)
assert question_json_unbased_res.status_code == 200
question_json = json.loads(question_json_unbased_res.content)
# question_json_res = client.get(question1.get_question_json(answers=False, based=True))
# As we store references to the questions we have to check individually
assert q in exam_json["questions"].keys()
q_json = Question.objects.get(pk=q).get_question_json(based=False)
assert question_json == q_json
assert len(exam_json["questions"]) == 3
exam.active = False
exam.save()
res = client.get(exam_metadata["url"])
assert res.status_code == 404
exam.active = True
exam.save()
if testing_cid_user:
post_cid = cid_user.cid
else:
post_cid = f"u-{current_user.pk}"
post_json = {
"eid": f"long/{exam.pk}",
"cid": post_cid,
"start_time": "1659618141.731",
"answers": [[]],
}
# Test submission
# Start with 0 answers
res = client.post(reverse("global_exam_answers_submit"), data=post_json)
json_res = json.loads(res.content)
assert json_res["success"] == True
assert json_res["question_count"] == 0
post_json = {
"eid": f"long/{exam.pk+10}",
"cid": post_cid,
"start_time": "1659618141.731",
"answers": [[]],
}
res = client.post(reverse("global_exam_answers_submit"), data=post_json)
json_res = json.loads(res.content)
assert json_res["success"] == False
# Test invalid cid
post_json = {
"eid": f"long/{exam.pk+10}",
"cid": 1,
"start_time": "1659618141.731",
"answers": [[]],
}
res = client.post(reverse("global_exam_answers_submit"), data=post_json)
json_res = json.loads(res.content)
assert json_res["success"] == False
assert json_res["error"] in ("Invalid data", "user / cid mismatch")
# Long answers currently send a seperate object for each component
valid_answers = []
valid_answers_extra = []
valid_answers_by_question = defaultdict(dict)
sections = (
("1", "answer observations"),
("2", "answer interpretation"),
("3", "answer principle diagnosis"),
("4", "answer differential diagnosis"),
("5", "answer management"),
)
i = 0
for qid in exam_json["questions"]:
# Long answers submit 5 sections
for n, name in sections:
post_data = {
"eid": exam_json["eid"],
"cid": post_cid,
"qid": qid,
"qidn": n,
"ans": f"{name} - {q}",
}
if testing_cid_user:
post_data["passcode"] = cid_user.passcode
valid_answers_extra.append(post_data)
valid_answers_by_question[i][n] = post_data
i = i + 1
post_json = {
"eid": exam_json["eid"],
"cid": post_cid,
"start_time": "1659618141.731",
"answers": json.dumps([*valid_answers, *valid_answers_extra]),
}
# Test submission
# give three answers
res = client.post(reverse("global_exam_answers_submit"), data=post_json)
json_res = json.loads(res.content)
assert json_res["success"] is True
assert json_res["question_count"] == 5 * 3
# Check the answers now exist in the database
if testing_cid_user:
answers = UserAnswer.objects.filter(cid=cid_user.cid)
else:
answers = UserAnswer.objects.filter(user=cid_user)
assert answers.count() == 3
# We can then check the answers have been submitted (and can be viewed by the user)
if testing_cid_user:
# Get overview scores page (invalid passcode)
AssertNotFound(
client,
reverse(
f"cid_scores", kwargs={"cid": cid_user.cid, "passcode": "AAAA"}
),
)
cid_scores_url = reverse(
f"cid_scores",
kwargs={"cid": cid_user.cid, "passcode": cid_user.passcode},
)
else:
cid_scores_url = reverse(f"user_scores")
# Get overview scores page
cid_scores_res = client.get(cid_scores_url)
cid_scores_soup = BeautifulSoup(cid_scores_res.content, "html.parser")
search_exam = (
cid_scores_soup.find("div", {"id": "exam-assigned"})
.find("ul", {"class": exam.app_name})
.find("li", attrs={"data-exam-id": exam.pk})
)
assert search_exam
assert str(exam) in str(search_exam)
assert len(search_exam.find("button", {"class": "start-button"})) > 0
# assert "Active" in assigned_exams # check it is active
invalid_exam = cid_scores_soup.find_all(
"li", attrs={"data-exam-id": exam.pk + 5}
)
assert not invalid_exam # Check we can't find an invalid exam
# Check that we have a results link
results_exam = (
cid_scores_soup.find("div", {"id": "exam-results"})
.find("ul", {"class": exam.app_name})
.find_all("li", attrs={"data-exam-id": exam.pk})
)
assert results_exam
assert str(exam) in str(results_exam)
assert str(exam) + " test" not in str(results_exam)
assert "Results Published" not in str(
results_exam
) # It should not be published yet
if testing_cid_user:
AssertNotFound(
client,
reverse(
f"{exam.app_name}:exam_scores_cid_user",
kwargs={"pk": exam.pk, "cid": cid_user.cid, "passcode": "AAAA"},
),
)
AssertNotFound(
client,
reverse(
f"{exam.app_name}:exam_scores_cid_user",
kwargs={
"pk": exam.pk,
"cid": cid_user_2001.cid,
"passcode": cid_user_2001.passcode,
},
),
)
exam_scores_url = reverse(
f"{exam.app_name}:exam_scores_cid_user",
kwargs={
"pk": exam.pk,
"cid": cid_user.cid,
"passcode": cid_user.passcode,
},
)
else:
exam_scores_url = reverse(
f"{exam.app_name}:exam_scores_user",
kwargs={"pk": exam.pk},
)
# Load the exam results page
cid_exam_scores_res = client.get(exam_scores_url)
# Load the exam results page
cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
# Check that we have an alert that exams are not published
alert = cid_exam_scores_soup.find("div", {"class": "alert"})
assert "Results are not currently published." in str(alert)
answer_lis = cid_exam_scores_soup.find(
"ul", {"class": "score-answer-list"}
).find_all("li", {"class": "user-answer-li"})
assert len(answer_lis) == len(valid_answers_by_question)
for n in range(
len(valid_answers_by_question)
): # Check no answers are visible if results not published
lis = answer_lis[n]
#n = str(n + 1) # questions are not zero indexed (lists are)
for qidn, section_title in sections:
answer = valid_answers_by_question[n][qidn]
li = lis.find("li", attrs={"data-qidn": qidn})
assert li["class"][0].lower() in section_title
assert answer["ans"] in str(li) # Check that the saved answer is shown
# assert not li.find(
# "span", {"class": "correct-answer"}
# ) # and that the correct answer isn't
assert (
cid_exam_scores_soup.find("div", {"class": "score-overview"}) is None
) # check we hide the score overview if not published
# Add another question to the exam
create_question(exam)
# Load the exam results page (again)
cid_exam_scores_res = client.get(exam_scores_url)
cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
answer_lis = cid_exam_scores_soup.find(
"ul", {"class": "score-answer-list"}
).find_all("li", {"class": "user-answer-li"})
assert len(answer_lis) == len(valid_answers_by_question) + 1
assert "Not answered" in str(answer_lis[-1])
assert str(answer_lis[-1]).count("Not answered") == 5
# Publish the results!
exam.publish_results = True
exam.save()
# Load the exam results page (again)
cid_exam_scores_res = client.get(exam_scores_url)
cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
assert (
cid_exam_scores_soup.find("div", {"class": "alert"}) is None
) # Check our warning banner is gone
answer_lis = cid_exam_scores_soup.find(
"ul", {"class": "score-answer-list"}
).find_all("li", {"class": "user-answer-li"})
assert len(answer_lis) == len(valid_answers_by_question) + 1
assert "Not answered" in str(answer_lis[-1])
assert str(answer_lis[-1]).count("Not answered") == 5
# Repetition....
for n in range(
len(valid_answers_by_question)
): # Check no answers are visible if results not published
lis = answer_lis[n]
#n = str(n + 1) # questions are not zero indexed (lists are)
for qidn, section_title in sections:
answer = valid_answers_by_question[n][qidn]
li = lis.find("li", attrs={"data-qidn": qidn})
assert li["class"][0].lower() in section_title
assert answer["ans"] in str(li) # Check that the saved answer is shown
# For long cases an unmarked answers is given a score of 4
# Each user needs marking individually
assert (
cid_exam_scores_soup.find("div", {"class": "score-overview"})
.find("span", {"id": "total-score"})
.string
== "3 (3 unmarked)"
)
# Mark the questions
# For longs this needs to be done for each user
question: Question
for question in exam.exam_questions.all():
answers = question.get_unmarked_user_answers()
for answer in answers:
answer.score = UserAnswer.ScoreOptions.EIGHT
answer.save()
#try:
# new_ans = Answer(
# question=question,
# answer=question.get_unmarked_user_answers().pop(),
# status=Answer.MarkOptions.CORRECT,
# )
# new_ans.save()
#except IndexError: # Final question does not have an answer
# pass
# TODO: test double marking
# Load the exam results page (again)
cid_exam_scores_res = client.get(exam_scores_url)
cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
assert (
cid_exam_scores_soup.find("div", {"class": "score-overview"})
.find("span", {"id": "total-score"})
.string
== "27.0"
)
generated_questions.pop(-1).delete()
users_tested = users_tested + 1
assert users_tested == 3
# Some quick tests to check for user access to the marking page
client.login(username="marker", password="test1234")
# Load the mark overview page
mark_overview_res = client.get(reverse(
f"{exam.app_name}:mark_overview",
kwargs={"pk": exam.pk},
))
assert mark_overview_res.status_code == 403
exam.markers.add(User.objects.get(username="marker"))
mark_overview_res = client.get(reverse(
f"{exam.app_name}:mark_overview",
kwargs={"pk": exam.pk},
))
assert mark_overview_res.status_code == 200
mark_overview_soup = BeautifulSoup(mark_overview_res.content, "html.parser")
to_mark = mark_overview_soup.find("ul", {"id": "question-mark-list"}).find_all("li")
assert len(to_mark) == 3
for n in range(0,3):
mark_question_res = client.get(reverse(
f"{exam.app_name}:mark",
kwargs={"exam_id": exam.pk, "sk": n},
))
assert mark_question_res.status_code == 200
mark_question_soup = BeautifulSoup(mark_question_res.content, "html.parser")
print(mark_question_soup)
#mark_links = mark_question_soup.find_all("a", {"class": "mark-answer-link"})
#assert len(mark_links) == 2
#for link in mark_links:
# mark_res = client.get(link["href"])
# assert mark_res.status_code == 200
mark_question_res = client.get(reverse(
f"{exam.app_name}:mark",
kwargs={"exam_id": exam.pk, "sk": 4},
))
assert mark_question_res.status_code == 404
mark_question_soup = BeautifulSoup(mark_question_res.content, "html.parser")