644 lines
21 KiB
Python
644 lines
21 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 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 = 1
|
|
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)
|
|
|
|
print(cid_user)
|
|
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
|
|
print(f"{exam_metadata=}")
|
|
|
|
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"]:
|
|
print("check question:", q)
|
|
# 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")
|
|
|
|
print(cid_user, testing_cid_user)
|
|
|
|
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,
|
|
},
|
|
)
|
|
print(test)
|
|
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"] == "Invalid data"
|
|
|
|
# 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 questions in exam_json["questions"]:
|
|
for qid in 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)
|
|
|
|
pprint(valid_answers_by_question)
|
|
print(cid_user)
|
|
|
|
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:
|
|
print(n, qidn)
|
|
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
|
|
|
|
print(cid_exam_scores_soup.find("div", {"class": "score-overview"}))
|
|
|
|
# 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
|
|
== "4 (3 unmarked)"
|
|
)
|
|
|
|
# Mark the questions
|
|
# For longs this needs to be done for each user
|
|
question: Question
|
|
for question in exam.exam_questions.all():
|
|
print(question)
|
|
|
|
answers = question.get_unmarked_user_answers()
|
|
print(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
|
|
== "28.0"
|
|
)
|
|
|
|
generated_questions.pop(-1).delete()
|
|
users_tested = users_tested + 1
|