Django management commands

Throughout the previous chapters -- including this one -- you've relied on management commands invoked through the manage.py script included in all Django projects. For example, to start the development server of a Django project you've used the runserver command (e.g. python manage.py runserver) and to consolidate a project's static resources you've used the collectstatic command (e.g. python manage.py collectstatic).

Django management commands are included as part of Django apps and are designed to fulfill repetitive or complex tasks through a one keyword command line instruction. Every Django management command is backed by a script that contains the step-by-step Python logic to fulfill its duties. So when you type python manage.py runserver, behind the scenes Django triggers a much more complex Python routine.

If you type python manage.py (i.e. without a command) on a Django project, you'll see a list of Django management commands classified by app (e.g. auth, django, staticfiles). From this list you can gain insight into the various management commands available on all your Django apps.

I'll describe the purpose of Django management commands associated with core or third party Django apps as they come up in the book, just as I've done up to this point (e.g. static file management commands in static file topics, model management commands in model topics).

What I'll do next is describe how to create custom management commands in your Django apps, so you can simplify the execution of routine or complex tasks through a single instruction.

Custom management command structure

Custom management commands are structured as Python classes that inherit their behavior from the Django django.core.management.base.BaseCommand class. This last class provides the necessary structure to execute any Python logic (e.g. file system, database or Django specific) and at the same time process arguments typically used with Django management commands. Listing 5-33 illustrates one of the most Django management commands possible.

Listing 5-33. Django management command class with no arguments

from django.core.management.base import BaseCommand, CommandError
from django.conf import settings

class Command(BaseCommand):
    help = 'Send test emails'
    
    def handle(self, *args, **options):
        for admin_name,email in settings.ADMINS:
            try:
                self.stdout.write(self.style.WARNING("About to send email to %s" % (email)))
                # Logic to send email here
                # Any other Python logic can also go here
                self.stdout.write(self.style.SUCCESS('Successfully sent email to "%s"' % email))
                raise Exception
            except Exception:
                raise CommandError('Failed to send test email')

Notice in listing 5-33, the management command class must be named Command and inherit its behavior from the Django BaseCommand class. Next, there's a help attribute to describe the purpose of the management command. If you type python manage.py help <task_file_name> or python manage.py <task_file_name> --help Django outputs the value of the help attribute.

The handle method contains the core command logic and is automatically run when invoking the command. Notice the handle method declares three input argument: self to reference the class instance; *args to reference arguments of the method itself; and **options to reference arguments passed as part of the management command. The task logic in listing 5-33 only uses the self reference. The other task management example -- in listing 5-34 -- illustrates how to use arguments.

The task logic in listing 5-33 is limited to looping over the ADMINS value in settings.py and outputting the task results. However, there's no limit to the logic you can execute inside the handle method, so long as it's valid Python.

Although standard Python try/except blocks work as expected inside Django management tasks, there are two syntax particularities you need to be aware of when creating Django management tasks: outputting messages and error handling.

To send output messages while executing task logic -- success or informative -- you can see listing 5-33 uses the self.stdout.write reference, which represents the standard output channel where management tasks run. In addition, you can see self.stdout.write uses both the self.style.WARNING and self.style.SUCCESS to declare the actual messages to output. The wrapping of messages inside self.style.* is optional, but outputs colored formatted messages (e.g. SUCCESS in green font, WARNING in yellow font) in accordance with Django syntax coloring roles[11].

To send error messages while executing task logic, you can use the self.stderr.write reference, which represents the standard error channel where management tasks run. And to terminate the execution of a management task due to an error, you can raise the django.core.management.base.CommandError exception -- as it's done in listing 5-33 -- which accepts an error message, that gets sent to the self.stderr.write channel.

In most circumstances, it's rare to have a fixed Django management command like the one in listing 5-33 that uses no arguments to alter its logical workflow. For example, the Django runserver command accepts argument like addrport and --nothreading to influence how a web server is launched.

Django management commands can use two types of arguments: positional arguments -- where the order in which they're declared gives them their meaning -- or named arguments -- which are preceded by names with two dashes -- (a.k.a.flags) to give them their meaning.

Although the **options argument of the handle() method -- as shown in listing 5-33 -- provides access to a management command's arguments to alter the logical workflow. In order to use arguments in a custom Django management command, you must also declare the add_arguments() method.

The add_arguments() method must define a management task's arguments, including their type -- positional or named -- default value, choice values and help message, among other things. In essence, the add_arguments() method works as a pre-processor to command arguments, which are then made available in the **options argument of the handle() method.

The parser reference of the add_arguments(self,parser) signature, is an argument parser based on the standard Python argparse package[12] designed to easily process command line arguments for Python scripts.

To add command arguments inside the add_arguments() method you do so via the parser.add_argument() method, as illustrated in listing 5-34.

Listing 5-34. Django management task class with arguments

from django.core.management.base import BaseCommand, CommandError
from django.conf import settings

