lunes, 13 de noviembre de 2023

17.1.- Crear etiquetas y filtros de plantilla personalizados.

Crear etiquetas y filtros de plantilla personalizados.

Django nos ofrece una gran variedad de etiquetas de plantillas, tales como {% if  %} or {% block %}. Las hemos usado a lo largo de todos los capítulos anteriores. Puedes encontrar una referencia completa de las etiquetas de plantillas en https://docs.djangoproject.com/en/4.2/ref/templates/builtins/.

Pero Django también nos permite personalizar estas etiquetas de plantillas a nuestra necesidad. Es muy útil cuando necesitamos una funcionalidad que no esté contemplada por las etiquetas de Django. Siguiendo con nuestro proyecto podríamos crear una etiqueta para mostrar la lista de los últimos post que hayan sido publicados en el blog. Podríamos ponerla en un lateral de la página de forma que siempre este visible.

Creando etiquetas de plantilla personalizadas.

Django nos proporciona las siguientes funciones auxiliares que nos permiten crear fácilmente etiquetas personalizadas

  • simple_tag: Procesa los datos que la facilitemos y devuelve un string.
  • inclusion_tag: Procesa los datos que le facilitemos y devuelve una plantilla renderizada.
Las etiquetas de plantillas que creemos tienen que estar dentro de una aplicación de Django.

Dentro del directorio de la aplicación Blog vamos a crear un nuevo directorio llamado templatetags y dentro de el crearemos un archivo __init__.py. Crearemos tambien otro archivo llamado blog_tags.py. La estructura del directorio del que hemos hablado tiene que tener esta forma:

blog/
    __init__.py
    models.py
    ...
    templatetags/
        __init__.py
        blog_tags.py

La forma de llamar al archivo es importante. Utilizaremos el nombre del módulo para cargar nuestras etiquetas en las plantillas.

Implementando una etiqueta de plantilla "simple".

Vamos a empezar creando una etiqueta sencilla que nos devuelva el número total de post publicados en el blog.

Edita el archivo templatetags/blog_tags.py que acabamos de crear y añade el siguiente código.

PracticaDjango/Blog/templatetags/blog_tags.py

from django import template
from ..models import Post

register = template.Library()

@register.simple_tag
def posts_totales():
    return Post.objects.count()

Hemos creado nuestra primera etiqueta que devuelve el numero total de post. 

Cada módulo que contenga etiquetas de plantilla necesita definir una variable llamada register para que sea una etiqueta válida de la librería de Django. Esta variable es una instancia de template.Library(), y se usará para registrar todas las etiquetas y filtros de la aplicación.

En el código que acabamos de escribir, hemos definido una etiqueta llamada posts_totales a través de una simple función de Python. Hemos añadido el decorador @register.simple_tag a la función para registrarla como una etiqueta. Djando usará el nombre de la función como nombre de la etiqueta. Si quieres registrarla usando un nombre diferente, entonces debes especificar el nuevo nombre en un atributo de esta forma: @register.simple_tag(name='mi_etiqueta').

Nota: siempre después de crear nuevas etiquetas o filtros hay que reiniciar el servidor de desarrollo de Django para que se tomen los cambios.

Antes de usar las etiquetas debemos hacerlas disponibles para las plantillas usando al principio de la plantilla la etiqueta {% load %}. Como dijimos anteriormente necesitamos usar el nombre del módulo de Python que contiene la etiqueta que hemos creado.

Vamos a modificar la plantilla que renderiza la lista de post publicados, para que en la parte de la derecha flotando se muestre el número total de post publicados.

Edita el archivo Blog/lista.html de la aplicación Blog y añade la etiqueta {% load blog_tags %} para cargar nuestras etiquetas personalizadas, después de la etiqueta {% extends %} que debe ser la primera en aparecer. Después usa la etiquetas posts_totales para que se muestre el número total de post publicados. Debe quedar un código similar a este:

PracticaDjango/Blog/templates/Blog/lista.html

<!--Cargamos la plantilla base-->
{% extends "Proyecto_web_app/base.html" %}

<!-- Cargamos las etiquetas personalizada -->
{% load blog_tags %}

