create a custom BaseCommand for a Django app

doridoro

DoriDoro

Posted on May 7, 2024

create a custom BaseCommand for a Django app

Introduction:

Prerequisites:

I assume you know how to create a Django application, otherwise check out this article.

The official Django documentation about management commands.

In your Django project you will create in one app two python directories (management/commands/) and inside the commands folder, you create your commands-files, like:

# project tree

app_name/
    __init__.py
    management/
        __init__.py
        commands/
            __init__.py
            _private_command.py
            command_name.py
Enter fullscreen mode Exit fullscreen mode

The command-file will look like: (example of Django website, updated)

# command_name.py

from django.core.management.base import BaseCommand, CommandError
from polls.models import Question as Poll


class CustomCommand(BaseCommand):
    help = "Closes the specified poll for voting"

    def add_arguments(self, parser):
        parser.add_argument("poll_ids", nargs="+", type=int)

    def handle(self, *args, **options):
        for poll_id in options["poll_ids"]:
            try:
                poll = Poll.objects.get(pk=poll_id)
            except Poll.DoesNotExist:
                raise CommandError('Poll "%s" does not exist' % poll_id)

            poll.opened = False
            poll.save()

            self.stdout.write(
                self.style.SUCCESS('Successfully closed poll "%s"' % poll_id)
            )
Enter fullscreen mode Exit fullscreen mode

You create a new class and inherit from BaseCommand.

  • The 'help =' will display the help text when calling python manage.py help or more precise for this custom command: python manage.py help command_name.

  • def add_arguments(self, parser): will specify the arguments that your custom command should accept. It is part of the setup process and is optional. Your custom command does not need any arguments or you can create optional arguments.

  • def handle(self, *args, **options): This method is where the actual logic of the command is implemented. It is the entry point for the execution of the command, and it is automatically called when the command is run.

additional information to the parser.add_argument():

What it does:

  • It defines the arguments that the command will accept.
  • It specifies the type of each argument.
  • It can set default values for optional arguments.
  • It provides help text for each argument, which is displayed when the --help option is used.

parser.add_argument("poll_ids", nargs="+", type=int) in our example:

  • "poll_ids" is the name of the argument.
  • nargs="+" is only necessary when the provided arguments value can be one or more values. (nargs="+": This indicates that the argument can take one or more values. The + means that one or more arguments are expected.)
  • type=int defines the accepted type, in or case an int/number.

You can add positional arguments or optional arguments:

  • parser.add_argument('positional_arg', type=int, help='This is a positional argument')
  • parser.add_argument('-o', '--optional', type=str, help='This is an optional argument')

additional information to the handle():

To access the arguments you will use options.

  • to access positional arguments, you assign options['positional_arg'] to a variable like: pos_arg_value = options['positional_arg']
  • to access an optional argument, you do the same, like: opt_arg_value = options['optional']

Create a custom BaseCommand to inherit to a child BaseCommand

Let's say we create a simple Django app where we create an employee.

# our project tree:

employee/
    __init__.py
    management/
        __init__.py
        commands/
            __init__.py
            employee_create.py  # the employee command
    models.py
utils/
    __init__.py
    management/
        __init__.py
        commands/
            __init__.py
            custom_command.py  # the custom command
Enter fullscreen mode Exit fullscreen mode

Create the parent custom BaseCommand in file: custom_command.py

Note: this file will evolve

# utils/management/commands/custom_command.py

class CustomCommand(BaseCommand):

    help = "Custom BaseCommand"
    action = None

    def __init__(self, *args, **options):
        super().__init__(*args, **options)
        self.object = None
        self.fields = []
        self.fields_to_update = []
        self.update_table = []


    def get_data(self):
        return dict()

    def make_changes(self, data):
        return None

    def display_result(self, data):
        pass


def handle(self, *args, **options):
        super().handle(*args, **options)

        validated_data = self.get_data()
        self.make_changes(validated_data)
        self.display_result(validated_data)

Enter fullscreen mode Exit fullscreen mode

