14. Newspaper app
Habrá una página de artículos en la que los periodistas podrán publicar, establecer permisos para que sólo el autor de un artículo pueda editarlo o borrarlo, y finalmente añadir la posibilidad de que otros usuarios escriban comentarios en cada uno de ellos, lo que introducirá el concepto de claves externas.
14.1. Articles app
- No hay reglas rígidas sobre cómo llamar a las aplicaciones, excepto que no se puede usar el nombre de una aplicación incorporada.
- Una regla general es usar el plural del nombre de una aplicación -
posts
,payments
,users
, etc.- a menos que hacerlo sea obviamente incorrecto como en el caso común deblog
donde el singular tiene más sentido. - Crear la aplicación para los nuevos artículos.
(news) $ python manage.py startapp articles
- Añadirla a
INSTALLED_APPS
y actualizar la zona horaria ya que se marcará la hora de los artículos. - Para averiguar la zona horaria correspondiente:
>>> from pytz import all_timezones, common_timezones
>>> 'Europe/Madrid' in all_timezones
True
FICHERO: newspaper_project/settings.py
`...`
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd Party
'crispy_forms',
# Local
'accounts.apps.AccountsConfig',
'pages.apps.PagesConfig',
'articles.apps.ArticlesConfig', # new
]
`...`
TIME_ZONE = 'Europe/Madrid' # new
- Se define el modelo de base de datos con cuatro campos:
title
,body
,date
, yauthor
. - Para el campo
autor
se hará referencia al modelo de usuario personalizadoaccounts.CustomUser
que se ha establecido en el archivosettings.py
comoAUTH_USER_MODEL
. Por lo tanto, si se importa la configuración podemos referirnos a ella comosettings.AUTH_USER_MODEL
o bien usando el métodoget_user_model
. - También implementamos práctica recomendada de definir un método
get_absolute_url
desde el principio y un método__str__
para ver el modelo en nuestra interfaz de administración.
FICHERO: articles/models.py
from django.contrib.auth import get_user_model
from django.db import models
from django.urls import reverse
class Article(models.Model):
title = models.CharField(max_length=255, verbose_name='Título')
body = models.TextField(verbose_name='Cuerpo')
date = models.DateTimeField(auto_now_add=True, verbose_name='Fecha')
author = models.ForeignKey(
get_user_model(), # ó settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
verbose_name='Autor'
)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('article_detail', args=[str(self.id)])
- Como se tiene una nueva aplicación y modelo, es hora de hacer un nuevo archivo de migración y luego aplicarlo a la base de datos.
(news) $ python manage.py makemigrations articles
(news) $ python manage.py migrate
- En este punto es conveniente ir a la administración para jugar con el modelo antes de construir las urls/vistas/plantillas necesarias para mostrar los datos en el sitio web. Pero primero necesitamos actualizar
admin.py
para que se muestre la nueva aplicación.
FICHERO: articles/admin.py
from django.contrib import admin
from .models import Article
admin.site.register(Article)
- Iniciar el servidor, ir a la página de administración y añadir algunos artículos de ejemplo.
- No se podrá añadir la fecha porque fue añadida automáticamente por Django en nuestro nombre y no puede ser cambiada en la administración.
- Se podrá hacer que la fecha sea editable -en aplicaciones más complejas es común tener un campo
created_at
yupdated_at
- pero para mantener las cosas simples, por ahora sólo se tendrádate
establecida por Django al momento de la creación.
14.2. URLs y Vistas
- Vamos a hacer que los artículos aparezcan en
articles/
.
FICHERO: # newspaper_project/urls.py
from django.contrib import admin
from django.urls import path, include # new
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('accounts.urls')),
path('accounts/', include('django.contrib.auth.urls')),
path('articles/', include('articles.urls')), # new
path('', include('pages.urls')),
]
FICHERO: articles/urls.py
from django.urls import path
from .views import ArticleListView
urlpatterns = [
path('', ArticleListView.as_view(), name='article_list'),
]
- Crear ahora la vista usando la genérica
ListView
de Django.
FICHERO: # articles/views.py
from django.views.generic import ListView
from .models import Article
class ArticleListView(ListView):
model = Article
template_name = 'article_list.html'
-
Los dos únicos campos que se necesitan especificar son el modelo de artículo y el nombre de la plantilla que será
article_list.html
. -
El último paso es crear la plantilla.
-
Bootstrap tiene un componente incorporado llamado
Cards
que se puede personalizar para los artículos individuales. Recordar queListView
devuelve un objeto llamado object_list que se puede iterar con un bucle for. - Dentro de cada artículo se muestra el título, el cuerpo, el autor y la fecha. Incluso se proporcionan enlaces a las funciones de "editar" y "borrar" que aún no se han construido.
FICHERO: templates/article_list.html
{% extends 'base.html' %}
{% block title %}Articles{% endblock %}
{% block content %}
{% for article in object_list %}
<div class="card">
<div class="card-header">
<span class="font-weight-bold">{{ article.title }}</span> ·
<span class="text-muted">por {{ article.author }} | {{ article.date }}</span>
</div>
<div class="card-body">
{{ article.body }}
</div>
<div class="card-footer text-center text-muted">
<a href="#">Editar</a> | <a href="#">Borrar</a>
</div>
</div>
<br>
{% endfor %}
{% endblock content %}
- Arracar el servidor y consultar la página
articles/
- Para más información de personalización consultar el siguiente enlace
14.3. Editar/Borrar
- Se necesitan nuevas urls, vistas y plantillas.
- Se puede aprovechar el hecho de que Django añade automáticamente una clave primaria a cada base de datos. Por lo tanto, el primer artículo con una clave primaria de
1
estará enarticles/1/edit/
y la ruta de borrado estará enarticles/1/delete/
.
FICHERO: articles/urls.py
from django.urls import path
from .views import(
ArticleListView,
ArticleUpdateView, # new
ArticleDetailView, # new
ArticleDeleteView, # new
)
urlpatterns = [
path('<int:pk>/edit/', ArticleUpdateView.as_view(), name='article_edit'), # new
path('<int:pk>/', ArticleDetailView.as_view(), name='article_detail'), # new
path('<int:pk>/delete/', ArticleDeleteView.as_view(), name='article_delete'),# new
path('', ArticleListView.as_view(), name='article_list'),
]
- Ahora se escribirán las vistas que usarán las vistas genéricas basadas en clases de Django para
DetailView
,UpdateView
yDeleteView
. Especificamos qué campos pueden ser actualizados -title
ybody
- y dónde redirigir al usuario después de borrar un artículo:article_list
.
FICHERO: articles/views.py
from django.views.generic import ListView, DetailView
from django.views.generic.edit import UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Article
class ArticleListView(ListView):
model = Article
template_name = 'article_list.html'
class ArticleDetailView(DetailView):
model = Article
template_name = 'article_detail.html'
class ArticleUpdateView(UpdateView):
model = Article
fields = ['title', 'body', ]
template_name = 'article_edit.html'
class ArticleDeleteView(DeleteView):
model = Article
template_name = 'article_delete.html'
success_url = reverse_lazy('article_list')
- Finalmente se necesita añadir las nuevas plantillas.
(news) $ touch templates/article_detail.html
(news) $ touch templates/article_edit.html
(news) $ touch templates/article_delete.html
-
Se empezará con la página de detalles que mostrará el título, la fecha, el cuerpo y el autor con enlaces para editar y borrar. También enlazará hacia atrás con todos los artículos.
-
El nombre de la ruta de edición es
article_edit
y tenemos que pasarle su clave principalarticle.pk
. -
El nombre de la ruta de borrado es
article_delete
y también necesita una clave primariaarticle.pk
. -
La página de artículos es una
ListView
por lo que no necesita que se le pase ningún argumento adicional.
FICHERO: templates/article_detail.html
{% extends 'base.html' %}
{% block content %}
<div class="article-entry">
<h2>{{ object.title }}</h2>
<p>por {{ object.author }} | {{ object.date }}</p>
<p>{{ object.body }}</p>
</div>
<p><a href="{% url 'article_edit' article.pk %}">Editar</a> | <a href="{% url 'article_delete' article.pk %}">Borrar</a></p>
<p>Volver a la <a href="{% url 'article_list' %}">lista de artículos</a>.</p>
{% endblock content %}
- Para las páginas de edición y borrado se puede usar el estilo del botón de Bootstrap para hacer que el botón de edición sea azul claro y el de borrado rojo.
FICHERO: templates/article_edit.html
{% extends 'base.html' %}
{% block content %}
<h1>Editar</h1>
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-info ml-2" type="submit">Actualizar</button>
</form>
{% endblock %}
FICHERO: templates/article_delete.html
{% extends 'base.html' %}
{% block content %}
<h1>Borrar</h1>
<form action="" method="post">{% csrf_token %}
<p>¿Estás seguro de que quieres borrar "{{ article.title }}"?</p>
<button class="btn btn-danger ml- " type="submit">Confirmar</button>
</form>
{% endblock %}
- Como paso final se añaden los enlaces de edición y borrado a la página de listas en la clase
div
para elcard-foot
. Serán los mismos que los añadidos a la página detalle.
FICHERO: templates/article_list.html
`...`
<div class="card-footer text-center text-muted">
<a href="{% url 'article_edit' article.pk %}">Editar</a> |
<a href="{% url 'article_delete' article.pk %}">Borrar</a>
</div>
`...`
- Arrancar el servidor e ir a la página de
articles/
y pulsar sobreEdit
. - Si se actualiza el campo "título" y se hace clic en actualizar, el usuario será redirigido a la página de detalles que mostrará el nuevo cambio.
- Si se hace clic en el enlace
Delete
el usuario será redirigido a la página de borrado. - Si se presiona el aterrador botón rojo
Confirm
el usuario será redirigido a la página de artículos donde ahora habrá una entrada menos.
14.Crear Page
El paso final es una página de creación de nuevos artículos que podemos hacer con el CreateView
de Django. Nuestros tres pasos son crear una vista, una url y una plantilla. Este flujo ya debería resultar bastante familiar. En nuestro archivo de vistas agregamos CreateView
a las importaciones de la parte superior y hacemos una nueva clase ArticleCreateView
que especifica nuestro modelo, plantilla y los campos disponibles.
FICHERO: articles/views.py
`...`
from django.views.generic.edit import (
CreateView,
UpdateView,
DeleteView
)
`...`
class ArticleCreateView(CreateView):
model = Article
template_name = 'article_new.html'
fields = ('title', 'body', 'author',)
Nótese que nuestros campos tienen autor ya que queremos asociar un nuevo artículo con un autor, sin embargo una vez que un artículo ha sido creado no queremos que un usuario pueda cambiar el autor, por lo que ArticleUpdateView sólo tiene los campos ['title', 'body',] . Actualizar nuestro archivo de urls con la nueva ruta para la vista.
FICHERO: articles/urls.py
from django.urls import path
from .views import (
ArticleListView,
ArticleUpdateView,
ArticleDetailView,
ArticleDeleteView,
ArticleCreateView, # new
)
urlpatterns = [
path('<int:pk>/edit/', ArticleUpdateView.as_view(), name='article_edit'),
path('<int:pk>/', ArticleDetailView.as_view(), name='article_detail'),
path('<int:pk>/delete/',ArticleDeleteView.as_view(), name='article_delete'),
path('new/', ArticleCreateView.as_view(), name='article_new'), # new
path('', ArticleListView.as_view(), name='article_list'),
]
Luego, salga del servidor Control+c para crear una nueva plantilla llamada article_new.html
.
(news) $ touch templates/article_new.html
Y actualizarlo con el siguiente código HTML.
FICHERO: templates/article_new.html
{% extends 'base.html' %}
{% block content %}
<h1>Nuevo artículo</h1>
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-success ml- " type="submit">Guardar</button>
</form>
{% endblock content %}
Como paso final deberíamos añadir un enlace para crear nuevos artículos en nuestro navegador para que sea accesible en todas partes del sitio para los usuarios registrados.
FICHERO: templates/base.html
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="{% url 'home' %}">Newspaper</a>
{% if user.is_authenticated %}
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a href="{% url 'article_new' %}">+ Nuevo</a>
</li>
</ul>
{% endif %}
`...`
¿Y por qué no usar Bootstrap para mejorar nuestra página web original ahora también? Podemos actualizar templates/home.html
como sigue.
FICHERO: templates/home.html
{% extends 'base.html' %}
{% block title %}Home{% endblock title %}
{% block content %}
<br/>
<div class="jumbotron">
<h1 class="display-4">Newspaper app</h1>
<p class="lead">Un Periódico con Django.</p>
<p class="lead">
<a class="btn btn-primary btn-lg" href="{% url 'article_list' %}"
role="button">Ver los artículos</a>
</p>
</div>
{% endblock content %}
Ya hemos terminado. Vamos a confirmar que todo funciona como se esperaba. Arranca el servidor de nuevo python manage.py runserver
y navega a nuestra página web en: http://127.0.0.1:8000/.
Haz clic en el enlace "+ Nuevo" en la parte superior del navegador y serás redirigido a nuestra página de creación.
Adelante, crea un nuevo artículo. Luego haz clic en el botón "Guardar". Serás redirigido a la página de detalles. ¿Por qué? Porque en nuestro archivo models.py
establecemos el método get_absolute_url
en article_detail
. Este es un buen enfoque porque si más tarde cambiamos el patrón de url de la página de detalles a, digamos, articles/details/4/
, la redirección seguirá funcionando. Se utilizará cualquier ruta asociada a article_detail
; no hay código duro para la ruta en sí misma.
Ten en cuenta también que la clave principal aquí está en el URL. Aunque sólo estamos mostrando tres artículos ahora mismo, Django no reordena las claves primarias sólo porque hayamos borrado una. En la práctica, la mayoría de los sitios del mundo real no borran nada; en su lugar, "ocultan" los campos borrados, ya que esto facilita el mantenimiento de la integridad de una base de datos y da la opción de "recuperar" más adelante si es necesario. Con nuestro enfoque actual, una vez que algo se borra, ¡se va para siempre! Haga clic en el enlace de "Todos los artículos" para ver nuestra nueva página de artículos.
Hay un nuevo artículo en la parte inferior, como se esperaba.
Conclusión...
Hemos creado una aplicación de artículos dedicados con la funcionalidad de CRUD. Pero aún no hay permisos o autorizaciones, lo que significa que cualquiera puede hacer cualquier cosa. Un usuario desconectado puede visitar todas las URLs y cualquier usuario conectado puede editar o eliminar un artículo existente, ¡incluso uno que no sea suyo! En el próximo capítulo añadiremos permisos y autorizaciones a nuestro proyecto para arreglar esto.