Regis da Silva
Publicado em:

Thu 03 December 2015

←Home

Criando novos comandos no django-admin

Veja aqui como criar o seu próprio comando para ser usado com o django-admin ou manage.py do Django.

O django-admin ou manage.py já tem um bocado de comandos interessantes, os mais utilizados são:

  • startproject - cria novos projetos.
  • startapp - cria novas apps.
  • makemigrations - cria novas migrações baseadas nas mudanças detectadas nos modelos Django.
  • migrate - sincroniza o banco de dados com as novas migrações.
  • createsuperuser - cria novos usuários.
  • test - roda os testes da aplicação.
  • loaddata - carrega dados iniciais a partir de um json, por exemplo, python manage.py loaddata fixtures.json
  • shell - inicializa um interpretador Python interativo.
  • dbshell - acessa o banco de dados através da linha de comando, ou seja, você pode executar comandos sql do banco, por exemplo, diretamente no terminal.
  • inspectdb - retorna todos os modelos Django que geraram as tabelas do banco de dados.
  • runserver - roda o servidor local do projeto Django.

Mas de repente você precisa criar um comando personalizado conforme a sua necessidade. A palavra chave é BaseCommand ou Writing custom django-admin commands.

Começando do começo

Importante: estamos usando Django 1.8.12 e Python 3.

Criando o projeto

Eu usei esta sequência de comandos para criar o projeto.

# Criando djangoproject.
mkdir djangoproject
cd djangoproject

# Criando virtualenv
virtualenv -p python3 .venv

# Ativando o .venv.
source .venv/bin/activate
# Diminuindo o caminho do prompt (opcional)
PS1="(`basename \"$VIRTUAL_ENV\"`)\e[1;34m:/\W\033[00m$ "

# Instalando o Django
pip install django==1.8.12
pip freeze > requirements.txt

# Criando o projeto myproject ...
django-admin.py startproject myproject .
cd myproject

# Criando a app 'core' ...
python ../manage.py startapp core
cd ..

# Editando settings.py"
sed -i "/django.contrib.staticfiles/a\    'myproject.core'," myproject/settings.py

Pronto! Agora nós já temos um projetinho Django funcionando. Note que o nome da app é core.

Criando as pastas

Para criarmos um novo comando precisamos das seguintes pastas:

core
├── management
│   ├── __init__.py
│   ├── commands
│   │   ├── __init__.py
│   │   ├── novocomando.py

No nosso caso, teremos 3 novos comandos, então digite, estando na pasta djangoproject

mkdir -p core/management/commands
touch core/management/__init__.py
touch core/management/commands/{__init__.py,hello.py,initdata.py,search.py}

Sintaxe do novo comando

Importante: estamos usando Django 1.8.12 e Python 3.

O Django 1.8 usa o argparse como parser de argumentos do command, mais informações em module-argparse.

from django.core.management.base import BaseCommand, CommandError
from optparse import make_option

class Command(BaseCommand):
    help = 'Texto de ajuda aqui.'
    option_list = BaseCommand.option_list + (
        make_option('--awards', '-a',
                    action="store_true",
                    help='Ajuda da opção aqui.'),
    )

    def handle(self, **options):
        self.stdout.write('Hello world.')
        if options['awards']:
            self.stdout.write('Awards')

Entendeu? Basicamente o handle é a função que executa o comando principal, no caso o self.stdout.write('Hello world.'), ou seja, se você digitar o comando a seguir ele imprime a mensagem na tela.

$ python manage.py hello
Hello World

--awards é um argumento opcional, você também pode digitar -a.

$ python manage.py hello -a
Hello World
Awards

action="store_true" significa que ele armazena um valor verdadeiro.

Obs: A partir do Django 1.8 os comandos de argumentos opcionais são baseados em **options.

Veja uma outra forma de escrever

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

class Command(BaseCommand):

    def add_arguments(self, parser):
        # Argumento nomeado (opcional)
        parser.add_argument('--awards', '-a',
                            action='store_true',
                            help='Ajuda da opção aqui.')

    def handle(self, *args, **options):
        self.stdout.write('Hello world.')
        if options['awards']:
            self.stdout.write('Awards')

A diferença é que aqui usamos parser.add_argument ao invés de make_option.

hello.py

from django.core.management.base import BaseCommand, CommandError
# minimalista
class Command(BaseCommand):
    help = 'Print hello world'

    def handle(self, **options):
        self.stdout.write('Hello World')

Uso

$ python manage.py hello

initdata.py

Objetivo: Obter alguns filmes de uma api e salvar os dados no banco.

api: omdbapi.com

models.py

from django.db import models

class Movie(models.Model):
    title = models.CharField(u'título', max_length=100)
    year = models.PositiveIntegerField('ano', null=True, blank=True)
    released = models.CharField(u'lançamento', max_length=100, default='', blank=True)
    director = models.CharField('diretor', max_length=100, default='', blank=True)
    actors = models.CharField('atores', max_length=100, default='', blank=True)
    poster = models.URLField('poster', null=True, blank=True)
    imdbRating = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
    imdbID = models.CharField(max_length=50, default='', blank=True)

    class Meta:
        ordering = ['title']
        verbose_name = 'filme'
        verbose_name_plural = 'filmes'

    def __str__(self):
        return self.title

Não se esqueça de fazer

python manage.py makemigrations
python manage.py migrate

admin.py

Vamos visualizar pelo admin.

from django.contrib import admin
from core.models import Movie

admin.site.register(Movie)

Instale o requests

pip install requests

initdata.py

