diff --git a/aleksis/apps/alsijil/data_checks.py b/aleksis/apps/alsijil/data_checks.py
new file mode 100644
index 0000000000000000000000000000000000000000..98decd9aaa5cfac0ace215146ab9740a19a1927b
--- /dev/null
+++ b/aleksis/apps/alsijil/data_checks.py
@@ -0,0 +1,189 @@
+import logging
+
+from django.db.models import F
+from django.db.models.expressions import ExpressionWrapper, Func, Value
+from django.db.models.fields import DateField
+from django.db.models.functions import Concat
+from django.db.models.query_utils import Q
+from django.utils.translation import gettext as _
+
+from aleksis.core.data_checks import DataCheck, IgnoreSolveOption, SolveOption
+
+
+class DeleteRelatedObjectSolveOption(SolveOption):
+    name = "delete"
+    verbose_name = _("Delete object")
+
+    @classmethod
+    def solve(cls, check_result: "DataCheckResult"):
+        check_result.related_object.delete()
+        check_result.delete()
+
+
+class SetGroupsWithCurrentGroupsSolveOption(SolveOption):
+    name = "set_groups_of_person"
+    verbose_name = _("Set current groups")
+
+    @classmethod
+    def solve(cls, check_result: "DataCheckResult"):
+        person = check_result.related_object.person
+        check_result.related_object.groups_of_person.set(person.member_of.all())
+        check_result.delete()
+
+
+class ResetPersonalNoteSolveOption(SolveOption):
+    name = "reset_personal_note"
+    verbose_name = _("Reset personal note to defaults")
+
+    @classmethod
+    def solve(cls, check_result: "DataCheckResult"):
+        note = check_result.related_object
+        note.reset_values()
+        note.save()
+        check_result.delete()
+
+
+class NoPersonalNotesInCancelledLessonsDataCheck(DataCheck):
+    name = "no_personal_notes_in_cancelled_lessons"
+    verbose_name = _("Ensure that there are no personal notes in cancelled lessons")
+    problem_name = _("The personal note is related to a cancelled lesson.")
+    solve_options = {
+        DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption,
+        IgnoreSolveOption.name: IgnoreSolveOption,
+    }
+
+    @classmethod
+    def check_data(cls):
+        from .models import PersonalNote
+
+        personal_notes = PersonalNote.objects.filter(
+            lesson_period__substitutions__cancelled=True,
+            lesson_period__substitutions__week=F("week"),
+            lesson_period__substitutions__year=F("year"),
+        ).prefetch_related("lesson_period", "lesson_period__substitutions")
+
+        for note in personal_notes:
+            logging.info(f"Check personal note {note}")
+            cls.register_result(note)
+
+
+class NoGroupsOfPersonsSetInPersonalNotesDataCheck(DataCheck):
+    name = "no_groups_of_persons_set_in_personal_notes"
+    verbose_name = _("Ensure that 'groups_of_person' is set for every personal note")
+    problem_name = _("The personal note has no group in 'groups_of_person'.")
+    solve_options = {
+        SetGroupsWithCurrentGroupsSolveOption.name: SetGroupsWithCurrentGroupsSolveOption,
+        DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption,
+        IgnoreSolveOption.name: IgnoreSolveOption,
+    }
+
+    @classmethod
+    def check_data(cls):
+        from .models import PersonalNote
+
+        personal_notes = PersonalNote.objects.filter(groups_of_person__isnull=True)
+
+        for note in personal_notes:
+            logging.info(f"Check personal note {note}")
+            cls.register_result(note)
+
+
+weekday_to_date = ExpressionWrapper(
+    Func(
+        Concat(F("year"), F("week")), Value("IYYYIW"), output_field=DateField(), function="TO_DATE"
+    )
+    + F("lesson_period__period__weekday"),
+    output_field=DateField(),
+)
+
+
+class LessonDocumentationOnHolidaysDataCheck(DataCheck):
+    """Checks for lesson documentation objects on holidays.
+
+    This ignores empty lesson documentation as they are created by default.
+    """
+
+    name = "lesson_documentation_on_holidays"
+    verbose_name = _("Ensure that there are no filled out lesson documentations on holidays")
+    problem_name = _("The lesson documentation is on holidays.")
+    solve_options = {
+        DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption,
+        IgnoreSolveOption.name: IgnoreSolveOption,
+    }
+
+    @classmethod
+    def check_data(cls):
+        from aleksis.apps.chronos.models import Holiday
+
+        from .models import LessonDocumentation
+
+        holidays = Holiday.objects.all()
+
+        documentations = LessonDocumentation.objects.filter(
+            ~Q(topic="") | ~Q(group_note="") | ~Q(homework="")
+        ).annotate(actual_date=weekday_to_date)
+
+        q = Q()
+        for holiday in holidays:
+            q = q | Q(actual_date__gte=holiday.date_start, actual_date__lte=holiday.date_end)
+        documentations = documentations.filter(q)
+
+        for doc in documentations:
+            logging.info(f"Lesson documentation {doc} is on holidays")
+            cls.register_result(doc)
+
+
+class PersonalNoteOnHolidaysDataCheck(DataCheck):
+    """Checks for personal note objects on holidays.
+
+    This ignores empty personal notes as they are created by default.
+    """
+
+    name = "personal_note_on_holidays"
+    verbose_name = _("Ensure that there are no filled out personal notes on holidays")
+    problem_name = _("The personal note is on holidays.")
+    solve_options = {
+        DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption,
+        IgnoreSolveOption.name: IgnoreSolveOption,
+    }
+
+    @classmethod
+    def check_data(cls):
+        from aleksis.apps.chronos.models import Holiday
+
+        from .models import PersonalNote
+
+        holidays = Holiday.objects.all()
+
+        personal_notes = PersonalNote.objects.filter(
+            ~Q(remarks="") | Q(absent=True) | ~Q(late=0) | Q(extra_marks__isnull=False)
+        ).annotate(actual_date=weekday_to_date)
+
+        q = Q()
+        for holiday in holidays:
+            q = q | Q(actual_date__gte=holiday.date_start, actual_date__lte=holiday.date_end)
+        personal_notes = personal_notes.filter(q)
+
+        for note in personal_notes:
+            logging.info(f"Personal note {note} is on holidays")
+            cls.register_result(note)
+
+
+class ExcusesWithoutAbsences(DataCheck):
+    name = "excuses_without_absences"
+    verbose_name = _("Ensure that there are no excused personal notes without an absence")
+    problem_name = _("The personal note is marked as excused, but not as absent.")
+    solve_options = {
+        ResetPersonalNoteSolveOption.name: ResetPersonalNoteSolveOption,
+        IgnoreSolveOption.name: IgnoreSolveOption,
+    }
+
+    @classmethod
+    def check_data(cls):
+        from .models import PersonalNote
+
+        personal_notes = PersonalNote.objects.filter(excused=True, absent=False)
+
+        for note in personal_notes:
+            logging.info(f"Check personal note {note}")
+            cls.register_result(note)
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 0129ee4bcd6aceee29059c3c2df9e0d06ce05678..dc92c009fcb7fa2e40d90e66f0878bdee552b61e 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -1,9 +1,17 @@
 from django.db import models
