En este capítulo crearemos un blog para que la gente pueda escribir mensajes sobre nuestra empresa y servicios.
- creacion de blog:
$ python manage.py startapp Blog
- creación del modelo.
Las entradas del blog van a tener un titulo, un contenido , una imagen y luego tanto para las categorías del blog como para la información del post una fecha de creación y otra de actualización. Para ello abrimos el archivo models.py de esta nueva aplicación del blog y creamos los modelos 'Categoria' y 'Post'. Es muy parecido a como hicimos para la aplicación servicio.
Empecemos con la creación del modelo para las categorías. Estas son las diferentes etiquetas que se pueden aplicar a cada post. En un post puede haber una o varias de estas etiquetas y cada una de las etiquetas puede ser aplicada a uno o a varios post. Esto en SQL es lo que se conoce como una relación ManyToMany. Este tipo de relaciones en SQL se suelen resolver creando una tabla intermedia (post_categorias por ejemplo) pero Django nos permite hacerlo de una forma más sencilla como veremos a continuación.
Creación del modelo Categoría.
PracticaDjango/Blog/models.py:
from django.db import models
from django.utils import timezone
# Create your models here.
class Categoria(models.Model):
"""Diferentes tags para asignar a los post"""
nombre = models.CharField(max_length=50)
created = models.DateTimeField(default=timezone.now)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return self.nombre
Este es el modelo de datos que utilizaremos para registrar las diferentes categorías de los post. Cada categoría que podamos aplicar tendrá un nombre que identifique la etiqueta y una fecha de creación y de actualización. Veamos los campos que componen este modelo:
- nombre: este es el campo para las etiquetas que se aplicarán a las diferentes publicaciones. Es un campo CharField que se traduce en una columna VARCHAR en la base de datos SQL.
- created: este el campo que registrará la fecha de creación de las categorías. Es un campo DateTimeField que utilizaremos para crear la fecha y hora en que el objeto fue creado. Al usar default = timezone.now, se establecerá como valor de este campo la fecha de nuestra zona horaria en el momento de su creación. Esta variable se puede modificar posteriormente si fuese necesario a tráves del panel de administración de Django.
- updated: es un campo DateTimeField. Lo usaremos para registrar la última fecha y hora cuando se actualice el post. Usando auto_now, la fecha y hora se actualizarán automáticamente cuando se guarde el objeto.
Creación del modelo Post.
PracticaDjango/Blog/models.py:
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
# Para importar el usuario que esta creando el post
# Create your models here.
class Categoria(models.Model):
"""Diferentes tags para asignar a los post"""
nombre = models.CharField(max_length=50)
created = models.DateTimeField(default=timezone.now)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return self.nombre
class Post(models.Model):
titulo = models.CharField(max_length=250)
slug = models.CharField(max_length=250)
contenido = models.TextField()
autor = models.ForeignKey(User, on_delete=models.CASCADE, related_name="blog_posts")
categorias = models.ManyToManyField(Categoria, null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return self.titulo
Este es el modelo de datos que utilizaremos para mostrar las publicaciones del blog. Las publicaciones tendrán un título, una etiqueta corta y un cuerpo donde escribir el mensaje. Echemos un vistazo a los campos de este modelo:
- titulo: este es el campo para el título de la publicación. Es un campo CharField que se traduce en una columna VARCHAR en la base de datos SQL.
- slug: este es un campo SlugField que se traduce en una columna VARCHAR en la base de datos SQL. Un slug es una etiqueta corta que solo contiene letras, números, guiones bajos o guiones. Una publicación con el título "NBA2023: el video juego" podría tener un slug como "nba2023-video-juego". Usaremos el campo slug para crear URLS que sean amigables para los buscadores.
- contenido: este el campo donde se almacenará el contenido del mensaje que se quiera publicar. Es un campo TextField que se traducirá en una columna TEXT en la base de datos SQL.
- autor: es un campo que vamos a sacar de otra tabla de una base de datos ya existente en Django que nos dice que usuario esta conectado ese momento a la sesión. Es una relación 'One to Many' de SQL, puesto que un autor puede escribir varios post, pero en el que cada post solo puede tener un creador y no varios. Para saber quien es el usuario conectado importamos la tabla User desde django.contrib.auth.models. Además usamos el argumento on_delete=models.CASCADE para que si un usuario se da de baja de nuestro sitio web se borren todos los 'post' que haya publicado. Usamos related_name para especificar un nombre para la relación inversa, de autor a post. Esto nos permitirá acceder fácilmente a todos los post que haya escrito un usuario usando la notación User.blog_post lo que nos facilitará obtener todos los post que haya escrito un determinado autor. Si no especificamos un related_name, Django creará uno por defecto en este tipo de campos combinando el nombre del modelo en minúsculas y el sufijo _set. (post_set)
Añadiendo campos Datetime.
- created: es un campo DateTimeField. Lo usaremos para guardar la fecha y hora en que el post fue creado. A diferencia del campo con el mismo nombre que usamos en el modelo Categoría, al usar auto_now_add, la fecha y hora se guardarán automáticamente al crear el objeto.
- updated: es un campo DateTimeField. Lo usaremos para registrar la última fecha y hora cuando se actualice el post. Usando auto_now, la fecha y hora se actualizarán automáticamente cuando se guarde el objeto.
PracticaDjango/Blog/models.py:
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
# Para importar el usuario que esta creando el post
# Create your models here.
class Categoria(models.Model):
"""Diferentes tags para asignar a los post"""
nombre = models.CharField(max_length=50)
created = models.DateTimeField(default=timezone.now)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return self.nombre
class Post(models.Model):
titulo = models.CharField(max_length=250)
slug = models.CharField(max_length=250)
contenido = models.TextField()
autor = models.ForeignKey(User, on_delete=models.CASCADE, related_name="autor")
categorias = models.ManyToManyField(Categoria, null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
# Model meta Options - https://docs.djangoproject.com/en/4.1/topics/db/models/#meta-options
class Meta:
verbose_name = "post"
verbose_name_plural = "posts"
ordering = [-updated]
def __str__(self):
return self.titulo
Añadiendo un índice a la base de datos.
Vamos a crear un índice para poder indexar la base de datos por el campo 'updated'. Esto mejorará el rendimiento para las consultas en las que se utilice un filtro para mostrar los resultados en base a este campo. Cuando haya muchos post esperamos que esto mejore el rendimiento ya que por defecto hemos establecido anteriormente que los resultados se muestren por la fecha de actualización.
Edita el archivo models.py de la aplicación Blog para que tenga el siguiente código. Las líneas nuevas aparecen resaltadas.
PracticaDjango/Blog/models.py:
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
# Para importar el usuario que esta creando el post
# Create your models here.
class Categoria(models.Model):
"""Diferentes tags para asignar a los post"""
nombre = models.CharField(max_length=50)
created = models.DateTimeField(default=timezone.now)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return self.nombre
class Post(models.Model):
titulo = models.CharField(max_length=250)
slug = models.CharField(max_length=250)
contenido = models.TextField()
autor = models.ForeignKey(User, on_delete=models.CASCADE, related_name="autor")
categorias = models.ManyToManyField(Categoria, null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
# Model meta Options - https://docs.djangoproject.com/en/4.1/topics/db/models/#meta-options
class Meta:
verbose_name = "post"
verbose_name_plural = "posts"
ordering = [-updated]
indexes = [
models.Index(fields=['-updated']),
]
def __str__(self):
return self.titulo
Hemos agregado el atributo 'indexes' a la clase Meta del modelo Post. Esta opción nos permite crear índices o indexar la base de datos, utilizando uno o varios campos, en orden ascendente o descendente. Para ello hemos utilizado el campo '-updated'. La creación de este índice se incluirá en la migración de la base de datos que generaremos a continuación para nuestros modelos.
nota: El ordenamiento de índices no es compatible en MySQL. Si utilizas MySQL como base de datos, se creará un índice descendente como un índice normal.
- registrar la aplicación.
Necesitamos activar la aplicación del Blog en el proyecto, para que Django rastree la aplicación y se a posible crear las tablas en la base de datos de los modelos. Para ello nos vamos al archivo settings.py que recoge la configuración general del proyecto y que esta dentro del directorio PracticaDjango, buscamos el apartado INSTALLED_APPS y registramos la aplicación:
PracticaDjango/PracticaDjango/settings.py:
...
# Application definition
INSTALLED_APPS = [
# my applications
'Proyecto_web_app',
'Servicios',
'Blog',
# third party applications
'bootstrap5',
# default aplications
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
- migraciones de la base de datos
$ python manage.py makemigrations Blog
$ python manage.py migrate
Django habrá creado el archivo 0001_initial.py dentro del directorio 'migrations' de la aplicación 'Blog'. Si estas interesado en ver el código SQL que ha generado la base de datos puedes usar la instrucción siguiente:
python manage.py sqlmigrate blog 0001
- preparar el panel de administración para meter los datos.
PracticaDjango/Blog/templates/Blog/admin.py
from django.contrib import admin # Register your models here. # Importar del modelo tanto la categoría como el post from .models import Categoria, Post class Categoria_Admin(admin.ModelAdmin): # Campos de solo lectura en el panel de administración. readonly_fields = ('created', 'updated') class Post_Admin(admin.ModelAdmin): # Campos de solo lectura en el panel de administración. readonly_fields = ('created', 'updated') # Registramos todo lo anterior. admin.site.register(Categoria, Categoria_Admin) admin.site.register(Post, Post_Admin)
(venv)$ python manage.py runserver
PracticaDjango/Blog/templates/Blog/admin.py
from django.contrib import admin # Register your models here. # Importar del modelo tanto la categoría como el post from .models import Categoria, Post class Categoria_Admin(admin.ModelAdmin): # Campos de solo lectura en el panel de administración. readonly_fields = ('created', 'updated') @admin.register(Post) class Post_Admin(admin.ModelAdmin): # Campos de solo lectura en el panel de administración. readonly_fields = ('created', 'updated') list_display = ['titulo', 'autor', 'created', 'updated'] # Registramos todo lo anterior. admin.site.register(Categoria, Categoria_Admin)
PracticaDjango/Blog/templates/Blog/admin.py
from django.contrib import admin # Register your models here. # Importar del modelo tanto la categoría como el post from .models import Categoria, Post class Categoria_Admin(admin.ModelAdmin): # Campos de solo lectura en el panel de administración. readonly_fields = ('created', 'updated') @admin.register(Post) class Post_Admin(admin.ModelAdmin): # Campos de solo lectura en el panel de administración. readonly_fields = ('created', 'updated') list_display = ['titulo', 'autor', 'created', 'updated'] list_filter = ['status', 'created', 'publish', 'author'] search_fields = ['titulo', 'contenido'] prepopulated_fields = {'slug': ('titulo',)} raw_id_fields = ['autor'] date_hierarchy = 'updated' ordering = ['updated', 'created'] # Registramos todo lo anterior. admin.site.register(Categoria, Categoria_Admin)
- configurar las vistas
PracticaDjango/Proyecto_web_app/views.py
from django.shortcuts import render, HttpResponse # Create your views here. def home(request): return render(request, 'Proyecto_web_app/inicio.html') def tienda(request): return render(request, 'Proyecto_web_app/tienda.html') # Esto lo borramos o comentamos. # def blog(request): # return render(request, 'Proyecto_web_app/blog.html') def contacto(request): return render(request, 'Proyecto_web_app/contacto.html')
Edita el archivo views.py de la aplicación Blog e introduce este código.
PracticaDjango/Blog/views.py:
from django.shortcuts import render from .models import Post # Create your views here. def lista_post(request): posts=Post.objects.all() return render(request, 'Blog/lista.html', {'posts': posts})
PracticaDjango/Blog/templates/Blog/views.py
from django.http import Http404 def detalle_post(request, id): try: post = Post.objects.get(id=id) except Post.DoesNotExist: raise Http404("Post no encontrado.") return render(request, 'Blog/detalle.html', {'post': post})
PracticaDjango/Blog/templates/Blog/views.py
from django.shortcuts import render, get_object_or_404
# ...
def detalle_post(request, id):
post = get_object_or_404(Post, id=id)
return render(request, 'Blog/detalle.html', {'post': post})
Añadiendo las URL´s para las vistas.
PracticaDjango/Proyecto_web_app/urls.py
from django.urls import path from . import views app_name = 'Proyecto_web_app' urlpatterns = [ path('', views.home, name='home'), path('tienda/', views.tienda, name='tienda'), # borramos o comentamos la parte que hace referencia al blog # porque ahora está en otra aplicación. # path('blog/', views.blog, name='blog'), path('contacto/', views.contacto, name='contacto'), ]
PracticaDjango/Blog/urls.py
from django.urls import path from . import views app_name = 'Blog' urlpatterns = [ # post views path('', views.lista_post, name='lista_post'), path('<int:id>/', views.detalle_post, name='detalle_post'), ]
PracticaDjango/PracticaDjango/urls.py
from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('', include('Proyecto_web_app.urls', namespace='Proyecto_web_app')), path('servicios/', include('Servicios.urls', namespace='Servicios')), path('blog/', include('Blog.urls', namespace='Blog')), ] urlpatterns+=static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
- Creando la plantilla para las vistas.
PracticaDjango/Blog/
templates/ Blog/
PracticaDjango/Blog/templates/Blog/lista.html
<!--Cargamos la plantilla base--> {% extends "Proyecto_web_app/base.html" %} <!-- Establecemos el titulo de la página --> {% block title %}Blog{% endblock %} {% block content %} <div class="container-fluid bg-white" style="margin-bottom: 50px;"> <h1 class="text-center">Listado de Post publicados</h1> {% for post in posts %} <h2> <a href="{% url 'Blog:detalle_post' post.id %}">{{post.titulo}}</a> </h2> <p class="date">Publicado {{ post.autor }} por {{ post.updated }} </p> {{ post.contenido|truncatewords:30|linebreaks }} {% endfor %} </div> {% endblock %}
Con la etiqueta de plantilla {% extends %}, le decimos a Django que herede de la plantilla Proyecto_web_app/base.html.
Luego, llenamos los bloques de título y contenido de la plantilla base con el contenido que hemos definido en los bloques 'title' y 'content'. Posteriormente iteramos a través de las publicaciones y mostramos su título, fecha de publicación, autor y contenido, incluido un enlace en el título a la URL detallada del post. Construimos la URL usando la etiqueta de plantilla {% url %} proporcionada por Django.
Esta etiqueta de plantilla te permite crear URL dinámicamente por su nombre. Usamos Blog:detalle_post para que consulte la URL de detalle_post en el espacio de nombres del Blog. Pasamos el parámetro post.id que se necesita para construir la URL para cada publicación o post.
En el contenido de la publicación, aplicamos dos filtros de plantilla:
- truncatewords: trunca el valor al número de palabras especificadas
y
- linebreaks: los saltos de línea se convierten en saltos de línea HTML.
PracticaDjango/Blog/templates/Blog/detalle.html
{% extends "Proyecto_web_app/base.html" %} {% block title %}{{ post.title }}{% endblock %} {% block content %} <div class="container-fluid bg-white" style="margin-bottom: 150px;"> <h1>{{ post.titulo }}</h1> <p class="date">Publicado {{ post.autor }} por {{ post.updated }} </p> {{ post.contenido|linebreaks }} <!-- Contenedor para centrar el botón de regresar a la lista de posts --> <div class="d-flex justify-content-center align-items-center"> <a href="{% url 'Blog:lista_post' %}" class="btn btn-primary">Regresar</a> </div> </div> {% endblock %}
PracticaDjango/Proyecto_web_app/templates/Proyecto_web_app/nav.html
<ul class="nav justify-content-center my-2"> <li class="nav-item px-lg-4"> {% url 'Proyecto_web_app:home' as home_url %} <a class="nav-link text-uppercase fw-bold text-expanded {% if request.path == home_url %} text-black{% endif %}" href="{{ home_url }}">Inicio</a> </li> <li class="nav-item px-lg-4"> {% url 'Servicios:servicios' as servicios_url %} <a class="nav-link text-uppercase fw-bold text-expanded {% if request.path == servicios_url %} text-black{% endif %}" href="{{ servicios_url }}">Servicios</a> </li> <li class="nav-item px-lg-4"> {% url 'Blog:lista_post' as blog_url %} <a class="nav-link text-uppercase fw-bold text-expanded {% if request.path == blog_url %} text-black{% endif %}" href="{{ blog_url }}">Blog</a> </li> <li class="nav-item px-lg-4"> {% url 'Tienda:tienda' as tienda_url %} <a class="nav-link text-uppercase fw-bold text-expanded {% if request.path == tienda_url %} text-black{% endif %}" href="{{ tienda_url }}">Tienda</a> </li> <li class="nav-item px-lg-4"> {% url 'Contacto:contacto' as contacto_url %} <a class="nav-link text-uppercase fw-bold text-expanded {% if request.path == contacto_url %} text-black{% endif %}" href="{{ contacto_url }}">Contacto</a> </li> </ul>
Para finalizar tendremos que ir a la consola de administración de Django y crear algunos post del blog de prueba. Luego si ejecutamos el servidor quedaría algo así:
y entramos en uno de ellos para ver que todo funciona correctamente:
PracticaDjango/Proyecto_web_app/templates/Proyecto_web_app/nav.html
... <li class="nav-item px-lg-4"> {% url 'Blog:lista_post' as blog_url %} <a class="nav-link text-uppercase fw-bold text-expanded {% if request.path|slice:':6' == blog_url %} text-black{% endif %}" href="{{ blog_url }}">Blog</a> </li> ...
Estaría bien poder buscar los diferentes post por categorías de búsqueda. Es decir que en la parte inferior de la página nos apareciesen unos enlaces a las diferentes categorías, de forma que al pulsar en ellas se mostrasen solo los post que pertenecen a esas categorías. Por ejemplo:
Lo primero que tenemos que hacer es añadir los enlaces, tal como muestra la imagen superior al final de la página. Lo hacemos añadiendo al final del archivo de la plantilla el siguiente código, justo antes del {% endblock %} final:
PracticaDjango/Blog/templates/Blog/blog.html
.... <section> <div style="width: 75%; margin: auto; color: white; text-align: center;"> Categorías: {% for categoria in categorias %} <a href="{% url 'Blog:detalle_post' categoria.id %}" class="link-info"> <span style="color:rgb(2, 7, 59)">{{ categoria.nombre }}</span> </a> {% endfor %} </div> </section> </div> {% endblock %}
A la plantilla le hemos pasado las diferentes categorías que pueden tener los post publicados. Esto lo conseguimos modificando la vista en el archivo views.py.
PracticaDjango/Blog/views.py
from django.shortcuts import render, get_object_or_404, get_list_or_404 from . models import Post,Categoria from django.http import Http404 # Create your views here. def lista_post(request): posts = get_list_or_404(Post) categorias = get_list_or_404(Categoria) return render(request, 'Blog/lista.html', {'posts': posts, 'categorias': categorias}) ...
Para que cuando pulsemos los enlaces de las categorías se nos muestren los post que tienen esa categoría y no todos como hasta ahora, tenemos que registrar una nueva ruta:
PracticaDjango/Blog/urls.py
from django.urls import path # load views of these applications. from . import views urlpatterns = [ path('', views.blog, name='Blog'), path('categoria/<int:categoria_id>/', views.categoria, name='categoria'), ]
Esta URL será obligatorio pasarle un argumento que es la categoría cuyos post nos interesa mostrar.
Luego añadimos al archivo de vistas la nueva función 'categoría' que se ejecutará cuando se acceda a la ruta anteriormente creada pasándole el id de la categoría como parámetro:
PracticaDjango/Blog/views.py
from django.shortcuts import render, get_object_or_404, get_list_or_404 from . models import Post,Categoria from django.http import Http404 # Create your views here. def lista_post(request): posts = get_list_or_404(Post) categorias = get_list_or_404(Categoria) return render(request, 'Blog/lista.html', {'posts': posts, 'categorias': categorias}) def detalle_post(request, id): """Muestra todos los post""" post = get_object_or_404(Post, id=id) return render(request, 'Blog/detalle.html', {'post': post}) def categoria(request, categoria_id): """Muesta una lista de post en base a su categoría""" categoria=Categoria.objects.get(id=categoria_id) posts=Post.objects.filter(categorias=categoria_id) return render(request, 'Blog/categoria.html', {'categoria': categoria, 'posts': posts})
Lo que hace esta función es mirar a que categoría pertenece es id que le hemos pasado como parámetro, a continuación filtra todos los post que contienen esa categoría y vuelve a renderizar la pagina que muestra los blogs, pero solamente con los post que contienen esa categoría y no con todos. Ese archivo que se renderizará será casi idéntico a blog.html. Crea una nueva plantilla llamada categoria.html y agrégale este código.
PracticaDjango/Blog/templates/Blog/categoria.html
<!--Cargamos la plantilla base--> {% extends "Proyecto_web_app/base.html" %} <!-- Establecemos el titulo de la página --> {% block title %}Post por Categorias{% endblock %} {% block content %} <div class="container-fluid bg-white" style="margin-bottom: 50px;"> <h1 class="text-center">Listado de Post publicados por "{{categoria}}"</h1> {% for post in posts %} <h2> <a href="{% url 'Blog:detalle_post' post.id %}">{{post.titulo}}</a> </h2> <p class="date">Publicado {{ post.autor }} por {{ post.updated }} </p> {{ post.contenido|truncatewords:30|linebreaks }} {% endfor %} </div> <!-- Contenedor para centrar el botón de regresar a la lista de posts --> <div class="d-flex justify-content-center align-items-center"> <a href="{% url 'Blog:lista_post' %}" class="btn btn-primary">Regresar</a> </div> {% endblock %}