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
+53 -34
View File
@@ -338,7 +338,21 @@ class Answer(models.Model):
def save(self, *args, **kwargs):
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):
if self.answer:
@@ -551,11 +565,30 @@ class UserAnswer(UserAnswerBase):
def save(self, *args, **kwargs):
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)
def clean(self):
if self.answer:
self.answer = self.answer.strip()
self.answer_compare = get_answer_compare(self.answer)
else:
self.answer = ""
self.answer_compare = ""
# def get_compare_string(self):
# # strip here should be unneccasry (providing clean is now working)
@@ -569,30 +602,34 @@ class UserAnswer(UserAnswerBase):
return self.answer
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:
mark = 2
return 2
elif self.score == Answer.MarkOptions.HALF_MARK:
mark = 1
return 1
elif self.score == Answer.MarkOptions.INCORRECT:
mark = 0
return mark
return 0
q = self.question
ans = self.answer_compare
if ans == "":
self.score == Answer.MarkOptions.INCORRECT
self.save()
if self.score != Answer.MarkOptions.INCORRECT:
self.score = Answer.MarkOptions.INCORRECT
self.save()
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"
if marked_ans is not None:
self.score = marked_ans.status
self.save()
if self.score != marked_ans.status:
self.score = marked_ans.status
self.save()
if marked_ans.status == Answer.MarkOptions.CORRECT:
mark = 2
@@ -600,27 +637,9 @@ class UserAnswer(UserAnswerBase):
mark = 1
elif marked_ans.status == Answer.MarkOptions.INCORRECT:
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:
mark = "unmarked"
if self.score != Answer.MarkOptions.UNMARKED:
self.score = Answer.MarkOptions.UNMARKED
self.save()
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 %}
{% for question in questions %}
+11 -49
View File
@@ -85,57 +85,19 @@
<div class="d-flex justify-content-between align-items-center mb-1">
<h5 class="mb-0">Answers as a table</h5>
<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 class="collapse show" id="answers-table">
<div class="table-responsive">
<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 %}
{% comment %} <th><a href="{% url exam.app_name|add:':exam_scores_cid_user' exam.pk cid %}">{{cid}}</a></th> {% endcomment %}
<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 %}
{% 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 class="collapse show" id="answers-table-container">
<div hx-get="{% url exam.app_name|add:':exam_scores_all' exam.pk %}?table_only=true"
hx-trigger="load"
hx-swap="outerHTML">
<div class="text-center py-4">
<div class="spinner-border text-info" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<div class="text-muted mt-2">Loading answers 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>
+95 -121
View File
@@ -3796,6 +3796,9 @@ class ExamViews(View, LoginRequiredMixin):
else:
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
user_answers_map = {ua.question_id: ua for ua in user_answers_qs}
@@ -3927,19 +3930,7 @@ class ExamViews(View, LoginRequiredMixin):
return max_score
def exam_scores_all(self, request, pk):
"""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_
"""
"""The exam scores pages. Displays all user scores in a tabular format."""
exam: ExamBase = get_object_or_404(self.Exam, pk=pk)
if not exam.exam_mode:
@@ -3949,21 +3940,17 @@ class ExamViews(View, LoginRequiredMixin):
if request.user not in exam.author.all():
raise PermissionDenied
# user_answers_and_marks = defaultdict(list)
user_answers_marks = defaultdict(list)
# user_answers = defaultdict(list)
# user_names = {}
# cid_passcodes = {}
table_only = request.GET.get("table_only") == "true" or request.headers.get("HX-Target") == "answers-table-container"
user_answers_marks = defaultdict(list)
by_question = defaultdict(dict)
unmarked = set()
cached_scores = 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))
if self.app_name in ("rapids"):
if self.app_name in ("rapids",):
user_answers_callstates = defaultdict(list)
user_answers_callstates_counted = {}
@@ -3977,34 +3964,31 @@ class ExamViews(View, LoginRequiredMixin):
questions = list(questions_qs)
question_index_map = {q: i for i, q in enumerate(questions)}
# We could prefetch the UserAnswers.answers here (if we didn't cache them)
# Also select_related the user to avoid per-answer user lookups in the loop
cid_user_answers = (
# Fetch and prefetch the UserAnswers
cid_user_answers_qs = (
self.UserAnswer.objects.select_related("question", "user")
.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)
cids = set()
plain_cids = set()
user_ids = set()
cids_user_id_map = {}
# Loop through all candidates
for cid_user_answer in cid_user_answers:
# Convoluted (probably...)
if cid_user_answer.user is None:
cid = f"c/{cid_user_answer.cid}"
cids_user_id_map[cid] = cid
# cid_passcodes[cid] = cid_user_answer.passcode
cids.add(cid)
plain_cids.add(cid_user_answer.cid)
else:
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}"
if not name.strip():
name = cid_user_answer.user.username
@@ -4013,53 +3997,41 @@ class ExamViews(View, LoginRequiredMixin):
user_ids.add(cid_user_answer.user.pk)
s = cid_user_answer
# user_names[cid] = cid
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()
if answer_score == "unmarked":
# Use precomputed map to avoid repeated scanning/indexing of questions
index = question_index_map.get(q)
if index is not None:
unmarked.add(index)
# user_answers[cid].append(ans)
user_answers_marks[cid].append(answer_score)
if self.app_name == "rapids":
callstate = s.get_answer_callstate()
by_question[q][cid] = (ans, answer_score, callstate)
user_answers_callstates[cid].append(callstate)
elif self.app_name in ("anatomy", "sbas", "longs", "shorts"):
by_question[q][cid] = (ans, answer_score)
else:
zipped_ans_scores = zip(ans, answer_score)
by_question[q][cid] = zipped_ans_scores
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"):
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_normalised = {}
user_answer_count = {}
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":
for question in questions:
# If either question or user do not exist we give a default
# 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)
missing_count = question_number - len(user_answers_marks[user])
for _ in range(missing_count):
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",):
user_scores[user] = sum(
@@ -4081,69 +4053,76 @@ class ExamViews(View, LoginRequiredMixin):
# ignore scores of 0 for stats
user_scores_list = [i for i in user_scores.values() if i > 0]
max_score = self.get_max_score(questions)
if len(user_scores_list) < 1:
mean = 0
median = 0
mode = 0
fig_html = ""
mean = 0
median = 0
mode = 0
fig_html = ""
if not table_only:
if len(user_scores_list) < 1:
mean = 0
median = 0
mode = 0
fig_html = ""
else:
# Exclude very low scores from statistics
lower_score_bound = max_score / 5
bounded_scores = [i for i in user_scores_list if i > lower_score_bound]
if bounded_scores:
user_scores_list = bounded_scores
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()
exam.stats_mean = mean
exam.stats_median = median
exam.stats_mode = mode
exam.stats_candidates = len(user_scores_list)
exam.stats_max_possible = max_score
exam.stats_min = min(user_scores_list)
exam.stats_max = max(user_scores_list)
exam.stats_graph = fig_html
user_data = {}
for u in cids:
user_data[u] = {
"score": user_scores[u],
"normalised_score": user_scores_normalised[u],
}
if self.app_name == "rapids":
counted_callstates = dict(Counter(user_answers_callstates[u]))
user_data[u]["callstates"] = str(counted_callstates)
user_answers_callstates_counted[u] = counted_callstates
exam.user_scores = user_data
exam.save()
else:
# Exclude very low scores from statistics
lower_score_bound = max_score / 5
bounded_scores = [i for i in user_scores_list if i > lower_score_bound]
if bounded_scores:
user_scores_list = bounded_scores
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()
exam.stats_mean = mean
exam.stats_median = median
exam.stats_mode = mode
exam.stats_candidates = len(user_scores_list)
exam.stats_max_possible = max_score
exam.stats_min = min(user_scores_list)
exam.stats_max = max(user_scores_list)
exam.stats_graph = fig_html
user_data = {}
for u in cids:
# uid = cids_user_id_map[u]
user_data[u] = {
"score": user_scores[u],
"normalised_score": user_scores_normalised[u],
}
if self.app_name == "rapids":
counted_callstates = dict(Counter(user_answers_callstates[u]))
user_data[u]["callstates"] = str(counted_callstates)
user_answers_callstates_counted[u] = counted_callstates
exam.user_scores = user_data
exam.save()
# 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_users = []
if missing_user_ids:
missing_users = User.objects.filter(pk__in=missing_user_ids)
@@ -4152,19 +4131,13 @@ class ExamViews(View, LoginRequiredMixin):
"cids": sorted(cids),
"missing_cids": valid_cid_users - plain_cids,
"missing_users": missing_users,
# "cid_passcodes": cid_passcodes,
"exam": exam,
"unmarked": unmarked,
"questions": questions,
"question_number": question_number,
"by_question": by_question,
# "user_answers": dict(user_answers),
# "user_answers_marks": dict(user_answers_marks),
"user_scores": user_scores,
"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,
"cids_user_id_map": cids_user_id_map,
"max_score": max_score,
@@ -4174,6 +4147,7 @@ class ExamViews(View, LoginRequiredMixin):
"plot": fig_html,
"cached_scores": cached_scores,
"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":
+1 -2
View File
@@ -1,5 +1,4 @@
{% extends 'generic/exam_scores_base.html' %}
{% extends base_template|default:'generic/exam_scores_base.html' %}
{% block table_answers %}
{% 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 %}
{% for question in questions %}
+89 -37
View File
@@ -141,7 +141,30 @@ class Answer(models.Model):
def save(self, *args, **kwargs):
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):
if self.answer:
@@ -823,10 +846,38 @@ class UserAnswer(UserAnswerBase):
def save(self, *args, **kwargs):
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)
def clean(self):
self.score = Answer.MarkOptions.UNMARKED
if not self.score:
self.score = Answer.MarkOptions.UNMARKED
if self.answer:
self.answer = self.answer.strip()
self.answer_compare = get_answer_compare(self.answer)
@@ -855,66 +906,67 @@ class UserAnswer(UserAnswerBase):
return self.callstate
def get_answer_score(self, cached=True):
if cached and self.score:
print("CACHED")
if cached and self.score and self.score != Answer.MarkOptions.UNMARKED:
if self.score == Answer.MarkOptions.CORRECT:
mark = 2
return 2
elif self.score == Answer.MarkOptions.HALF_MARK:
mark = 1
return 1
elif self.score == Answer.MarkOptions.INCORRECT:
mark = 0
return mark
print("NOT CACHED")
return 0
q = self.question
# First step we check that the normal/abnormal states match
if q.normal != self.normal:
if q.normal:
self.callstate = UserAnswer.CallStateOptions.OVERCALL
else:
self.callstate = UserAnswer.CallStateOptions.UNDERCALL
self.score = Answer.MarkOptions.INCORRECT
self.save()
# If they don't match the answer is wrong (score is 0)
new_callstate = UserAnswer.CallStateOptions.OVERCALL if q.normal else UserAnswer.CallStateOptions.UNDERCALL
if self.score != Answer.MarkOptions.INCORRECT or self.callstate != new_callstate:
self.score = Answer.MarkOptions.INCORRECT
self.callstate = new_callstate
self.save()
return 0
# If both are normal full marks
elif q.normal and self.normal:
self.callstate = UserAnswer.CallStateOptions.CORRECT
self.score = Answer.MarkOptions.CORRECT
self.save()
if self.score != Answer.MarkOptions.CORRECT or self.callstate != UserAnswer.CallStateOptions.CORRECT:
self.score = Answer.MarkOptions.CORRECT
self.callstate = UserAnswer.CallStateOptions.CORRECT
self.save()
return 2
# Then compare answer strings (as per anatomy questions)
ans = self.answer_compare
if ans == "":
self.score == Answer.MarkOptions.INCORRECT
self.save()
if self.score != Answer.MarkOptions.INCORRECT or self.callstate != UserAnswer.CallStateOptions.INCORRECTCALL:
self.score = Answer.MarkOptions.INCORRECT
self.callstate = UserAnswer.CallStateOptions.INCORRECTCALL
self.save()
return 0
# TODO: this should be cleaned up as we don't want duplicates...
# try:
# marked_ans = q.answers.get(answer_compare__iexact=ans)
# except Answer.DoesNotExist:
# marked_ans = None
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"
if marked_ans is not None:
self.score = marked_ans.status
new_callstate = UserAnswer.CallStateOptions.INCORRECTCALL
if marked_ans.status == Answer.MarkOptions.CORRECT:
self.callstate = UserAnswer.CallStateOptions.CORRECT
new_callstate = UserAnswer.CallStateOptions.CORRECT
mark = 2
elif marked_ans.status == Answer.MarkOptions.HALF_MARK:
self.callstate = UserAnswer.CallStateOptions.PARTIALCALL
new_callstate = UserAnswer.CallStateOptions.PARTIALCALL
mark = 1
elif marked_ans.status == Answer.MarkOptions.INCORRECT:
self.callstate = UserAnswer.CallStateOptions.INCORRECTCALL
new_callstate = UserAnswer.CallStateOptions.INCORRECTCALL
mark = 0
self.save()
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()
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 %}
{% 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 %}
+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 %}
{% 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.