diff --git a/aleksis/core/data_checks.py b/aleksis/core/data_checks.py index bf823ccc929cb2b0925f118743f87d7625985817..9f2a0ac07337d3e966c36ad26d85f43d88420c36 100644 --- a/aleksis/core/data_checks.py +++ b/aleksis/core/data_checks.py @@ -11,6 +11,31 @@ from .util.core_helpers import celery_optional, get_site_preferences class SolveOption: + """Define a solve option for one or more data checks. + + Solve options are used in order to give the data admin typical + solutions to a data issue detected by a data check. + + Example definition + + .. code-block:: python + + from aleksis.core.data_checks import SolveOption + from django.utils.translation import gettext as _ + + class DeleteSolveOption(SolveOption): + name = "delete" # has to be unqiue + verbose_name = _("Delete") # should make use of i18n + + @classmethod + def solve(cls, check_result: "DataCheckResult"): + check_result.related_object.delete() + check_result.delete() + + After the solve option has been successfully executed, + the corresponding data check result has to be deleted. + """ + name: str = "default" verbose_name: str = "" @@ -25,11 +50,87 @@ class IgnoreSolveOption(SolveOption): @classmethod def solve(cls, check_result: "DataCheckResult"): + """Mark the object as solved without doing anything more.""" check_result.solved = True check_result.save() class DataCheck: + """Define a data check. + + Data checks should be used to search objects of + any type which are broken or need some extra action. + + Defining data checks + -------------------- + Data checks are defined by inheriting from the class DataCheck + and registering the inherited class in the data check registry. + + Example: + + .. code-block:: python + + from aleksis.core.data_checks import DataCheck, DATA_CHECK_REGISTRY + from django.utils.translation import gettext as _ + + @DATA_CHECK_REGISTRY.register + class ExampleDataCheck(DataCheck): + name = "example" # has to be unique + verbose_name = _("Ensure that there are no examples.") + problem_name = _("There is an example.") # should both make use of i18n + + solve_options = { + IgnoreSolveOption.name: IgnoreSolveOption + } + + @classmethod + def check_data(cls): + from example_app.models import ExampleModel + + wrong_examples = ExampleModel.objects.filter(wrong_value=True) + + for example in wrong_examples: + cls.register_result(example) + + Solve options are used in order to give the data admin typical solutions to this specific issue. + They are defined by inheriting from SolveOption. + More information about defining solve options can be find there. + + The dictionary ``solve_options`` should include at least the IgnoreSolveOption, + but preferably also own solve options. The keys in this dictionary + have to be ``<YourOption>SolveOption.name`` + and the values must be the corresponding solve option classes. + + The class method ``check_data`` does the actual work. In this method + your code should find all objects with issues and should register + them in the result database using the class method ``register_result``. + + Data checks have to be registered in the central registry. + This can be done by decorating the class with + ``@DATA_CHECK_REGISTRY.register`` or adding it later + by ``DATA_CHECK_REGISTRY.register(<YourCheck>DataCheck)``. + + Executing data checks + --------------------- + The data checks can be executed by using the + celery task named ``aleksis.core.data_checks.check_data``. + We recommend to create a periodic task in the backend + which executes ``check_data`` on a regular base (e. g. every day). + + .. warning:: + To use the option described above, you must have setup celery properly. + + Notifications about results + --------------------------- + The data check tasks can notify persons via email + if there are new data issues. You can set these persons + by adding them to the preference + ``Email recipients for data checks problem emails`` in the site configuration. + + To enable this feature, you also have to activate + the preference ``Send emails if data checks detect problems``. + """ # noqa: D412 + name: str = "" verbose_name: str = "" problem_name: str = "" @@ -38,15 +139,26 @@ class DataCheck: @classmethod def check_data(cls): + """Find all objects with data issues and register them.""" pass @classmethod - def solve(cls, check_result: "DataCheckResult", solve_option: str = "default"): + def solve(cls, check_result: "DataCheckResult", solve_option: str): + """Execute a solve option for an object detected by this check. + + :param check_result: The result item from database + :param solve_option: The name of the solve option that should be executed + """ with reversion.create_revision(): cls.solve_options[solve_option].solve(check_result) @classmethod def register_result(cls, instance) -> "DataCheckResult": + """Register an object with data issues in the result database. + + :param instance: The affected object + :return: The database entry + """ from aleksis.core.models import DataCheckResult ct = ContentType.objects.get_for_model(instance) @@ -57,12 +169,15 @@ class DataCheck: class DataCheckRegistry: + """Create central registry for all data checks in AlekSIS.""" + def __init__(self): self.data_checks = [] self.data_checks_by_name = {} self.data_checks_choices = [] def register(self, check: DataCheck): + """Add a new data check to the registry.""" self.data_checks.append(check) self.data_checks_by_name[check.name] = check self.data_checks_choices.append((check.name, check.verbose_name)) @@ -74,6 +189,7 @@ DATA_CHECK_REGISTRY = DataCheckRegistry() @celery_optional def check_data(): + """Execute all registered data checks and send email if activated.""" for check in DATA_CHECK_REGISTRY.data_checks: logging.info(f"Run check: {check.verbose_name}") check.check_data()