Dica 1.1 - Django boilerplate
Versões usadas no vídeo: Python 3.8.7 e Django 3.1.8 (django-boilerplate 1.0.0, abril de 2021).
Na Dica 1 vimos como criar um projeto rapidamente com o boilerplatesimple.sh, um script guardado num gist. Agora ele virou um repositório no GitHub, o django-boilerplate, e ficou mais completo: além do settings.py configurado, o projeto já nasce com três apps:
- accounts: login e logout, da forma mais simples possível.
- core: models abstratos prontos para reaproveitar (UUID, data de criação/modificação, endereço, documentos, ativo) e um exemplo de management command.
- crm: um model
Personde exemplo, que herda os models abstratos docore, já registrado no admin.
No vídeo o repositório estava na versão 1.0.0, com Python 3.8.7 e Django 3.1.8. Depois ele continuou sendo atualizado (a versão desta página, quando foi publicada, usava o Django 4.0.2); aqui descrevemos o que o vídeo mostra.
Pré-requisitos
- Linux (o script roda em Unix; ele usa o
sed -ido GNU e ocp -rdo Linux, por isso não funciona sem ajustes no macOS). - Python 3 e git.
Pacotes usados
- Python 3.8.7
- Django 3.1.8
- dj-database-url
- django-extensions
- django-localflavor
- isort
- python-decouple
O script também instala o ipdb para depuração, mas não o coloca no requirements.txt.
Baixando o boilerplate
O script copia vários arquivos do repositório, por isso ele espera encontrar o clone em /tmp/django-boilerplate. Clone nesse caminho e copie o script para a pasta onde você quer criar o projeto:
git clone https://github.com/rg3915/django-boilerplate.git /tmp/django-boilerplate
# Copy this file to your actual folder.
cp /tmp/django-boilerplate/boilerplatesimple.sh .
Se a pasta /tmp/django-boilerplate já existir (de um uso anterior), o git clone reclama:
fatal: destination path '/tmp/django-boilerplate' already exists and is not an empty directory.
Nesse caso, basta seguir com o cp (ou apagar a pasta antiga e clonar de novo, para pegar a versão mais recente).
Para não ter que lembrar dos comandos, o README sugere um alias:
alias bsimple='git clone https://github.com/rg3915/django-boilerplate.git /tmp/django-boilerplate;
cp /tmp/django-boilerplate/boilerplatesimple.sh .
printf "Type:\n`tput setaf 2`source boilerplatesimple.sh myproject\n"'
Criando o projeto
Rode o script com source, passando o nome do projeto (se não passar nada, o nome é myproject):
source boilerplatesimple.sh myproject
Primeiro ele pergunta a versão do Django. Enter escolhe o padrão, a 3.1.8:
Select Django version:
2 - 2.2.20
3 - 3.1.8
Choose from 2, 3 [3]:
>>> You chose Django 3.1.8.
>>> The name of the project is 'myproject'.
>>> Creating .gitignore
>>> Creating README.md
>>> Creating virtualenv
>>> .venv is created
>>> activate the .venv
>>> Installing the Django
Ele cria e ativa a .venv, instala os pacotes, gera o .env e cria o projeto e os apps:
>>> Creating contrib/env_gen.py
>>> Running contrib/env_gen.py
Success!
Type: cat .env
>>> Creating the project 'myproject' ...
>>> Creating the app 'core' ...
>>> Creating the app 'accounts' ...
>>> Creating the app 'crm' ...
>>> Editing settings.py
Replace LANGUAGE_CODE to pt-br? [Y/n]
Responda Y (ou Enter) para trocar o LANGUAGE_CODE para pt-br e o TIME_ZONE para America/Sao_Paulo. Em seguida ele escreve os arquivos dos apps:
>>> Editing urls.py
>>> Editing accounts/urls.py
>>> Editing core/models.py
>>> Editing core/urls.py
>>> Editing core/views.py
>>> Editing management/commands.
>>> Editing crm/admin.py
>>> Editing crm/forms.py
>>> Editing crm/models.py
>>> Editing crm/urls.py
Por fim roda makemigrations e migrate (repare no crm.0001_initial) e pergunta pelo superusuário:
Applying crm.0001_initial... OK
Applying sessions.0001_initial... OK
Create superuser? [Y/n]
>>> Creating a 'admin' user ...
>>> The password must contain at least 8 characters.
>>> Password suggestions: demodemo
Password:
O usuário é sempre admin; digite a senha (no vídeo, uma senha simples, só para desenvolvimento). O script termina com os avisos de não publicar o .env e move o boilerplatesimple.sh para /tmp.
O requirements.txt gerado lista só os pacotes principais: Django==3.1.8 na primeira linha e, em seguida, dj-database-url, django-extensions, django-localflavor, isort e python-decouple, com as versões que o pip instalou no momento (o script as pega do pip freeze).
A estrutura do projeto
.
├── contrib
│ └── env_gen.py
├── manage.py
├── myproject
│ ├── asgi.py
│ ├── accounts
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── models.py
│ │ ├── tests.py
│ │ ├── urls.py
│ │ └── views.py
│ ├── core
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── management
│ │ │ └── commands
│ │ │ ├── hello.py
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ ├── tests.py
│ │ ├── urls.py
│ │ └── views.py
│ ├── crm
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── forms.py
│ │ ├── models.py
│ │ ├── tests.py
│ │ ├── urls.py
│ │ └── views.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── README.md
└── requirements.txt
Os apps ficam dentro da pasta do projeto, por isso são importados como myproject.core, myproject.crm etc.
O .env
O contrib/env_gen.py gera uma SECRET_KEY e uma senha aleatórias e grava o .env, já com exemplos comentados para banco e e-mail:
# contrib/env_gen.py
"""
Python SECRET_KEY generator.
"""
import random
chars = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!?@#$%^&*()"
size = 50
secret_key = "".join(random.sample(chars, size))
chars = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!?@#$%_"
size = 20
password = "".join(random.sample(chars, size))
CONFIG_STRING = """
DEBUG=True
SECRET_KEY=%s
ALLOWED_HOSTS=127.0.0.1,.localhost,0.0.0.0
#DATABASE_URL=postgres://USER:PASSWORD@HOST:PORT/NAME
#DB_NAME=
#DB_USER=
#DB_PASSWORD=%s
#DB_HOST=localhost
#DEFAULT_FROM_EMAIL=
#EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
#EMAIL_HOST=localhost
#EMAIL_PORT=
#EMAIL_HOST_USER=
#EMAIL_HOST_PASSWORD=
#EMAIL_USE_TLS=True
""".strip() % (secret_key, password)
# Writing our configuration file to '.env'
with open('.env', 'w') as configfile:
configfile.write(CONFIG_STRING)
print('Success!')
print('Type: cat .env')
O settings.py
O script copia um settings.py modelo do repositório e troca o nome do projeto, a versão e, conforme a versão do Django escolhida, o BASE_DIR (com pathlib no Django 3.1, com os.path no 2.2). Com Django 3.1.8 e pt-br, o resultado é:
# myproject/settings.py
# ... (veja o arquivo completo no GitHub)
from pathlib import Path
from decouple import Csv, config
from dj_database_url import parse as dburl
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# ...
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv())
# ...
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
default_dburl = 'sqlite:///' + str(BASE_DIR / 'db.sqlite3')
DATABASES = {
'default': config('DATABASE_URL', default=default_dburl, cast=dburl),
}
# ...
USE_THOUSAND_SEPARATOR = True
DECIMAL_SEPARATOR = ','
# ...
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR.joinpath('staticfiles')
LOGIN_URL = '/admin/login/'
LOGIN_REDIRECT_URL = 'core:index'
# LOGOUT_REDIRECT_URL = 'core:index'
Código completo: settings.py (modelo, com os marcadores que o script substitui)
Os destaques: SECRET_KEY, DEBUG e ALLOWED_HOSTS vêm do .env (python-decouple); o banco vem de DATABASE_URL, com SQLite como padrão (dj-database-url); separador de milhar e vírgula decimal para o formato brasileiro; e o login redireciona para core:index.
As URLs
O urls.py principal inclui as URLs dos três apps. As de accounts ficam sem namespace, para que os nomes login e logout funcionem direto (é o que o Django espera):
# myproject/urls.py
from django.urls import include, path
from django.contrib import admin
urlpatterns = [
path('', include('myproject.core.urls', namespace='core')),
path('accounts/', include('myproject.accounts.urls')), # without namespace
path('crm/', include('myproject.crm.urls', namespace='crm')),
path('admin/', admin.site.urls),
]
App accounts: login e logout
Usa as views prontas do Django:
# myproject/accounts/urls.py
from django.contrib.auth.views import LoginView, LogoutView
from django.urls import path
urlpatterns = [
path('login/', LoginView.as_view(), name='login'),
path('logout/', LogoutView.as_view(), name='logout'),
]
App core
Views e URLs
Uma view simples, que devolve um HTML direto, e uma versão comentada com template e login_required, para quando você criar o seu index.html:
# myproject/core/views.py
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import render
# @login_required
def index(request):
return HttpResponse('<h1>Django</h1><p>Página simples.</p>')
# @login_required
# def index(request):
# template_name = 'index.html'
# return render(request, template_name)
# myproject/core/urls.py
from django.urls import path
from myproject.core import views as v
app_name = 'core'
urlpatterns = [
path('', v.index, name='index'),
]
Models abstratos
O core/models.py não cria nenhuma tabela: todos os models são abstratos (abstract = True), feitos para serem herdados pelos models dos outros apps. Cada um acrescenta um grupo de campos:
UuidModel: umuuidúnico.TimeStampedModel:createdemodified, preenchidos automaticamente.CreatedBy: quem criou o registro (ForeignKeyparaUser).Address: endereço completo, com a UF usando os estados dodjango-localflavor.Document: CPF, RG e CNH.Active:activeeexist_deleted(para "deletar" sem apagar do banco).
# myproject/core/models.py
import uuid
from django.contrib.auth.models import User
from django.db import models
from localflavor.br.br_states import STATE_CHOICES
class UuidModel(models.Model):
uuid = models.UUIDField(unique=True, editable=False, default=uuid.uuid4)
class Meta:
abstract = True
# ... (TimeStampedModel, CreatedBy, Address e Document: veja o arquivo completo no GitHub)
class Active(models.Model):
active = models.BooleanField('ativo', default=True)
exist_deleted = models.BooleanField(
'existe/deletado',
default=True,
help_text='Se for True o item existe. Se for False o item foi deletado.'
)
class Meta:
abstract = True
Código completo: models.py
Management command
O core também traz um exemplo de comando personalizado do manage.py, com um argumento opcional:
# myproject/core/management/commands/hello.py
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Print hello world.'
def add_arguments(self, parser):
# Argumento nomeado
parser.add_argument(
'--awards', '-a',
action='store_true',
help='Ajuda da opção awards.'
)
def handle(self, *args, **options):
self.stdout.write('Hello world.')
if options['awards']:
self.stdout.write('Awards')
python manage.py hello -a
Hello world.
Awards
App crm: o exemplo
O Person herda de cinco models abstratos do core e só declara nome, sobrenome e e-mail. Todos os outros campos (uuid, criado em, endereço, CPF, ativo...) vêm das classes herdadas:
# myproject/crm/models.py
from django.db import models
from django.urls import reverse_lazy
from myproject.core.models import (
Active,
Address,
Document,
TimeStampedModel,
UuidModel
)
class Person(UuidModel, TimeStampedModel, Address, Document, Active):
first_name = models.CharField('nome', max_length=50)
last_name = models.CharField('sobrenome', max_length=50, null=True, blank=True) # noqa E501
email = models.EmailField(null=True, blank=True)
class Meta:
ordering = ('first_name',)
verbose_name = 'pessoa'
verbose_name_plural = 'pessoas'
@property
def full_name(self):
return f'{self.first_name} {self.last_name or ""}'.strip()
def __str__(self):
return self.full_name
# def get_absolute_url(self):
# return reverse_lazy('crm:person_detail', kwargs={'pk': self.pk})
# myproject/crm/admin.py
from django.contrib import admin
from .models import Person
@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
list_display = ('__str__', 'email', 'active')
# readonly_fields = ('slug',)
# list_display_links = ('name',)
search_fields = ('first_name', 'last_name', 'email')
list_filter = ('active',)
# date_hierarchy = 'created'
# ordering = ('-created',)
# actions = ('',)
# myproject/crm/forms.py
from django import forms
from .models import Person
class PersonForm(forms.ModelForm):
class Meta:
model = Person
fields = '__all__'
# myproject/crm/urls.py
from django.urls import path
from myproject.crm import views as v
app_name = 'crm'
urlpatterns = [
# path(),
]
Rodando
python manage.py runserver
Entre em http://localhost:8000/admin/ com o usuário admin. O admin (em português, por causa do pt-br) mostra Usuários, Grupos e, no app CRM, Pessoas. No vídeo é cadastrada uma pessoa de exemplo, e a lista mostra as colunas Pessoa, Email e Ativo, a busca e o filtro "Por ativo".
Este passa a ser o ponto de partida dos projetos das próximas dicas.
Observação: o django-admin.py usado pelo script foi removido no Django 4.0 (o comando atual é django-admin), e o USE_L10N deixou de existir no Django 5.0. As versões mais novas do django-boilerplate já tratam disso.