Skip to content
Snippets Groups Projects
Verified Commit 07106d1b authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Init data check system and add first check for personal notes

parent 5833dfa2
No related branches found
No related tags found
1 merge request!92Resolve "Add task for checking plausibility of data"
Pipeline #3777 passed
import logging
from django.contrib.contenttypes.models import ContentType
from django.db.models import F
from django.utils.translation import gettext as _
import reversion
from calendarweek import CalendarWeek
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]
def check_data():
for check in DATA_CHECKS:
logging.info(f"Run check: {check.verbose_name}")
check.check_data()
...@@ -60,6 +60,12 @@ MENUS = { ...@@ -60,6 +60,12 @@ MENUS = {
"icon": "label", "icon": "label",
"validators": ["menu_generator.validators.is_superuser"], "validators": ["menu_generator.validators.is_superuser"],
}, },
{
"name": _("Check data"),
"url": "check_data",
"icon": "label",
"validators": ["menu_generator.validators.is_superuser"],
},
], ],
} }
] ]
......
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.urls import reverse
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from calendarweek import CalendarWeek from calendarweek import CalendarWeek
from aleksis.apps.alsijil.data_checks import (
DATA_CHECKS_BY_NAME,
DATA_CHECKS_CHOICES,
DataCheck,
)
from aleksis.apps.chronos.mixins import WeekRelatedMixin from aleksis.apps.chronos.mixins import WeekRelatedMixin
from aleksis.apps.chronos.models import LessonPeriod from aleksis.apps.chronos.models import LessonPeriod
from aleksis.apps.chronos.util.date import get_current_year from aleksis.apps.chronos.util.date import get_current_year
...@@ -102,6 +110,15 @@ class PersonalNote(ExtensibleModel, WeekRelatedMixin): ...@@ -102,6 +110,15 @@ class PersonalNote(ExtensibleModel, WeekRelatedMixin):
def __str__(self): def __str__(self):
return f"{date_format(self.date)}, {self.lesson_period}, {self.person}" 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: class Meta:
verbose_name = _("Personal note") verbose_name = _("Personal note")
verbose_name_plural = _("Personal notes") verbose_name_plural = _("Personal notes")
...@@ -208,3 +225,28 @@ class ExtraMark(ExtensibleModel): ...@@ -208,3 +225,28 @@ class ExtraMark(ExtensibleModel):
ordering = ["short_name"] ordering = ["short_name"]
verbose_name = _("Extra mark") verbose_name = _("Extra mark")
verbose_name_plural = _("Extra marks") 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"))
@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")
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load data_helpers %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block browser_title %}{% blocktrans %}Data checks{% endblocktrans %}{% endblock %}
{% block page_title %}{% blocktrans %}Data checks{% endblocktrans %}{% endblock %}
{% block content %}
<a class="btn green waves-effect waves-light" href="#">
<i class="material-icons left">refresh</i>
{% trans "Check data again" %}
</a>
{% if results %}
<div class="card">
<div class="card-content">
<i class="material-icons left medium red-text">warning</i>
<span class="card-title">{% trans "The system detected some problems with your data." %}</span>
<p>{% blocktrans %}Please go through all data and check whether some extra action is
needed.{% endblocktrans %}</p>
</div>
</div>
{% else %}
<div class="card">
<div class="card-content">
<i class="material-icons left medium green-text">check_circle</i>
<span class="card-title">{% trans "Everything is fine." %}</span>
<p>{% blocktrans %}The system hasn't detected any problems with your data.{% endblocktrans %}</p>
</div>
</div>
{% endif %}
{% if results %}
<table>
<thead>
<tr>
<th></th>
<th colspan="2">{% trans "Affected object" %}</th>
<th>{% trans "Detected problem" %}</th>
<th>{% trans "Show details" %}</th>
<th>{% trans "Options to solve the problem" %}</th>
</tr>
</thead>
<tbody>
{% for result in results %}
<tr>
<td>
<code>{{ result.id }}</code>
</td>
<td>{% verbose_name_object result.related_object %}</td>
<td>{{ result.related_object }}</td>
<td>{{ result.related_check.problem_name }}</td>
<td>
<a class="btn-flat waves-effect waves-light" href="{{ result.related_object.get_absolute_url }}">
{% trans "Show object" %}
</a>
</td>
<td>
{% for option_name, option in result.related_check.solve_options.items %}
<a class="btn waves-effect waves-light" href="{% url "data_check_solve" result.pk option_name %}">
{{ option.verbose_name }}
</a>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}
...@@ -68,4 +68,10 @@ urlpatterns = [ ...@@ -68,4 +68,10 @@ urlpatterns = [
views.ExcuseTypeDeleteView.as_view(), views.ExcuseTypeDeleteView.as_view(),
name="delete_excuse_type", name="delete_excuse_type",
), ),
path("data_check/", views.DataCheckView.as_view(), name="check_data",),
path(
"data_check/<int:id_>/<str:solve_option>/",
views.solve_data_check_view,
name="data_check_solve",
),
] ]
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from typing import Optional from typing import Any, Dict, Optional
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db.models import Count, Exists, OuterRef, Q, Subquery, Sum from django.db.models import Count, Exists, OuterRef, Q, QuerySet, Subquery, Sum
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseNotFound from django.http import Http404, HttpRequest, HttpResponse, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import DetailView from django.views.generic import DetailView, ListView
import reversion import reversion
from calendarweek import CalendarWeek from calendarweek import CalendarWeek
...@@ -15,6 +15,7 @@ from django_tables2 import SingleTableView ...@@ -15,6 +15,7 @@ from django_tables2 import SingleTableView
from reversion.views import RevisionMixin from reversion.views import RevisionMixin
from rules.contrib.views import PermissionRequiredMixin from rules.contrib.views import PermissionRequiredMixin
from aleksis.apps.alsijil.data_checks import check_data
from aleksis.apps.chronos.managers import TimetableType from aleksis.apps.chronos.managers import TimetableType
from aleksis.apps.chronos.models import LessonPeriod, TimePeriod from aleksis.apps.chronos.models import LessonPeriod, TimePeriod
from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk
...@@ -32,7 +33,13 @@ from .forms import ( ...@@ -32,7 +33,13 @@ from .forms import (
RegisterAbsenceForm, RegisterAbsenceForm,
SelectForm, SelectForm,
) )
from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote from .models import (
DataCheckResult,
ExcuseType,
ExtraMark,
LessonDocumentation,
PersonalNote,
)
from .tables import ExcuseTypeTable, ExtraMarkTable from .tables import ExcuseTypeTable, ExtraMarkTable
...@@ -751,3 +758,36 @@ class ExcuseTypeDeleteView(AdvancedDeleteView, PermissionRequiredMixin, Revision ...@@ -751,3 +758,36 @@ class ExcuseTypeDeleteView(AdvancedDeleteView, PermissionRequiredMixin, Revision
template_name = "core/pages/delete.html" template_name = "core/pages/delete.html"
success_url = reverse_lazy("excuse_types") success_url = reverse_lazy("excuse_types")
success_message = _("The excuse type has been deleted.") 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_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
check_data()
return context
def get_queryset(self) -> QuerySet:
return DataCheckResult.objects.filter(solved=False).order_by("check")
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()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment