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) 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 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, DataCheckResult ct = ContentType.objects.get_for_model(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}") sub = note.lesson_period.get_substitution( CalendarWeek(week=note.week, year=note.year) ) 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)