This commit is contained in:
Ross
2021-12-19 13:09:55 +00:00
parent 07dbdfe0e4
commit fccf25e2a1
16 changed files with 256 additions and 37 deletions
+11 -1
View File
@@ -158,6 +158,16 @@ class AnatomyQuestion(models.Model):
return reverse("anatomy:question_detail", kwargs={"pk": self.pk}) return reverse("anatomy:question_detail", kwargs={"pk": self.pk})
def get_primary_answer(self): def get_primary_answer(self):
ans = self.answers.filter(
proposed=False, status=Answer.MarkOptions.CORRECT
).first()
# ans = self.answers.first()
if ans is None:
return "None yet..."
else:
return ans.answer
if ( if (
self.answers.filter( self.answers.filter(
proposed=False, status=Answer.MarkOptions.CORRECT proposed=False, status=Answer.MarkOptions.CORRECT
@@ -434,7 +444,7 @@ class CidUserAnswer(models.Model):
max_length=1, max_length=1,
choices=Answer.MarkOptions.choices, choices=Answer.MarkOptions.choices,
default=Answer.MarkOptions.UNMARKED, default=Answer.MarkOptions.UNMARKED,
blank=True blank=True,
) )
def __str__(self): def __str__(self):
+1 -1
View File
@@ -33,7 +33,7 @@
{% endif %} {% endif %}
<ol id="full-question-list" class="sortable"> <ol id="full-question-list" class="sortable">
{% for question in questions.all %} {% for question in questions %}
<li data-question_pk={{question.pk}}> <li data-question_pk={{question.pk}}>
<span class="flex-col"> <span class="flex-col">
@@ -57,14 +57,14 @@
{% for question in questions %} {% for question in questions %}
<tr> <tr>
<td><a href="{% url 'anatomy:mark' exam_pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter}}</a> <td><a href="{% url 'anatomy:mark' exam_pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter}}</a>
{% if question.normal %}
[N]
{% else %}
[A]
{% endif %}
</td> </td>
{% for cid in cids %} {% comment %} {% for cid in cids %}
<td class="anatomy-ans user-answer-score-{{score_by_question|get_item:question|get_item:cid}}" title="answer score: {{score_by_question|get_item:question|get_item:cid}}">{{ans_by_question|get_item:question|get_item:cid}}</td> <td class="anatomy-ans user-answer-score-{{score_by_question|get_item:question|get_item:cid}}" title="answer score: {{score_by_question|get_item:question|get_item:cid}}">{{ans_by_question|get_item:question|get_item:cid}}</td>
{% endfor %} {% endcomment %}
{% for cid in cids %}
{% with by_question|get_item:question|get_item:cid as ans_score %}
<td class="anatomy-ans user-answer-score-{{ans_score.1}}" title="answer score: {{ans_score.1}}">{{ans_score.0}}</td>
{% endwith %}
{% endfor %} {% endfor %}
</tr> </tr>
{% endfor %} {% endfor %}
+1 -1
View File
@@ -1004,7 +1004,7 @@ def question_save_annotation(request, pk):
GenericExamViews = ExamViews( GenericExamViews = ExamViews(
Exam, AnatomyQuestion, CidUserAnswer, "anatomy", "anatomy", loadJsonAnswer Exam, AnatomyQuestion, Answer, CidUserAnswer, "anatomy", "anatomy", loadJsonAnswer
) )
+70 -15
View File
@@ -59,6 +59,7 @@ import random
import plotly.express as px import plotly.express as px
# from rad.views import get_question_and_content_type # from rad.views import get_question_and_content_type
from django.db.models import Prefetch
def normaliseRapidsScore(score): def normaliseRapidsScore(score):
@@ -240,10 +241,18 @@ def generic_exam_json_edit(request):
class ExamViews(View, LoginRequiredMixin): class ExamViews(View, LoginRequiredMixin):
def __init__( def __init__(
self, exam, question, cid_user_answer, app, question_type, loadJsonAnswer self,
exam,
question,
answer,
cid_user_answer,
app,
question_type,
loadJsonAnswer,
): ):
self.Exam = exam self.Exam = exam
self.Question = question self.Question = question
self.Answer = answer
self.CidUserAnswer = cid_user_answer self.CidUserAnswer = cid_user_answer
self.app_name = app self.app_name = app
self.question_type = question_type self.question_type = question_type
@@ -414,7 +423,7 @@ class ExamViews(View, LoginRequiredMixin):
# print(Exam.objects.all()) # print(Exam.objects.all())
# exams = Exam.objects.all() # exams = Exam.objects.all()
# print("test", Exam.objects.all().get(id=pk)) # print("test", Exam.objects.all().get(id=pk))
exam = get_object_or_404(self.Exam, pk=pk) exam = get_object_or_404(self.Exam.objects.prefetch_related("author"), pk=pk)
if request.user not in exam.author.all(): if request.user not in exam.author.all():
if not self.check_user_access(request.user, pk): if not self.check_user_access(request.user, pk):
@@ -428,7 +437,38 @@ class ExamViews(View, LoginRequiredMixin):
if can_edit: if can_edit:
notes = self.exam_notes(request, pk) notes = self.exam_notes(request, pk)
questions = exam.exam_questions.all() if self.app_name == "anatomy":
questions = (
exam.exam_questions.select_related(
"modality",
"structure",
"body_part",
"region",
"examination",
"question_type",
).all()
# .prefetch_related(
# Prefetch(
# "answers",
# queryset=self.Answer.objects.filter(
# proposed=False, status=self.Answer.MarkOptions.CORRECT
# ),
# )
# )
)
elif self.app_name == "rapids":
questions = (
exam.exam_questions.select_related()
.all()
.prefetch_related("images", "abnormality", "region", "examination")
)
elif self.app_name == "physics":
questions = (
exam.exam_questions.select_related("category").all()
# .prefetch_related("images", "abnormality", "region", "examination")
)
else:
questions = exam.exam_questions.select_related().all()
question_number = len(questions) question_number = len(questions)
@@ -868,13 +908,15 @@ class ExamViews(View, LoginRequiredMixin):
user_answers = defaultdict(list) user_answers = defaultdict(list)
user_names = {} user_names = {}
score_by_question = defaultdict(dict) by_question = defaultdict(dict)
ans_by_question = defaultdict(dict)
unmarked = set() unmarked = set()
questions = exam.exam_questions.all() questions = exam.exam_questions.all()
cid_user_answers = self.CidUserAnswer.objects.filter(question__in=questions, exam__id=pk) # We could prefect the CidUserAnswers.answers here (if we didn't cache them)
cid_user_answers = self.CidUserAnswer.objects.select_related("question").filter(
question__in=questions, exam__id=pk
)
cids = set() cids = set()
@@ -905,15 +947,25 @@ class ExamViews(View, LoginRequiredMixin):
user_answers_marks[cid].append(answer_score) user_answers_marks[cid].append(answer_score)
user_answers_and_marks[cid].append((ans, answer_score)) user_answers_and_marks[cid].append((ans, answer_score))
score_by_question[q][cid] = answer_score if self.app_name in ("rapids", "anatomy", "sbas"):
ans_by_question[q][cid] = ans by_question[q][cid] = (ans, answer_score)
else:
zipped_ans_scores = zip(ans, answer_score)
by_question[q][cid] = zipped_ans_scores
user_scores = {} user_scores = {}
user_scores_normalised = {} user_scores_normalised = {}
for user in user_answers_marks: for user in user_answers_marks:
user_scores[user] = sum(
[i for i in user_answers_marks[user] if i != "unmarked"] if self.app_name in ("rapids", "anatomy", "sbas"):
) user_scores[user] = sum(
[i for i in user_answers_marks[user] if i != "unmarked"]
)
else:
user_scores[user] = sum(
[sum(i) for i in user_answers_marks[user] if i != "unmarked"]
)
if self.app_name == "rapids": if self.app_name == "rapids":
user_scores_normalised[user] = normaliseRapidsScore( user_scores_normalised[user] = normaliseRapidsScore(
sum([i for i in user_answers_marks[user] if i != "unmarked"]) sum([i for i in user_answers_marks[user] if i != "unmarked"])
@@ -923,7 +975,12 @@ class ExamViews(View, LoginRequiredMixin):
user_scores_list = list(user_scores.values()) user_scores_list = list(user_scores.values())
max_score = len(questions) * 2 if self.app_name in ("rapids", "anatomy"):
max_score = len(questions) * 2
elif self.app_name == "physics":
max_score = len(questions) * 5
else:
max_score = len(questions)
if len(user_scores_list) < 1: if len(user_scores_list) < 1:
mean = 0 mean = 0
@@ -962,7 +1019,6 @@ class ExamViews(View, LoginRequiredMixin):
exam.stats_graph = fig_html exam.stats_graph = fig_html
exam.save() exam.save()
return render( return render(
request, request,
f"{self.app_name}/exam_scores_new.html", f"{self.app_name}/exam_scores_new.html",
@@ -971,8 +1027,7 @@ class ExamViews(View, LoginRequiredMixin):
"exam": exam, "exam": exam,
"unmarked": unmarked, "unmarked": unmarked,
"questions": questions, "questions": questions,
"score_by_question": score_by_question, "by_question": by_question,
"ans_by_question": ans_by_question,
"user_answers": dict(user_answers), "user_answers": dict(user_answers),
"user_answers_marks": dict(user_answers_marks), "user_answers_marks": dict(user_answers_marks),
"user_scores": user_scores, "user_scores": user_scores,
+1 -1
View File
@@ -1170,7 +1170,7 @@ def long_series_order_upload_filename(request, pk):
return redirect("longs:long_series_detail", pk=pk) return redirect("longs:long_series_detail", pk=pk)
GenericExamViews = ExamViews(Exam, Long, CidUserAnswer, "longs", "long", loadJsonAnswer) GenericExamViews = ExamViews(Exam, Long, None, CidUserAnswer, "longs", "long", loadJsonAnswer)
class ExamCreate(RevisionMixin, LoginRequiredMixin, CreateView): class ExamCreate(RevisionMixin, LoginRequiredMixin, CreateView):
@@ -0,0 +1,73 @@
{% extends 'physics/exams.html' %}
{% block content %}
<div id="stats-plot">{{plot|safe}}</div>
<div class="physics">
<h2>{{ exam.name }}</h2>
</div>
<div id="stats-block">
<h3>Stats</h3>
Candidates: {{cids|length}}<br />
Max score: {{max_score}}<br />
Mean: {{mean}}, Median {{median}}, Mode {{mode}}
</div>
<div>
<table class="table table-dark table-striped table-hover table-sm cid-score-table">
<tr>
<th>Candidate ID</th>
<th>Score</th>
{% comment %} <th>Normalised Score</th> {% endcomment %}
</tr>
{% comment %} {% for user, value in user_answers_marks.items %} {% endcomment %}
{% for cid in cids %}
<tr>
<td><a href="{% url 'cid_scores_admin' cid %}">{{cid}}</a></td>
<td>{{user_scores|get_item:cid}}</td>
{% comment %} <td>{{user_scores_normalised|get_item:user}}</td> {% endcomment %}
</tr>
{% endfor %}
</table>
</div>
<div>
<h3>Answers as a table</h3>
<table class="table table-dark table-striped table-hover table-sm col-sm">
<thead class="thead-dark">
<tr>
<th>Candidate</th>
{% for cid in cids %}
{% comment %} <th><a href="{% url 'physics:exam_scores_cid_user' exam.pk cid %}">{{cid}}</a></th> {% endcomment %}
<th><a href="">{{cid}}</a></th>
{% endfor %}
</tr>
</thead>
{% for question in questions %}
<tr>
<td>Question {{forloop.counter}}
</td>
{% for cid in cids %}
<td>
<ol class="physics-ans" style="list-style-type: lower-alpha;">
{% with by_question|get_item:question|get_item:cid as ans_score %}
{% for ans, score in ans_score %}
<li class="user-answer-score-{{score}}" title="answer score: {{score}}">{{ans}}</li>
{% endfor %}
{% endwith %}
</ol>
</td>
{% endfor %}
</tr>
{% endfor %}
<tr>
<td>Score:</td>
{% for cid in cids %}
<td>{{user_scores|get_item:cid}}</td>
{% endfor %}
</tr>
</table>
</div>
{% endblock %}
+1 -1
View File
@@ -26,7 +26,7 @@ urlpatterns = [
name="exam_finish", name="exam_finish",
), ),
path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"), path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"),
path("exam/<int:pk>/scores", views.exam_scores_cid, name="exam_scores_cid"), path("exam/<int:pk>/scores", views.GenericExamViews.exam_scores_cid, name="exam_scores_cid"),
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,
+1 -1
View File
@@ -464,7 +464,7 @@ def loadJsonAnswer(answer):
GenericExamViews = ExamViews( GenericExamViews = ExamViews(
Exam, Question, CidUserAnswer, "physics", "physics", loadJsonAnswer Exam, Question, None, CidUserAnswer, "physics", "physics", loadJsonAnswer
) )
GenericViews = GenericViewBase("physics", Question, CidUserAnswer, Exam) GenericViews = GenericViewBase("physics", Question, CidUserAnswer, Exam)
+6 -1
View File
@@ -658,7 +658,7 @@ class CidUserAnswer(models.Model):
max_length=1, max_length=1,
choices=Answer.MarkOptions.choices, choices=Answer.MarkOptions.choices,
default=Answer.MarkOptions.UNMARKED, default=Answer.MarkOptions.UNMARKED,
blank=True blank=True,
) )
def __str__(self): def __str__(self):
@@ -700,6 +700,7 @@ class CidUserAnswer(models.Model):
def get_answer_score(self, cached=True): def get_answer_score(self, cached=True):
if cached and self.score: if cached and self.score:
print("CACHED")
if self.score == Answer.MarkOptions.CORRECT: if self.score == Answer.MarkOptions.CORRECT:
mark = 2 mark = 2
elif self.score == Answer.MarkOptions.HALF_MARK: elif self.score == Answer.MarkOptions.HALF_MARK:
@@ -708,6 +709,7 @@ class CidUserAnswer(models.Model):
mark = 0 mark = 0
return mark return mark
print("NOT CACHED")
q = self.question q = self.question
@@ -731,6 +733,9 @@ class CidUserAnswer(models.Model):
mark = "unmarked" mark = "unmarked"
if marked_ans is not None: if marked_ans is not None:
self.score = marked_ans.status
self.save()
if marked_ans.status == Answer.MarkOptions.CORRECT: if marked_ans.status == Answer.MarkOptions.CORRECT:
mark = 2 mark = 2
elif marked_ans.status == Answer.MarkOptions.HALF_MARK: elif marked_ans.status == Answer.MarkOptions.HALF_MARK:
+8 -5
View File
@@ -31,11 +31,11 @@
<th>Score</th> <th>Score</th>
<th>Normalised Score</th> <th>Normalised Score</th>
</tr> </tr>
{% for user, value in user_answers_marks.items %} {% for cid in cids %}
<tr> <tr>
<td><a href="{% url 'cid_scores_admin' user %}">{{user}}</a></td> <td><a href="{% url 'cid_scores_admin' cid %}">{{cid}}</a></td>
<td>{{user_scores|get_item:user}}</td> <td>{{user_scores|get_item:cid}}</td>
<td>{{user_scores_normalised|get_item:user}}</td> <td>{{user_scores_normalised|get_item:cid}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
@@ -63,7 +63,10 @@
{% endif %} {% endif %}
</td> </td>
{% for cid in cids %} {% for cid in cids %}
<td class="rapid-ans user-answer-score-{{score_by_question|get_item:question|get_item:cid}}" title="answer score: {{score_by_question|get_item:question|get_item:cid}}">{{ans_by_question|get_item:question|get_item:cid}}</td> {% comment %} <td class="rapid-ans user-answer-score-{{score_by_question|get_item:question|get_item:cid}}" title="answer score: {{score_by_question|get_item:question|get_item:cid}}">{{ans_by_question|get_item:question|get_item:cid}}</td> {% endcomment %}
{% with by_question|get_item:question|get_item:cid as ans_score %}
<td class="anatomy-ans user-answer-score-{{ans_score.1}}" title="answer score: {{ans_score.1}}">{{ans_score.0}}</td>
{% endwith %}
{% endfor %} {% endfor %}
</tr> </tr>
{% endfor %} {% endfor %}
+1 -1
View File
@@ -1048,7 +1048,7 @@ class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView):
GenericExamViews = ExamViews( GenericExamViews = ExamViews(
Exam, Rapid, CidUserAnswer, "rapids", "rapid", loadJsonAnswer Exam, Rapid, Answer, CidUserAnswer, "rapids", "rapid", loadJsonAnswer
) )
+4 -1
View File
@@ -196,13 +196,16 @@ class CidUserAnswer(models.Model):
def __str__(self): def __str__(self):
return "{}/{}/{}".format(self.cid, self.exam, self.question.pk) return "{}/{}/{}".format(self.cid, self.exam, self.question.pk)
def get_answer_string(self):
return self.answer
def get_answer(self): def get_answer(self):
return self.answer return self.answer
def get_correct(self): def get_correct(self):
return self.answer == self.question.best_answer return self.answer == self.question.best_answer
def get_score(self): def get_answer_score(self):
if self.get_correct(): if self.get_correct():
return 1 return 1
return 0 return 0
+70
View File
@@ -0,0 +1,70 @@
{% extends 'sbas/exams.html' %}
{% block content %}
<div id="stats-plot">{{plot|safe}}</div>
<div class="sbas">
<h2>{{ exam.name }}</h2>
</div>
<div id="stats-block">
<h3>Stats</h3>
Candidates: {{cids|length}}<br />
Max score: {{max_score}}<br />
Mean: {{mean}}, Median {{median}}, Mode {{mode}}
</div>
<div>
<table class="table table-dark table-striped table-hover table-sm cid-score-table">
<tr>
<th>Candidate ID</th>
<th>Score</th>
{% comment %} <th>Normalised Score</th> {% endcomment %}
</tr>
{% comment %} {% for user, value in user_answers_marks.items %} {% endcomment %}
{% for cid in cids %}
<tr>
<td><a href="{% url 'cid_scores_admin' cid %}">{{cid}}</a></td>
<td>{{user_scores|get_item:cid}}</td>
{% comment %} <td>{{user_scores_normalised|get_item:user}}</td> {% endcomment %}
</tr>
{% endfor %}
</table>
</div>
<div>
<h3>Answers as a table</h3>
<table class="table table-dark table-striped table-hover table-sm col-sm">
<thead class="thead-dark">
<tr>
<th>Candidate</th>
{% for cid in cids %}
{% comment %} <th><a href="{% url 'sbas:exam_scores_cid_user' exam.pk cid %}">{{cid}}</a></th> {% endcomment %}
<th><a href="">{{cid}}</a></th>
{% endfor %}
</tr>
</thead>
{% for question in questions %}
<tr>
<td><a href="">Question {{forloop.counter}}</a>
</td>
{% comment %} {% for cid in cids %}
<td class="sbas-ans user-answer-score-{{score_by_question|get_item:question|get_item:cid}}" title="answer score: {{score_by_question|get_item:question|get_item:cid}}">{{ans_by_question|get_item:question|get_item:cid}}</td>
{% endfor %} {% endcomment %}
{% for cid in cids %}
{% with by_question|get_item:question|get_item:cid as ans_score %}
<td class="sbas-ans user-answer-score-{{ans_score.1}}" title="answer score: {{ans_score.1}}">{{ans_score.0}}</td>
{% endwith %}
{% endfor %}
</tr>
{% endfor %}
<tr>
<td>Score:</td>
{% for cid in cids %}
<td>{{user_scores|get_item:cid}}</td>
{% endfor %}
</tr>
</table>
</div>
{% endblock %}
+1 -1
View File
@@ -28,7 +28,7 @@ urlpatterns = [
path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"), path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"),
path( path(
"exam/<int:pk>/scores", "exam/<int:pk>/scores",
cache_page(60 * 1)(views.exam_scores_cid), views.GenericExamViews.exam_scores_cid,
name="exam_scores_cid", name="exam_scores_cid",
), ),
path( path(
+1 -1
View File
@@ -383,7 +383,7 @@ def loadJsonAnswer(answer):
GenericExamViews = ExamViews( GenericExamViews = ExamViews(
Exam, Question, CidUserAnswer, "sbas", "sbas", loadJsonAnswer Exam, Question, None, CidUserAnswer, "sbas", "sbas", loadJsonAnswer
) )