Categories
Python

Take advantage of Django’s system checks

Today, let’s go back to the topic of the first post in this series of Django tips.

At the time, I focused on the python manage.py check --deploy command. In this article, I will explore the feature on which it is built and how it can be quite handy for many other scenarios.

So, the System Check Framework, what is it?

The system check framework is a set of static checks for validating Django projects. It detects common problems and provides hints for how to fix them. The framework is extensible so you can easily add your own checks.

Django documentation

We already have linters, formatters, and static analysis tools that we run on the CI of our projects. Many companies also have strict peer review processes before accepting any changes to the code. However, this framework can still very useful for many projects in situations such as:

  • Detect misconfigurations when someone is setting a complex new development environment, and help them correctly resolve the issues.
  • Warn developers when project-specific approaches are not being followed.
  • Ensure everything is well configured and correctly set up before deploying to production. Otherwise, make the deployment fail.

With the framework, the above points are a bit easier to implement, since “system checks” has access to the real state of the app.

During development, checks are executed on commands such as runserver and migrate, and before deployment a call to manage.py check can be executed before starting the app.

Django itself makes heavy use of this framework. I’m sure you have already seen messages such as the ones shown below, during development.

Screenshot of a Django app being launched and system checks warnings showing in the terminal.

I’m not going to dig deeper into the details, since the Django documentation is excellent (list of built-in checks). Let’s just build a practical example.

A practical example

Recently, I was watching a talk and the speaker mentioned a situation when a project should block Django’s reverse foreign keys, to prevent accidental data leakages. The precise situation is not relevant for this post, given it is a bit more complex, but let’s assume this is the requirement.

The reverse foreign key feature needs to be disabled on every foreign key field. We can implement a system check to ensure we haven’t forgotten any. It would look very similar to this:

# checks.py
from django.apps import apps
from django.core import checks
from django.db.models import ForeignKey


@checks.register(checks.Tags.models)
def check_foreign_keys(app_configs, **kwargs):
    errors = []

    for app in apps.get_app_configs():
        if "site-packages" in app.path:
            continue

        for model in app.get_models():
            fields = model._meta.get_fields()
            for field in fields:
                errors.extend(check_field(model, field))

    return errors


def check_field(model, field):
    if not isinstance(field, ForeignKey):
        return []

    rel_name = getattr(field, "_related_name", None)
    if rel_name and rel_name.endswith("+"):
        return []

    error = checks.CheckMessage(
        checks.ERROR,
        f'FK "{field.name}" reverse lookups enabled',
        'Add "+" at the end of the "related_name".',
        obj=model,
        id="example.E001",
    )
    return [error]

Then ensure the check is active by loading it on apps.py:

# apps.py
from django.apps import AppConfig


class ExampleConfig(AppConfig):
    name = "example"

    def ready(self):
        super().ready()
        from .checks import check_foreign_keys

This would do the trick and produce the following output when trying to run the development server:

$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

...

ERRORS:
example.Order: (example.E001) FK "client" reverse lookups enabled
        HINT: Add "+" at the end of the "related_name".
example.Order: (example.E001) FK "source" reverse lookups enabled
        HINT: Add "+" at the end of the "related_name".
example.OrderRequest: (example.E001) FK "source" reverse lookups enabled
        HINT: Add "+" at the end of the "related_name".

On the other hand, if your test is only important for production, you should use @checks.register(checks.Tags.models, deploy=True). This way, the check will only be executed together with all other security checks when running manage.py check --deploy.

Doing more with the framework

There are also packages on PyPI, such as django-extra-checks, that implement checks for general Django good practices if you wish to enforce them on your project.

To end this post, that is already longer than I initially desired, I will leave a couple of links to other resources if you want to continue exploring:

One reply on “Take advantage of Django’s system checks”

Comments are closed.