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()