try to improve the scores pages

This commit is contained in:
Ross
2026-07-06 12:33:30 +01:00
parent 7bd1475daf
commit 50b80fd0c7
12 changed files with 287 additions and 250 deletions
+50 -31
View File
@@ -338,7 +338,21 @@ class Answer(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.clean() self.clean()
return super(Answer, self).save(*args, **kwargs) res = super(Answer, self).save(*args, **kwargs)
# Update matching UserAnswer objects in real-time
UserAnswer.objects.filter(
question=self.question,
answer_compare__iexact=self.answer_compare
).update(score=self.status)
return res
def delete(self, *args, **kwargs):
# Update matching UserAnswer objects back to UNMARKED in real-time
UserAnswer.objects.filter(
question=self.question,
answer_compare__iexact=self.answer_compare
).update(score=Answer.MarkOptions.UNMARKED)
return super(Answer, self).delete(*args, **kwargs)
def clean(self): def clean(self):
if self.answer: if self.answer:
@@ -551,11 +565,30 @@ class UserAnswer(UserAnswerBase):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.clean() self.clean()
# Auto-mark on save from existing template answers
ans = self.answer_compare
if ans == "":
self.score = Answer.MarkOptions.INCORRECT
else:
marked_ans = Answer.objects.filter(
question_id=self.question_id,
answer_compare__iexact=ans
).first()
if marked_ans is not None:
self.score = marked_ans.status
else:
self.score = Answer.MarkOptions.UNMARKED
return super(UserAnswer, self).save(*args, **kwargs) return super(UserAnswer, self).save(*args, **kwargs)
def clean(self): def clean(self):
if self.answer: if self.answer:
self.answer = self.answer.strip()
self.answer_compare = get_answer_compare(self.answer) self.answer_compare = get_answer_compare(self.answer)
else:
self.answer = ""
self.answer_compare = ""
# def get_compare_string(self): # def get_compare_string(self):
# # strip here should be unneccasry (providing clean is now working) # # strip here should be unneccasry (providing clean is now working)
@@ -569,28 +602,32 @@ class UserAnswer(UserAnswerBase):
return self.answer return self.answer
def get_answer_score(self, cached=True): def get_answer_score(self, cached=True):
if cached and self.score: if cached and self.score and self.score != Answer.MarkOptions.UNMARKED:
if self.score == Answer.MarkOptions.CORRECT: if self.score == Answer.MarkOptions.CORRECT:
mark = 2 return 2
elif self.score == Answer.MarkOptions.HALF_MARK: elif self.score == Answer.MarkOptions.HALF_MARK:
mark = 1 return 1
elif self.score == Answer.MarkOptions.INCORRECT: elif self.score == Answer.MarkOptions.INCORRECT:
mark = 0 return 0
return mark
q = self.question q = self.question
ans = self.answer_compare ans = self.answer_compare
if ans == "": if ans == "":
self.score == Answer.MarkOptions.INCORRECT if self.score != Answer.MarkOptions.INCORRECT:
self.score = Answer.MarkOptions.INCORRECT
self.save() self.save()
return 0 return 0
marked_ans = q.answers.filter(answer_compare__iexact=ans).first() marked_ans = None
for a in q.answers.all():
if a.answer_compare.lower() == ans.lower():
marked_ans = a
break
mark = "unmarked" mark = "unmarked"
if marked_ans is not None: if marked_ans is not None:
if self.score != marked_ans.status:
self.score = marked_ans.status self.score = marked_ans.status
self.save() self.save()
@@ -600,27 +637,9 @@ class UserAnswer(UserAnswerBase):
mark = 1 mark = 1
elif marked_ans.status == Answer.MarkOptions.INCORRECT: elif marked_ans.status == Answer.MarkOptions.INCORRECT:
mark = 0 mark = 0
return mark
if (
q.answers.filter(
answer_compare__iexact=ans, status=Answer.MarkOptions.CORRECT
).first()
is not None
):
mark = 2
elif (
q.answers.filter(
answer_compare__iexact=ans, status=Answer.MarkOptions.HALF_MARK
).first()
is not None
):
mark = 1
elif q.answers.filter(
answer_compare__iexact=ans, status=Answer.MarkOptions.INCORRECT
).first():
mark = 0
else: else:
mark = "unmarked" if self.score != Answer.MarkOptions.UNMARKED:
self.score = Answer.MarkOptions.UNMARKED
self.save()
return mark return mark
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends 'generic/exam_scores_base.html' %} {% extends base_template|default:'generic/exam_scores_base.html' %}
{% block table_answers %} {% block table_answers %}
{% for question in questions %} {% for question in questions %}
+11 -49
View File
@@ -85,57 +85,19 @@
<div class="d-flex justify-content-between align-items-center mb-1"> <div class="d-flex justify-content-between align-items-center mb-1">
<h5 class="mb-0">Answers as a table</h5> <h5 class="mb-0">Answers as a table</h5>
<div> <div>
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="collapse" data-bs-target="#answers-table" aria-expanded="true">Toggle answers</button> <button class="btn btn-sm btn-outline-secondary" data-bs-toggle="collapse" data-bs-target="#answers-table-container" aria-expanded="true">Toggle answers</button>
</div> </div>
</div> </div>
<div class="collapse show" id="answers-table"> <div class="collapse show" id="answers-table-container">
<div class="table-responsive"> <div hx-get="{% url exam.app_name|add:':exam_scores_all' exam.pk %}?table_only=true"
<table class="table table-dark table-striped table-hover table-sm col-sm small"> hx-trigger="load"
<thead class="thead-dark"> hx-swap="outerHTML">
<tr> <div class="text-center py-4">
<th>Candidate</th> <div class="spinner-border text-info" role="status">
{% for cid in cids %} <span class="visually-hidden">Loading...</span>
{% comment %} <th><a href="{% url exam.app_name|add:':exam_scores_cid_user' exam.pk cid %}">{{cid}}</a></th> {% endcomment %} </div>
<th> <div class="text-muted mt-2">Loading answers table...</div>
</div>
{% if cid|slice:":1" == "u" %}
<a href="{% url exam.app_name|add:':exam_scores_user_admin' exam.pk cid|slice:'2:' %}">
{% else %}
<a href="{% url exam.app_name|add:':exam_scores_cid_user_admin' exam.pk cid|slice:'2:' %}">
{% endif %}
{{cids_user_id_map|get_item:cid}}</a></th>
{% endfor %}
</tr>
</thead>
{% block table_answers %}
{% endblock table_answers %}
{% comment %} {% for question in questions %}
<tr>
<td><a href="{% url exam.app_name|add:':mark' exam_pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter}}</a>
{% if question.normal %}
[N]
{% else %}
[A]
{% endif %}
</td>
{% for cid in cids %}
{% with by_question|get_item:question|get_item:cid as ans_score %}
<td class="user-answer-score-{{ans_score.1}}" title="answer score: {{ans_score.1}} [{{ans_score.2}}]">{{ans_score.0}}</td>
{% endwith %}
{% endfor %}
</tr>
{% endfor %} {% endcomment %}
<tr>
<td>Score:</td>
{% for cid in cids %}
<td>{{user_scores|get_item:cid}}</td>
{% endfor %}
</tr>
</table>
</div> </div>
</div> </div>
@@ -0,0 +1,27 @@
<div class="table-responsive" id="answers-table">
<table class="table table-dark table-striped table-hover table-sm col-sm small">
<thead class="thead-dark">
<tr>
<th>Candidate</th>
{% for cid in cids %}
<th>
{% if cid|slice:":1" == "u" %}
<a href="{% url exam.app_name|add:':exam_scores_user_admin' exam.pk cid|slice:'2:' %}">
{% else %}
<a href="{% url exam.app_name|add:':exam_scores_cid_user_admin' exam.pk cid|slice:'2:' %}">
{% endif %}
{{cids_user_id_map|get_item:cid}}</a>
</th>
{% endfor %}
</tr>
</thead>
{% block table_answers %}
{% endblock table_answers %}
<tr>
<td>Score:</td>
{% for cid in cids %}
<td>{{user_scores|get_item:cid}}</td>
{% endfor %}
</tr>
</table>
</div>
+35 -61
View File
@@ -3796,6 +3796,9 @@ class ExamViews(View, LoginRequiredMixin):
else: else:
user_answers_qs = self.UserAnswer.objects.filter(user=user, exam__id=pk).select_related("question") user_answers_qs = self.UserAnswer.objects.filter(user=user, exam__id=pk).select_related("question")
if self.app_name in ("anatomy", "rapids"):
user_answers_qs = user_answers_qs.prefetch_related("question__answers")
# Map question_id -> UserAnswer for O(1) lookup # Map question_id -> UserAnswer for O(1) lookup
user_answers_map = {ua.question_id: ua for ua in user_answers_qs} user_answers_map = {ua.question_id: ua for ua in user_answers_qs}
@@ -3927,19 +3930,7 @@ class ExamViews(View, LoginRequiredMixin):
return max_score return max_score
def exam_scores_all(self, request, pk): def exam_scores_all(self, request, pk):
"""The exam scores pages. Displays all user scores in a tabular format. """The exam scores pages. Displays all user scores in a tabular format."""
Args:
request (_type_): _description_
pk (_type_): _description_
Raises:
Http404: _description_
PermissionDenied: _description_
Returns:
_type_: _description_
"""
exam: ExamBase = get_object_or_404(self.Exam, pk=pk) exam: ExamBase = get_object_or_404(self.Exam, pk=pk)
if not exam.exam_mode: if not exam.exam_mode:
@@ -3949,21 +3940,17 @@ class ExamViews(View, LoginRequiredMixin):
if request.user not in exam.author.all(): if request.user not in exam.author.all():
raise PermissionDenied raise PermissionDenied
# user_answers_and_marks = defaultdict(list) table_only = request.GET.get("table_only") == "true" or request.headers.get("HX-Target") == "answers-table-container"
user_answers_marks = defaultdict(list)
# user_answers = defaultdict(list)
# user_names = {}
# cid_passcodes = {}
user_answers_marks = defaultdict(list)
by_question = defaultdict(dict) by_question = defaultdict(dict)
unmarked = set() unmarked = set()
cached_scores = True cached_scores = True
valid_cid_users = set(exam.valid_cid_users.all().values_list("cid", flat=True)) valid_cid_users = set(exam.valid_cid_users.all().values_list("cid", flat=True))
valid_user_users = set(exam.valid_user_users.all().values_list("pk", flat=True)) valid_user_users = set(exam.valid_user_users.all().values_list("pk", flat=True))
if self.app_name in ("rapids"): if self.app_name in ("rapids",):
user_answers_callstates = defaultdict(list) user_answers_callstates = defaultdict(list)
user_answers_callstates_counted = {} user_answers_callstates_counted = {}
@@ -3977,34 +3964,31 @@ class ExamViews(View, LoginRequiredMixin):
questions = list(questions_qs) questions = list(questions_qs)
question_index_map = {q: i for i, q in enumerate(questions)} question_index_map = {q: i for i, q in enumerate(questions)}
# We could prefetch the UserAnswers.answers here (if we didn't cache them) # Fetch and prefetch the UserAnswers
# Also select_related the user to avoid per-answer user lookups in the loop cid_user_answers_qs = (
cid_user_answers = (
self.UserAnswer.objects.select_related("question", "user") self.UserAnswer.objects.select_related("question", "user")
.filter(question__in=questions, exam__id=pk) .filter(question__in=questions, exam__id=pk)
) )
if self.app_name in ("anatomy", "rapids"):
cid_user_answers_qs = cid_user_answers_qs.prefetch_related("question__answers")
cid_user_answers = list(cid_user_answers_qs)
question_number = len(questions) question_number = len(questions)
cids = set() cids = set()
plain_cids = set() plain_cids = set()
user_ids = set() user_ids = set()
cids_user_id_map = {} cids_user_id_map = {}
# Loop through all candidates # Loop through all candidates
for cid_user_answer in cid_user_answers: for cid_user_answer in cid_user_answers:
# Convoluted (probably...)
if cid_user_answer.user is None: if cid_user_answer.user is None:
cid = f"c/{cid_user_answer.cid}" cid = f"c/{cid_user_answer.cid}"
cids_user_id_map[cid] = cid cids_user_id_map[cid] = cid
# cid_passcodes[cid] = cid_user_answer.passcode
cids.add(cid) cids.add(cid)
plain_cids.add(cid_user_answer.cid) plain_cids.add(cid_user_answer.cid)
else: else:
cid = f"u/{cid_user_answer.user.pk}" cid = f"u/{cid_user_answer.user.pk}"
# cids_user_id_map[cid] = cid_user_answer.user.username
name = f"{cid_user_answer.user.first_name} {cid_user_answer.user.last_name}" name = f"{cid_user_answer.user.first_name} {cid_user_answer.user.last_name}"
if not name.strip(): if not name.strip():
name = cid_user_answer.user.username name = cid_user_answer.user.username
@@ -4013,32 +3997,23 @@ class ExamViews(View, LoginRequiredMixin):
user_ids.add(cid_user_answer.user.pk) user_ids.add(cid_user_answer.user.pk)
s = cid_user_answer s = cid_user_answer
# user_names[cid] = cid
q = cid_user_answer.question q = cid_user_answer.question
# 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.get_answer_string()
answer_score = s.get_answer_score() answer_score = s.get_answer_score()
if answer_score == "unmarked": if answer_score == "unmarked":
# Use precomputed map to avoid repeated scanning/indexing of questions
index = question_index_map.get(q) index = question_index_map.get(q)
if index is not None: if index is not None:
unmarked.add(index) unmarked.add(index)
# user_answers[cid].append(ans)
user_answers_marks[cid].append(answer_score) user_answers_marks[cid].append(answer_score)
if self.app_name == "rapids": if self.app_name == "rapids":
callstate = s.get_answer_callstate() callstate = s.get_answer_callstate()
by_question[q][cid] = (ans, answer_score, callstate)
user_answers_callstates[cid].append(callstate) user_answers_callstates[cid].append(callstate)
if table_only:
ans = s.get_answer_string()
if self.app_name == "rapids":
by_question[q][cid] = (ans, answer_score, callstate)
elif self.app_name in ("anatomy", "sbas", "longs", "shorts"): elif self.app_name in ("anatomy", "sbas", "longs", "shorts"):
by_question[q][cid] = (ans, answer_score) by_question[q][cid] = (ans, answer_score)
else: else:
@@ -4049,17 +4024,14 @@ class ExamViews(View, LoginRequiredMixin):
user_scores_normalised = {} user_scores_normalised = {}
user_answer_count = {} user_answer_count = {}
for user in user_answers_marks: for user in user_answers_marks:
# For longs we have a default score of 3 (not 0) if unanswered
# so we need to check for those
if self.app_name == "longs": if self.app_name == "longs":
for question in questions: missing_count = question_number - len(user_answers_marks[user])
# If either question or user do not exist we give a default for _ in range(missing_count):
# not answered == score of 3
if user not in by_question[question]:
# print("NOT in")
by_question[question][user] = ("Not answered", 3.0)
user_answers_marks[user].append(3.0) user_answers_marks[user].append(3.0)
if table_only:
for question in questions:
if user not in by_question[question]:
by_question[question][user] = ("Not answered", 3.0)
if self.app_name in ("physics",): if self.app_name in ("physics",):
user_scores[user] = sum( user_scores[user] = sum(
@@ -4081,9 +4053,14 @@ class ExamViews(View, LoginRequiredMixin):
# ignore scores of 0 for stats # ignore scores of 0 for stats
user_scores_list = [i for i in user_scores.values() if i > 0] user_scores_list = [i for i in user_scores.values() if i > 0]
max_score = self.get_max_score(questions) max_score = self.get_max_score(questions)
mean = 0
median = 0
mode = 0
fig_html = ""
if not table_only:
if len(user_scores_list) < 1: if len(user_scores_list) < 1:
mean = 0 mean = 0
median = 0 median = 0
@@ -4127,7 +4104,6 @@ class ExamViews(View, LoginRequiredMixin):
user_data = {} user_data = {}
for u in cids: for u in cids:
# uid = cids_user_id_map[u]
user_data[u] = { user_data[u] = {
"score": user_scores[u], "score": user_scores[u],
"normalised_score": user_scores_normalised[u], "normalised_score": user_scores_normalised[u],
@@ -4139,11 +4115,14 @@ class ExamViews(View, LoginRequiredMixin):
user_answers_callstates_counted[u] = counted_callstates user_answers_callstates_counted[u] = counted_callstates
exam.user_scores = user_data exam.user_scores = user_data
exam.save() exam.save()
else:
# If table_only and rapids, populate user_answers_callstates_counted
if self.app_name == "rapids":
for u in cids:
user_answers_callstates_counted[u] = dict(Counter(user_answers_callstates[u]))
missing_user_ids = valid_user_users - user_ids missing_user_ids = valid_user_users - user_ids
missing_users = [] missing_users = []
if missing_user_ids: if missing_user_ids:
missing_users = User.objects.filter(pk__in=missing_user_ids) missing_users = User.objects.filter(pk__in=missing_user_ids)
@@ -4152,19 +4131,13 @@ class ExamViews(View, LoginRequiredMixin):
"cids": sorted(cids), "cids": sorted(cids),
"missing_cids": valid_cid_users - plain_cids, "missing_cids": valid_cid_users - plain_cids,
"missing_users": missing_users, "missing_users": missing_users,
# "cid_passcodes": cid_passcodes,
"exam": exam, "exam": exam,
"unmarked": unmarked, "unmarked": unmarked,
"questions": questions, "questions": questions,
"question_number": question_number, "question_number": question_number,
"by_question": by_question, "by_question": by_question,
# "user_answers": dict(user_answers),
# "user_answers_marks": dict(user_answers_marks),
"user_scores": user_scores, "user_scores": user_scores,
"user_scores_normalised": user_scores_normalised, "user_scores_normalised": user_scores_normalised,
# "user_scores_list": user_scores_list,
# "user_names": user_names,
# "user_answers_and_marks": user_answers_and_marks,
"user_answer_count": user_answer_count, "user_answer_count": user_answer_count,
"cids_user_id_map": cids_user_id_map, "cids_user_id_map": cids_user_id_map,
"max_score": max_score, "max_score": max_score,
@@ -4174,6 +4147,7 @@ class ExamViews(View, LoginRequiredMixin):
"plot": fig_html, "plot": fig_html,
"cached_scores": cached_scores, "cached_scores": cached_scores,
"can_edit": exam.check_user_can_edit(request.user), "can_edit": exam.check_user_can_edit(request.user),
"base_template": "generic/exam_scores_partial_base.html" if table_only else "generic/exam_scores_base.html",
} }
if self.app_name == "rapids": if self.app_name == "rapids":
+1 -2
View File
@@ -1,5 +1,4 @@
{% extends base_template|default:'generic/exam_scores_base.html' %}
{% extends 'generic/exam_scores_base.html' %}
{% block table_answers %} {% block table_answers %}
{% for question in questions %} {% for question in questions %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends 'generic/exam_scores_base.html' %} {% extends base_template|default:'generic/exam_scores_base.html' %}
{% block table_answers %} {% block table_answers %}
{% for question in questions %} {% for question in questions %}
+82 -30
View File
@@ -141,7 +141,30 @@ class Answer(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.clean() self.clean()
return super(Answer, self).save(*args, **kwargs) res = super(Answer, self).save(*args, **kwargs)
# Update matching UserAnswer objects in real-time
status = self.status
callstate = UserAnswer.CallStateOptions.INCORRECTCALL
if status == Answer.MarkOptions.CORRECT:
callstate = UserAnswer.CallStateOptions.CORRECT
elif status == Answer.MarkOptions.HALF_MARK:
callstate = UserAnswer.CallStateOptions.PARTIALCALL
UserAnswer.objects.filter(
question=self.question,
normal=False,
answer_compare__iexact=self.answer_compare
).update(score=status, callstate=callstate)
return res
def delete(self, *args, **kwargs):
# Update matching UserAnswer objects back to UNMARKED in real-time
UserAnswer.objects.filter(
question=self.question,
normal=False,
answer_compare__iexact=self.answer_compare
).update(score=Answer.MarkOptions.UNMARKED, callstate=None)
return super(Answer, self).delete(*args, **kwargs)
def clean(self): def clean(self):
if self.answer: if self.answer:
@@ -823,9 +846,37 @@ class UserAnswer(UserAnswerBase):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.clean() self.clean()
# Auto-mark on save based on existing template answers
q = self.question
if q.normal != self.normal:
self.callstate = UserAnswer.CallStateOptions.OVERCALL if q.normal else UserAnswer.CallStateOptions.UNDERCALL
self.score = Answer.MarkOptions.INCORRECT
elif q.normal and self.normal:
self.callstate = UserAnswer.CallStateOptions.CORRECT
self.score = Answer.MarkOptions.CORRECT
else:
ans = self.answer_compare
if ans == "":
self.score = Answer.MarkOptions.INCORRECT
self.callstate = UserAnswer.CallStateOptions.INCORRECTCALL
else:
marked_ans = Answer.objects.filter(question_id=self.question_id, answer_compare__iexact=ans).first()
if marked_ans is not None:
self.score = marked_ans.status
if marked_ans.status == Answer.MarkOptions.CORRECT:
self.callstate = UserAnswer.CallStateOptions.CORRECT
elif marked_ans.status == Answer.MarkOptions.HALF_MARK:
self.callstate = UserAnswer.CallStateOptions.PARTIALCALL
elif marked_ans.status == Answer.MarkOptions.INCORRECT:
self.callstate = UserAnswer.CallStateOptions.INCORRECTCALL
else:
self.score = Answer.MarkOptions.UNMARKED
self.callstate = None
return super(UserAnswer, self).save(*args, **kwargs) return super(UserAnswer, self).save(*args, **kwargs)
def clean(self): def clean(self):
if not self.score:
self.score = Answer.MarkOptions.UNMARKED self.score = Answer.MarkOptions.UNMARKED
if self.answer: if self.answer:
self.answer = self.answer.strip() self.answer = self.answer.strip()
@@ -855,66 +906,67 @@ class UserAnswer(UserAnswerBase):
return self.callstate return self.callstate
def get_answer_score(self, cached=True): def get_answer_score(self, cached=True):
if cached and self.score: if cached and self.score and self.score != Answer.MarkOptions.UNMARKED:
print("CACHED")
if self.score == Answer.MarkOptions.CORRECT: if self.score == Answer.MarkOptions.CORRECT:
mark = 2 return 2
elif self.score == Answer.MarkOptions.HALF_MARK: elif self.score == Answer.MarkOptions.HALF_MARK:
mark = 1 return 1
elif self.score == Answer.MarkOptions.INCORRECT: elif self.score == Answer.MarkOptions.INCORRECT:
mark = 0 return 0
return mark
print("NOT CACHED")
q = self.question q = self.question
# First step we check that the normal/abnormal states match # First step we check that the normal/abnormal states match
if q.normal != self.normal: if q.normal != self.normal:
if q.normal: new_callstate = UserAnswer.CallStateOptions.OVERCALL if q.normal else UserAnswer.CallStateOptions.UNDERCALL
self.callstate = UserAnswer.CallStateOptions.OVERCALL if self.score != Answer.MarkOptions.INCORRECT or self.callstate != new_callstate:
else:
self.callstate = UserAnswer.CallStateOptions.UNDERCALL
self.score = Answer.MarkOptions.INCORRECT self.score = Answer.MarkOptions.INCORRECT
self.callstate = new_callstate
self.save() self.save()
# If they don't match the answer is wrong (score is 0)
return 0 return 0
# If both are normal full marks
elif q.normal and self.normal: elif q.normal and self.normal:
self.callstate = UserAnswer.CallStateOptions.CORRECT if self.score != Answer.MarkOptions.CORRECT or self.callstate != UserAnswer.CallStateOptions.CORRECT:
self.score = Answer.MarkOptions.CORRECT self.score = Answer.MarkOptions.CORRECT
self.callstate = UserAnswer.CallStateOptions.CORRECT
self.save() self.save()
return 2 return 2
# Then compare answer strings (as per anatomy questions)
ans = self.answer_compare ans = self.answer_compare
if ans == "": if ans == "":
self.score == Answer.MarkOptions.INCORRECT if self.score != Answer.MarkOptions.INCORRECT or self.callstate != UserAnswer.CallStateOptions.INCORRECTCALL:
self.score = Answer.MarkOptions.INCORRECT
self.callstate = UserAnswer.CallStateOptions.INCORRECTCALL
self.save() self.save()
return 0 return 0
# TODO: this should be cleaned up as we don't want duplicates... marked_ans = None
# try: for a in q.answers.all():
# marked_ans = q.answers.get(answer_compare__iexact=ans) if a.answer_compare.lower() == ans.lower():
# except Answer.DoesNotExist: marked_ans = a
# marked_ans = None break
marked_ans = q.answers.filter(answer_compare__iexact=ans).first()
mark = "unmarked" mark = "unmarked"
if marked_ans is not None: if marked_ans is not None:
self.score = marked_ans.status new_callstate = UserAnswer.CallStateOptions.INCORRECTCALL
if marked_ans.status == Answer.MarkOptions.CORRECT: if marked_ans.status == Answer.MarkOptions.CORRECT:
self.callstate = UserAnswer.CallStateOptions.CORRECT new_callstate = UserAnswer.CallStateOptions.CORRECT
mark = 2 mark = 2
elif marked_ans.status == Answer.MarkOptions.HALF_MARK: elif marked_ans.status == Answer.MarkOptions.HALF_MARK:
self.callstate = UserAnswer.CallStateOptions.PARTIALCALL new_callstate = UserAnswer.CallStateOptions.PARTIALCALL
mark = 1 mark = 1
elif marked_ans.status == Answer.MarkOptions.INCORRECT: elif marked_ans.status == Answer.MarkOptions.INCORRECT:
self.callstate = UserAnswer.CallStateOptions.INCORRECTCALL new_callstate = UserAnswer.CallStateOptions.INCORRECTCALL
mark = 0 mark = 0
if self.score != marked_ans.status or self.callstate != new_callstate:
self.score = marked_ans.status
self.callstate = new_callstate
self.save()
else:
if self.score != Answer.MarkOptions.UNMARKED or self.callstate is not None:
self.score = Answer.MarkOptions.UNMARKED
self.callstate = None
self.save() self.save()
return mark return mark
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends 'generic/exam_scores_base.html' %} {% extends base_template|default:'generic/exam_scores_base.html' %}
{% block table_answers %} {% block table_answers %}
{% for question in questions %} {% for question in questions %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends 'generic/exam_scores_base.html' %} {% extends base_template|default:'generic/exam_scores_base.html' %}
{% block table_answers %} {% block table_answers %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends 'generic/exam_scores_base.html' %} {% extends base_template|default:'generic/exam_scores_base.html' %}
{% block table_answers %} {% block table_answers %}
{% for question in questions %} {% for question in questions %}
+6 -2
View File
@@ -1,3 +1,7 @@
We need to rework the scores pages for all of the app exams. we need to try to make sure that we keep all of the content that currently exists but display it in a more usable format. at the same time we need to optimise the request as for some of the app with large number of users this results in a huge number of sql queries. it may be sensible to split some of the page content into different sections that can be loaded via htmx partials (so that they are also reusable elsewhere). Remove the cache links such as "refresh the scores" if they are no longer required.
At the same time we need to have a look at how the scores are calculated, some of the apps currently use exam caching. if possible we want to avoid this, if not we want it to be fully transparent to the end user. Load the stats panels as htmx partials. please also add additional stats that would be useful and style it more nicely.
the anatomy app features a new marking mode mark2. we want to port a similar new marking mode to the rapids app. it should be styled in a similar way to the mark2 mode in anatomy and bring across any new features (that are applicable)
Both the candidate list and the answers table should allow filtering by candidates to allow them to be quickly viewed.