Infrastructure de contrôle du système

L’infrastructure de contrôle du système est un ensemble de contrôles statiques pour valider les projets Django. Elle détecte les problèmes courants et fournit des conseils sur la façon de les corriger. L’infrastructure est extensible de sorte que vous pouvez facilement ajouter vos propres contrôles.

Les contrôles peuvent être déclenchées de manière explicite par la commande check. Les contrôles sont déclenchés implicitement avant la plupart des commandes, y compris runserver et migrate. Pour des raisons de performance, les contrôles ne sont pas effectués dans le contexte de la pile WSGI utilisée en mode déployé. Si vous avez besoin d’effectuer les contrôles systèmes sur le serveur de déploiement, appelez explicitement la commande check.

Les erreurs graves empêcheront les commandes de Django (comme runserver) de fonctionner, tout court. Les problèmes mineurs sont signalés dans la console. Si vous avez inspecté la cause d’un avertissement et que vous êtes content de l’ignorer, vous pouvez masquer des avertissements spécifiques en utilisant le réglage SILENCED_SYSTEM_CHECKS dans votre fichier de réglages du projet.

Une liste complète de tous les contrôles pouvant être levées par Django peuvent être trouvés dans la référence des contrôles système.

Écrire vos propres contrôles

Le système est souple et vous permet d’écrire des fonctions qui effectuent tout autre type de contrôle dont vous auriez besoin. Ce qui suit est un exemple de squelette d’une fonction de contrôle :

from django.core.checks import Error, register


@register()
def example_check(app_configs, **kwargs):
    errors = []
    # ... your check logic here
    if check_failed:
        errors.append(
            Error(
                "an error",
                hint="A hint.",
                obj=checked_object,
                id="myapp.E001",
            )
        )
    return errors

La fonction de contrôle doit accepter un argument app_configs; cet argument est la liste des applications qui doivent être inspectés. Si None, la vérification doit être effectué sur toutes les applications installées du projet.

Le contrôle recevra un paramètre nommé databases. Il s’agit d’une liste des alias de bases de données dont les connexions peuvent être utilisées pour inspecter la configuration au niveau base de données. Si databases vaut None, le contrôle ne doit utiliser aucune connexion de base de données.

Le paramètre **kwargs est nécessaire pour prendre en compte de futures modifications.

Messages

La fonction doit renvoyer une liste de messages. Si aucun problème n’est détecté suite à la vérification, la fonction de contrôle doit renvoyer une liste vide.

Les avertissements et les erreurs levés par la méthode de contrôle doivent être des instances de CheckMessage. Une instance de CheckMessage encapsule une seule erreur à signaler ou un seul avertissement. Elle fournit également des conseils et le contexte applicable au message, et un identifiant unique qui est utilisé à des fins de filtrage.

Le concept est très similaire aux messages du système de message ou de la journalisation. Les messages sont étiquettés avec un level indiquant la gravité du message.

Il existe également des raccourcis pour faciliter la création des messages avec les niveaux de base. Lorsque vous utilisez ces classes, vous pouvez omettre le paramètre level car il est sous-entendu par le nom de la classe.

Enregistrement et étiquetage des contrôles

Finalement, votre fonction de contrôle doit être explicitement inscrite dans le registre des contrôles systèmes. Les contrôles doivent être inscrits dans un fichier qui est chargé au moment du chargement de l’application. Par exemple, dans la méthode AppConfig.ready().

register(*tags)(function)

Vous pouvez passer autant d’étiquettes que vous le souhaitez à register pour labeliser votre contrôle. L’étiquetage des contrôles est utile car elle vous permet de n’exécuter qu’un certain groupe de contrôles. Par exemple, pour enregistrer un contrôle de compatibilité, vous feriez l’appel suivant :

from django.core.checks import register, Tags


@register(Tags.compatibility)
def my_check(app_configs, **kwargs):
    # ... perform compatibility checks and collect errors
    return errors

Vous pouvez inscrire des « contrôles de déploiement » qui ne sont applicables qu’à un fichier de réglages de production de la manière suivante :

@register(Tags.security, deploy=True)
def my_check(app_configs, **kwargs): ...

Ces contrôles ne seront exécutés que si l’option check --deploy est utilisée.

Vous pouvez aussi employer register comme une fonction au lieu d’un décorateur en lui transmettant un objet exécutable (habituellement une fonction) comme premier paramètre.

Le code ci-dessous est équivalent au code ci-dessus :

def my_check(app_configs, **kwargs): ...


register(my_check, Tags.security, deploy=True)

Contrôles sur les champs, modèles, gestionnaires et bases de données

Dans certains cas, vous n’aurez pas besoin d’enregistrer votre fonction de contrôle – vous pouvez la greffer sur un enregistrement existant.

