diff --git a/aleksis/apps/alsijil/migrations/0023_add_tardiness_and_rework_constraints.py b/aleksis/apps/alsijil/migrations/0023_add_tardiness_and_rework_constraints.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f59e274474385145966806b8c776565d015432f
--- /dev/null
+++ b/aleksis/apps/alsijil/migrations/0023_add_tardiness_and_rework_constraints.py
@@ -0,0 +1,96 @@
+# Generated by Django 4.2.10 on 2024-06-18 09:44
+# Updated for more custom logic
+
+from django.db import migrations, models
+
+
+def forwards__one_value_per_note(apps, schema_editor):
+    # We get the model from the versioned app registry;
+    # if we directly import it, it'll be the wrong version
+    NewPersonalNote = apps.get_model("alsijil", "NewPersonalNote")  # noqa
+    db_alias = schema_editor.connection.alias
+
+    NewPersonalNote.objects.using(db_alias).filter(note="").update(note=None)
+
+
+def reverse__one_value_per_note(apps, schema_editor):
+    NewPersonalNote = apps.get_model("alsijil", "NewPersonalNote")  # noqa
+    db_alias = schema_editor.connection.alias
+
+    NewPersonalNote.objects.using(db_alias).filter(note=None).update(note="")
+
+
+def forwards__unique_extra_mark_documentation(apps, schema_editor):
+    NewPersonalNote = apps.get_model("alsijil", "NewPersonalNote")  # noqa
+    db_alias = schema_editor.connection.alias
+
+    duplicates = (NewPersonalNote.objects.using(db_alias)
+                  .values("documentation", "extra_mark", "person")
+                  .annotate(count=models.Count("id"))
+                  .filter(count__gt=1, extra_mark__isnull=False))
+
+    # Iterate over duplicates and delete the extra instances
+    for duplicate in duplicates:
+        pks = (NewPersonalNote
+               .objects
+               .using(db_alias)
+               .filter(person=duplicate["person"], documentation=duplicate["documentation"], extra_mark=duplicate["extra_mark"])
+               .values_list("pk", flat=True)
+               )[1:]
+        NewPersonalNote.objects.using(db_alias).filter(pk__in=pks).delete()
+
+def reverse__unique_extra_mark_documentation(apps, schema_editor):
+    # Nothing to do, we cannot bring back the deleted objects, but they were duplicate data, so they are not needed anyway.
+    pass
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('alsijil', '0022_documentation_participation_touched_at'),
+    ]
+
+    operations = [
+        migrations.RemoveConstraint(
+            model_name='newpersonalnote',
+            name='unique_absence_per_documentation',
+        ),
+        migrations.AddField(
+            model_name='newpersonalnote',
+            name='tardiness',
+            field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Tardiness'),
+        ),
+        migrations.AlterField(
+            model_name='newpersonalnote',
+            name='note',
+            field=models.TextField(blank=True, null=True, verbose_name='Note'),
+        ),
+        migrations.RunPython(forwards__one_value_per_note, reverse__one_value_per_note),
+        migrations.AddConstraint(
+            model_name='newpersonalnote',
+            constraint=models.CheckConstraint(
+                check=models.Q(
+                    models.Q(('extra_mark__isnull', True), ('note__isnull', False), ('tardiness__isnull', True)),
+                    models.Q(('extra_mark__isnull', False), ('note__isnull', True), ('tardiness__isnull', True)),
+                    models.Q(('extra_mark__isnull', True), ('note__isnull', True), ('tardiness__isnull', False)),
+                    _connector='OR'
+                ),
+                name='one_value_per_personal_note'
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='newpersonalnote',
+            constraint=models.UniqueConstraint(
+                condition=models.Q(('tardiness', None), _negated=True),
+                fields=('person', 'documentation', 'tardiness'),
+                name='unique_person_documentation_tardiness'
+            ),
+        ),
+        migrations.RunPython(forwards__unique_extra_mark_documentation, reverse__unique_extra_mark_documentation),
+        migrations.AddConstraint(
+            model_name='newpersonalnote',
+            constraint=models.UniqueConstraint(
+                condition=models.Q(('extra_mark', None), _negated=True),
+                fields=('person', 'documentation', 'extra_mark'),
+                name='unique_person_documentation_extra_mark'
+            ),
+        ),
+    ]
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 9e60596c343c6917af6894db3272b38a4d591372..73c3c6b43a5cae0021b36a539684601b9ec68410 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -828,7 +828,7 @@ class NewPersonalNote(ExtensibleModel):
         null=True,
     )
 
-    note = models.TextField(blank=True, verbose_name=_("Note"))
+    note = models.TextField(blank=True, null=True, verbose_name=_("Note"))
     extra_mark = models.ForeignKey(
         ExtraMark, on_delete=models.PROTECT, blank=True, null=True, verbose_name=_("Extra Mark")
     )
@@ -842,8 +842,20 @@ class NewPersonalNote(ExtensibleModel):
         verbose_name_plural = _("Personal Notes")
         constraints = [
             models.CheckConstraint(
-                check=~Q(note="") | Q(extra_mark__isnull=False),
-                name="unique_absence_per_documentation",
+                check=Q(note__isnull=False, extra_mark__isnull=True, tardiness__isnull=True) |
+                      Q(note__isnull=True, extra_mark__isnull=False, tardiness__isnull=True) |
+                      Q(note__isnull=True, extra_mark__isnull=True, tardiness__isnull=False),
+                name="one_value_per_personal_note",
+            ),
+            models.UniqueConstraint(
+                fields=["person", "documentation", "tardiness"],
+                name="unique_person_documentation_tardiness",
+                condition=~Q(tardiness=None)
+            ),
+            models.UniqueConstraint(
+                fields=["person", "documentation", "extra_mark"],
+                name="unique_person_documentation_extra_mark",
+                condition=~Q(extra_mark=None)
             ),
         ]