What does these methods of the custom BaseCommand do?

  • get_data(): Prompts the user for data/information and returns a dictionary, which gets the user's input.

  • make_changes(data): Verifies if the user's input from 'get_data' exists. Makes queries to verify the user's input from the database and makes the changes. It takes a dictionary with the data (user's input) as argument.

  • display_result(data): Prints the created employee or can print a success message, etc.

  • handle(): With super().handle(*args, **options) calls the handle method in parent Command (the BaseCommand handle method). And after it calls the get_data(), the make_changes() and display_result() method.

To be able to create an employee, we need an employee model. The employee model will look like:

# employee/models.py

from django.db import models
from django.utils.translation import gettext_lazy as _

class Employee(models.Model):
    email = models.EmailField(_("email address"), unique=True, verbose_name=_("first name"))
    first_name = models.CharField(max_length=100, verbose_name=_("first name"))
    last_name = models.CharField(max_length=100, verbose_name=_("last name"))
Enter fullscreen mode Exit fullscreen mode

Create the child Command to create an employee

# employee/management/commands/employee_create.py

from employee.models import Employee
from utils.custom_command import CustomCommand


class Command(CustomCommand):
    help = "Prompts to create a new employee."
    action = "CREATE"


    def get_data(self):
        self.stdout.write("Enter details to create an employee:")

        return {
            "email": input("Email address: "),
            "password": input("Password: "),
            "first_name": input("First name: "),
            "last_name": input("Last name: "),
        }

    def make_changes(self, data):
        employee_exists = Employee.objects.filter(email=data.pop("email", None))
            ).first()
        if employee_exists:
            call_command()
        self.object = Employee.objects.create(**data)
            return self.object

    def display_result(self, data):
        self.stdout.write("You have successfully created this employee:")
        self.stdout.write("
            Name: f'{self.object.first_name}' '{self.object.last_name}'
            Email: self.object.email
        ")

Enter fullscreen mode Exit fullscreen mode
  • action = "CREATE": we assign the value CREATE to the class attribute action. This will have an importance in the evolved version of the CustomCommand.

  • get_data():
    1) We display with self.stdout.write() a message on the terminal. It is equivalent to the print() but in Django Management Commands we use the stdout.

2) In return we prompt inputs for the user to enter the data.

  • make_changes(): With employee_exists we verify inside the database if this employee already exists. If that employee does not exists already, we create a new employee instance, save this employee instance in the instance attribute self.object and return this new created employee instance.

  • display_result(): With self.stdout.write() we print a message for the user, that the creation of the employee was successful and we display the full name and the email address of the new created employee.


We can improve here, and simplify the display of the full name. We can create a property in the models.py file and use this property to display the full name of the employee.

we would update the models.py file like this:

# employee/models.py

from django.db import models
from django.utils.translation import gettext_lazy as _

class Employee(models.Model):
    email = models.EmailField(_("email address"), unique=True, verbose_name=_("first name"))
    first_name = models.CharField(max_length=100, verbose_name=_("first name"))
    last_name = models.CharField(max_length=100, verbose_name=_("last name"))

    # NEW: add this property:
    @property
    def get_full_name(self):
        return f"{self.first_name} {self.last_name}"
Enter fullscreen mode Exit fullscreen mode
# employee/management/commands/employee_create.py

    def display_result(self, data):
        self.stdout.write("You have successfully created this employee:")
        self.stdout.write("
            Name: self.object.full_name  # simplify this 
            Email: self.object.email
        ")
Enter fullscreen mode Exit fullscreen mode

What do we have to change in CustomCommand?

Until now the action has no effect. We will change that.

# utils/management/commands/custom_command.py

class CustomCommand(BaseCommand):

    help = "Custom BaseCommand"
    action = None

    def __init__(self, *args, **options):
        super().__init__(*args, **options)
        self.object = None
        self.fields = []
        self.fields_to_update = []
        self.update_table = []


    def get_data(self):
        return dict()

    def make_changes(self, data):
        return None

    def display_result(self, data):
        pass

    def create(self):
        validated_data = self.get_data()
        self.make_changes(validated_data)
        self.display_result(validated_data)

def handle(self, *args, **options):
        super().handle(*args, **options)

        if self.action == 'CREATE':
           self.create()

Enter fullscreen mode Exit fullscreen mode

Now every Management Command inherit from CustomCommand with action = 'CREATE' will trigger the create method of the CustomCommand to create an instance.




Advice: If you ever have to call a Management Command, keep in mind if you want to use call_command(), the name of the command you are about to call, has to be Command() otherwise it will throw an error.
💖 💪 🙅 🚩
doridoro
DoriDoro

Posted on May 7, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related