From bf344f20e1619796f4ae13069481918d9d8f623f Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 5 Jan 2026 13:47:45 +0000 Subject: [PATCH] Add flagging functionality for exam questions with a reusable Flag model and toggle feature --- generic/migrations/0031_flag.py | 32 ++++++ generic/models.py | 42 ++++++++ .../physics/partials/exam_flag_button.html | 17 ++++ .../physics/partials/exam_take_fragment.html | 93 +++++++++--------- physics/urls.py | 11 +++ physics/views.py | 98 ++++++++++++++++++- 6 files changed, 246 insertions(+), 47 deletions(-) create mode 100644 generic/migrations/0031_flag.py create mode 100644 physics/templates/physics/partials/exam_flag_button.html diff --git a/generic/migrations/0031_flag.py b/generic/migrations/0031_flag.py new file mode 100644 index 00000000..e2887c2d --- /dev/null +++ b/generic/migrations/0031_flag.py @@ -0,0 +1,32 @@ +# Generated by Django 5.2.7 on 2026-01-05 12:41 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('generic', '0030_ciduser_notes'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Flag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('note', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('cid_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='flags', to='generic.ciduser')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='flags', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('-created_at',), + }, + ), + ] diff --git a/generic/models.py b/generic/models.py index 6b973fb2..2110b507 100644 --- a/generic/models.py +++ b/generic/models.py @@ -1704,6 +1704,48 @@ class CidUserExam(models.Model): self.save() +class Flag(models.Model): + """A reusable flag model that can be attached to any object via a + GenericForeignKey. Useful for marking questions, items or other objects + across apps. + """ + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey("content_type", "object_id") + + # Optional actor who created the flag (either a user or a CID user) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="flags", + ) + cid_user = models.ForeignKey( + "CidUser", + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="flags", + ) + + note = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ("-created_at",) + + def __str__(self) -> str: + actor = None + if self.user: + actor = f"user:{self.user.username}" + elif self.cid_user: + actor = f"cid:{self.cid_user.cid}" + else: + actor = "anon" + return f"Flag [{actor}] -> {self.content_type}({self.object_id})" + + CID_GROUP_EXAMS = ( ("SBAs", "sba_cid_user_groups"), ("Physics", "physics_cid_user_groups"), diff --git a/physics/templates/physics/partials/exam_flag_button.html b/physics/templates/physics/partials/exam_flag_button.html new file mode 100644 index 00000000..e5d0cfec --- /dev/null +++ b/physics/templates/physics/partials/exam_flag_button.html @@ -0,0 +1,17 @@ +{% comment %}A small flag/unflag button partial. Re-rendered by HTMX when toggled.{% endcomment %} +
+ {% load static %} + {% if flagged %} +
+ {% csrf_token %} + + +
+ {% else %} +
+ {% csrf_token %} + + +
+ {% endif %} +
diff --git a/physics/templates/physics/partials/exam_take_fragment.html b/physics/templates/physics/partials/exam_take_fragment.html index bf696598..4a4286f8 100644 --- a/physics/templates/physics/partials/exam_take_fragment.html +++ b/physics/templates/physics/partials/exam_take_fragment.html @@ -55,6 +55,9 @@
+
+ {% include 'physics/partials/exam_flag_button.html' %} +
{% if previous > -1 %} {% endif %} @@ -294,59 +297,59 @@ })(); -