diff --git a/TODO.md b/TODO.md
index 80de9cb2..bd23675e 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,6 +1,9 @@
# Current target
-- User management
-- SQUASH MIGRATIONS
+- Exam dates
+- Exam groups (and compare scores)
+- Exam results reviewing
+
+
# Ongoing
diff --git a/anatomy/migrations/0005_exam_stats_graph.py b/anatomy/migrations/0005_exam_stats_graph.py
new file mode 100644
index 00000000..11afddfa
--- /dev/null
+++ b/anatomy/migrations/0005_exam_stats_graph.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2024-01-08 22:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('anatomy', '0004_exam_candidates_only_alter_exam_active_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='exam',
+ name='stats_graph',
+ field=models.TextField(default=0),
+ ),
+ ]
diff --git a/generic/models.py b/generic/models.py
index 802d9fe9..3a061dad 100644
--- a/generic/models.py
+++ b/generic/models.py
@@ -759,7 +759,7 @@ class ExamBase(ExamCollectionGenericBase):
stats_max_possible = models.FloatField(default=0)
- # stats_graph = models.TextField(default=0)
+ stats_graph = models.TextField(default=0)
user_scores = models.JSONField(default=dict)
@@ -868,29 +868,50 @@ class ExamBase(ExamCollectionGenericBase):
stats = self.get_exam_stats(name=False)
exam_text.append(f"Stats\n{'-'*len('Stats')}\n{stats}")
+ rapids_extra = ""
if self.app_name == "rapids":
- exam_text.append(f"Answers breakdown: {callstate}")
+ rapids_extra = f"Answers breakdown: {callstate}"
+ exam_text.append(rapids_extra)
msg = "\n\n".join(exam_text)
- return msg
+ html_msg = f"""
+
Candidate: {user.first_name} [{user.email}]
+ {exam_name}
+
+ Results
+ Score: {user_score} / {int(self.stats_max_possible)} ({percentage})
+
+ Stats
+ {stats}
+ View all stats (including graph) here
+
+"""
+ if self.app_name == "rapids":
+ html_msg =f"{html_msg}
{rapids_extra}"
+
+ return msg, html_msg
def email_user_results(self, user, resend=False, additional_emails=None):
# if self.results_email_sent and not resend:
# return False, "Already sent."
# Get a list of taken exams
- msg = self.generate_user_report(user)
+ msg, html_msg = self.generate_user_report(user)
if not user.email:
return [False, "User has no email"]
emails = [user.email]
- supervisor_email = user.userprofile.supervisor.email
+ extra = ""
- if supervisor_email:
+ if user.userprofile.supervisor is not None:
+ supervisor_email = user.userprofile.supervisor.email
emails.append(supervisor_email)
+ else:
+ extra = "No supervisor"
+
if additional_emails is not None:
emails.extend(additional_emails)
@@ -902,14 +923,14 @@ class ExamBase(ExamCollectionGenericBase):
"no-reply@penracourses.org.uk",
emails,
fail_silently=False,
- # html_message=html_msg,
+ html_message=html_msg,
)
except SMTPException as e:
return [False, e]
# self.results_email_sent = True
# self.save()
- return [True, ""]
+ return [True, extra]
class ExamUserStatus(models.Model):
diff --git a/generic/urls.py b/generic/urls.py
index e83be7ee..24136abd 100755
--- a/generic/urls.py
+++ b/generic/urls.py
@@ -225,6 +225,11 @@ def generic_exam_urls(generic_exam_view: GenericExamViews):
generic_exam_view.exam_scores_all,
name="exam_scores_all",
),
+ path(
+ "exam//stats",
+ generic_exam_view.exam_stats,
+ name="exam_stats",
+ ),
path(
"exam//scores/refresh",
generic_exam_view.exam_scores_refresh,
diff --git a/generic/views.py b/generic/views.py
index 06be6347..999b14e6 100644
--- a/generic/views.py
+++ b/generic/views.py
@@ -2162,7 +2162,7 @@ class ExamViews(View, LoginRequiredMixin):
exam.stats_min = min(user_scores_list)
exam.stats_max = max(user_scores_list)
- # exam.stats_graph = fig_html
+ exam.stats_graph = fig_html
user_data = {}
for u in cids:
@@ -2225,6 +2225,22 @@ class ExamViews(View, LoginRequiredMixin):
template_variables,
)
+ def exam_stats(self, request, exam_id):
+ exam = get_object_or_404(self.Exam, pk=exam_id)
+
+ if not exam.exam_mode:
+ raise Http404("Packet not in exam mode")
+
+ template_variables = {
+ "exam": exam
+ }
+
+ return render(
+ request,
+ "exam_stats.html",
+ template_variables,
+ )
+
class GenericViewBase:
def __init__(self, app_name, QuestionObject, UserAnswerObject, ExamObject):
diff --git a/longs/migrations/0014_exam_stats_graph.py b/longs/migrations/0014_exam_stats_graph.py
new file mode 100644
index 00000000..63eed0dc
--- /dev/null
+++ b/longs/migrations/0014_exam_stats_graph.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2024-01-08 22:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('longs', '0013_alter_longseriesimage_image_blake3_hash'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='exam',
+ name='stats_graph',
+ field=models.TextField(default=0),
+ ),
+ ]
diff --git a/physics/migrations/0003_exam_stats_graph.py b/physics/migrations/0003_exam_stats_graph.py
new file mode 100644
index 00000000..23b2e8a2
--- /dev/null
+++ b/physics/migrations/0003_exam_stats_graph.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2024-01-08 22:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('physics', '0002_exam_candidates_only_alter_exam_active_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='exam',
+ name='stats_graph',
+ field=models.TextField(default=0),
+ ),
+ ]
diff --git a/rapids/migrations/0003_exam_stats_graph.py b/rapids/migrations/0003_exam_stats_graph.py
new file mode 100644
index 00000000..7d973f3e
--- /dev/null
+++ b/rapids/migrations/0003_exam_stats_graph.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2024-01-08 22:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('rapids', '0002_exam_candidates_only_alter_exam_active_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='exam',
+ name='stats_graph',
+ field=models.TextField(default=0),
+ ),
+ ]
diff --git a/sbas/migrations/0004_exam_stats_graph.py b/sbas/migrations/0004_exam_stats_graph.py
new file mode 100644
index 00000000..181b620c
--- /dev/null
+++ b/sbas/migrations/0004_exam_stats_graph.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2024-01-08 22:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('sbas', '0003_alter_useranswer_answer'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='exam',
+ name='stats_graph',
+ field=models.TextField(default=0),
+ ),
+ ]
diff --git a/templates/exam_stats.html b/templates/exam_stats.html
new file mode 100644
index 00000000..74dd836a
--- /dev/null
+++ b/templates/exam_stats.html
@@ -0,0 +1,20 @@
+{% extends 'base.html' %}
+
+{% block content %}
+ {{exam.name}}
+
+
+ {% if exam.stats_candidates < 4 %}
+ Not enough data points...
+ {% else %}
+
+ {{exam.stats_graph|safe}}
+
+ Questions: {{question_number}}
+ Available marks: {{exam_stats_max_possible}}
+ Mean: {{exam.stats_mean}}, Median {{exam.stats_median}}, Mode {{exam.stats_mode}}
+ Top score: {{exam.stats_max}}
+
+ Note: scores < 10% of the total score are excluded from these statistics
+ {% endif %}
+{% endblock %}
\ No newline at end of file