<!-- 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>

    <div id="sidebar">
    <h2>El Blog</h2>
    <p>
        Hay escritos {% posts_totales %} posts hasta el momento.
    </p>
    </div>

    {% for post in posts %}
    ...

Vamos a añadir un poco de código de CSS para que el cajón se muestre en gris flotando en la parte superior derecha de la pantalla. Abre el archivo style.css y añade al final el siguiente código:

PracticaDjango/Proyecto_web_app/static/Proyecto_web_app/css/style.css

...
#sidebar { 
    float:right;
    width:30%;
    padding:10px;
    background:#efefef; 
    height:100%;
}
Necesitaras parar el servidor de desarrollo para que los cambios tengan efecto. Así que si lo tenias encendido pulsa CTrl + C y ejecútalo de nuevo usando el siguiente comando:

python manage.py runserver

Abre en tu navegador la dirección http://127.0.0.1:8000/blog/ y deberías ver el número total de post publicados como se puede ver en la siguiente imagen:

el numero total de post incluidos en la barra lateral



Implementando una etiqueta de plantilla "inclusion".


Vamos a crear otra etiqueta para mostrar los últimos post que se han publicado en el blog. Esta vez utilizaremos una etiqueta de plantilla "inclusion". Usando este tipo de etiquetas podemos devolver al usarla una plantilla con sus variables de contexto incluidas.

Edita el archivo templatetags/blog_tags.py y añade el siguiente código:

PracticaDjango/Blog/templatetags/blog_tags.py

#...
@register.inclusion_tag("Blog/ultimos_posts.html")
def mostrar_ultimos_posts(contador=5):
    ultimos_posts = Post.objects.order_by('-created')[:contador]
    return {'ultimos_posts': ultimos_posts}

En este código hemos registrado una nueva etiqueta usando el decorador @register.inclusion_tag. Hemos especificado la plantilla que será renderizada junto con las variables de contexto usando la plantilla "Blog/ultimos_posts.html". La plantilla de la etiqueta admitirá opcionalmente un parámetro llamado "contador" cuyo valor de defecto está establecido en 5. Este parámetro nos permitirá especificar el número de posts que queramos mostrar. Usaremos este párametro para limitar el número de valores obtenidos en la consulta Post.objects.order_by('-created')[:contador] al número que especifiquemos en el contador. 

Date cuenta de que la función devuelve un diccionario de variables en lugar de un único valor. La etiqueta creada con "inclusion" devuelve un diccionario de valores, las cuales se usan como contexto para renderizar la plantilla especificada. La etiqueta que hemos creado nos permite limitar el número de elementos que se mostrarán. Por ejemplo, si queremos mostrar 3 elementos usaremos {% mostrar_ultimos_posts 3 %}.

Ahora crearemos la plantilla que renderizará la etiqueta. Para ello crea una archivo dentro del directorio 'templates' de la aplicación Blog llamado ultimos_posts.html.

PracticaDjango/Blog/templates/Blog/ultimos_posts.html

<ul>
    {% for post in ultimos_posts %}
    <li>
        <a href="{{ post.get_absolute_url }}">{{ post.titulo }}</a>
    </li>
    {% endfor %}
</ul>
Con el código superior, se muestra una lista desordenada de post usando la variable ultimos_posts, devuelta por la etiqueta que creamos. Ahora edita el archivo lista.html de este mismo directorio y añadiremos la etiqueta que hemos creado para que se muestren los 3 últimos posts creados.


<!--Cargamos la plantilla base-->
{% extends "Proyecto_web_app/base.html" %}

<!-- Cargamos la etiqueta personalizada -->
{% load blog_tags %}

<!-- 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>
    <div id="sidebar">
    <h2>El Blog</h2>
    <p>
        Hay escritos {% posts_totales %} posts hasta el momento.
    </p>
    <h3>Últimos Posts</h3>
    {% mostrar_ultimos_posts 3 %}
</div>

    {% for post in posts %}
    <h2>
        <a href="{{ post.get_absolute_url }}">{{post.titulo}}</a>
    </h2>
    <p class="date">Publicado {{ post.autor }} por {{ post.updated }} </p>
    {{ post.contenido|truncatewords:30|linebreaks }}
    {% endfor %}