class Command(BaseCommand):
    help = 'Clean up stores'
    
    def add_arguments(self, parser):
        # Positional arguments are standalone name
        parser.add_argument('store_id')
        
        # Named (optional) arguments start with --
        parser.add_argument(
            '--delete',
            default=False,
            help='Delete store instead of cleaning it up',
        )
    def handle(self, *args, **options):
        # Access arguments inside **options dictionary
#options={'store_id': '1', 'settings': None, 'pythonpath': None, # 'verbosity': 1, 'traceback': False, 'no_color': False, 'delete': False}

The management command in listing 5-34 declares both a positional and a named argument. Notice both arguments are added with the parser.add_argument() method. The difference being, named arguments use leading dashes -- and If omitted an argument is assumed to be positional.

Positional arguments by definition are required. So in the case of listing 5-34, the store_id argument is expected (e.g. python manage.py cleanupstores 1 , where 1 is the store_id), otherwise Django throws a 'too few arguments' error.

Named arguments are always optional. And because named arguments are optional, you can see in listing 5-34 the --delete argument declares a default=False value, ensuring the argument always receives a default value to run the logic inside the handle() method.

The --delete argument in listing 5-34 also uses the help attribute to define a descriptive text about the purpose of the argument. In addition to default and help, the parser.add_argument() method supports a wide variety of attributes, based on the Python argparse package -- see the previous footnote to consult some of the arguments support by this method.

Finally, you can see in listing 5-34 the handle() method gets access to the command arguments via the **options dictionary, where the values can then be used toward the structuring of the command logic. Note the additional arguments available in **options -- settings, pythonpath,etc -- are inherited by default due to the BaseCommand class.

Custom management command installation

All Django management tasks are placed inside individual Python files (i.e. one command per file) and stored inside an app directory structure under the /management/commands/ folder. Listing 5-35 shows the folder structure for a couple of apps with custom management tasks.

Listing 5-35. Django management task folder structure and location

+-<BASE_DIR_project_name>
|
+-manage.py 
|
|
+---+-<PROJECT_DIR_project_name>
    |
    +-__init__.py
    +-settings.py
    +-urls.py
    +-wsgi.py
    |
    +-about(app)-+
    |            +-__init__.py
    |            +-models.py
    |            +-tests.py
    |            +-views.py
    |            +-management-+
    |                         +-__init__.py
    |                         +-commands-+
    |                                    +-__init__.py
    |                                    | 
    |                                    |
    |                                    +-sendtestemails.py
    |                                    
    +-stores(app)-+
                 +-__init__.py
                 +-models.py
                 +-tests.py
                 +-views.py
                 +-management-+
                              +-__init__.py
                              +-commands-+
                                         +-__init__.py
                                         |
                                         |
                                         +-cleanupstores.py
                                         +-updatemenus.py

As you can see in listing 5-35, the about app has a single management command inside the /management/commands/ folder and the stores app has two management commands nested inside its own /management/commands/ folder.

Caution To ensure the visibility of an app's management commands, don't forget to add the empty __init__.py files to the /management/ and /commands/ folders as shown in listing 5-35 and declare the apps as part of INSTALLED_APPS in a project's settings.py file.

Management command automation

Django management commands are typically run from the command line, requiring human intervention. However, there can be times when it's helpful or necessary to automate the execution of management commands from other locations (e.g. a Django view method or shell).

For example, if a user uploads an image in a Django application and you want the image to become publicly accessible, you'll need to run the collectstatic command so the image makes its way to the public & consolidation location (STATIC_ROOT) . Similarly, you may want to run a cleanuprofile command every time a user logs in.

To automate the execution of management commands Django offers the django.core.management.call_command() method. Listing 5-36 illustrates the various ways in which you can use the call_command() method.

Listing 5-36. Django management automation with call_command()

from django.core import management

# Option 1, no arguments
management.call_command('sendtestemails')

# Option 2, no pause to wait for input
management.call_command('collectstatic', interactive=False)

# Option 3, command input with Command()
from django.core.management.commands import loaddata
management.call_command(loaddata.Command(), 'stores', verbosity=0)

# Option 4, positional and named command arguments
management.call_command('cleanupdatastores', 1, delete=True) 

The first option in listing 5-35 executes a management without any arguments. The second option in listing 5-35 uses the interactive=False argument to indicate the command must not pause for user input (e.g. collectstatic always asks if you're sure if you want to overwrite pre-existing files, the interactive=False argument avoids this pause and need for input).

The third option in listing 5-35 invokes the management command by first importing it and then invoking its Command() class directly vs. using the command string value. And finally, the fourth option -- just like the third -- in listing 5-35, uses a positional argument -- declared as a standalone value (e.g. 'stores', 1) and a named argument -- declared as a key=value (e.g. verbosity=0, delete=True).

  1. https://docs.djangoproject.com/en/1.11/ref/django-admin/#syntax-coloring     

  2. https://docs.python.org/3/library/argparse.html