Czasami potrzebujemy wykonać jakieś proste, powtarzalne czynności, które można łatwo zautomatyzować. Django posiada mechanizm, który umożliwia dodawanie nowych komend uruchamianych przez manage.py. Wywołanie komendy będzie miało postać:
./manage.py customcommand --with-parameter
Dodatkowo możemy w ten sposób zaprogramować jakieś zadania uruchamiane cyklicznie poprzez crona. Dzięki temu nie musimy wywoływać adresu url, tylko uruchamiamy polecenie z linii komend.
Pierwsza komenda
Najprostszym przykładem komendy będzie wypisanie wiadomości na konsolkę. Dla przykładu niech projekt nazywa się sample a moduł blog. W katalogu sample/blog/management/commands tworzymy plik blog.py. Nazwa pliku jest nazwą komendy. Plik będzie posiadał klasę Command dziedziczącą po BaseCommand. Metoda handle będzie odpowiedzialna za obsłużenie wywołanej komendy. Przykładowy plik wygląda następująco:
from django.core.management import BaseCommand | |
class Command(BaseCommand): | |
def handle(self, *args, **options): | |
self.stdout.write("Basic command") |
Użycie self.stdout.write ułatwi późniejsze testowanie komendy, które jest opisane w dalszej części artykułu
Aby sprawdzić poprawność działania komendy, wywołujemy z linii komend ./manage.py blog. W wyniku na konsoli powinniśmy ujrzeć napis: Basic command.
Przekazywanie argumentów
Każda komenda może przyjmować argumenty, które będą modyfikować jej działanie. Załóżmy, że chcemy mieć kontrolę nad tym czy zostanie wypisany dodatkowy tekst oraz ile razy zostanie on wypisany. Pierwszym krokiem będzie dodanie parametru mówiącego o tym czy wyświetlić dodatkowy tekst. Do klasy należy dodać metodę add_arguments, która definiuje jakie argumenty będzie przyjmować komenda. Dla przykładowego argumentu powinna ona wyglądać następująco:
def add_arguments(self, parser): | |
parser.add_argument('-d', '--display', | |
action='store_true', | |
default=False, | |
dest='show', | |
help='display text') |
Pierwszy argument definiuje skróconą wersję parametru, kolejny długą. Możemy więc dodać parametr na dwa sposoby:
./manage.py blog -d #- lub - ./manage.py blog --display
Kolejny parametr definiuje jaka akcja zostanie wykonana jeśli ten parametr się pojawi. Domyślnie wartość jest po prostu zapisywana w pamięci, jednak w tym przypadku chcielibyśmy otrzymać wartość True jeśli parametr się pojawi oraz False jeśli go nie ma. Akcja store_true powoduje zapisanie True jeśli parametr się pojawi (analogicznie istnieje akcja store_false) a wartość domyślna (default=False) powoduje zapisanie False do parametru jeśli go nie ma. Parametr dest definiuje nazwę opcji, pod którą będzie zapisana wartość. Ostatni parametr, help definiuje opis parametru.
Po dodaniu argumentu komendy możemy przejść do użycia go w metodzie handle. Należy zmodyfikować metodę do następującej postaci:
def handle(self, *args, **options): | |
print("Basic command") | |
if options['show']: | |
self.stdout.write("Optional text") |
Dodane zostały linie 4 i 5, w których testujemy opcję show. W przypadku gdy show jest prawdą (pojawił się parametr -d lub –display) wyświetlany jest tekst.
Mając cały kod gotowy, możemy uruchomić komendę i sprawdzić, czy działa poprawnie.
Kolejnym krokiem będzie dodanie opcji pozwalającej zdefiniować, ile razy ma zostać wyświetlona linijka tekstu. Do metody add_arguments należy dodać następujący kod:
parser.add_argument("-r", "--repeat", help="how many times repeat text", default=1, type=int)
Parametry są analogiczne jak w poprzednim przykładzie, z tą różnicą, że zostawiamy domyślną akcję, oraz definiujemy typ parametru na liczbę całkowitą (type=int).
Metodę handle modyfikujemy poprzez dodanie pętli:
def handle(self, *args, **options): | |
print("Basic command") | |
if options['show']: | |
for i in range(0, options['repeat']): | |
self.stdout.write("Optional text") |
Wykonanie polecenia:
./manage.py blog -d -r 3
Spowoduje wyświetlenie tekstu:
Basic command Optional text Optional text Optional text
Tekst pomocy
W przypadku gdy uruchomimy skrypt manage.py bez argumentów, wyświetlona zostanie lista dostępnych akcji. Dodana przez nas akcja również będzie uwzględniona na wydruku. Uruchomienie manage.py help blog wyświetli listę argumentów wraz z opisem. Na liście pojawią się wszystkie argumenty przyjmowane przez Django, takie jak settings, version, oraz zdefiniowane przez nas wraz z opisem podanym w parametrze help. Przykładowy wydruk z terminala:
./manage.py help blog [DEBUG] 2016-01-10 18:16:28,293: Initialization usage: manage.py blog [-h] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [-r REPEAT] [-d] optional arguments: -h, --help show this help message and exit --version show program's version number and exit -v {0,1,2,3}, --verbosity {0,1,2,3} Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output --settings SETTINGS The Python path to a settings module, e.g. "myproject.settings.main". If this isn't provided, the DJANGO_SETTINGS_MODULE environment variable will be used. --pythonpath PYTHONPATH A directory to add to the Python path, e.g. "/home/djangoprojects/myproject". --traceback Raise on CommandError exceptions --no-color Don't colorize the command output. -r REPEAT, --repeat REPEAT how many times repeat text -d, --display display text
Testowanie komendy
Do stworzonej komendy powinniśmy dodać testy, aby upewnić się, że działa ona prawidłowo. Zacznijmy od podstawowego testu komendy bez parametrów. Klasa testowa będzie wyglądać następująco:
from django.core.management import call_command | |
from django.test import TestCase | |
from django.utils.six import StringIO | |
class BlogCommandTest(TestCase): | |
def test_basic_command_output(self): | |
out = StringIO() | |
call_command('blog', stdout=out) | |
self.assertIn('Basic command\n', out.getvalue()) |
Uruchomienie testu powinno pokazać, że komenda działa prawidłowo. Możemy dodać jeszcze kilka testów sprawdzających zachowanie komendy z parametrami:
def test_command_with_display_parameter(self): | |
out = StringIO() | |
call_command('blog', '--display', stdout=out) | |
self.assertEquals('Basic command\nOptional text\n', out.getvalue()) | |
def test_command_with_display_and_repeatparameter(self): | |
out = StringIO() | |
call_command('blog', '--display', '-r 3', stdout=out) | |
self.assertEquals('Basic command\nOptional text\nOptional text\nOptional text\n', out.getvalue()) |
Podsumowanie
Mechanizm komend pozwala zautomatyzować powtarzalne czynności. Czas poświęcony na implementację mechanizmu komend, pozwala zaoszczędzić dużo czasu przy każdym późniejszym uruchomieniu komendy. Więcej na temat dodawania własnych komend można przeczytać w dokumentacji Django pod adresem: https://docs.djangoproject.com/en/1.9/howto/custom-management-commands/. Dokumentacja parametrów parsowania argumentów znajduje się pod adresem: https://docs.python.org/3/library/optparse.html#module-optparse
Pełny kod klasy obsługującej komendę
from django.core.management import BaseCommand | |
class Command(BaseCommand): | |
def add_arguments(self, parser): | |
parser.add_argument("-r", "--repeat", help="how many times repeat text", default=1, type=int) | |
parser.add_argument('-d', '--display', | |
action='store_true', | |
dest='show', | |
default=False, | |
help='display text') | |
def handle(self, *args, **options): | |
self.stdout.write("Basic command") | |
if options['show']: | |
for i in range(0, options['repeat']): | |
self.stdout.write("Optional text") |