</div>

<!-- {% include "Proyecto_web_app/paginacion.html" with page=posts %} -->
{% include "Proyecto_web_app/paginacion.html" with page=page_obj %}

<section>
    <div style="width: 75%; margin: auto; color: white; text-align: center;">
        Categorías:
        {% for categoria in categorias %}
        <a href="{% url 'Blog:categoria' categoria.id %}" class="link-info">
            <span style="color:rgb(2, 7, 59)">{{ categoria.nombre }}</span>
        </a>&nbsp;&nbsp;&nbsp;
        {% endfor %}
    </div>
</section>

{% endblock %}
Se llama a la etiqueta de la plantilla, pasándole el numero de posts a mostrar y la plantilla se renderiza en el lugar que la hayamos colocado con el contexto dado.

Ahora volvamos al navegador y recarguemos las página. La barra lateral debería tener un aspecto parecido a esto:




Creando una etiqueta de plantilla que devuelva un conjunto de consultas.

Finalmente crearemos una etiqueta "simple" que devuelva un valor. Guardaremos el resultado en una variable que podamos reutilizar, en vez de mostrarla directamente. Crearemos una etiqueta para mostrar el post más comentado.

Edita el archivo templatetags/blog_tags.py y  añade la siguiente importación y esta etiqueta de plantilla al mismo.

PracticaDjango/Blog/templatetags/blog_tags.py

@register.simple_tag
def obtener_posts_mas_comentados(contador=5):
    return Post.objects.annotate(comentarios_totales=Count("comentarios")).order_by(
        "-comentarios_totales"
    )[:contador]