+from django.urls import reverse
 from django.utils.formats import date_format
 from django.utils.translation import gettext_lazy as _
 
 from calendarweek import CalendarWeek
 
+from aleksis.apps.alsijil.data_checks import (
+    ExcusesWithoutAbsences,
+    LessonDocumentationOnHolidaysDataCheck,
+    NoGroupsOfPersonsSetInPersonalNotesDataCheck,
+    NoPersonalNotesInCancelledLessonsDataCheck,
+    PersonalNoteOnHolidaysDataCheck,
+)
 from aleksis.apps.alsijil.managers import PersonalNoteManager
 from aleksis.apps.chronos.mixins import WeekRelatedMixin
 from aleksis.apps.chronos.models import LessonPeriod
@@ -45,6 +53,13 @@ class PersonalNote(ExtensibleModel, WeekRelatedMixin):
     and remarks about a student in a single lesson period.
     """
 
+    data_checks = [
+        NoPersonalNotesInCancelledLessonsDataCheck,
+        NoGroupsOfPersonsSetInPersonalNotesDataCheck,
+        PersonalNoteOnHolidaysDataCheck,
+        ExcusesWithoutAbsences,
+    ]
+
     objects = PersonalNoteManager()
 
     person = models.ForeignKey("core.Person", models.CASCADE, related_name="personal_notes")
@@ -97,6 +112,14 @@ class PersonalNote(ExtensibleModel, WeekRelatedMixin):
     def __str__(self):
         return f"{date_format(self.date)}, {self.lesson_period}, {self.person}"
 
+    def get_absolute_url(self):
+        return (
+            reverse(
+                "lesson_by_week_and_period", args=[self.year, self.week, self.lesson_period.pk],
+            )
+            + "#personal-notes"
+        )
+
     class Meta:
         verbose_name = _("Personal note")
         verbose_name_plural = _("Personal notes")
@@ -117,6 +140,8 @@ class LessonDocumentation(ExtensibleModel, WeekRelatedMixin):
     Non-personal, includes the topic and homework of the lesson.
     """
 
+    data_checks = [LessonDocumentationOnHolidaysDataCheck]
+
     week = models.IntegerField()
     year = models.IntegerField(verbose_name=_("Year"), default=get_current_year)
 
@@ -160,6 +185,14 @@ class LessonDocumentation(ExtensibleModel, WeekRelatedMixin):
             if changed:
                 lesson_documentation.save()
 
+    def __str__(self):
+        return f"{self.lesson_period}, {date_format(self.date)}"
+
+    def get_absolute_url(self):
+        return reverse(
+            "lesson_by_week_and_period", args=[self.year, self.week, self.lesson_period.pk],
+        )
+
     def save(self, *args, **kwargs):
         if get_site_preferences()["alsijil__carry_over"] and (
             self.topic or self.homework or self.group_note