rapid exams

This commit is contained in:
Ross
2021-01-25 14:13:51 +00:00
parent 697a7c63f2
commit 4893b1e90b
4 changed files with 631 additions and 66 deletions
+17 -23
View File
@@ -572,21 +572,6 @@ def active_exams(request):
return JsonResponse(active_exams)
def question_save_annotation(request, pk):
if request.is_ajax() and request.method == "POST":
question = get_object_or_404(AnatomyQuestion, pk=pk)
question.image_annotations = request.POST.get("annotation")
print(question.image_annotations)
question.save()
data = {"status": "success"}
return JsonResponse(data, status=200)
else:
data = {"status": "error"}
return JsonResponse(data, status=400)
def exam_json(request, pk):
exam = get_object_or_404(Exam, pk=pk)
@@ -813,13 +798,6 @@ def exam_scores_cid_user(request, pk, sk):
)
def cid_selector(request):
return render(
request,
"anatomy/cid_selector.html",
)
# @login_required
# def exam_scores(request, pk):
# exam = get_object_or_404(Exam, pk=pk)
@@ -1155,4 +1133,20 @@ class AnatomyQuestionView(SingleTableMixin, FilterView):
table_class = AnatomyQuestionTable
template_name = "anatomy/anatomy_question_view.html"
filterset_class = AnatomyQuestionFilter
filterset_class = AnatomyQuestionFilter
def question_save_annotation(request, pk):
if request.is_ajax() and request.method == "POST":
question = get_object_or_404(AnatomyQuestion, pk=pk)
question.image_annotations = request.POST.get("annotation")
print(question.image_annotations)
question.save()
data = {"status": "success"}
return JsonResponse(data, status=200)
else:
data = {"status": "error"}
return JsonResponse(data, status=400)
+72 -37
View File
@@ -1,40 +1,69 @@
from django.forms import ModelForm, ModelMultipleChoiceField, ModelChoiceField, ChoiceField
from django.forms import (
ModelForm,
ModelMultipleChoiceField,
ModelChoiceField,
ChoiceField,
)
from django.forms import inlineformset_factory
from rapids.models import Abnormality, Examination, Note, Rapid, RapidCreationDefault, RapidImage, Region, Answer
from rapids.models import (
Abnormality,
Examination,
Note,
Rapid,
RapidCreationDefault,
RapidImage,
Region,
Answer,
CidUserAnswer,
Examm,
)
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms.widgets import RadioSelect, TextInput, Textarea
class RapidAnswerForm(ModelForm):
class Meta:
model = CidUserAnswer
fields = ("answer",)
class MarkRapidQuestionForm(Form):
# correct = forms.CharField(required=False)
# half_correct = forms.CharField(required=False)
# incorrect = forms.CharField(required=False)
marked_answers = CharField(required=False)
class ExaminationForm(ModelForm):
class Meta:
model = Examination
fields = ['examination']
fields = ["examination"]
class NoteForm(ModelForm):
class Meta:
model = Note
fields = ['note']
fields = ["note"]
class RegionForm(ModelForm):
class Meta:
model = Region
fields = ['name']
fields = ["name"]
class AbnormalityForm(ModelForm):
class Meta:
model = Abnormality
fields = ['name']
fields = ["name"]
class RapidCreationDefaultForm(ModelForm):
class Meta:
model = RapidCreationDefault
fields = ['site']
fields = ["site"]
exclude = ["author"]
@@ -45,46 +74,50 @@ class RapidForm(ModelForm):
# include <script src="/admin/jsi18n"></script>
# in the template.
css = {
'all': ['css/widgets.css'],
"all": ["css/widgets.css"],
}
# Adding this javascript is crucial
js = ['jsi18n.js', "tesseract.min.js"]
js = ["jsi18n.js", "tesseract.min.js"]
def __init__(self, *args, **kwargs):
super(RapidForm, self).__init__(*args, **kwargs)
#self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100}
#self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100}
self.fields['abnormality'] = ModelMultipleChoiceField(
# self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100}
# self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100}
self.fields["abnormality"] = ModelMultipleChoiceField(
required=False,
queryset=Abnormality.objects.all(),
widget=FilteredSelectMultiple(verbose_name="Abnormality",
is_stacked=False))
widget=FilteredSelectMultiple(verbose_name="Abnormality", is_stacked=False),
)
self.fields['region'] = ModelMultipleChoiceField(
self.fields["region"] = ModelMultipleChoiceField(
required=False,
queryset=Region.objects.all(),
widget=FilteredSelectMultiple(verbose_name="Region",
is_stacked=False))
widget=FilteredSelectMultiple(verbose_name="Region", is_stacked=False),
)
self.fields['examination'] = ModelMultipleChoiceField(
self.fields["examination"] = ModelMultipleChoiceField(
queryset=Examination.objects.all(),
widget=FilteredSelectMultiple(verbose_name="Examination",
is_stacked=False))
widget=FilteredSelectMultiple(verbose_name="Examination", is_stacked=False),
)
self.fields['laterality'] = ChoiceField(
choices=Rapid.LATERALITY_CHOICES,
required=True,
widget=RadioSelect())
self.fields["laterality"] = ChoiceField(
choices=Rapid.LATERALITY_CHOICES, required=True, widget=RadioSelect()
)
class Meta:
model = Rapid
#fields = ['due_back']
#fields = '__all__'
# fields = ['due_back']
# fields = '__all__'
fields = [
"normal", "abnormality", "region", "laterality", "examination",
"site", "feedback"
"normal",
"abnormality",
"region",
"laterality",
"examination",
"site",
"feedback",
]
#fields = ['question', 'feedback', 'subspecialty', 'references']
# fields = ['question', 'feedback', 'subspecialty', 'references']
widgets = {
# "normal": RadioSelect(
# choices=[(True, 'Yes'),
@@ -92,14 +125,16 @@ class RapidForm(ModelForm):
}
ImageFormSet = inlineformset_factory(Rapid,
RapidImage,
fields=['image', 'feedback_image'],
exclude=[],
can_delete=True,
extra=4,
max_num=10,
field_classes="testing")
ImageFormSet = inlineformset_factory(
Rapid,
RapidImage,
fields=["image", "feedback_image"],
exclude=[],
can_delete=True,
extra=4,
max_num=10,
field_classes="testing",
)
AnswerFormSet = inlineformset_factory(
Rapid,
+22 -5
View File
@@ -8,19 +8,36 @@ urlpatterns = [
path("", views.index, name="index"),
path("author/<int:pk>/", views.author_detail, name="author_detail"),
path("author/", views.author_list, name="author_list"),
path("question/", views.RapidView.as_view(), name="rapid_view"),
#path("unchecked/", views.unchecked_list, name="unchecked_list"),
#path("verified/<int:pk>/", views.verified_detail, name="verified_detail"),
path("<int:pk>/", views.rapid_detail, name="rapid_detail"),
path("<int:pk>/split", views.rapid_split, name="rapid_split"),
path("<int:pk>/clone", views.RapidClone.as_view(), name="rapid_clone"),
path("question/<int:pk>/", views.rapid_detail, name="rapid_detail"),
path("question/<int:pk>/split", views.rapid_split, name="rapid_split"),
path("question/<int:pk>/clone", views.RapidClone.as_view(), name="rapid_clone"),
#path("verified/", views.verified, name="verified"),
#path("all_questions/", views.all_questions, name="all_questions"),
path("<int:pk>/scrap", views.rapid_scrap, name="rapid_scrap"),
path("question/<int:pk>/scrap", views.rapid_scrap, name="rapid_scrap"),
path("exam/<int:pk>/<int:sk>/mark", views.mark, name="mark"),
path("exam/<int:pk>/mark", views.mark_overview, name="mark_overview"),
path("exam/<int:pk>/<int:sk>/", views.exam_take, name="exam_take"),
path("exam/<int:pk>/question/<int:sk>/", views.exam_question_detail, name="exam_question_detail"),
path("exam/<int:pk>/", views.exam_overview, name="exam_overview"),
path("exam/<int:pk>/json_edit", views.exam_json_edit, name="exam_json_edit"),
path("exam/<int:pk>/scores", views.exam_scores_cid,
name="exam_scores_cid"),
path("exam/<int:pk>/scores/<int:sk>/", views.exam_scores_cid_user,
name="exam_scores_cid_user"),
path("exam/<int:pk>/toggle_active", views.exam_toggle_active, name="exam_toggle_active"),
path("exam/<int:pk>/toggle_results_published", views.exam_toggle_results_published, name="exam_toggle_results_published"),
path("exam/submit", views.postExamAnswers, name="exam_answers_submit"),
path("exam/", views.exam_list, name="exam_list"),
path("exam/json/", views.active_exams, name="active_exams"),
path("exam/json/<int:pk>", views.exam_json, name="exam_json"),
path("exam/json/<int:pk>/recreate", views.exam_json_recreate, name="exam_json_recreate"),
path("create/", views.RapidCreate.as_view(), name="rapid_create"),
path("create/defaults",
views.RapidCreationDefaultView.as_view(),
name="rapid_create_defaults"),
path("view/", views.RapidView.as_view(), name="rapid_view"),
path("region/create/", views.create_region, name="create_region"),
path("region/ajax/get_region_id",
views.get_region_id,
+520 -1
View File
@@ -481,4 +481,523 @@ class RapidView(SingleTableMixin, FilterView):
table_class = RapidTable
template_name = "rapids/view.html"
filterset_class = RapidFilter
filterset_class = RapidFilter
def loadJsonAnswer(answer):
# As access is not restricted make sure the data appears valid
if (not isinstance(answer["cid"], int)) or (
not isinstance(answer["eid"], int) or (not isinstance(answer["ans"], str))
):
return JsonResponse({"success": False, "error": "cid or eid or answers not defined"})
# The model should catch invalid data but this should be less intensive
max_int = 999999999999999999999
if answer["cid"] > max_int or answer["eid"] > max_int:
return JsonResponse({"success": False, "error": "invalid cid"})
exam = get_object_or_404(Exam, pk=answer["eid"])
if not exam.active:
return JsonResponse(
{"success": False, "error": "No active exam: {}".format(answer["eid"])}
)
exiting_answers = CidUserAnswer.objects.filter(
question__id=answer["qid"], exam__id=answer["eid"], cid=answer["cid"]
)
if not exiting_answers:
ans = CidUserAnswer(answer=answer["ans"], cid=answer["cid"])
ans.question_id = answer["qid"]
ans.exam_id = answer["eid"]
ans.full_clean()
ans.save()
else:
# Update an existing answer
# should never be more than one (famous last words)
ans = exiting_answers[0]
ans.answer = answer["ans"]
ans.full_clean()
ans.save()
return True
def exam_json_edit(request, pk):
if request.is_ajax() and request.method == "POST":
exam = get_object_or_404(Exam, pk=pk)
if request.POST.get("set_open_access") == "true":
state = True
else:
state = False
questions_changed = []
for q in exam.exam_questions.all():
q.open_access = state
q.save()
questions_changed.append(q.pk)
data = {"status": "success", "questions": questions_changed, "state": state}
return JsonResponse(data, status=200)
else:
data = {"status": "error"}
return JsonResponse(data, status=400)
@csrf_exempt
def postExamAnswers(request):
if request.is_ajax and request.method == "POST":
n = 0
# horrible but it works
for k in request.POST.dict():
print(k)
for answer in json.loads(k):
ret = loadJsonAnswer(answer)
if ret is not True:
return ret
n = n + 1
# print(UserAnswer.objects.filter(exam__id=q["eid"]))
# print(request.urlencode())
return JsonResponse({"success": True, "question_count": n})
return JsonResponse({"success": False, "error": "Invalid data"})
# @user_passes_test(user_is_admin, login_url="/accounts/login")
@login_required
def mark_overview(request, pk):
exam = get_object_or_404(Exam, pk=pk)
questions = exam.exam_questions.all()
return render(
request, "rapids/mark_overview.html", {"exam": exam, "questions": questions}
)
# @user_passes_test(user_is_admin, login_url="/accounts/login")
@login_required
def mark(request, pk, sk):
exam = get_object_or_404(Exam, pk=pk)
questions = exam.exam_questions.all()
n = sk
question_details = {
"total": len(questions),
"current": n + 1,
}
try:
question = questions[sk]
except IndexError:
raise Http404("Exam question does not exist")
answers_dict = {}
for ans in question.answers.all():
answers_dict[ans.answer_compare] = ans
marked_answers_set = set(answers_dict.keys())
# correct_answers = [i.answer.lower() for i in question.answers.all()]
# half_correct_answers = [
# i.answer.lower() for i in question.half_mark_answers.all()
# ]
# incorrect_answers = [
# i.answer.lower() for i in question.incorrect_answers.all()
# ]
unmarked_user_answers = question.GetUnmarkedAnswers()
if request.method == "POST":
form = MarkRapidQuestionForm(request.POST)
# *******************************
# TODO: convert to JSON request
# *******************************
if form.is_valid():
# If skip button is pressed skip the question without marking
if "skip" in request.POST:
return redirect("rapids:mark", pk=pk, sk=n + 1)
cd = form.cleaned_data
# correct = cd.get("correct")
# half_correct = cd.get("half_correct")
# incorrect = cd.get("incorrect")
marked_answers = json.loads(cd.get("marked_answers"))
## This is mildly dangerous as we delete all answers
# question.answers.all().delete()
# question.half_mark_answers.all().delete()
# question.incorrect_answers.all().delete()
# This should probably use json (or something else...)
# ajax.....
for ans in marked_answers["correct"]:
ans = ans.strip()
if ans == "":
continue
a = Answer.objects.filter(
answer__iexact=ans, question_id=question.pk
).first()
if a is None:
a = Answer()
a.question_id = question.pk
a.answer = ans
a.status = Answer.MarkOptions.CORRECT
a.save()
# for ans in half_correct.split("--//--"):
for ans in marked_answers["half-correct"]:
ans = ans.strip()
if ans == "":
continue
a = Answer.objects.filter(
answer__iexact=ans, question_id=question.pk
).first()
if a is None:
a = Answer()
a.question_id = question.pk
a.answer = ans
a.status = Answer.MarkOptions.HALF_MARK
a.save()
for ans in marked_answers["incorrect"]:
ans = ans.strip()
if ans == "":
continue
a = Answer.objects.filter(
answer__iexact=ans, question_id=question.pk
).first()
if a is None:
a = Answer()
a.question_id = question.pk
a.answer = ans
a.status = Answer.MarkOptions.INCORRECT
a.save()
# answer = form.save(commit=False)
# answer.user = request.user
# answer.question = question
# answer.published_date = timezone.now()
# answer.save()
if "next" in request.POST:
return redirect("rapids:mark", pk=pk, sk=n + 1)
elif "previous" in request.POST:
return redirect("rapids:mark", pk=pk, sk=n - 1)
# Reset user answers (relies on the functional javascript)
unmarked_user_answers = set()
else:
form = MarkRapidQuestionForm()
correct_answers = question.answers.filter(status=Answer.MarkOptions.CORRECT)
half_mark_answers = question.answers.filter(status=Answer.MarkOptions.HALF_MARK)
incorrect_answers = question.answers.filter(status=Answer.MarkOptions.INCORRECT)
return render(
request,
"rapids/mark.html",
{
"exam": exam,
"form": form,
"question": question,
"question_details": question_details,
"user_answers": unmarked_user_answers,
"correct_answers": correct_answers,
"half_mark_answers": half_mark_answers,
"incorrect_answers": incorrect_answers,
},
)
def exam_toggle_results_published(request, pk):
if request.is_ajax() and request.method == "POST":
exam = get_object_or_404(Exam, pk=pk)
exam.publish_results = (
True if request.POST.get("publish_results") == "true" else False
)
exam.save()
data = {"status": "success", "publish_results": exam.publish_results}
return JsonResponse(data, status=200)
else:
data = {"status": "error"}
return JsonResponse(data, status=400)
def exam_toggle_active(request, pk):
if request.is_ajax() and request.method == "POST":
exam = get_object_or_404(Exam, pk=pk)
exam.active = True if request.POST.get("active") == "true" else False
exam.save()
data = {"status": "success", "active": exam.active}
return JsonResponse(data, status=200)
else:
data = {"status": "error"}
return JsonResponse(data, status=400)
def active_exams(request):
exams = Exam.objects.all()
print(exams)
active_exams = {"exams": []}
for exam in exams:
if exam.active:
active_exams["exams"].append(
{
"name": exam.get_exam_name(),
"url": request.build_absolute_uri(exam.get_json_url()),
}
)
return JsonResponse(active_exams)
def exam_json(request, pk):
exam = get_object_or_404(Exam, pk=pk)
if not exam.active:
raise Http404("No available exam")
exam_json_cache = cache.get("exam_json_{}".format(pk))
if exam_json_cache is not None and not exam.recreate_json:
exam_json_cache["cached"] = True
return JsonResponse(exam_json_cache)
questions = exam.exam_questions.all()
exam_questions = defaultdict(dict)
exam_order = []
for q in questions:
exam_order.append(q.id)
exam_questions[q.id] = {
"title": "{}".format(q.description),
"question": str(q.question_type),
"images": [image_as_base64(q.image)],
"annotations": [str(q.image_annotations)],
"type": "rapid",
}
exam_json = {
"eid": exam.id,
"cached": False,
"exam_type": "rapid",
"exam_name": exam.name,
"exam_mode": True,
"exam_order": exam_order,
"questions": exam_questions,
}
if exam.time_limit:
exam_json["exam_time"] = exam.time_limit
exam.recreate_json = False
exam.save()
cache.set("exam_json_{}".format(pk), exam_json, 3600)
return JsonResponse(exam_json)
@login_required
def exam_json_recreate(request, pk):
exam = get_object_or_404(Exam, pk=pk)
exam.recreate_json = True
exam.save()
return redirect("rapids:exam_overview", pk=pk)
@login_required
def exam_scores_cid(request, pk):
exam = get_object_or_404(Exam, pk=pk)
questions = exam.exam_questions.all()
cids = (
CidUserAnswer.objects.filter(question__in=questions)
.order_by("cid")
.values_list("cid", flat=True)
.distinct()
)
user_answers_and_marks = defaultdict(list)
user_answers_marks = defaultdict(list)
user_answers = defaultdict(list)
user_names = {}
by_question = defaultdict(list)
unmarked = set()
# Loop through all candidates
for cid in cids:
# Convoluted (probably...)
user_names[cid] = cid
for q in questions:
# Get user answer
s = q.cid_user_answers.filter(cid=cid).first()
if not s:
# skip if no answer
user_answers_marks[cid].append(0)
user_answers[cid].append("")
by_question[q].append(("", 0))
continue
ans = s.answer
answer_score = s.get_answer_score()
if answer_score == "unmarked":
index = exam.get_question_index(q)
unmarked.add(index)
user_answers[cid].append(ans)
user_answers_marks[cid].append(answer_score)
user_answers_and_marks[cid].append((ans, answer_score))
by_question[q].append((ans, answer_score))
user_scores = {}
for user in user_answers_marks:
user_scores[user] = sum(
[i for i in user_answers_marks[user] if i != "unmarked"]
)
user_scores_list = list(user_scores.values())
if len(user_scores_list) < 1:
mean = 0
median = 0
mode = 0
fig_html = ""
else:
mean = statistics.mean(user_scores_list)
median = statistics.median(user_scores_list)
try:
mode = statistics.mode(user_scores_list)
except statistics.StatisticsError:
mode = "No unique mode"
df = user_scores_list
fig = px.histogram(
df,
x=0,
title="{}: distribution of scores".format(exam),
labels={"0": "Score"},
height=400,
width=600,
)
fig_html = fig.to_html()
max_score = len(questions) * 2
return render(
request,
"rapids/exam_scores.html",
{
"cids": cids,
"exam": exam,
"unmarked": unmarked,
"questions": questions,
"by_question": by_question,
"user_answers": dict(user_answers),
"user_answers_marks": dict(user_answers_marks),
"user_scores": user_scores,
"user_scores_list": user_scores_list,
"user_names": user_names,
"user_answers_and_marks": user_answers_and_marks,
"max_score": max_score,
"mean": mean,
"median": median,
"mode": mode,
"plot": fig_html,
},
)
def exam_scores_cid_user(request, pk, sk):
exam = get_object_or_404(Exam, pk=pk)
# TODO:Need some kind of test for cid
cid = sk
questions = exam.exam_questions.all()
answers_and_marks = []
answers_marks = []
answers = []
for q in questions:
# Get user answer
user_answer = q.cid_user_answers.filter(cid=cid).first()
if not user_answer:
# skip if no answer
answers_marks.append(0)
answers.append("")
answer_score = 0
ans = "Not answered"
else:
ans = user_answer.answer
answer_score = user_answer.get_answer_score()
correct_answer = q.GetPrimaryAnswer()
if not exam.publish_results:
correct_answer = "*****"
answer_score = 0
answers.append(ans)
answers_marks.append(answer_score)
answers_and_marks.append((ans, answer_score, correct_answer))
if "unmarked" in answers_marks:
answered = [i for i in answers_marks if type(i) == int]
total_score = sum(answered)
unmarked_number = len(answers_marks) - len(answered)
total_score = "{} ({} unmarked)".format(total_score, unmarked_number)
else:
total_score = sum(answers_marks)
max_score = len(questions) * 2
return render(
request,
"rapids/exam_scores_user.html",
{
"exam": exam,
"cid": cid,
"questions": questions,
"answers": answers,
"answers_marks": answers_marks,
"total_score": total_score,
"max_score": max_score,
"answers_and_marks": answers_and_marks,
},
)