En el código superior, hemos creado un conjunto de consultas o "queryset" que nos va a devolver un objeto con todos los posts que existen, lo hemos hecho con "Post.objects". A continuación agregamos una anotación al conjunto de resultados. En este caso, cuenta el número total de comentarios para cada post (.annotate(comentarios_totales=Count("comentarios") y lo guarda en un campo llamado "comentarios_totales". Para contar utilizamos la función "Count" de Django. Ten en cuenta que definimos "comentarios" al definir el modelo en models.py como un related_name lo cual nos da todos los comentarios de un determinado post. Finalmente ordenamos los resultados por orden descendente basandonos en los comentarios totales, de mas a menos comentarios.

Además de Count, Django nos ofrece las funciones para agregar objetos, Avg, Max, Min y Sum. Puedes encontrar más información sobre estas funciones en https://docs.djangoproject.com/en/4.2/topics/db/aggregation/.

Ahora edita la plantilla Blog/lista.html y añade el codigo resaltado en amarillo:


<!--Cargamos la plantilla base-->
{% extends "Proyecto_web_app/base.html" %}

<!-- Cargamos la etiqueta personalizada -->
{% load blog_tags %}

<!-- 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>
    <div id="sidebar">
        <h2>El Blog</h2>
        <p>
            Hay escritos {% posts_totales %} posts hasta el momento.
        </p>
        <h3>Últimos Posts</h3>
        {% mostrar_ultimos_posts 3 %}
        <h3>Post más comentados</h3>
        {% obtener_posts_mas_comentados as posts_mas_comentados %}
        <ul>
            {% for post in posts_mas_comentados %}
            <li>
                <a href="{{ post.get_absolute_url }}">{{ post.titulo }}</a>
            </li>
            {% endfor %}
        </ul>
    </div>

    {% for post in posts %}
    <h2>
        <a href="{{ post.get_absolute_url }}">{{post.titulo}}</a>
    </h2>
    <p class="date">Publicado {{ post.autor }} por {{ post.updated }} </p>
    {{ post.contenido|truncatewords:30|linebreaks }}
    {% endfor %}
</div>

<!-- {% include "Proyecto_web_app/paginacion.html" with page=posts %} -->
{% include "Proyecto_web_app/paginacion.html" with page=page_obj %}

<section>
    <div style="width: 75%; margin: auto; color: white; text-align: center;">
        Categorías:
        {% for categoria in categorias %}
        <a href="{% url 'Blog:categoria' categoria.id %}" class="link-info">
            <span style="color:rgb(2, 7, 59)">{{ categoria.nombre }}</span>
        </a>&nbsp;&nbsp;&nbsp;
        {% endfor %}
    </div>
</section>

{% endblock %}
En el código anterior, almacenamos el resultado que proporciona la etiqueta que hemos creado en una variable personalizada ("obtener_posts_mas_comentados") usando el argumento "as" seguido del nombre de la variable ('posts_mas_comentados"). Luego usamos para una lista desordenada para devolver los resultados. 

Ahora vuelve a abrir el navegador y refresca la página para ver el resultado final que debería parecerse a este.

la plantilla lista.html, incluida la barra lateral que muestra los posts más comentados


Ahora que tienes una mayor idea de como construir plantillas personalizadas puedes encontrar más información en https://docs.djangoproject.com/en/4.2/howto/custom-template-tags/.

Implementando filtros personalizados.


Django viene con una gran variedad de filtros de plantilla integrados que nos permiten modificar las variables de las plantillas. Al final estas son funciones de Python que toman uno o dos parámetros, el valor de la variable a la que se aplica el filtro, y un argumento opcional. Devuelven un valor que puede ser mostrado o tratado por otro filtro.

Un filtro se puede escribir en una plantilla como {{ variable|mi_filtro }}. Los filtros en los que se pasa un argumento apareceran en las plantillas de esta forma {{variable|mi_filtro:"foo" }}. Por ejemplo, puedes usar el filtro 'capfirst', para poner en mayúsculas el primer carácter de una expresión, escribiendo {{ variable|capfirst }}. También puedes aplicar cuantos filtros quieras a una variable de la siguiente manera {{ variable|filtro1|filtro2|filtroN }} y cada filtro se aplicará sobre la salida del filtro anterior.

Puedes encontrar la lista completa de filtros de plantilla integrados en https://docs.djangoproject.com/en/4.2/ref/templates/builtins/#built-in-filter-reference.

Creando un filtro de plantilla para utilizar el método de Escritura MARKDOWN

Vamos a ver como crear un filtro personalizado que nos permita usar MARKDOWN en las publicaciones de nuestro Blog y luego convertir el cuerpo del post en lenguaje HTML en las plantillas.

MARKDOWN es un método de escritura de texto plano que es muy sencilla de usar y está pensado para que luego pueda convertirse en lenguaje HTML. Aprender la sintexis de MARKDOWN es mucho más fácil que aprender HTML. Podemos aprender el uso básico de MARKDOWN en el siguiente enlace https://markdown.es/.

Lo primero que tenemos que hacer es instalar el módulo markdown  a través de pip usando el siguiente comando en el shell:

pip install markdown

Después edita el archivo templatetags/blog_tags.py e incluye el siguiente código:

PracticaDjango/Blog/templatetags/blog_tags.py

#...
# Para utilizar markdown en el cuerpo del post.
from django.utils.safestring import mark_safe
import markdown
#...
@register.filter(name='markdown')
def markdown_format(texto):
    return mark_safe(markdown.markdown(texto))

Registramos el filtro de la plantilla igual que registramos las etiquetas. Para prevenir un conflicto de nombres entre el nombre de la función y el nombre del filtro, hemos llamado a la función markdown_format y a el filtro 'markdown' usando el atributo name, para poder usarlo en las plantillas como {{ variable|markdown }}

Django no tiene en cuenta el código HTML generado por los filtros, se escapa de él. Los caracteres HTML se reemplazan con sus caracteres codificados en HTML. Por ejemplo, la etiqueta <p> se convierte en &lt;p&gt; (menos en símbolo, la letra p y más que en símbolo)

Usamos la función mark_safe proporcionada por Django para marcar el resultado como HTML seguro para ser renderizado. De forma predeterminada, Django no confía en ningún código HTML y lo escapará, tal como vimos anteriormente, antes de colocarlo. Las únicas excepciones son las variables que están marcadas como a salvo de escapes. Esto evita que Django genere código potencialmente peligroso y te permite crear excepciones para devolver código HTML seguro.

Edita el código de la plantilla detalle.htm de la aplicación Blog y añade el código sombreado en amarillo:

PracticaDjango/Blog/templates/Blog/detalle.html

{% extends "Proyecto_web_app/base.html" %}

{% load blog_tags %}

{% block title %}Detalle de un Post{% 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|markdown }}    

    {% with comentarios.count as total_comentarios %}
    <h2>
        {{ total_comentarios }} comentario{{ total_comentarios|pluralize }}
    </h2>
    {% endwith %}

    {% for comentario in comentarios %}
    <div class="comment">
        <p class="info">
            Comentario {{ forloop.counter }} de {{ comentario.autor }} -
            {{ comentario.created }}
        </p>
        {{ comentario.cuerpo|linebreaks }}
    </div>

    {% empty %}
    <p>No hay comentarios aún.</p>

    {% endfor %}

    <!-- 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>
    {% include "Blog/form_comentario.html" %}
</div>
{% endblock %}

Hemos remplazado el filtro "linebreaks" de la etiqueta {{post.contenido}} con el filtro markdown que hemos creado. Este filtro no solo transforma los saltos de línea en etiquetas <p>, sino que también transforma el formato markdown en HTML.

Edita el código de la plantilla lista.htm de la aplicación Blog y añade el código sombreado en amarillo:

PracticaDjango/Blog/templates/Blog/lista.html

<!--Cargamos la plantilla base-->
{% extends "Proyecto_web_app/base.html" %}

<!-- Cargamos la etiqueta personalizada -->
{% load blog_tags %}

<!-- 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>
    <div id="sidebar">
        <h2>El Blog</h2>
        <p>
            Hay escritos {% posts_totales %} posts hasta el momento.
        </p>
        <h3>Últimos Posts</h3>
        {% mostrar_ultimos_posts 3 %}
        <h3>Post más comentados</h3>
        {% obtener_posts_mas_comentados as posts_mas_comentados %}
        <ul>
            {% for post in posts_mas_comentados %}
            <li>
                <a href="{{ post.get_absolute_url }}">{{ post.titulo }}</a>
            </li>
            {% endfor %}
        </ul>
    </div>

    {% for post in posts %}
    <h2>
        <a href="{{ post.get_absolute_url }}">{{post.titulo}}</a>
    </h2>
    <p class="date">Publicado {{ post.autor }} por {{ post.updated }} </p>
    <!-- {{ post.contenido|truncatewords:30|linebreaks }} -->
    {{ post.contenido|markdown|truncatewords_html:15}}
    {% endfor %}
</div>

<!-- {% include "Proyecto_web_app/paginacion.html" with page=posts %} -->
{% include "Proyecto_web_app/paginacion.html" with page=page_obj %}

<section>
    <div style="width: 75%; margin: auto; color: white; text-align: center;">
        Categorías:
        {% for categoria in categorias %}
        <a href="{% url 'Blog:categoria' categoria.id %}" class="link-info">
            <span style="color:rgb(2, 7, 59)">{{ categoria.nombre }}</span>
        </a>&nbsp;&nbsp;&nbsp;
        {% endfor %}
    </div>
</section>

{% endblock %}

Hemos realizado lo mismo que en el archivo anterior. Solamente hemos usado "truncate_html" en vez de "truncate" porque el código generado ya es html.

Ahora abre el navegador, ve a la consola de administración de Django y añade un nuevo post con el siguiente contenido:

Este es un post usando markdown
--------------------------------------

*Este texto esta en cursiva*  y **este en negrita**.

Aquí hay una lista:

- Uno
- Dos
- Tres

Y un  [link to the Django website](https://www.djangoproject.com/).
Una vez creado, ve de nuevo al navegador localiza el nuevo post y haz clic en el título para verlo en detalle. Su aspecto debería de ser algo como esto:

Post usando un filtro markdown




Como puedes ver los filtros de plantilla son muy útiles para personalizar el formato de una plantilla. puedes encontrar más información en https://docs.djangoproject.com/en/4.2/howto/custom-template-tags/#writing-custom-template-filters.



No hay comentarios:

Publicar un comentario