When developing Django projects, you sometimes need to write one-off scripts to automate specific tasks. There are two ways to run these types of commands in Django. The first is to write a plain Python script, which you can then call by running python file_name.py, and the other is to use the django-admin command. These are run by calling Python manage.py command_name.
For this article, I’ll demonstrate with a blog application that has only three database tables: User, Category, and Post.
For the first example, we will try to list all system users using the following script:
from django.contrib.auth import get_user_model User = get_user_model() # retrieve all users users = User.objects.all() # loop through all users for user in users: print(f'user is {user.get_full_name()} and their username is {user.get_username()}')Copy the code
You can name the script list_users.py and run it through python list_users.py, but you get the following error:
django.core.exceptions.ImproperlyConfigured: Requested setting AUTH_USER_MODEL, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings
Copy the code
One might assume that if the script is in Djangos project directory, it will work. However, this is not true either. This is because the script does not know which project the script will apply to. Because we may have multiple projects in a single computer or virtual environment. Therefore, it is important to give the script some context information.
We will do this by modifying the script slightly.
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'projectname.settings')
import django
django.setup()
from django.contrib.auth import get_user_model
User = get_user_model()
users = User.objects.all()
for user in users:
print(f'user is {user.get_full_name()} and their username is {user.get_username()}')
Copy the code
Here, we specify the project’s Settings, and not only that, we can call the django.setup () method. This method configures the Settings, logs, and populates the application registry. In short, we want to make our script aware of our project context.
Please note that the import order is very important, do not adjust, there is a pit.
If you run the script again, all users will print to the terminal without error.
Next, we’ll create an app named Posts by running Django-admin startApp posts.
The application will contain our blog post model
from django.db import models from django.contrib.auth import get_user_model User = get_user_model() # Create your models here. class CommonInfo(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: abstract = True ordering = ('-created_at',) # blog post category class Category(CommonInfo): name = models.CharField(max_length=255) def __str__(self): return self.name # blog post instance class Post(CommonInfo): title = models.CharField(max_length=255) category = models.ForeignKey(Category,related_name='posts',on_delete=models.PROTECT) author = models.ForeignKey(User,related_name='posts',on_delete=models.PROTECT) content = models.TextField() published = models.BooleanField(default=False) def __str__(self): return f'{self.title} by {self.author.get_full_name()}'Copy the code
For this example, we will create an instance of the blog post from the command line. The script name is create_post.py
import os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'commands.settings') import django django.setup() from django.contrib.auth import get_user_model from posts.models import Category, Post User = get_user_model() def select_category(): # retrieve categories. (You can create some examples from the django admin) categories = Category.objects.all().order_by('created_at') print('Please select a category for your post: ') for category in categories: print(f'{category.id}: {category}') category_id = input() category = Category.objects.get(id=category_id) return category def select_author(): # retrieve all users users = User.objects.all() print('Please select an author for your post: ') for user in users: print(f'{user.id}: {user}') user_id = input() user = User.objects.get(id=user_id) return user def create_post(): title = input("Title of your post: ") content = input("Long post content: ") category = select_category() author = select_author() Post(**locals()).save() print('Post created successfully! ') if __name__ == "__main__": create_post()Copy the code
Here, we are creating an instance of a blog post. Notice how do we deal with the ForeignKey relationship? This is to ensure that the object instance of the related database table is assigned to this field.
By running python create_post.py and prompting us for some input.
As mentioned earlier, the django-admin command is executed by running Python manage.py command_name. Migrate and collectstatic. To get a list of available commands, run Python manage.py help. This displays a list of available commands and the Django App folder in which they reside.
To register custom admin commands, add a management \ Commands directory to your Django application folder. In our case, it will be in posts \ Management \ Commands.
Once set up, we can initialize our custom scripts in the commands folder. For the first example, we’ll write a command to mark previously created blog posts as published.
Create a file and name it publish_post.py
from django.core.management.base import BaseCommand, CommandError
from posts.models import Category, Post
class Command(BaseCommand):
help = 'Marks the specified blog post as published.'
# allows for command line args
def add_arguments(self, parser):
parser.add_argument('post_id', type=int)
def handle(self, *args, **options):
try:
post = Post.objects.get(id=options['post_id'])
except Post.DoesNotExist:
raise CommandError(f'Post with id {options["post_id"]} does not exist')
if post.published:
self.stdout.write(self.style.ERROR(f'Post: {post.title} was already published'))
else:
post.published = True
post.save()
self.stdout.write(self.style.SUCCESS(f'Post: {post.title} successfully published'))
Copy the code
Django administrative commands consist of a class named Command, which is derived from BaseCommand.
To receive parameters, the class uses Argparse. The add_arguments method allows our function to accept arguments.
In our case, the function expects a parameter that will be assigned the key post_id
The handle () function then evaluates the input and executes our logic.
In the example above, the desired parameter type is called a positional parameter, which must be supplied to run the function. To do this, we run Python manage.py publish_post 1 (or any publish primary key)
As the name implies, another type of argument, called optional arguments, can be applied to a method, and its absence does not affect the execution of the function.
An example is provided below. We will initialize a file and name it edit_post.py. The code is as follows:
from django.core.management.base import BaseCommand, CommandError
from posts.models import Category, Post
class Command(BaseCommand):
help = 'Edits the specified blog post.'
def add_arguments(self, parser):
parser.add_argument('post_id', type=int)
# optional arguments
parser.add_argument('-t', '--title',type=str, help='Indicate new name of the blog post.')
parser.add_argument('-c', '--content',type=str, help='Indicate new blog post content.')
def handle(self, *args, **options):
title = options['title']
content = options['content']
try:
post = Post.objects.get(id=options['post_id'])
except Post.DoesNotExist:
raise CommandError(f'Post with id {options["post_id"]} does not exist')
if title or content:
if title:
old_title = post.title
post.title = title
post.save()
self.stdout.write(self.style.SUCCESS(f'Post: {old_title} has been update with a new title, {post.title}'))
if content:
post.content = content
post.save()
self.stdout.write(self.style.SUCCESS('Post: has been update with new text content.'))
else:
self.stdout.write(self.style.NOTICE('Post content remains the same as no arguments were given.'))
Copy the code
In this case, we are just editing the blog post title or content. So we can run Python manage.py edit_post 2 -t “new title” to edit only the title
Or python manage.py edit_post -c “new content” only edits the content. If we want to edit the title and content with ‘Python manage.py edit_post 2 -t “new title again” -c “new title again”, we can provide two arguments.