O código a seguir é longo, mas basicamente temos

  • print_red(name) função que imprime um texto em vermelho (opcional)
  • get_html(year) função que lê os dados da api usando requests, e depois escolhe um filme randomicamente a partir de 2 letras
  • get_movie(year) se o dicionário conter {'Response': 'True', ...} então retorna um dicionário do filme localizado
  • save() salva os dados no banco
  • handle(movies, year) este é o comando principal. Busca os filmes várias vezes, conforme definido pela variável movies, e salva os n filmes.
# -*- coding: utf-8 -*- #

import random
import string
import requests
from django.core.management.base import BaseCommand, CommandError
from django.core.exceptions import ValidationError
from optparse import make_option
from core.models import Movie


class Command(BaseCommand):
    help = """Faz o crawler numa api de filmes e retorna os dados.
    Uso: python manage.py initdata
    ou: python manage.py initdata -m 20
    ou: python manage.py initdata -m 20 -y 2015"""
    option_list = BaseCommand.option_list + (
        make_option('--movies', '-m',
                    dest='movies',
                    default=10,
                    help='Define a quantidade de filmes a ser inserido.'),
        make_option('--year', '-y',
                    dest='year',
                    action='store',
                    default=None,
                    help='Define o ano de lançamento do filme.'),
    )

    def print_red(self, name):
        """imprime em vermelho"""
        print("\033[91m {}\033[00m".format(name))

    def get_html(self, year):
        """
        Le os dados na api http://www.omdbapi.com/ de forma aleatoria
        e escolhe um filme buscando por 2 letras
        """

        # Escolhe duas letras aleatoriamente
        letters = ''.join(random.choice(string.ascii_lowercase) for _ in range(2))

        # Se não for definido o ano, então escolhe um randomicamente
        if year is None:
            year = str(random.randint(1950, 2015))
        url = 'http://www.omdbapi.com/?t={letters}*&y={year}&plot=short&r=json'.format(letters=letters, year=str(year))
        return requests.get(url).json()

    def get_movie(self, year, **kwargs):
        """ Retorna um dicionário do filme """

        movie = self.get_html(year)
        j = 1  # contador

        # Faz a validação de Response. Se a resposta for falsa, então busca outro filme.
        while movie['Response'] == 'False' and j < 100:
            movie = self.get_html(year)
            self.print_red('Tentanto %d vezes\n' % j)
            j += 1
        return movie

    def save(self, **kwargs):
        """SALVA os dados"""
        try:
            Movie.objects.create(**kwargs)
        except ValidationError as e:
            self.print_red(e.messages)
            self.print_red('O objeto não foi salvo.\n')

    def handle(self, movies, year, **options):
        """ se "movies" não for nulo, transforma em inteiro """

        self.verbosity = int(options.get('verbosity'))

        if movies is not None:
            movies = int(movies)

        # busca os filmes n vezes, a partir da variavel "movies"
        for i in range(movies):
            # verifica as validações
            m = self.get_movie(year)
            if m['imdbRating'] == "N/A":
                m['imdbRating'] = 0.0

            # Transforma "year" em inteiro
            if "–" in m['Year']:
                m['Year'] = year

            data = {
                "title": m['Title'],
                "year": m['Year'],
                "released": m['Released'],
                "director": m['Director'],
                "actors": m['Actors'],
                "poster": m['Poster'],
                "imdbRating": m['imdbRating'],
                "imdbID": m['imdbID'],
            }

            self.save(**data)

            if self.verbosity > 0:
                self.stdout.write('\n {0} {1} {2}'.format(i + 1, data['year'], data['title']))
        if self.verbosity > 0:
            self.stdout.write('\nForam salvos %d filmes' % movies)

Uso

Usage: python manage.py initdata [options] 

Faz o crawler numa api de filmes e retorna os dados.
     Uso: python manage.py initdata
     ou: python manage.py initdata -m 20
     ou: python manage.py initdata -m 20 -y 2015

search.py

Objetivo: Localizar o filme pelo título ou ano de lançamento.

from django.core.management.base import BaseCommand, CommandError
from optparse import make_option
from core.models import Movie


class Command(BaseCommand):
    help = """Localiza um filme pelo título ou ano de lançamento.
    Uso: python manage.py search -t 'Ted 2'
    ou: python manage.py search -y 2015
    ou: python manage.py search -t 'a' -y 2015"""

    option_list = BaseCommand.option_list + (
        make_option('--title', '-t',
                    dest='title',
                    default=None,
                    help='Localiza um filme pelo título.'),
        make_option('--year', '-y',
                    dest='year',
                    default=None,
                    help='Localiza um filme pelo ano de lançamento.'),
    )

    def handle(self, title=None, year=None, **options):
        """ dicionário de filtros """
        self.verbosity = int(options.get('verbosity'))

        filters = {
            'title__istartswith': title,
            'year': year
        }

        filter_by = {key: value for key, value in filters.items() if value is not None}
        queryset = Movie.objects.filter(**filter_by)

        if self.verbosity > 0:
            for movie in queryset:
                self.stdout.write("{0} {1}".format(movie.year, movie.title))
            self.stdout.write('\n{0} filmes localizados.'.format(queryset.count()))

Uso

Usage: python manage.py search [options] 

Localiza um filme pelo título ou ano de lançamento.
     Uso: python manage.py search -t 'Ted 2'
     ou: python manage.py search -y 2015
     ou: python manage.py search -t 'a' -y 2015

Aqui tem um exemplo legal que eu usei como ideia pra fazer este post.

Mais algumas referências:

Writing custom django-admin commands

Zachary Voase: Fixing Django Management Commands

Adding Custom Commands to manage.py and django-admin.py by dave

Topo
comments powered by Disqus