Django Avanzado¶
Incluir una plantilla html en todas las plantillas de la app¶
Si quieres que en todas las plantillas de la app aparezca cierto código html común, debes de:
- Crear el directorio
<proyecto>/<app>/templates/includes
. - Dentro de
includes
crear tu plantilla html común para los demás. - En cada plantilla html de tu app, en el dentro de tu
{% block content %}
, añadirla con{% include '<app>/includes/<template>.html' %}
Definir estructuras en los urlspattern¶
Suponte que tienes varias apps, donde hay una vista que se llama igual, por ejemplo create
. En el urls.py
le tienes puesto name=<app>_create
para que no coincidan. Para no tener que hacer eso, se puede usar una reestructura de los urlpatterns.
<app>/urls.py
-> Le pones el nombre que quieras al urlpatterns
, pero ahora en vez de ser una lista como antes, va a ser una tupla, que tiene en su primer miembro el urlpatterns
, y en el segundo un nombre de referencia. Ahora puedes asignar nombres genéricos en el urlpatterns
, como es el caso de create
que lo vas a usar en más urls.py
de otras apps.
from django.urls import path
from .views import PageListView, PageDetailView, PageCreate
pages_patterns = ([
path('', PageListView.as_view(), name='pages'),
path('<int:pk>/<slug:slug>/', PageDetailView.as_view(), name='page'),
path('create/', PageCreate.as_view(), name='create'),
], 'pages')
<proyecto>/<proyecto>/urls.py
-> Importa tu urlpatterns
tuneado (en este caso pages_urlpatterns
), y lo incluyes directamente en la función include()
.
from django.contrib import admin
from django.urls import path, include
from pages.urls import pages_patterns
urlpatterns = [
path('', include('core.urls')),
path('pages/', include(pages_patterns)),
path('admin/', admin.site.urls),
]
<..>/template/<..>
-> Lo único que te falta es que ahora, en todas las templates que lo enlazas con {% urls '<vista>' ... %}
debes de poner {% urls '<urls de donde vengo>:<vista>' ... %}
. En este ejemplo sería:
{% urls 'pages:pages' ... %}
Cuando es un template fuera de la app, que enlaza desde elurls.py
del proyecto a la app.{% urls 'pages:create' ... %}
Cuando es un template dentro de la app, que enlaza desde elurls.py
de la propia app.
Vistas Basadas en Clases¶
En el views.py
importo el modelo que quiera, por ejemplo TemplateView
. Hago que hereden de mi modelo, defino su template, y modifico el contexto con la función get_context_data()
.
from django.views.generic.base import TemplateView
class HomePageView(TemplateView):
template_name = 'core/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Mi chachi web'
return context
class SamplePageView(TemplateView):
template_name = 'core/sample.html'
En el urls.py
importo las vistas y las llamo con la función as_view()
.
from django.urls import path
from .views import HomePageView, SamplePageView
urlpatterns = [
path('', HomePageView.as_view(), name="home"),
path('sample/', SamplePageView.as_view(), name="sample"),
]
Si la modificación en el contexto es poca, podría hacer en el views.py
from django.views.generic.base import TemplateView
from django.shortcuts import render
class HomePageView(TemplateView):
template_name = 'core/home.html'
def get(self, request, *args, **kwargs):
context = {'title': 'Mi chachi web'}
return render(request, self.template_name, context)
ListView¶
Cuando tenemos un modelo, y queremos ver listados varios objetos, una buena opción es usar una ListView
. Si solo quisieramos ver uno en detalle, deberías de optar por DetailView
.
views.py
-> Tendremos que importar ListView
, dentro de la clase definirle que modelo de la bbdd vamos a usar.
from django.views.generic.list import ListView
from .models import Page
# Create your views here.
class PageListView(ListView):
model = Page
urls.py
-> Modificar las urls
from django.urls import path
from .views import PageListView
urlpatterns = [
path('', PageListView.as_view(), name='pages'),
]
templates/<app>/<modelo>_list.html
-> Veremos que si arrancamos el server da un error, y es porque esta vista usa un template llamado <modelo>_list.html
. Así que en nuestra carpeta de templates, deberemos de llamar así a su template html. Además, donde tengamos el for
para mostrar todos los elementos, debemos de usar el objeto object
o el nombre del modelo (en este caso page
)
# Este sería el genérico
{% for page in object_list %}
# Este sería más específico, pero para el caso es igual
{% for page in page_list %}
Paginación¶
En las ListView
puedes mostrar solo cierto número de objetos en lugar de todo a la vez, y poner un índice típico de
<- Anterior 1, 2, ... Siguiente ->
<app>/views.py
-> Donde tengas la ListView
tienes que añadirle el campo paginated_by
y ponerle un número. Por ejemplo, paginated_by = 3
son 3 elementos por página.
from django.views.generic.list import ListView
from registration.models import Profile
# Create your views here.
class ProfileListView(ListView):
model = Profile
template_name = 'profiles/profile_list.html'
paginate_by = 3
<app>/templates/<app>/<template_name>
-> En el template tienes que hacerle el menú para navegar entre páginas. En el ejemplo se adjunta uno hecho con bootstrap.
<!-- Menú de paginación -->
{% if is_paginated %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item ">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">«</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">«</a>
</li>
{% endif %}
{% for i in paginator.page_range %}
<li class="page-item {% if page_obj.number == i %}active{% endif %}">
<a class="page-link" href="?page={{ i }}">{{ i }}</a>
</li>
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item ">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">»</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">»</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
En este ejemplo te saldrá un warning diciendo que no están ordenados. Para que no salga, te vas al models.py
de la app donde usas ese modelo de la ListView
, le creas una clase Meta
y le pones el atributo ordering
indicandole en una lista porque que campos los ordenas.
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
avatar = models.ImageField(upload_to=custom_upload_to, null=True,
blank=True)
bio = models.TextField(null=True, blank=True)
link = models.URLField(max_length=200, null=True, blank=True)
class Meta:
ordering = ['user__username']
DetailView¶
Para ver en detalle un único elemento de la bbdd, se suele usar DetailView
views.py
-> Importar DetailView
, así como el objeto del modelo de la bbdd y definirlo dentro de la clase.
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from .models import Page
# Create your views here.
class PageListView(ListView):
model = Page
class PageDetailView(DetailView):
model = Page
urls.py
-> Ojo en el pattern, que usa pk
como índice.
from django.urls import path
from .views import PageListView, PageDetailView
urlpatterns = [
path('', PageListView.as_view(), name='pages'),
path('<int:pk>/<slug:slug>/', PageDetailView.as_view(), name='page'),
]
template/<app>/<modelo>_detail.html
-> DetailView
usa un template html llamado <modelo>_detail.html
, así que debemos nombrar así al nuestro para que lo lea. De nuevo, tenemos que usar object
dentro del template o el nombre del modelo (page
en este caso).
Vistas CRUD¶
Son un tipo de vistas basadas en clases, que están pensadas para interactuar con la bbdd.
Create Read Update Delete
CreateView - Vista CRUD¶
Sirve para poder crear una página donde un admin pueda editar / crear un objeto de la bbdd.
views.py
-> Importa CreateView
, añade el modelo de la bbdd así como los campos a editar. El campo sucess_url
es la vista que se pondrá cuando hayas completado el formulario
from django.shortcuts import render, get_object_or_404, get_list_or_404
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView
from django.urls import reverse_lazy
from .models import Page
# Create your views here.
class PageListView(ListView):
model = Page
class PageDetailView(DetailView):
model = Page
class PageCreate(CreateView):
model = Page
fields = ['title', 'content', 'order']
success_url = reverse_lazy('pages:pages')
urls.py
-> Importa la vista.
from django.urls import path
from .views import PageListView, PageDetailView, PageCreate
pages_patterns = ([
path('', PageListView.as_view(), name='pages'),
path('<int:pk>/<slug:slug>/', PageDetailView.as_view(), name='page'),
path('create/', PageCreate.as_view(), name='create'),
], 'pages')
template/<app>/<modelo>_form.html
-> CreateView
usa una plantilla llamada <modelo>_form.html
, así que debes de renombrar así la tuya.
UpdateView - Vista CRUD¶
Es muy parecida a la anterior, pero ahora lo que vas a hacer es actualizar en lugar de crear.
views.py
-> La importas y solo necesitas pasarle 3 atributos, el modelo, los campos y por último el template_name_suffix
. Con este campo se le indica que archivo tiene que leer cuando se ejecute (por defecto se pone _update_form
). Para que devuelva una página al ser actualizado, habría que usar como antes el atributo succes_url
. Si quieres que vuelva al mismo sitio (como en el ejemplo), sobreescribes la función get_success_url
y devuelves el mismo objeto (si le añades algún parámetro, luego en el template puedes hacer que le indique al usuario que la actualización ha sido correcta).
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse_lazy
from .models import Page
# Create your views here.
class PageListView(ListView):
model = Page
class PageDetailView(DetailView):
model = Page
class PageCreateView(CreateView):
model = Page
fields = ['title', 'content', 'order']
success_url = reverse_lazy('pages:pages')
class PageUpdateView(UpdateView):
model = Page
fields = ['title', 'content', 'order']
template_name_suffix = '_update_form'
def get_success_url(self):
return reverse_lazy('pages:update', args=[self.object.id]) + '?ok'
urls.py
-> Debes de pasarle un pk
para poder saber cual objeto editar.
from django.urls import path
from .views import PageListView, PageDetailView, PageCreateView, PageUpdateView
pages_patterns = ([
path('', PageListView.as_view(), name='pages'),
path('<int:pk>/<slug:slug>/', PageDetailView.as_view(), name='page'),
path('create/', PageCreateView.as_view(), name='create'),
path('update/<int:pk>/', PageUpdateView.as_view(), name='update'),
], 'pages')
template/<app>/<modelo>_update_form
-> Por último, el template que usa esta vista es <modelo>_update_form
, que se lo has definido en el views.py
.
DeleteView - Vista CRUD¶
Vista para borrar un objeto.
views.py
-> Importar la clase DeleteView
y pasarle el modelo así como la url para cuando el borrado se haya completado.
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Page
# Create your views here.
class PageListView(ListView):
model = Page
class PageDetailView(DetailView):
model = Page
class PageCreateView(CreateView):
model = Page
fields = ['title', 'content', 'order']
success_url = reverse_lazy('pages:pages')
class PageUpdateView(UpdateView):
model = Page
fields = ['title', 'content', 'order']
template_name_suffix = '_update_form'
def get_success_url(self):
return reverse_lazy('pages:update', args=[self.object.id]) + '?ok'
class PageDeleteView(DeleteView):
model = Page
success_url = reverse_lazy('pages:pages')
urls.py
-> Tienes que pasarle un pk o un slug para borrar el elemento.
from django.urls import path
from .views import PageListView, PageDetailView, PageCreateView, \
PageUpdateView, PageDeleteView
pages_patterns = ([
path('', PageListView.as_view(), name='pages'),
path('<int:pk>/<slug:slug>/', PageDetailView.as_view(), name='page'),
path('create/', PageCreateView.as_view(), name='create'),
path('update/<int:pk>/', PageUpdateView.as_view(), name='update'),
path('delete/<int:pk>/', PageDeleteView.as_view(), name='delete'),
], 'pages')
template/<app>/<modelo>_confirm_delete.html
-> Por último tienes que crear el formulario de borrado en una template que se llame <modelo>_confirm_delete.html
Formularios para Modelos¶
Dentro de tu app, creas un fichero llamado forms.py
e importas from django import forms
, para crear la estructura de los campos. Al enlazarle un modelo al formulario, este se genera automáticamente.
forms.py
-> Importas el módulo forms
y usas su clase ModelForm
. Ahora debes de crear una clase Meta
dentro, con el modelo y los campos. Con el atributo widgets
le indicas que tipo de elemento html va a ser y dentro le pones los css que va a usar con el attrs
y su diccionario. Con labels
puedes cambiarle el texto del título del campo, quitándolo (como en el ejemplo) o poniéndole otro.
from django import forms
from .models import Page
class PageForm(forms.ModelForm):
class Meta:
model = Page
fields = ['title', 'content', 'order']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control',
'placeholder': 'Título'}),
'content': forms.Textarea(attrs={'class': 'form-control',
'placeholder': 'Contenido'}),
'order': forms.NumberInput(attrs={'class': 'form-control',
'placeholder': 'Orden'})
}
labels = { 'title': '', 'content': '', 'order': ''}
views.py
-> Importas el formulario y lo metes dentro de su vista. Ahora el campo fields
no es necesario porque va en el formulario.
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse_lazy
from .models import Page
from .forms import PageForm
class PageCreateView(CreateView):
model = Page
form_class = PageForm
success_url = reverse_lazy('pages:pages')
class PageUpdateView(UpdateView):
model = Page
form_class = PageForm
template_name_suffix = '_update_form'
def get_success_url(self):
return reverse_lazy('pages:update', args=[self.object.id]) + '?ok'
ckeditor adaptativo¶
Para añadir y hacer adaptativo el ckeditor tienes que hacer lo siguiente:
<app>/static/<app>/css/custom_ckeditor.css
-> Creas un fichero css donde editas los estilos del ckeditor.
.django-ckeditor-widget, .cke_editor_id_content {
width: 100% !important;
max-width: 821px !important;
}
En el template que uses común para todos los demás templates, importalo y añade el link al css donde lo has a tuneado.
{% load static %}
<script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
<script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
<link href="{% static 'pages/css/custom_ckeditor.css' %}" rel="stylesheet">
Usuarios registrados¶
Es probable que muchos de los formularios solo quieras que los puedan usar usuarios admin o registrados. Siempre que no seas un usuario loggeado, Django te detecta como usuario anónimo, AnonymousUser
.
views.py
-> Para que en tu vista puedas detectar si el usuario está logeado, debes de reescribir el método dispatch
de la clase. En él miras si el usuario está dentro del staff, y si no, lo rediriges donde quieras (por ejemplo, al login del admin).
from django.views.generic.edit import CreateView
from django.urls import reverse_lazy
from .models import Page
from .forms import PageForm
from django.shortcuts import redirect
# Create your views here.
class PageCreateView(CreateView):
model = Page
form_class = PageForm
success_url = reverse_lazy('pages:pages')
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
return redirect(reverse_lazy('admin:login'))
return super(PageCreateView, self).dispatch(request, *args, **kwargs)
Usuarios registrados - Mixins Manual¶
Si quieres que esa reedireción de antes se use en más vistas, no hace falta que copias ese método en cada vista. Existe un mecanismo llamado Mixin
que permite escribirlo una vez y usarlo en todas.
views.py
-> Creas una clase, que herede de object
(en Python 3, por defecto, todas las clases heredan de object
) y le pones el método dispatch
de antes. Luego en cada vista haces que herede primero de esta nueva clase y luego de su modelo de vista.
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Page
from .forms import PageForm
from django.shortcuts import redirect
# Create your views here.
class StaffRequiredMixin():
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
return redirect(reverse_lazy('admin:login'))
return super(StaffRequiredMixin, self).dispatch(request, *args, **kwargs)
class PageCreateView(StaffRequiredMixin, CreateView):
model = Page
form_class = PageForm
success_url = reverse_lazy('pages:pages')
class PageUpdateView(StaffRequiredMixin, UpdateView):
model = Page
form_class = PageForm
template_name_suffix = '_update_form'
def get_success_url(self):
return reverse_lazy('pages:update', args=[self.object.id]) + '?ok'
class PageDeleteView(StaffRequiredMixin, DeleteView):
model = Page
success_url = reverse_lazy('pages:pages')
Usuarios registrados - Mixins Decorado¶
El punto anterior ya se ha automatizado en Django usando decoradores.
views.py
-> Tienes que importar por un lado el method_decorator
, que permite usar decoradores de Django, y por otro el decorador staff_member_required
. Ahora en el método dispatch
puedes quitar el if
con la redirección, ya que se va a hacer sola. Ahora en la url al entrar, incluso te dirá donde debe de ir despues de loguearse.
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Page
from .forms import PageForm
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.contrib.admin.views.decorators import staff_member_required
# Create your views here.
class StaffRequiredMixin():
@method_decorator(staff_member_required)
def dispatch(self, request, *args, **kwargs):
return super(StaffRequiredMixin, self).dispatch(request, *args, **kwargs)
class PageCreateView(StaffRequiredMixin, CreateView):
model = Page
form_class = PageForm
success_url = reverse_lazy('pages:pages')
class PageUpdateView(StaffRequiredMixin, UpdateView):
model = Page
form_class = PageForm
template_name_suffix = '_update_form'
def get_success_url(self):
return reverse_lazy('pages:update', args=[self.object.id]) + '?ok'
class PageDeleteView(StaffRequiredMixin, DeleteView):
model = Page
success_url = reverse_lazy('pages:pages')
Pero tener una clase solo para eso no tiene sentido, así que se puede usar el decorador en las propias vistas usando el method_decorator
y pasándole el decorador, así como el método a decorar
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Page
from .forms import PageForm
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.contrib.admin.views.decorators import staff_member_required
# Create your views here.
@method_decorator(staff_member_required, name="dispatch")
class PageCreateView(CreateView):
model = Page
form_class = PageForm
success_url = reverse_lazy('pages:pages')
@method_decorator(staff_member_required, name="dispatch")
class PageUpdateView(UpdateView):
model = Page
form_class = PageForm
template_name_suffix = '_update_form'
def get_success_url(self):
return reverse_lazy('pages:update', args=[self.object.id]) + '?ok'
@method_decorator(staff_member_required, name="dispatch")
class PageDeleteView(DeleteView):
model = Page
success_url = reverse_lazy('pages:pages')
Existen 2 decoradores más que son interesantes: login_required
y permission_required
.
Auth Views¶
Django tiene vistas de autenticación ya creadas: login, logout, ... No hace falta crearlas, él las genera.
<proyecto>/<proyecto>/urls.py
-> Generar las urls para las vistas desde accounts
.
from django.contrib import admin
from django.urls import path, include
from pages.urls import pages_patterns
urlpatterns = [
path('', include('core.urls')),
path('pages/', include(pages_patterns)),
path('admin/', admin.site.urls),
# Paths de Auth
path('accounts/', include('django.contrib.auth.urls')),
]
Si abres la url accounts te dice todos las urls que hay, como accounts/login
.
LoginView¶
Si abres accounts/login
da error de template. Busca el template <app>/login.html
, así que hay que crearlo.
<app>/templates/<app>/login.html
settings.py
-> En el settings del proyecto tendremos que añadir la app al principio de todo, ya que muchos formularios pueden estar con conflicto con otras aplicaciones. También debemos de crear al final las redirecciones con la variable LOGIN_REDIRECT_URL
donde se le indica donde ir al loguearse.
# ...
INSTALLED_APPS = [
'registration.apps.RegistrationConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'ckeditor',
'core.apps.CoreConfig',
'pages.apps.PagesConfig',
]
# ...
# Auth redirects
LOGIN_REDIRECT_URL = 'home'
Luego en el template donde lo vayas a usar simplemente usa {% url 'login' %}
LogoutView¶
Es parecido al anterior. En este caso solo tienes que definir la variable LOGOUT_REDIRECT_URL
en el settings
Y luego en algún template puedes enlazar la acción con {% url 'logout' %}
<ul class="navbar-nav">
{% if not request.user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{% url 'login' %}">Acceder</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'logout' %}">Salir</a>
</li>
{% endif %}
</ul>
Registro de Usuario¶
Se suele hacer en una app aparte (registration
por ejemplo).
<app>/views.py
-> Importa el formulario de creación de usuario UserCreationForm
y una vista genérica de creación como CreateView
. Dentro de esta se le pasa el formulario en el atributo form_class
así como su template. También debes de devolver la redirección con algún parámetro que permita saber que todo ha ido correcto.
from django.contrib.auth.forms import UserCreationForm
from django.views.generic import CreateView
from django.urls import reverse_lazy
# Create your views here.
class SignUpView(CreateView):
form_class = UserCreationForm
template_name = 'registration/signup.html'
def get_success_url(self):
return reverse_lazy('login') + '?register'
<app>/urls.py
from django.urls import path, include
from .views import SignUpView
urlpatterns = [
path('signup/', SignUpView.as_view(), name='signup'),
]
<proyecto>/urls.py
from django.contrib import admin
from django.urls import path, include
from pages.urls import pages_patterns
urlpatterns = [
path('', include('core.urls')),
path('pages/', include(pages_patterns)),
path('admin/', admin.site.urls),
# Paths de Auth
path('accounts/', include('django.contrib.auth.urls')),
path('accounts/', include('registration.urls')),
]
<app>/templates/<app>/<template_name>
-> Es un formulario normal y corriente.
{% extends 'core/base.html' %}
{% load static %}
{% block title %}Registro{% endblock %}
{% block content %}
<style>.errorlist{color:red;}</style>
<main role="main">
<div class="container">
<div class="row mt-3">
<div class="col-md-9 mx-auto mb-5">
<form action="" method="post">{% csrf_token %}
<h3 class="mb-4">Registro</h3>
{{form.as_p}}
<p><input type="submit" class="btn btn-primary btn-block" value="Confirmar"></p>
</form>
</div>
</div>
</div>
</main>
{% endblock %}
<app>/templates/<app>/<success_url>
-> Para poder ver si te ha ido bien podrías añadir
{% if 'register' in request.GET %}
<p style="color:green;">Usuario registrado correctamente, ya puedes loguearte.</p>
{% endif %}
Customizar el registro¶
Para customizar el formulario de registro debes de trastearlo en tiempo de ejecución para no perder el que ya hay hecho.
<app>/views.py
-> Tienes que sobreescribir el método get_form()
, modificando sus campos, que son: username
, password1
y password2
.
from django.contrib.auth.forms import UserCreationForm
from django.views.generic import CreateView
from django.urls import reverse_lazy
from django import forms
# Create your views here.
class SignUpView(CreateView):
form_class = UserCreationForm
template_name = 'registration/signup.html'
def get_success_url(self):
return reverse_lazy('login') + '?register'
def get_form(self, form_class=None):
form = super(SignUpView, self).get_form()
# Customización en tiempo real
form.fields['username'].widget = forms.TextInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Nombre de usuario'})
form.fields['password1'].widget = forms.PasswordInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Contraseña'})
form.fields['password2'].widget = forms.PasswordInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Repite la contraseña'})
return form
<app>/templates/<app>/<template_name>
-> Si quieres quitar todos los labels
, lo puedes hacer desde el fichero anterior o simplemente en tu template pones
Añadir email como requisito¶
Para que añadas un email al registro lo mejor es extender el formulario ya existente en Django.
<app>/forms.py
-> Crea este fichero para extender el formulario. Añades el atributo email
. En la clase Meta
añades el atributo. con el método clean_<field>
haces una validación, en este caso clean_email
lo haces para que el email
sea único para cada usuario (no puede haber dos usuarios con el mismo email) Si el email no existe, puede registrarse, sino, lanza un error de validación.
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
class UserCreationFormWithEmail(UserCreationForm):
email = forms.EmailField(
required=True,
help_text='Requerido, 254 caracteres como máximo y que sea válido')
class Meta:
model = User
fields = ('username', 'email', 'password1', 'password2')
def clean_email(self):
email = self.cleaned_data['email']
if User.objects.filter(email=email).exists():
raise forms.ValidationError('El email ya existe, usa otro.')
return email
<app>/views.py
-> Cambiar el formulario anterior por el nuevo extendido, así como las modificaciones de su campo.
from .forms import UserCreationFormWithEmail
from django.views.generic import CreateView
from django.urls import reverse_lazy
from django import forms
# Create your views here.
class SignUpView(CreateView):
form_class = UserCreationFormWithEmail
template_name = 'registration/signup.html'
def get_success_url(self):
return reverse_lazy('login') + '?register'
def get_form(self, form_class=None):
form = super(SignUpView, self).get_form()
# Customización en tiempo real
form.fields['username'].widget = forms.TextInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Nombre de usuario'})
form.fields['email'].widget = forms.EmailInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Email'})
form.fields['password1'].widget = forms.PasswordInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Contraseña'})
form.fields['password2'].widget = forms.PasswordInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Repite la contraseña'})
return form
Recuperar contraseña¶
Existen diferentes opciones, pero una típica es tener un servidor de correo SMTP que manda email con el link para el proceso de recuperación de contraseña. Aquí vas a hacer un truco para debug, que es tener un fichero local donde almacenas las credenciales.
<proyecto>/settings.py
-> Añades lo siguiente al final. Con EMAIL_BACKEND
le estas diciendo que use un backend de ficheros de prueba para el email. Con EMAIL_FILE_PATH
le indicas donde guardar ese fichero.
# Email
if DEBUG:
EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
EMAIL_FILE_PATH = os.path.join(BASE_DIR, 'sent_emails')
else:
# Configuración del servidor de correo SMTP real de producción
pass
Ahora te va a tocar sobreescribir los 4 templates que Django usa para recuperar la contraseña, ya que los que él ofrece son de forma admin, y tu quieres verlo como un usuario que ve el frontend. Los templates que debes crear son:
password_reset_form.html
-> Pedir nueva contraseña.password_reset_done.html
-> Email con instrucciones enviado.password_reset_confirm.html
-> Definir nueva contraseña.password_reset_complete.html
-> Contraseña cambiada.
Por último, en el template login deberías de añadir la típica línea para si has olvidado la contraseña
Para ver todos los templates de Django para cuentas puedes verlo en
Perfil de Usuario¶
Crear perfil¶
<app>/models.py
-> Creamos un modelo Profile
, que tiene que tener una relación 1-1 con un usuario, una foto, biografía, web.
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
avatar = models.ImageField(upload_to='profiles', null=True, blank=True)
bio = models.TextField(null=True, blank=True)
link = models.URLField(max_length=200, null=True, blank=True)
settings.py
-> Hay que despachar los ficheros media del avatar y no tocar la redirección del login.
# Auth redirects
#LOGIN_REDIRECT_URL = 'pages:pages'
LOGOUT_REDIRECT_URL = 'home'
# Media Files
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
urls.py
-> Añadir al urlpatterns
del proyecto donde se despachan los ficheros media.
from django.contrib import admin
from django.urls import path, include
from pages.urls import pages_patterns
from django.conf import settings
urlpatterns = [
path('', include('core.urls')),
path('pages/', include(pages_patterns)),
path('admin/', admin.site.urls),
# Paths de Auth
path('accounts/', include('django.contrib.auth.urls')),
path('accounts/', include('registration.urls')),
]
if settings.DEBUG:
from django.conf.urls.static import static
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
<app>/views.py
-> Crear su vista, con el decorador login_required
para cuando estés logueado.
from .forms import UserCreationFormWithEmail
from django.views.generic import CreateView
from django.urls import reverse_lazy
from django import forms
from django.views.generic.base import TemplateView
from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required
# Create your views here.
class SignUpView(CreateView):
form_class = UserCreationFormWithEmail
template_name = 'registration/signup.html'
def get_success_url(self):
return reverse_lazy('login') + '?register'
def get_form(self, form_class=None):
form = super(SignUpView, self).get_form()
# Customización en tiempo real
form.fields['username'].widget = forms.TextInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Nombre de usuario'})
form.fields['email'].widget = forms.EmailInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Email'})
form.fields['password1'].widget = forms.PasswordInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Contraseña'})
form.fields['password2'].widget = forms.PasswordInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Repite la contraseña'})
return form
@method_decorator(login_required, name='dispatch')
class ProfileUpdate(TemplateView):
template_name = 'registration/profile_form.html'
<app>/templates/<app>/<template_name>
-> Crear su template.
{% extends 'core/base.html' %}
{% load static %}
{% block title %}Perfil{% endblock %}
{% block content %}
<main role="main">
<div class="container">
<div class="row mt-3 mb-5">
<div class="col-md-9 mx-auto">
<h3>Perfil</h3>
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<div class="text-center">
<input type="submit" class="btn btn-primary btn-block" value="Actualizar" />
</div>
</form>
</div>
</div>
</div>
</main>
{% endblock %}
<app>/urls.py
-> Redireccionarla en la urls.
from django.urls import path, include
from .views import SignUpView, ProfileUpdate
urlpatterns = [
path('signup/', SignUpView.as_view(), name='signup'),
path('profile/', ProfileUpdate.as_view(), name='profile'),
]
core/templates/base.html
-> Añadir un enlace.
Por último hacemos las migraciones.
Perfil editable¶
<app>views.py
-> Tienes que hacer que la vista herede de UpdateView
(poniéndole su modelo y que campos son editables). Para que no de error a la hora de editar, se debe sobreescribir el método get_object
, donde hay que obtener el usuario de la propia request
para poder usarlo en la vista. Al obtener el usuario, si este no existe, la queryset puede petar, así que por eso hay que usar get_or_create
.
from .forms import UserCreationFormWithEmail
from django.views.generic import CreateView
from django.urls import reverse_lazy
from django import forms
from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required
from django.views.generic.edit import UpdateView
from .models import Profile
# Create your views here.
class SignUpView(CreateView):
form_class = UserCreationFormWithEmail
template_name = 'registration/signup.html'
def get_success_url(self):
return reverse_lazy('login') + '?register'
def get_form(self, form_class=None):
form = super(SignUpView, self).get_form()
# Customización en tiempo real
form.fields['username'].widget = forms.TextInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Nombre de usuario'})
form.fields['email'].widget = forms.EmailInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Email'})
form.fields['password1'].widget = forms.PasswordInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Contraseña'})
form.fields['password2'].widget = forms.PasswordInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Repite la contraseña'})
return form
@method_decorator(login_required, name='dispatch')
class ProfileUpdate(UpdateView):
model = Profile
fields = ['avatar', 'bio', 'link']
success_url = reverse_lazy('profile')
template_name = 'registration/profile_form.html'
def get_object(self):
# Obtener (o crear si no existe) el objeto a editar
profile, created = Profile.objects.get_or_create(user=self.request.user)
return profile
En el template, para poder ver el url de la imagen del avatar tienes que usar esto en el formulario
Customizando formulario de Perfil¶
<app>/forms.py
-> Debes de importar el modelo. Crear un formulario que herede de forms.ModelForm
. Dentro crear su clase Meta
con el modelo, los campos y sus widgets para modificarlos.
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from .models import Profile
class UserCreationFormWithEmail(UserCreationForm):
email = forms.EmailField(
required=True,
help_text='Requerido, 254 caracteres como máximo y que sea válido')
class Meta:
model = User
fields = ('username', 'email', 'password1', 'password2')
def clean_email(self):
email = self.cleaned_data['email']
if User.objects.filter(email=email).exists():
raise forms.ValidationError('El email ya existe, usa otro.')
return email
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['avatar', 'bio', 'link']
widgets = {
'avatar': forms.ClearableFileInput(attrs={
'class': 'form-control-file mt-3'
}),
'bio': forms.Textarea(attrs={
'class': 'form-control mt-3',
'rows': 3,
'placeholder': 'Biografia'
}),
'link': forms.URLInput(attrs={
'class': 'form-control mt-3',
'placeholder': 'Enlace'
}),
}
<app>/views.py
-> Importa el formulario nuevo y cambia el atributo model
por el de form_class
, poniendole el formulario. El campo fields
ya no es necesario pues está en el formulario.
from .forms import UserCreationFormWithEmail, ProfileForm
# ...
@method_decorator(login_required, name='dispatch')
class ProfileUpdate(UpdateView):
form_class = ProfileForm
success_url = reverse_lazy('profile')
template_name = 'registration/profile_form.html'
def get_object(self):
# Obtener (o crear si no existe) el objeto a editar
profile, _ = Profile.objects.get_or_create(user=self.request.user)
return profile
<app>/templates/<app>/<template_name>
-> Modifica el formulario a pelo a tu gusto. En este ejemplo se ha puesto una imagen por defecto no-avatar.jpg
en <app>/static/<app>/img/no-avatar.jpg
para cuando el usuario no tenga, o borre la imagen, aparezca esta.
{% extends 'core/base.html' %}
{% load static %}
{% block title %}Perfil{% endblock %}
{% block content %}
<style>.errorlist{color:red;} label{display:none}</style>
<main role="main">
<div class="container">
<div class="row mt-3">
<div class="col-md-9 mx-auto mb-5">
<form action="" method="post" enctype="multipart/form-data">{% csrf_token %}
<div class="row">
<!-- Previa del avatar -->
<div class="col-md-2">
{% if request.user.profile.avatar %}
<img src="{{request.user.profile.avatar.url}}" class="img-fluid">
<p class="mt-1">¿Borrar? <input type="checkbox" id="avatar-clear" name="avatar-clear" /></p>
{% else %}
<img src="{% static 'registration/img/no-avatar.jpg' %}" class="img-fluid">
{% endif %}
</div>
<!-- Formulario -->
<div class="col-md-10">
<h3>Perfil</h3>
<input type="file" name="avatar" class="form-control-file mt-3" id="id_avatar">
{{ form.bio }}
{{ form.link }}
<input type="submit" class="btn btn-primary btn-block mt-3" value="Actualizar">
</div>
</div>
</form>
</div>
</div>
</div>
</main>
{% endblock %}
Email editable¶
Como es solo un campo, y para no tener que destrozar los modelos de formularios y vistas que ya estamos usando, lo mejor es que hagas un formulario solo para este campo.
<app>/templates/<app>/<template_perfil>
-> Añades un enlace para editar el email.
<app>/forms.py
-> Creas el nuevo formulario EmailForm
basándote en uno anterior. Ahora lo que vas a recuperar es un objeto que ya existe y que debe permitir cambiarlo si no existe el nuevo valor.
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from .models import Profile
class UserCreationFormWithEmail(UserCreationForm):
email = forms.EmailField(
required=True,
help_text='Requerido, 254 caracteres como máximo y que sea válido')
class Meta:
model = User
fields = ('username', 'email', 'password1', 'password2')
def clean_email(self):
email = self.cleaned_data['email']
if User.objects.filter(email=email).exists():
raise forms.ValidationError('El email ya existe, usa otro.')
return email
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['avatar', 'bio', 'link']
widgets = {
'avatar': forms.ClearableFileInput(attrs={
'class': 'form-control-file mt-3'
}),
'bio': forms.Textarea(attrs={
'class': 'form-control mt-3',
'rows': 3,
'placeholder': 'Biografia'
}),
'link': forms.URLInput(attrs={
'class': 'form-control mt-3',
'placeholder': 'Enlace'
}),
}
class EmailForm(forms.ModelForm):
email = forms.EmailField(
required=True,
help_text='Requerido, 254 caracteres como máximo y que sea válido')
class Meta:
model = User
fields = ['email']
def clean_email(self):
email = self.cleaned_data['email']
if 'email' in self.changed_data:
if User.objects.filter(email=email).exists():
raise forms.ValidationError('El email ya existe, usa otro.')
return email
<app>/views.py
-> Creas su vista EmailUpdate
y lo obtienes (el email) de la propia request
.
from .forms import UserCreationFormWithEmail, ProfileForm, EmailForm
from django.views.generic import CreateView
from django.urls import reverse_lazy
from django import forms
from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required
from django.views.generic.edit import UpdateView
from .models import Profile
# Create your views here.
class SignUpView(CreateView):
form_class = UserCreationFormWithEmail
template_name = 'registration/signup.html'
def get_success_url(self):
return reverse_lazy('login') + '?register'
def get_form(self, form_class=None):
form = super(SignUpView, self).get_form()
# Customización en tiempo real
form.fields['username'].widget = forms.TextInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Nombre de usuario'})
form.fields['email'].widget = forms.EmailInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Email'})
form.fields['password1'].widget = forms.PasswordInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Contraseña'})
form.fields['password2'].widget = forms.PasswordInput(
attrs={'class': 'form-control mb2',
'placeholder': 'Repite la contraseña'})
return form
@method_decorator(login_required, name='dispatch')
class ProfileUpdate(UpdateView):
form_class = ProfileForm
success_url = reverse_lazy('profile')
template_name = 'registration/profile_form.html'
def get_object(self):
# Obtener (o crear si no existe) el objeto a editar
profile, _ = Profile.objects.get_or_create(user=self.request.user)
return profile
@method_decorator(login_required, name='dispatch')
class EmailUpdate(UpdateView):
form_class = EmailForm
success_url = reverse_lazy('profile')
template_name = 'registration/profile_email_form.html'
def get_object(self):
# Obtener el objeto a editar
return self.request.user
<app>/urls.py
-> Creas su endpoint / url asociada a su vista.
from django.urls import path
from .views import SignUpView, ProfileUpdate, EmailUpdate
urlpatterns = [
path('signup/', SignUpView.as_view(), name='signup'),
path('profile/', ProfileUpdate.as_view(), name='profile'),
path('profile/email/', EmailUpdate.as_view(), name='profile_email'),
]
<app>/templates/<app>/template_name
-> Por último creas la template.
{% extends 'core/base.html' %}
{% load static %}
{% block title %}Email{% endblock %}
{% block content %}
<style>.errorlist{color:red;} label{display:none}</style>
<main role="main">
<div class="container">
<div class="row mt-3">
<div class="col-md-9 mx-auto mb-5">
<form action="" method="post">{% csrf_token %}
<h3 class="mb-4">Email</h3>
{{form.as_p}}
<p><input type="submit" class="btn btn-primary btn-block" value="Actualizar"></p>
</form>
</div>
</div>
</div>
</main>
{% endblock %}
Contraseña Editable¶
Al igual que sucedía anteriormente cuando querías cambiar la contraseña (Recuperar Contraseña), Django tiene urls de Admin para editar la contraseña. Tu vas a usar:
password_change/
-> Vista con nombrepassword_change
y template que usa un formulariopassword_change_form-html
.password_change/done
-> Vista con nombrepassword_change_done
y templatepassword_change_done.html
.
Solo tienes que crear estos dos templates
<app>/templates/<app>/password_change_form.html
{% extends 'core/base.html' %}
{% load static %}
{% block title %}Cambio de contraseña{% endblock %}
{% block content %}
<style>.errorlist{color:red;}</style>
<main role="main">
<div class="container">
<div class="row mt-3">
<div class="col-md-9 mx-auto mb-5">
<form action="" method="post">{% csrf_token %}
<h3 class="mb-4">Cambio de contraseña</h3>
<p>Por favor, introduzca su contraseña antigua por seguridad, y después introduzca dos veces la nueva contraseña para verificar que la ha escrito correctamente.</p>
{{form.old_password.errors}}
<p><input type="password" name="old_password" autofocus="" required="" id="id_old_password"class="form-control" placeholder="Contraseña antigua"></p>
{{form.new_password1.errors}}
<p><input type="password" name="new_password1" required="" id="id_new_password1" class="form-control" placeholder="Contraseña nueva"></p>
{{form.new_password2.errors}}
<p><input type="password" name="new_password2" required="" id="id_new_password2" class="form-control" placeholder="Contraseña nueva (confirmación)"></p>
<p><input type="submit" class="btn btn-primary btn-block" value="Cambiar mi contraseña"></p>
</form>
</div>
</div>
</div>
</main>
{% endblock %}
<app>/templates/<app>/password_change_done.html
{% extends 'core/base.html' %}
{% load static %}
{% block title %}Contraseña cambiada correctamente{% endblock %}
{% block content %}
<main role="main">
<div class="container">
<div class="row mt-3">
<div class="col-md-9 mx-auto mb-5">
<h3 class="mb-4">Contraseña cambiada correctamente</h3>
<p>Puedes volver a tu perfil haciendo clic <a href="{% url 'profile' %}">aquí</a>.</p>
</div>
</div>
</div>
</main>
{% endblock %}
Optimizar guardado de avatar¶
Si se guardan todas las imágenes a pelo de un usuario, esto es impracticable a la larga. Lo ideal es que cuando el usuario cargue una imagen nueva, borres la anterior y la sustituyas por la nueva. Para hacer esto tienes que:
<app>/models.py
-> En la app donde tengas la gestión del perfil, en el campo avatar
le pasas al argumento upload_to
una función, en este caso custom_upload_to
. A esa función tienes que pasarle primero la instancia el perfil, y luego el nombre del fichero del nuevo avatar. Una vez dentro recuperas el perfil y borras su avatar. Por último devuelves el path con el nombre donde se va a guardar la nueva imagen.
from django.db import models
from django.contrib.auth.models import User
from django.dispatch import receiver
from django.db.models.signals import post_save
def custom_upload_to(instance, filename):
old_instance = Profile.objects.get(pk=instance.pk)
old_instance.avatar.delete()
return 'profiles/' + filename
# Create your models here.
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
avatar = models.ImageField(upload_to=custom_upload_to, null=True,
blank=True)
bio = models.TextField(null=True, blank=True)
link = models.URLField(max_length=200, null=True, blank=True)
@receiver(post_save, sender=User)
def ensure_profile_exists(sender, instance, **kwargs):
if kwargs.get('created', False):
Profile.objects.get_or_create(user=instance)
print('Se acaba de crear un usuario y su perfil enlazado')
Señales¶
Una señal es una función que se ejecuta después de que ocurra un evento. Por ejemplo, si quisieras saber cuando se ha creado un usuario correctamente, puedes hacer lo siguiente.
<app>/models.py
-> Creas la función ensure_profile_exists
. Importas el decorador receiver
y también la señal post_save
. Decoras la función indicándole el decorador, la señal, y quien la envía (en este caso User
). De los kwargs
compruebas si se acaba de crear el usuario y ya puedes operar con él.
from django.db import models
from django.contrib.auth.models import User
from django.dispatch import receiver
from django.db.models.signals import post_save
# Create your models here.
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
avatar = models.ImageField(upload_to='profiles', null=True, blank=True)
bio = models.TextField(null=True, blank=True)
link = models.URLField(max_length=200, null=True, blank=True)
@receiver(post_save, sender=User)
def ensure_profile_exists(sender, instance, **kwargs):
if kwargs.get('created', False):
Profile.objects.get_or_create(user=instance)
print('Se acaba de crear un usuario y su perfil enlazado')
TDD¶
Tests Simples¶
Los tests de cada app se crean en <app>/test.py
. Un ejemplo fácil para que te quedes con la copla, sería testear el ejemplo de la señal.
<app>/test.py
-> Tienes que importar el modelo que vayas a testear, Profile
, y en este caso vas a necesitar de User
también. Creas una clase para pasar los tests de tu modelo. Con el método setUp
generas los datos que necesites para hacer los tests. Luego generas un método por cada test y que siga el patrón test_<cosa_a_probar>
. En este caso pruebas que se haya creado un perfil después de haber creado un usuario. En tu ejemplo obtienes el perfil que se acaba de crear por setUp
y lanzas un assert
si coincide.
from django.test import TestCase
from django.contrib.auth.models import User
from .models import Profile
# Create your tests here.
class ProfileTestCase(TestCase):
def setUp(self):
User.objects.create_user('test', 'test@test.com', 'test1234')
def test_profile_exists(self):
exists = Profile.objects.filter(user__username='test').exists()
self.assertEqual(exists, True)
Para que se ejecuten el test debes hacer
# Testear todo
python manage.py test
# Testear la <app>
python manage.py test <app>
# Testear la <clase> de la <app>
python manage.py test <app>.<clase>
# Teastear el <método> de la <clase> de la <app>
python manage.py test <app>.<clase>.<método>
Cada vez que acaba de ejecutarse un método se borra la bbdd temporal y vuelve a ejecutarse setUp
y luego el siguiente método.
Ejemplo de Test con señales¶
Suponte una app messenger
con dos modelos:
messenger/models.py
-> Message
tiene un usuario, contenido y fecha de creacion. Thread
tiene usuarios, mensajes.
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class Message(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['created']
class ThreadManager(models.Manager):
def find(self, user1, user2):
# self es lo mismo que Thread.objects.all()
queryset = self.filter(users=user1).filter(users=user2)
if len(queryset):
return queryset[0]
def find_or_create(self, user1, user2):
thread = self.find(user1, user2)
if not thread:
thread = Thread.objects.create()
thread.users.add(user1, user2)
return thread
class Thread(models.Model):
users = models.ManyToManyField(User, related_name='threads')
messages = models.ManyToManyField(Message)
# Model Manager
objects = ThreadManager()
messenger/test.py
-> Abajo tienes el código, pero fíjate en el método test_add_message_from_user_not_in_thread
. Quieres testear si un usuario que no está en un hilo, y el test falla.
from django.test import TestCase
from django.contrib.auth.models import User
from .models import Message, Thread
# Create your tests here.
class ThreadTestCase(TestCase):
def setUp(self):
self.user1 = User.objects.create_user('user1', None, 'test1234')
self.user2 = User.objects.create_user('user2', None, 'test1234')
self.user3 = User.objects.create_user('user3', None, 'test1234')
self.thread = Thread.objects.create()
def test_add_to_thread(self):
self.thread.users.add(self.user1, self.user2)
self.assertEqual(len(self.thread.users.all()), 2)
def test_filter_thread_by_users(self):
self.thread.users.add(self.user1, self.user2)
threads = Thread.objects.filter(users=self.user1).\
filter(users=self.user2)
self.assertEqual(self.thread, threads[0])
def test_filter_non_existent_thread(self):
threads = Thread.objects.filter(users=self.user1).\
filter(users=self.user2)
self.assertEqual(len(threads), 0)
def test_add_message_to_thread(self):
self.thread.users.add(self.user1, self.user2)
message1 = Message.objects.create(user=self.user1, content='Jelou')
message2 = Message.objects.create(user=self.user2, content='Hola')
self.thread.messages.add(message1, message2)
self.assertEqual(len(self.thread.messages.all()), 2)
for message in self.thread.messages.all():
print(f'{message.user}: {message.content}')
def test_add_message_from_user_not_in_thread(self):
self.thread.users.add(self.user1, self.user2)
message1 = Message.objects.create(user=self.user1, content='Jelou')
message2 = Message.objects.create(user=self.user2, content='Hola')
message3 = Message.objects.create(user=self.user3, content='Shurmanos')
self.thread.messages.add(message1, message2, message3)
self.assertEqual(len(self.thread.messages.all()), 2)
def test_find_thread_with_custom_manager(self):
self.thread.users.add(self.user1, self.user2)
thread = Thread.objects.find(self.user1, self.user2)
self.assertEqual(self.thread, thread)
def test_find_or_create_thread_with_custom_manager(self):
self.thread.users.add(self.user1, self.user2)
thread = Thread.objects.find_or_create(self.user1, self.user2)
self.assertEqual(self.thread, thread)
thread = Thread.objects.find_or_create(self.user1, self.user3)
self.assertIsNotNone(self.thread, thread)
Para solucionar esto, la manera de hacerlo es capturar con una señal cuando se va a introducir un mensaje a un hilo, y no hacerlo. Cuando se añade un elemento a la bbdd, saltan al menos dos eventos, pre_add
y post_add
. Te interesa captar el primero con una señal y borrar lo que no vaya a ser guardado
messenger/models.py
-> Usa la señal m2m_changed
para captar cuando se va a introducir un elemento. Creas la función messages_changed
para manipular ese evento, con un bucle recorres todos elementos pk_set
y los guardas en un conjunto a parte los que no quieres false_pk_set
y eliminas del conjunto los elementos que no deben estar. Por último a m2m_changed
le conectas la función messages_changed
y le pones como sender
(emisor) el flujo/caudal de mensajes de los hilos, Thread.messages.through
.
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import m2m_changed
# Create your models here.
class Message(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['created']
class ThreadManager(models.Manager):
def find(self, user1, user2):
# self es lo mismo que Thread.objects.all()
queryset = self.filter(users=user1).filter(users=user2)
if len(queryset):
return queryset[0]
def find_or_create(self, user1, user2):
thread = self.find(user1, user2)
if not thread:
thread = Thread.objects.create()
thread.users.add(user1, user2)
return thread
class Thread(models.Model):
users = models.ManyToManyField(User, related_name='threads')
messages = models.ManyToManyField(Message)
# Model Manager
objects = ThreadManager()
def messages_changed(sender, **kwargs):
instance = kwargs.pop('instance', None)
action = kwargs.pop('action', None)
pk_set = kwargs.pop('pk_set', None)
print(f'{instance} {action} {pk_set}')
# Busca los mensajes que no deben de estar en el hilo
false_pk_set = set()
if action == 'pre_add':
for msg_pk in pk_set:
msg = Message.objects.get(pk=msg_pk)
if msg.user not in instance.users.all():
print(f'{msg.user} no forma parte del hilo')
false_pk_set.add(msg_pk)
# Borra del hilo los mensajes que no deben de estar
pk_set.difference_update(false_pk_set)
m2m_changed.connect(messages_changed, sender=Thread.messages.through)
Model Manager¶
Para trabajar con bases de datos, Django tiene su propio ORM que llevas usando todo el rato. Cada vez que trabajas con un modelo, como con User
, tienes atributos y métodos ya definidos. Si quisieras extenderlos, lo que debes crear es un Model Manager.
<app>/models.py
-> En el models.py
de tu app, debes de crear una clase que herede de
from django.db import models
from django.contrib.auth.models import User
class ThreadManager(models.Manager):
def find(self, user1, user2):
# self es lo mismo que Thread.objects.all()
queryset = self.filter(users=user1).filter(users=user2)
if len(queryset):
return queryset[0]
def find_or_create(self, user1, user2):
thread = self.find(user1, user2)
if not thread:
thread = Thread.objects.create()
thread.users.add(user1, user2)
return thread
class Thread(models.Model):
users = models.ManyToManyField(User, related_name='threads')
messages = models.ManyToManyField(Message)
# Model Manager
objects = ThreadManager()
JS - Peticiones asíncronas¶
TODO:
Deploy¶
TODO:
recolectar los staticos con python manage.py collecstatic
Customizar el Admin¶
TODO: