diff --git a/aleksis/apps/alsijil/data_checks.py b/aleksis/apps/alsijil/data_checks.py
index 679de00ee61914f8b4962a38cda5ba47f42a7bec..96f21af6acc4f3a4499a6b2944db78bd46af2e96 100644
--- a/aleksis/apps/alsijil/data_checks.py
+++ b/aleksis/apps/alsijil/data_checks.py
@@ -2,50 +2,11 @@ import logging
 
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import F
-from django.db.models.aggregates import Count
 from django.utils.translation import gettext as _
 
-import reversion
 from calendarweek import CalendarWeek
-from templated_email import send_templated_mail
 
-from aleksis.core.util.core_helpers import celery_optional, get_site_preferences
-
-
-class SolveOption:
-    name: str = "default"
-    verbose_name: str = ""
-
-    @classmethod
-    def solve(cls, check_result: "DataCheckResult"):
-        pass
-
-
-class IgnoreSolveOption(SolveOption):
-    name = "ignore"
-    verbose_name = _("Ignore problem")
-
-    @classmethod
-    def solve(cls, check_result: "DataCheckResult"):
-        check_result.solved = True
-        check_result.save()
-
-
-class DataCheck:
-    name: str = ""
-    verbose_name: str = ""
-    problem_name: str = ""
-
-    solve_options = {IgnoreSolveOption.name: IgnoreSolveOption}
-
-    @classmethod
-    def check_data(cls):
-        pass
-
-    @classmethod
-    def solve(cls, check_result: "DataCheckResult", solve_option: str = "default"):
-        with reversion.create_revision():
-            cls.solve_options[solve_option].solve(check_result)
+from aleksis.core.data_checks import DATA_CHECK_REGISTRY, DataCheck, IgnoreSolveOption, SolveOption
 
 
 class DeleteRelatedObjectSolveOption(SolveOption):
@@ -58,6 +19,7 @@ class DeleteRelatedObjectSolveOption(SolveOption):
         check_result.delete()
 
 
+@DATA_CHECK_REGISTRY.register
 class NoPersonalNotesInCancelledLessonsDataCheck(DataCheck):
     name = "no_personal_notes_in_cancelled_lessons"
     verbose_name = _("Ensure that there are no personal notes in cancelled lessons")
@@ -87,49 +49,3 @@ class NoPersonalNotesInCancelledLessonsDataCheck(DataCheck):
             result = DataCheckResult.objects.get_or_create(
                 check=cls.name, content_type=ct, object_id=note.id
             )
-
-
-DATA_CHECKS = [NoPersonalNotesInCancelledLessonsDataCheck]
-DATA_CHECKS_BY_NAME = {check.name: check for check in DATA_CHECKS}
-DATA_CHECKS_CHOICES = [(check.name, check.verbose_name) for check in DATA_CHECKS]
-
-
-@celery_optional
-def check_data():
-    for check in DATA_CHECKS:
-        logging.info(f"Run check: {check.verbose_name}")
-        check.check_data()
-
-    if get_site_preferences()["alsijil__data_checks_send_emails"]:
-        send_emails_for_data_checks()
-
-
-def send_emails_for_data_checks():
-    """Notify one or more recipients about new problems with data.
-
-    Recipients can be set in dynamic preferences.
-    """
-    from .models import DataCheckResult  # noqa
-
-    results = DataCheckResult.objects.filter(solved=False, sent=False)
-
-    if results.exists():
-        results_by_check = results.values("check").annotate(count=Count("check"))
-
-        results_with_checks = []
-        for result in results_by_check:
-            results_with_checks.append(
-                (DATA_CHECKS_BY_NAME[result["check"]], result["count"])
-            )
-
-        send_templated_mail(
-            template_name="data_checks",
-            from_email=get_site_preferences()["mail__address"],
-            recipient_list=[
-                p.email
-                for p in get_site_preferences()["alsijil__data_checks_recipients"]
-            ],
-            context={"results": results_with_checks},
-        )
-
-        results.update(sent=True)
diff --git a/aleksis/apps/alsijil/menus.py b/aleksis/apps/alsijil/menus.py
index f25429476ba2dfc783ad23a8725877a5b9e20633..f90052a765147b8555840d84f24203b12f09b248 100644
--- a/aleksis/apps/alsijil/menus.py
+++ b/aleksis/apps/alsijil/menus.py
@@ -89,12 +89,6 @@ MENUS = {
                         ),
                     ],
                 },
