Django Intermedio¶
TODO:
Relación entre modelos¶
Los modelos se pueden conectar, a través de las claves foráneas, con dos tipos de relaciones:
- One-To-Many -> Uno a muchos
- Relacionas un campo de una tabla con varias tablas.
- Many-To-Many -> Machos a muchos
- Relacionas varios campos de una tabla con varias tablas.
Ejemplo de una one-to-many con el proyecto anterior. Creamos una app blog
que va a contener dos modelos, uno categoría y otro post:
- Cada post solo tiene un autor, pero cada autor (usuario) puede tener varias entradas (one-to-many).
- Si se elimina a un autor, se deben de eliminar todos sus post (en cascada).
- Cuando la relación es one-to-many se usa un campo
ForeignKey
(clave foránea). - La relación one-to-many es
User
-Post
.
- Cada post podrá tener varias categorías, y cada categoría puede estar en varios post (many-to-many).
- Cuando la relación es many-to-many se usa un campo
ManyToManyField
- La relación many-to-many es
Category
-Post
.
- Cuando la relación es many-to-many se usa un campo
Así que el models.py
quedaría
from django.db import models
from django.utils.timezone import now
from django.contrib.auth.models import User
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=100, verbose_name='Nombre')
created = models.DateTimeField(auto_now_add=True, verbose_name='Creado')
updated = models.DateTimeField(auto_now=True, verbose_name='Actualizado')
class Meta:
verbose_name = 'categoría'
verbose_name_plural = 'categorias'
ordering = ['-created']
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(max_length=200, verbose_name='Título')
content = models.TextField(verbose_name='Contenido')
published = models.DateTimeField(default=now,
verbose_name='Publicado')
image = models.ImageField(upload_to='blog', null=True, blank=True,
verbose_name='Imagen')
author = models.ForeignKey(User, on_delete=models.CASCADE,
verbose_name='Autor')
categories = models.ManyToManyField(Category, related_name='get_posts',
verbose_name='Categorías')
created = models.DateTimeField(auto_now_add=True, verbose_name='Creado')
updated = models.DateTimeField(auto_now=True, verbose_name='Actualizado')
class Meta:
verbose_name = 'entrada'
verbose_name_plural = 'entradas'
ordering = ['-created']
def __str__(self):
return self.title
Personalizar el Admin¶
El Admin permite ser customizado, para poder tener una mejor UX. Todos los cambios para que surtan efecto tendrán que estar hechos en el admin.py
, ya sea del proyecto o de cada apps.
Añadir más columnas¶
Igual que teníamos el campo readonly_fields
para mostrar campos ocultos, existe otro comando list_display
, al cual se le pasa una tupla con todos las columnas/campos que queremos tener.
Ordenar columnas¶
Para poder ordenador columnas se usa ordering
, donde se le pasa una tupla donde se ordena en función de izquierda a derecha (por ejemplo, primero por autor, luego por fecha de publicación, etc).
Formulario de búsqueda¶
Con search_fields
y una tupla con los campos. Hay que tener cuidado porque por ejemplo, si un campo en realidad se busca por subcampos que tiene dentro, hay que especificarlo y sino dará error. Por ejemplo, si quiero buscar por autor, en realidad quiero buscar por su usuario. O si quiero buscar por categorías, lo que quiero encontrar es el nombre de la categoŕia. La nomemclatura es <campos>__<subcampo>
(2 barras bajas). Por ejemplo:
Campos con fechas¶
Para los campos datetime
y similares, hay que definir una jerarquía de fechas con date_hierarchy
y la tupla con los campos.
Filtros de búsqueda¶
Para filtrar por campos se usa list_filter
seguida de la tupla de campos.
Crear campos propios para los filtros¶
Hay campos que pueden dar problemas, por ejemplo los many-to-many. Para eso, se crea una función auxiliar que devuelva lo que realmente quiero. Luego reescribo el método short_description
pasándole el nombre del nuevo campo.
En la siguiente sección hay un ejemplo final donde se ve esto en el post_categories()
.
Ejemplo con todo¶
Ejemplo en el admin.py
de un blog:
from django.contrib import admin
from .models import Category, Post
# Register your models here.
class CategoryAdmin(admin.ModelAdmin):
readonly_fields = ('created', 'updated')
class PostAdmin(admin.ModelAdmin):
readonly_fields = ('created', 'updated')
list_display = ('title', 'author', 'published', 'post_categories')
ordering = ('author', 'published')
search_fields = ('title', 'content', 'author__username',
'categories__name')
date_hierarchy = 'published'
list_filter = ('author__username', 'categories__name',)
def post_categories(self, obj):
return ', '.join(
[c.name for c in obj.categories.all().order_by('name')])
post_categories.short_description = 'Categorias'
admin.site.register(Category, CategoryAdmin)
admin.site.register(Post, PostAdmin)
Filtros / opciones en los template tags¶
En los template tags se le pueden añadir funcionalidades a las variables que se ponen, la forma es
Formatear fechas¶
Por ejemplo para poner un campo fecha en format DD/MM/YYYY
podríamos hacer
Añadir saltos de línea¶
Bucles for - primera y última iteración¶
Supongamos que queremos sacar todas las catergorías de un post. Esto es una relación many-to-many. Si uso
Devuelve None
, porque la query no se ejecuta. Para que esto no pase haríamos
Pero esto devuele algo que no es legible, lo lógico sería usar un bucle for así
Lo que pasa que esto no los pone separados por coma. Y si pongo la coma en el bucle, también la va a poner en el último elemento. Para que eso no pase existe un campo forloop
, que cuenta entre otras cosas con dos variables forloop.first
y forloop.last
. Así que ahora podríamos hacer
{% for category in post.categories.all %}
{{category.name}}{% if not forloop.last %}, {% end if%}
{% endfor %}
Pasar parámetros a las vistas¶
Todos los parámetros que se pasan en la url, se toman como string
, pero se pueden formatear. Si se manda un parámetro que no existe, debemos lanzar un 404
.
Ejemplo:
- En el
urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.blog, name='blog'),
path('category/<int:category_id>', views.category, name='category'),
]
- En el
views.py
from django.shortcuts import render, get_object_or_404
from .models import Post, Category
# Create your views here.
def blog(request):
template = 'blog/blog.html'
context = {'posts': Post.objects.all()}
return render(request, template, context)
def category(request, category_id):
template = 'blog/category.html'
category = get_object_or_404(Category, id=category_id)
context = {'category': category}
return render(request, template, context)
Buscar inversamente en relaciones¶
Cuando se tiene una relación entre campos, se puede buscar tanto en un sentido como en otro. Veámoslo con un ejemplo:
- Tenemos
Post
relacionado many-to-many conCategory
. - Si quiera buscar todos los post que hay en una categoria podría
- Buscar por cada post y filtrar por categoría.
- Buscar por categoría y sacar todos los posts
- La segunda opción de búsqueda es más eficiente y para hacerla tendría que hacer
- Pero esta forma queda bastante poco intuitiva.
- Para customizar esto usamos el campo
related_name
en el modelo delmodels.py
- Y ahora en el template
Procesador de contexto¶
El contexto en programación se refiere al estado de variables, objetos, etc, y su estado, que hay en un momento concreto. Cuando procesamos una vista, hasta ahora lo que devolvemos es la función render
a la que se le pasa:
- La request -> Que no deja de ser un objeto / variable que contiene información sobre la petición http.
- El template -> El html (y css+js+media) que tiene que cargar.
- El contexto -> Es un diccionario con variables que le pasamos para que puedan ser usadas en el template.
Hay veces en las que se necesita información de una app en distintas apps. Una buena manera es extender el contexto, añadiendo esa info de una app para poder ser consumida en las demás. Para hacer eso, usamos lo que se llama el Procesador de contexto. Para crear uno hacemos:
- En la app de la que queremos compartir su información, creamos un archivo llamado
processors.py
.- Creamos dentro una función, a la que se le pasa la
request
como parámetro. - Dentro creamos un diccionario con información.
- Devolvemos ese diccionario.
- Creamos dentro una función, a la que se le pasa la
- En el
settings.py
debemos de darlo de alta.- Vamos a la sección
TEMPLATES
. - Dentro de está
OPTIONS
, y dentro de esta se encuentracontext_processors
. - Añadimos lo siguiente
'<app>.processors.<funcion>'
.
- Vamos a la sección
Esto se ve más fácil con un ejemplo, donde vamos a añadir las RRSS a una web. Todas las RRSS están en una app social
- Crear app
social
y darla de alta en elsettings.py
-
Modificar su
models.py
.from django.db import models # Create your models here. class Link(models.Model): key = models.SlugField(verbose_name='Nombre clave', max_length=100, unique=True) name = models.CharField(verbose_name='Red social', max_length=200) url = models.URLField(verbose_name='Enlace', max_length=200, null=True, blank=True) created = models.DateTimeField(auto_now_add=True, verbose_name='Creado') updated = models.DateTimeField(auto_now=True, verbose_name='Actualizado') class Meta: verbose_name = 'enlace' verbose_name_plural = 'enlaces' ordering = ['name'] def __str__(self): return self.name
-
Modificar el
admin.py
, hacer las migraciones y migrarlas -
Como no hay vista, ni template pasamos directamente a la fase de procesador de contexto.
-
Crear el
processor.py
-
Darlo de alta en el
settings.py
- Arrancar el server, entrar al Admin y crear un par de objetos con por ejemplo las claves LINK_FACEBOOK, LINK_TWITTER y LINK_INSTAGRAM.
-
Modificar el
base.html
<!-- Pié de página --> <footer class="footer text-faded text-center py-5"> <div class="container"> <p class="m-0"> {% if LINK_TWITTER %} <a href="{{LINK_TWITTER}}" class="link"> <span class="fa-stack fa-lg"> <i class="fa fa-circle fa-stack-2x"></i> <i class="fa fa-twitter fa-stack-1x fa-inverse"></i> </span> </a> {% endif %} {% if LINK_FACEBOOK %} <a href="{{LINK_FACEBOOK}}" class="link"> <span class="fa-stack fa-lg"> <i class="fa fa-circle fa-stack-2x"></i> <i class="fa fa-facebook fa-stack-1x fa-inverse"></i> </span> </a> {% endif %} {% if LINK_INSTAGRAM %} <a href="{{LINK_INSTAGRAM}}" class="link"> <span class="fa-stack fa-lg"> <i class="fa fa-circle fa-stack-2x"></i> <i class="fa fa-instagram fa-stack-1x fa-inverse"></i> </span> </a> {% endif %} </p> <p class="m-0 mbt"> <a href="{% url 'sample' %}" class="link">Política de privacidad</a> · <a href="{% url 'sample' %}" class="link">Aviso legal</a> · <a href="{% url 'sample' %}" class="link">Cookies</a> </p> <p class="m-0 mbt1">© La Caffettiera 2018</p> </div> </footer>
Template Tags propios¶
Es una alternativa al Procesador de Contexto más flexible, pero también consume más recursos:
- En la app que su información sea compartida hay que crear:
- Carpeta
templatetags
- Dentro un
__init__.py
- Un fichero python,
ficherotags.py
, para los template tags .
- Carpeta
-
El
ficherotags.py
debe contener los template de django y el modelo de páginas -
Luego hay que crear un registro de la librería de templates
-
Ahora creamos una función,
<nuevotag>
, con el decorador@register.simple_tag
. - En esa función vamos a poder jugar con las páginas, con por ejemplo
Page.objects.all()
- Para poder usar
<nuevotag>
en un template hay que cargar el ficheroficherotags.py
.
Ejemplo:
- Tenemos los típicos links en el footer sobre las cookies y demás.
- Creamos una app
pages
-
models.py
from django.db import models # Create your models here. class Page(models.Model): title = models.CharField(verbose_name='Título', max_length=200) content = models.TextField(verbose_name='Contenido') created = models.DateTimeField(auto_now_add=True, verbose_name='Creado') updated = models.DateTimeField(auto_now=True, verbose_name='Actualizado') class Meta: verbose_name = 'página' verbose_name_plural = 'páginas' ordering = ['title'] def __str__(self): return self.title
-
views.py
-
urls.py
Proyecto
from django.urls import path, include urlpatterns = [ ... # Path del pages path('page/', include('pages.urls')), ... ]
App pages
-
pages/templatetags/pages_extra.py
-
core/templates/core/base.html
Ordenación directa de páginas¶
Si quisiera dar cierto orden a un tipo de elementos de una app, debemos de:
-
En el
models.py
añadir un campo por ejemplo -
En el
admin.py
-
Ahora en el Admin puedo administrar pesos para obtener esa ordenación.
Edición directa de páginas¶
En el propio procesador de contexto contamos con el usuario de la sesión en django.contrib.auth.context_processors.auth
. Así que si queremos que ciertas partes aparezcan si un usuario está autenticado, podemos hacer en el template base
{% if user.is_authenticated %}
<p><a href="{% url 'admin:<app>_<modelo>_<accion>' <objeto-id>%}">Editar</a></p>
{% endif %}
Con el ejemplo anterior sería base.html
{% if user.is_authenticated %}
<p><a href="{% url 'admin:pages_page_change' page.id %}">Editar</a></p>
{% endif %}
Editor WYSIWYG en el Admin¶
Para añadir un editor wysiswyg en el Admin podemos usar el ckeditor
-
Instalar
ckeditor
-
Añadirlo en las apps instaladas ->
settings.py
-
En el
models.py
de la app donde queremos tener el campo con el editor:- Importamos
ckeditor.fields.RichTextField
- Modificamos el campo deseado
- Hacemos las migraciones
- Importamos
Si queremos redefinir la barra del editor, en el settings.py
al final añadimos
El modo 'Basic'
es el más simple, pero podemos poner None
si queremos tener todas las opciones.
Repo Oficial -> aquí
Docu Oficial -> aquí
En su repo nos viene de ejemplo esto
CKEDITOR_CONFIGS = {
'default': {
'toolbar': 'Custom',
'toolbar_Custom': [
['Bold', 'Italic', 'Underline'],
['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
['Link', 'Unlink'],
['RemoveFormat', 'Source']
]
}
}
Para que el contenido creado en el editor se vea sin problemas en el template, tenemos que ponerle el filtro safe
al tag donde esté este contenido. En el ejemplo anterior en miproyecto/pages/templates/pages/sample.html
Formularios¶
Para usar formularios en una app:
- Crear en la app
forms.py
-
Al igual que con los modelos:
- Importamos el "modelo" ->
from django import forms
- Creamos una clase que herede del "modelo" ->
class MiClaseForm(forms.Form):
- Generamos los campos del formulario, por ejemplo
- Importamos el "modelo" ->
-
En el
views.py
importamos el o los formularios, y los añadimos al contexto de la vista. -
En el template lo dibujamos:
- Simplemente con
{{form}}
ya lo saca, pero totalmente desformateado. - Podemos darle un formato más como párrafos
{{form.as_p}}
. - Como si fuera una lista
- Como una tabla
- Simplemente con
-
Para añadirle el botón de enviar:
- Metemos el formulario dentro de un
<form>
- En el parámetro
action
será la web donde ir / acción a hacer cuando se envíe el formulario - En el
method
el método para enviar el formulario:POST
es el más recomendado.GET
no se recomienda.
- Dentro introducimos:
- Al principio
{% csrf_token %}
por motivos de seguridad. - Al final un
<input>
:- Con
type="submit"
- Y un texto a mostrar
value="Enviar"
.
- Con
- Al principio
- Metemos el formulario dentro de un
-
Validar si el formulario es correcto:
- En la misma vista del
views.py
miramos si accedemos con POST conrequest.method == 'POST'
- Recupero el formulario con
mi_formulario = MiClaseForm(data=request.POST)
- Compruebo si es valido con
mi_forumulario.is_valid()
- Formateo los valores por defecto si alguno está vacío
- Hago una redirección con la función
django.shortcuts.redirect
- Se recomiendo usar la función
django.urls.reverse
para la redirección
- Se recomiendo usar la función
- En la misma vista del
Ejemplo de la app contact
:
forms.py
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(label='Nombre', required=True)
email = forms.EmailField(label='Email', required=True)
content = forms.CharField(label='Contenido', required=True,
widget=forms.Textarea)
views.py
from django.shortcuts import render, redirect
from django.urls import reverse
from .forms import ContactForm
# Create your views here.
def contact(request):
if request.method == 'POST':
contact_form = ContactForm(data=request.POST)
if contact_form.is_valid():
name = request.POST.get('name', '')
email = request.POST.get('email', '')
content = request.POST.get('content', '')
return redirect(reverse('contact') + '?OK')
template = 'contact/contact.html'
context = {
'form': ContactForm(),
}
return render(request, template, context)
contact.html
<!-- Formulario de contacto -->
{% if 'OK' in request.GET %}
<p><strong>Su mensaje se ha reenviado correctamente</strong></p>
{% endif %}
<form action="" method="POST">
{% csrf_token %}
<table>
{{form.as_table}}
</table>
<input type="submit" value="Enviar">
</form>
Maquetar formulario¶
Para poder dar un aspecto visual propio a un formulario es muy sencillo:
- Tenemos que volver a tener en nuestro
form
:- Sus parámetros
action
+method
- El
{% csrf_token %}
al principio - El
<input type='submit' value='Enviar'>
al final
- Sus parámetros
- En cada campo:
- Poner el campo del formulario
- Añadirle despues al final el
{{form.<campo>.errors}}
- Modificar los estilos de cada campo en el
forms.py
:- Dentro de cada campo modificamos el parámetro
widget
.- A este se le pasa un tipo de campo
- Dentro de este tipo, generamos los elementos CSS dentro del diccionario
attrs
.
- Podemos ajustar también el tamaño con
min_length
omax_length
.
- Dentro de cada campo modificamos el parámetro
Ejemplo
contact.html
app contact
<!-- Formulario de contacto -->
{% if 'OK' in request.GET %}
<p><strong>Su mensaje se ha reenviado correctamente</strong></p>
{% endif %}
<form action="" method="POST">
{% csrf_token %}
<div class="form-group">
<label>Nombre *</label>
<div class="input-group">
{{form.name}}
</div>
{{form.name.errors}}
</div>
<div class="form-group">
<label>Email *</label>
<div class="input-group">
{{form.email}}
</div>
{{form.email.errors}}
</div>
<div class="form-group">
<label>Mensaje *</label>
<div class="input-group">
{{form.content}}
</div>
{{form.content.errors}}
</div>
<div class="text-center">
<input type="submit" class="btn btn-primary btn-block py-2" value="Enviar">
</div>
</form>
forms.py
app contact
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(label='Nombre', required=True,
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': 'Escribe tu nombre'
}
), min_length=3, max_length=100)
email = forms.EmailField(label='Email', required=True,
widget=forms.EmailInput(
attrs={
'class': 'form-control',
'placeholder': 'Escribe tu email'
}
), min_length=3, max_length=100)
content = forms.CharField(label='Contenido', required=True,
widget=forms.Textarea(
attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Escribe tu mensaje'
}
), min_length=10, max_length=1000)
Enviar emails¶
Para poder enviar emails hay configurar el correo, y existen muchas opciones.
En este ejemplo para pruebas uso mailtrap.io. Dentro de la demo_inbox
podemos buscar la configuración para Django. La copiamos y la pegamos al final del settings.py
# Email config
EMAIL_HOST = 'smtp.mailtrap.io'
EMAIL_HOST_USER = '9e9652ad27c050'
EMAIL_HOST_PASSWORD = 'a4e2f4e19b02dc'
EMAIL_PORT = '2525'
Para enviar el correo, el views.py
de la app correspondiente:
- Importamos la
django.core.mail.EmailMessage
- Dentro de esta tenemos que poner:
- Asunto
- Cuerpo
- Email de origen
- Email de destino
reply_to=[<objeto-EmailMessage>]
Ejemplo con views.py
de la app Contact anterior
from django.shortcuts import render, redirect
from django.urls import reverse
from django.core.mail import EmailMessage
from .forms import ContactForm
# Create your views here.
def contact(request):
if request.method == 'POST':
contact_form = ContactForm(data=request.POST)
if contact_form.is_valid():
name = request.POST.get('name', '')
email = request.POST.get('email', '')
content = request.POST.get('content', '')
mail = EmailMessage(
'MiAppDjango: Hola',
f'De {name} <{email}>\n\nEscribio:\n\n{content}',
'noreply@inbox.mailtrap.io',
['tuemail@email.com'],
reply_to=[email]
)
try:
mail.send()
except:
return redirect(reverse('contact') + '?FAIL')
else:
return redirect(reverse('contact') + '?OK')
template = 'contact/contact.html'
context = {
'form': ContactForm(),
}
return render(request, template, context)
Grupos, Usuarios y permisos¶
Todo se hace desde el Admin y es muy intuitivo.
Lo lógico es crear primero un grupo con unos permisos concretos (app que puede ver, editar, borrar).
Luego se crea un usuario con su nombre y contraseña. Si queremos que pueda acceder al admin debemos añadirlo como staff
.
Por último para ver si todo funciona, accedemos al admin con el nuevo usuario.
Si hay campos que queremos que aparezcan como solo lectura para cierto grupo, podemos editar el admin.py
de la app con:
- Creamos el método
def get_readonly_fields(self, request, obj=None)
- Dentro filtramos si el usuario pertenece a un grupo o no
- Si pertenece a un grupo devolvemos unos readonly
- Si no pertenece devolvemos otros
Ejemplo:
- Para una app
social
hemos creado un grupo llamadoPersonal
. - No queremos que pueda editar los campos
key
yname
que usaremos para las RRSS. - Su
admin.py
queda como
from django.contrib import admin
from .models import Link
# Register your models here.
class LinkAdmin(admin.ModelAdmin):
readonly_fields = ('created', 'updated')
def get_readonly_fields(self, request, obj=None):
if request.user.groups.filter(name='Personal').exists():
return ('created', 'updated', 'key', 'name')
else:
return ('created', 'updated')
admin.site.register(Link, LinkAdmin)
- Hacemos las migraciones.