Django Router
Versões usadas no vídeo: Django 5.1.3, Python 3.12, django-router 1.0.8 e django-extensions 3.2.3.
Veja a documentação
E veja o código no Github
Veja o CustomRouter
Veja o uso em views.py
O Flask, o FastAPI e até o Django Ninja definem rotas com um decorator em cima da função. O Django padrão não tem isso: ele trabalha com views e, separadamente, com um urls.py que liga cada url a cada view. Que tal usar a mesma ideia de decorator de rotas no Django, para renderizar os templates?
É isso que a biblioteca django-router faz: você escreve @router.path() em cima da view e não precisa mais manter o urls.py de cada app.
Nesta dica vamos ver:
- o uso básico, com as apps no mesmo nível da pasta do projeto;
- o problema das rotas geradas para as views de função;
- um
CustomRouterpara o caso em que as apps são subpastas da pasta do projeto (o jeito que eu gosto de trabalhar), gerando as rotas do jeito ideal.
Instalação
pip install django-router
Edite settings.py e coloque django_router em INSTALLED_APPS:
# settings.py
INSTALLED_APPS = [
...
'django_router',
...
]
E, no urls.py principal, importe o router e some router.urlpatterns no fim de urlpatterns:
# urls.py
from django_router import router
# a única vez que você precisa mexer num `urls.py`
urlpatterns = router.urlpatterns
# ou, junto com as urls que já existem
urlpatterns = [
...
] + router.urlpatterns
Pronto. Agora vamos ver isso em detalhe.
Apps no mesmo nível da pasta do projeto
Este é o jeito com que você provavelmente está acostumado: a pasta do projeto (aqui chamada apps, com settings.py e urls.py) e as apps accounts, core e crm ao lado dela.
.
├── accounts
│ └── views.py
├── apps
│ ├── asgi.py
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── core
│ └── views.py
├── crm
│ └── views.py
└── manage.py
Em settings.py, além do django_router, usamos o django_extensions, por causa do comando show_urls que veremos daqui a pouco:
# apps/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd apps
'django_extensions',
'widget_tweaks',
'django_seed',
'django_router',
# my apps
'accounts.apps.AccountsConfig',
'core.apps.CoreConfig',
'crm.apps.CrmConfig',
]
O urls.py principal:
# apps/urls.py
from django.contrib import admin
from django.urls import include, path
from django_router import router
# urlpatterns = router.urlpatterns
urlpatterns = [
path('', include('core.urls', namespace='core')),
path('accounts/', include('accounts.urls')), # without namespace
path('admin/', admin.site.urls),
] + router.urlpatterns
Repare que não existe mais um include('crm.urls'): o urls.py da app crm não é mais usado. No início do vídeo ele aparece assim, para comparação:
# crm/urls.py (não é mais necessário)
from django.urls import path
from apps.crm import views as v
app_name = 'crm'
urlpatterns = [
path('', v.person_list, name='person_list'),
]
Agora as rotas ficam nas próprias views, com @router.path(). No crm/views.py temos o CRUD de Person duas vezes, com views de função e com class based views. O comentário ao lado de cada decorator mostra a rota que o django-router gera:
# crm/views.py
from django_router import router
from django.db.models import Q
from django.shortcuts import redirect, render
from django.urls import reverse_lazy
from django.views.generic import (
CreateView,
DeleteView,
DetailView,
ListView,
UpdateView
)
from .forms import PersonForm
from .mixins import SearchMixin
from .models import Person
@router.path() # /crm/person_list/
def person_list(request):
template_name = 'crm/person_list.html'
object_list = Person.objects.all()
search = request.GET.get('search')
if search:
object_list = object_list.filter(
Q(first_name__icontains=search)
| Q(last_name__icontains=search)
| Q(email__icontains=search)
)
context = {'object_list': object_list}
return render(request, template_name, context)
@router.path() # /crm/person_detail/
# @router.path('person/<int:pk>/')
def person_detail(request, pk):
template_name = 'crm/person_detail.html'
obj = Person.objects.get(pk=pk)
context = {'object': obj}
return render(request, template_name, context)
@router.path() # /crm/person_create/
def person_create(request):
template_name = 'crm/person_form.html'
form = PersonForm(request.POST or None)
if request.method == 'POST':
if form.is_valid():
form.save()
return redirect('crm:person_list')
context = {'form': form}
return render(request, template_name, context)
@router.path() # /crm/person_update/
def person_update(request, pk):
template_name = 'crm/person_form.html'
instance = Person.objects.get(pk=pk)
form = PersonForm(request.POST or None, instance=instance)
if request.method == 'POST':
if form.is_valid():
form.save()
return redirect('crm:person_list')
context = {'form': form}
return render(request, template_name, context)
@router.path() # /crm/person_delete/
def person_delete(request, pk):
template_name = 'crm/person_confirm_delete.html'
obj = Person.objects.get(pk=pk)
if request.method == 'POST':
obj.delete()
return redirect('crm:person_list')
context = {'object': obj}
return render(request, template_name, context)
@router.path() # /crm/person/
class PersonListView(SearchMixin, ListView):
model = Person
paginate_by = 10
@router.path() # /crm/person/<int:pk>/
class PersonDetailView(DetailView):
model = Person
@router.path() # /crm/person/create/
class PersonCreateView(CreateView):
model = Person
form_class = PersonForm
@router.path() # /crm/person/<int:pk>/update/
class PersonUpdateView(UpdateView):
model = Person
form_class = PersonForm
@router.path() # /crm/person/<int:pk>/delete/
class PersonDeleteView(DeleteView):
model = Person
success_url = reverse_lazy('crm:person_list')
O SearchMixin só aplica o filtro de busca na ListView:
# crm/mixins.py
from django.db.models import Q
class SearchMixin:
def get_queryset(self):
queryset = super(SearchMixin, self).get_queryset()
search = self.request.GET.get('search')
if search:
return queryset.filter(
Q(first_name__icontains=search)
| Q(last_name__icontains=search)
| Q(email__icontains=search)
)
return queryset
Como o django-router monta as rotas
- O namespace é o primeiro pedaço do módulo da view:
crm.viewsviracrm. Todas as rotas da app ficam debaixo decrm/e com nomecrm:.... - Para views de função, a rota e o nome são o nome da função:
person_listvira/crm/person_list/, com nomecrm:person_list. - Para class based views, ele usa o nome do model e o tipo da view:
ListViewviraperson/,DetailViewviraperson/<int:pk>/,CreateViewviraperson/create/,UpdateViewviraperson/<int:pk>/update/eDeleteViewviraperson/<int:pk>/delete/. O<int:pk>entra sozinho para as views de um objeto só (SingleObjectMixin, exceto aCreateView). - Se quiser, você pode passar a rota e o nome direto no decorator:
@router.path('person/<int:pk>/', name='person_detail').
Vendo as rotas com show_urls
O comando show_urls, do django-extensions, lista todas as rotas do projeto:
python manage.py show_urls
As rotas do CRM (sem as do Admin) ficam assim:
/crm/person/ crm.views.PersonListView crm:person_list
/crm/person/<int:pk>/ crm.views.PersonDetailView crm:person_detail
/crm/person/<int:pk>/delete/ crm.views.PersonDeleteView crm:person_delete
/crm/person/<int:pk>/update/ crm.views.PersonUpdateView crm:person_update
/crm/person/create/ crm.views.PersonCreateView crm:person_create
/crm/person_create/ crm.views.person_create crm:person_create
/crm/person_delete/ crm.views.person_delete crm:person_delete
/crm/person_detail/ crm.views.person_detail crm:person_detail
/crm/person_list/ crm.views.person_list crm:person_list
/crm/person_update/ crm.views.person_update crm:person_update
As class based views ficaram certinhas. Mas tem uma coisa que eu não gostei: cadê o <int:pk> do person_detail, do person_update e do person_delete de função? Não tem. Como a biblioteca não sabe que a função recebe pk, essas três rotas simplesmente não funcionam. Daria para resolver passando a rota na mão (@router.path('person/<int:pk>/'), comentado no código acima), mas aí perderíamos a graça do decorator sem argumentos.
Apps como subpastas da pasta do projeto
O jeito que eu gosto de trabalhar é com as apps dentro da pasta principal apps, e cada app só com o views.py (o urls.py não é mais necessário):
.
├── apps
│ ├── accounts
│ │ └── views.py
│ ├── core
│ │ └── views.py
│ ├── crm
│ │ └── views.py
│ ├── asgi.py
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py
Agora o módulo da view é apps.crm.views, e o namespace padrão do django-router (o primeiro pedaço) seria apps, o que não queremos. Por isso, mantendo essa estrutura, eu preferi criar um CustomRouter.
Este é o projeto do repositório rg3915/django-router-tutorial. Para rodar:
git clone https://github.com/rg3915/django-router-tutorial.git
cd django-router-tutorial
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python contrib/env_gen.py
python manage.py migrate
python manage.py runserver
# requirements.txt
Django==5.1.3
django-ninja==1.3.0
dj-database-url==2.3.0
django-extensions==3.2.3
django-localflavor==4.0
django-widget-tweaks==1.5.0
Faker==33.0.0
isort==5.13.2
python-decouple==3.8
django-router==1.0.8
Nas apps, o name do apps.py passa a ter o prefixo apps. (por exemplo name = 'apps.crm'), e em settings.py:
# apps/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd apps
'django_extensions',
'widget_tweaks',
'django_router',
# my apps
'apps.accounts.apps.AccountsConfig',
'apps.core.apps.CoreConfig',
'apps.crm.apps.CrmConfig',
]
O código original do Router
O Router da biblioteca fica no __init__.py do pacote django_router. Os métodos que nos interessam são estes (versão 1.0.8):
# django_router/__init__.py (trecho da biblioteca)
class Router:
def __init__(self):
self._namespaces = dict()
def path(self, pattern=None, name=None, **kwargs):
def _wrapper(view):
self._push(path, pattern, view, name, kwargs)
return view
return _wrapper
...
def _push(self, func, pattern, view, name, kwargs):
namespace = view.__module__.split(".")[0]
self._namespaces.setdefault(namespace, []).append(
(func, pattern, view, name, kwargs)
)
def _get_params(self, view, parameter_map):
pattern_parts = []
name_parts = []
if settings.SIMPLE_AUTO_NAMING or not isinstance(view, type):
pattern_parts.append(from_camel(view.__name__))
name_parts.append(from_camel(view.__name__))
else:
...
@property
def urlpatterns(self):
...
urlpatterns.append(path(f"{namespace}/", (paths, namespace, namespace)))
return urlpatterns
router = Router()
_pushguarda cada view decorada, agrupada pelo namespace, que é o elemento[0]do módulo;_get_paramscalcula a rota e o nome: para função, é só o nome da função;urlpatternsmonta ospath()e coloca cada grupo debaixo def"{namespace}/".
O CustomRouter
Crie o arquivo apps/core/custom_router.py com a classe CustomRouter, herdando de Router e sobrescrevendo esses três métodos. As linhas alteradas estão marcadas com # <---:
# apps/core/custom_router.py
# ... (outros imports: veja o arquivo completo no GitHub)
from django_router import Router
# ...
class CustomRouter(Router):
def _push(self, func, pattern, view, name, kwargs):
# Get second element of tuple.
namespace = view.__module__.split(".")[1] # <---
self._namespaces.setdefault(namespace, []).append(
(func, pattern, view, name, kwargs)
)
def _get_params(self, view, parameter_map):
pattern_parts = []
name_parts = []
if settings.SIMPLE_AUTO_NAMING or not isinstance(view, type):
_view = from_camel(view.__name__) # <---
# Replace suffix.
suffix_replacements = {
'_list': '',
'_create': '/add',
'_detail': '/<int:pk>',
'_update': '/<int:pk>/update',
'_delete': '/<int:pk>/delete'
}
for suffix, replacement in suffix_replacements.items():
if _view.endswith(suffix):
_view = _view.replace(suffix, replacement)
break
pattern_parts.append(_view) # <---
name_parts.append(from_camel(view.__name__))
# ... (o resto do método é igual ao da biblioteca)
@property
def urlpatterns(self):
# ...
# "slugify" url
pattern = pattern.replace("_", "-")
# ...
# urlpatterns.append(path(f"{namespace}/", (paths, namespace, namespace)))
urlpatterns.append(path("", (paths, namespace, namespace))) # <---
return urlpatterns
router = CustomRouter()
Código completo: apps/core/custom_router.py
O que mudou:
_push: onde estava[0]eu coloquei[1], porque quero o nome da app, lembrando que ela é uma subpasta.apps.crm.viewsdá o namespacecrm._get_params: para as views de função, escrevi o dicionáriosuffix_replacements, que troca o sufixo do nome da função (verificado comendswith) pela rota correta:_listsome,_createvira/add,_detailvira/<int:pk>,_updatevira/<int:pk>/updatee_deletevira/<int:pk>/delete. Depois faço oappenddo resultado empattern_parts. O nome da rota continua sendo o nome da função. O resto do método permanece igual; só tive que copiá-lo da biblioteca para sobrescrever.urlpatterns: troco_por-na rota, para a url ficar no formato de slug; e, noappendfinal, tirei of"{namespace}/"e deixei"". ComoMODULE_PATH_MAPéTruepor padrão, o caminho do módulo entreappseviews(crm) já entra como prefixo da rota, então o namespace nopathduplicaria ocrm/.
Usando o CustomRouter
No urls.py principal, importe o router do custom_router em vez do django_router:
# apps/urls.py
from django.contrib import admin
from django.urls import include, path
# from django_router import router
from apps.core.custom_router import router
# urlpatterns = router.urlpatterns
urlpatterns = [
path('', include('apps.core.urls', namespace='core')),
path('accounts/', include('apps.accounts.urls')), # without namespace
path('admin/', admin.site.urls),
] + router.urlpatterns
Na app accounts, a SignUpView usa o router com a rota e o nome escritos explicitamente. Daria para usar só @router.path(), mas você pode escrever o nome se quiser:
# apps/accounts/views.py
# from django_router import router
from apps.core.custom_router import router
from django.urls import reverse_lazy
from django.views.generic import CreateView
from apps.accounts.forms import SignupForm
@router.path('signup/', name='signup')
# @router.path()
class SignUpView(CreateView):
form_class = SignupForm
success_url = reverse_lazy('login')
template_name = 'accounts/signup.html'
As rotas de login e logout continuam no apps/accounts/urls.py, com as views prontas do Django:
# apps/accounts/urls.py
from django.contrib.auth.views import LoginView, LogoutView
from django.urls import path
urlpatterns = [
path(
'login/',
LoginView.as_view(template_name='accounts/login.html'),
name='login'
),
path('logout/', LogoutView.as_view(), name='logout'),
]
E o apps/crm/views.py, que é onde mais mudou: o import do router agora vem do custom_router, e todos os decorators ficam só @router.path(). No fim, alguns exemplos de rota escrita do seu jeito:
# apps/crm/views.py
from apps.core.custom_router import router
# ... (outros imports: veja o arquivo completo no GitHub)
@router.path()
def person_list(request):
template_name = 'crm/person_list.html'
object_list = Person.objects.all()
# ...
@router.path()
def person_detail(request, pk):
# ...
# ... (person_create, person_update e person_delete)
@router.path()
class PersonListView(SearchMixin, ListView):
model = Person
paginate_by = 10
@router.path()
class PersonDetailView(DetailView):
model = Person
# ... (PersonCreateView, PersonUpdateView e PersonDeleteView: veja o arquivo completo no GitHub)
@router.path()
def person_export_csv(request):
return HttpResponse('export-person')
@router.path('person/export/csv/')
def person_export_csv2(request):
return HttpResponse('export-person')
@router.path('person/export/excel/')
def person_export_excel(request):
return HttpResponse('export-person')
Código completo: apps/crm/views.py
As views de exportação são só exemplos: person_export_csv mostra a rota automática (que não termina em nenhum dos sufixos do dicionário), e person_export_csv2 e person_export_excel mostram a rota escrita à mão.
O formulário e o model usados pelas views:
# apps/crm/forms.py
from django import forms
from .models import Person
class PersonForm(forms.ModelForm):
required_css_class = 'required'
class Meta:
model = Person
# fields = '__all__'
fields = (
'first_name',
'last_name',
'email',
'address',
'address_number',
'complement',
'district',
'city',
'uf',
'cep',
'country',
'cpf',
'rg',
'cnh',
'active',
)
def __init__(self, *args, **kwargs):
super(PersonForm, self).__init__(*args, **kwargs)
for field_name, field in self.fields.items():
field.widget.attrs['class'] = 'form-control'
self.fields['active'].widget.attrs['class'] = None
# apps/crm/models.py
from django.db import models
from django.urls import reverse_lazy
from apps.core.models import Active, Address, Document, TimeStampedModel
class Person(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})
def get_fields_verbose_names(self):
return [{'name': field.verbose_name, 'value': getattr(self, field.name)} for field in self._meta.fields]
Os models abstratos (TimeStampedModel, Address, Document e Active) estão em apps/core/models.py, e os templates (Bootstrap 4) em apps/crm/templates/crm/, no repositório.
Conferindo as rotas
python manage.py show_urls
Agora as rotas estão do jeito que eu queria (sem as do Admin):
/ apps.core.views.index core:index
/accounts/login/ django.contrib.auth.views.LoginView login
/accounts/logout/ django.contrib.auth.views.LogoutView logout
/accounts/signup/ apps.accounts.views.SignUpView accounts:signup
/crm/person-export-csv/ apps.crm.views.person_export_csv crm:person_export_csv
/crm/person/ apps.crm.views.PersonListView crm:person_list
/crm/person/ apps.crm.views.person_list crm:person_list
/crm/person/<int:pk>/ apps.crm.views.PersonDetailView crm:person_detail
/crm/person/<int:pk>/ apps.crm.views.person_detail crm:person_detail
/crm/person/<int:pk>/delete/ apps.crm.views.PersonDeleteView crm:person_delete
/crm/person/<int:pk>/delete/ apps.crm.views.person_delete crm:person_delete
/crm/person/<int:pk>/update/ apps.crm.views.PersonUpdateView crm:person_update
/crm/person/<int:pk>/update/ apps.crm.views.person_update crm:person_update
/crm/person/add/ apps.crm.views.person_create crm:person_create
/crm/person/create/ apps.crm.views.PersonCreateView crm:person_create
/crm/person/export/csv/ apps.crm.views.person_export_csv2 crm:person_export_csv2
/crm/person/export/excel/ apps.crm.views.person_export_excel crm:person_export_excel
As views de função person_list, person_detail, person_update e person_delete agora geram as mesmas rotas das class based views, com o <int:pk> onde precisa, e person_create virou /crm/person/add/; e os exemplos de exportação aparecem com a rota automática (/crm/person-export-csv/, com hífen) e com as rotas escritas à mão.
Repare que, como o exemplo tem o CRUD duas vezes, as views de função e as class based views geram a mesma url com o mesmo nome. Nesse caso o Django usa a primeira que aparece na lista. Num projeto de verdade você escolheria um dos dois estilos.
Conclusão
Com o django-router as rotas ficam junto das views, com um decorator, como no Flask, no FastAPI e no Django Ninja, e os urls.py das apps deixam de existir. Para views de função e para apps dentro de uma pasta principal, um CustomRouter de poucas linhas deixa as urls exatamente no formato que queremos.