-                {
-                    "name": _("Data checks"),
-                    "url": "check_data",
-                    "icon": "done_all",
-                    "validators": ["menu_generator.validators.is_superuser"],
-                },
             ],
         }
     ]
diff --git a/aleksis/apps/alsijil/migrations/0008_data_check_result.py b/aleksis/apps/alsijil/migrations/0008_data_check_result.py
deleted file mode 100644
index 7ea8edfd2414cf3280d496b23e51eff1cacf0fb0..0000000000000000000000000000000000000000
--- a/aleksis/apps/alsijil/migrations/0008_data_check_result.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# Generated by Django 3.0.10 on 2020-09-19 10:45
-
-import django.contrib.postgres.fields.jsonb
-import django.contrib.sites.managers
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ("contenttypes", "0002_remove_content_type_name"),
-        ("sites", "0002_alter_domain_unique"),
-        ("alsijil", "0007_personal_note_lesson_documentation_year"),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name="DataCheckResult",
-            fields=[
-                (
-                    "id",
-                    models.AutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                (
-                    "extended_data",
-                    django.contrib.postgres.fields.jsonb.JSONField(
-                        default=dict, editable=False
-                    ),
-                ),
-                (
-                    "check",
-                    models.CharField(
-                        choices=[
-                            (
-                                "no_personal_notes_in_cancelled_lessons",
-                                "Ensure that there are no personal notes in cancelled lessons",
-                            )
-                        ],
-                        max_length=255,
-                        verbose_name="Related data check task",
-                    ),
-                ),
-                ("object_id", models.CharField(max_length=255)),
-                (
-                    "solved",
-                    models.BooleanField(default=False, verbose_name="Issue solved"),
-                ),
-                (
-                    "sent",
-                    models.BooleanField(
-                        default=False, verbose_name="Notification sent"
-                    ),
-                ),
-                (
-                    "content_type",
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.CASCADE,
-                        to="contenttypes.ContentType",
-                    ),
-                ),
-                (
-                    "site",
-                    models.ForeignKey(
-                        default=1,
-                        editable=False,
-                        on_delete=django.db.models.deletion.CASCADE,
-                        to="sites.Site",
-                    ),
-                ),
-            ],
-            options={
-                "verbose_name": "Data check result",
-                "verbose_name_plural": "Data check results",
-            },
-            managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),],
-        ),
-    ]
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index cbcb143b3f4c94926b7a6f566be0aa1d95ebef56..d66d07e0569624905630662d8b10f67e2032a488 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -1,15 +1,10 @@
-from django.contrib.contenttypes.fields import GenericForeignKey
-from django.contrib.contenttypes.models import ContentType
 from django.db import models
 from django.urls import reverse
 from django.utils.formats import date_format
-from django.utils.functional import classproperty
 from django.utils.translation import gettext_lazy as _
 
-from cache_memoize import cache_memoize
 from calendarweek import CalendarWeek
 
-from aleksis.apps.alsijil.data_checks import DATA_CHECKS_BY_NAME, DATA_CHECKS_CHOICES, DataCheck
 from aleksis.apps.alsijil.managers import PersonalNoteManager
 from aleksis.apps.chronos.mixins import WeekRelatedMixin
 from aleksis.apps.chronos.models import LessonPeriod
@@ -228,32 +223,6 @@ class ExtraMark(ExtensibleModel):
         verbose_name_plural = _("Extra marks")
 
 