Les champs, les modèles, les gestionnaires de modèle et les moteurs de base de données implémentent tous une méthode check() qui est déjà enregistrée auprès du système de vérification. Si vous souhaitez ajouter des contrôles supplémentaires, vous pouvez étendre l’implémentation de la classe de base, effectuer tous contrôles supplémentaires nécessaires, et ajouter tous les messages à ceux générés par la classe de base. Il est recommandé que vous déléguiez chaque contrôle à des méthodes distinctes.

Prenons un exemple où vous implémentez un champ personnalisé nommé RangedIntegerField. Ce champ ajoute les arguments min et max au constructeur d”IntegerField. Vous pourriez ajouter une vérification afin de vous assurer que les utilisateurs fournissent une valeur min qui est inférieur ou égale à la valeur max. Le code suivant montre comment vous pouvez mettre en œuvre ce contrôle :

from django.core import checks
from django.db import models


class RangedIntegerField(models.IntegerField):
    def __init__(self, min=None, max=None, **kwargs):
        super().__init__(**kwargs)
        self.min = min
        self.max = max

    def check(self, **kwargs):
        # Call the superclass
        errors = super().check(**kwargs)

        # Do some custom checks and add messages to `errors`:
        errors.extend(self._check_min_max_values(**kwargs))

        # Return all errors and warnings
        return errors

    def _check_min_max_values(self, **kwargs):
        if self.min is not None and self.max is not None and self.min > self.max:
            return [
                checks.Error(
                    "min greater than max.",
                    hint="Decrease min or increase max.",
                    obj=self,
                    id="myapp.E001",
                )
            ]
        # When no error, return an empty list
        return []

Si vous vouliez ajouter des contrôles à un gestionnaire de modèle, vous prendriez la même approche avec votre sous-classe de Manager.

Si vous souhaitez ajouter un contrôle à une classe de modèle, l’approche est presque la même : la seule différence est que le contrôle est une méthode de classe, pas une méthode d’instance :

class MyModel(models.Model):
    @classmethod
    def check(cls, **kwargs):
        errors = super().check(**kwargs)
        # ... your own checks ...
        return errors

Écriture de tests

Les messages sont comparables. Cela vous permet d’écrire facilement des tests :

from django.core.checks import Error

errors = checked_object.check()
expected_errors = [
    Error(
        "an error",
        hint="A hint.",
        obj=checked_object,
        id="myapp.E001",
    )
]
self.assertEqual(errors, expected_errors)

Écriture de tests d’intégration

Étant donné le besoin d’inscrire certains contrôles au chargement de l’application, il peut être utile de tester leur intégration dans l’infrastructure des contrôles systèmes. Cela peut être fait en appelant la fonction call_command().

Par exemple, ce test démontre que le réglage SITE_ID doit être un nombre entier, ce qui est fait par un contrôle intégré au système des sites:

from django.core.management import call_command
from django.core.management.base import SystemCheckError
from django.test import SimpleTestCase, modify_settings, override_settings


class SystemCheckIntegrationTest(SimpleTestCase):
    @override_settings(SITE_ID="non_integer")
    @modify_settings(INSTALLED_APPS={"prepend": "django.contrib.sites"})
    def test_non_integer_site_id(self):
        message = "(sites.E101) The SITE_ID setting must be an integer."
        with self.assertRaisesMessage(SystemCheckError, message):
            call_command("check")

Considérez le contrôle suivant qui émet un avertissement lors du déploiement si un réglage personnalisé nommé ENABLE_ANALYTICS n’est pas défini à True:

from django.conf import settings
from django.core.checks import Warning, register


@register("myapp", deploy=True)
def check_enable_analytics_is_true_on_deploy(app_configs, **kwargs):
    errors = []
    if getattr(settings, "ENABLE_ANALYTICS", None) is not True:
        errors.append(
            Warning(
                "The ENABLE_ANALYTICS setting should be set to True in deployment.",
                id="myapp.W001",
            )
        )
    return errors

Sachant que ce contrôle ne produira pas de SystemCheckError, la présence du message d’avertissement dans la sortie stderr peut être testée comme ceci :

from io import StringIO

from django.core.management import call_command
from django.test import SimpleTestCase, override_settings


class EnableAnalyticsDeploymentCheckTest(SimpleTestCase):
    @override_settings(ENABLE_ANALYTICS=None)
    def test_when_set_to_none(self):
        stderr = StringIO()
        call_command("check", "-t", "myapp", "--deploy", stderr=stderr)
        message = (
            "(myapp.W001) The ENABLE_ANALYTICS setting should be set "
            "to True in deployment."
        )
        self.assertIn(message, stderr.getvalue())
Back to Top