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