-class DataCheckResult(ExtensibleModel):
-    check = models.CharField(
-        max_length=255,
-        verbose_name=_("Related data check task"),
-        choices=DATA_CHECKS_CHOICES,
-    )
-
-    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
-    object_id = models.CharField(max_length=255)
-    related_object = GenericForeignKey("content_type", "object_id")
-
-    solved = models.BooleanField(default=False, verbose_name=_("Issue solved"))
-    sent = models.BooleanField(default=False, verbose_name=_("Notification sent"))
-
-    @property
-    def related_check(self) -> DataCheck:
-        return DATA_CHECKS_BY_NAME[self.check]
-
-    def solve(self, solve_option: str = "default"):
-        self.related_check.solve(self, solve_option)
-
-    class Meta:
-        verbose_name = _("Data check result")
-        verbose_name_plural = _("Data check results")
-
-
 class AlsijilGlobalPermissions(ExtensibleModel):
     class Meta:
         managed = False
diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py
index 64393ba2084c5ff90fc080f34b2db5b16e643354..38f7a41a36b701a9499dc12697ea70d32d46e54a 100644
--- a/aleksis/apps/alsijil/urls.py
+++ b/aleksis/apps/alsijil/urls.py
@@ -69,11 +69,4 @@ urlpatterns = [
         views.ExcuseTypeDeleteView.as_view(),
         name="delete_excuse_type",
     ),
-    path("data_check/", views.DataCheckView.as_view(), name="check_data",),
-    path("data_check/run/", views.run_data_checks, name="data_check_run",),
-    path(
-        "data_check/<int:id_>/<str:solve_option>/",
-        views.solve_data_check_view,
-        name="data_check_solve",
-    ),
 ]
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index 9bcaecac84b45a2dfcfe81e74f9bcb9cc79d0bc2..2d757c6f90da8d728f75ce76dc210f36a8af5832 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -15,7 +15,6 @@ from django_tables2 import SingleTableView
 from reversion.views import RevisionMixin
 from rules.contrib.views import PermissionRequiredMixin, permission_required
 
-from aleksis.apps.alsijil.data_checks import check_data
 from aleksis.apps.chronos.managers import TimetableType
 from aleksis.apps.chronos.models import LessonPeriod, LessonSubstitution, TimePeriod
 from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk
@@ -37,7 +36,7 @@ from .forms import (
     RegisterAbsenceForm,
     SelectForm,
 )
-from .models import DataCheckResult, ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
+from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
 from .tables import ExcuseTypeTable, ExtraMarkTable
 from .util.alsijil_helpers import get_lesson_period_by_pk, get_timetable_instance_by_pk
 
@@ -840,46 +839,3 @@ class ExcuseTypeDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDelet
     template_name = "core/pages/delete.html"
     success_url = reverse_lazy("excuse_types")
     success_message = _("The excuse type has been deleted.")
-
-
-class DataCheckView(ListView):
-    model = DataCheckResult
-    template_name = "alsijil/data_check/list.html"
-    context_object_name = "results"
-
-    def get_queryset(self) -> QuerySet:
-        return DataCheckResult.objects.filter(solved=False).order_by("check")
-
-
-def run_data_checks(request: HttpRequest) -> HttpResponse:
-    check_data()
-    if is_celery_enabled():
-        messages.success(
-            request,
-            _(
-                "The data check has been started. Please note that it may take "
-                "a while before you are able to fetch the data on this page."
-            ),
-        )
-    else:
-        messages.success(request, _("The data check has been finished."))
-    return redirect("check_data")
-
-
-def solve_data_check_view(
-    request: HttpRequest, id_: int, solve_option: str = "default"
-):
-    result = get_object_or_404(DataCheckResult, pk=id_)
-    if solve_option in result.related_check.solve_options:
-        solve_option_obj = result.related_check.solve_options[solve_option]
-
-        msg = _(
-            f"The solve option '{solve_option_obj.verbose_name}' has been affected on the object '{result.related_object}' (type: {result.related_object._meta.verbose_name})."
-        )
-
-        result.solve(solve_option)
-
-        messages.success(request, msg)
-        return redirect("check_data")
-    else:
-        return HttpResponseNotFound()