Sobre este documento

Este guión de prácticas ha sido elaborado siguiendo la filosofía de los tutoriales de Django Girls, que cuentan con una licencia Creative Commons Attribution-ShareAlike. Este guión ha sido adaptado para las clases de SAT y SARO de los Grados que se imparten en la Escuela de Ingeniería de Telecomunicación de la Universidad Rey Juan Carlos.

Para este guión, partimos del estado del guión #4, que se puede encontrar en el siguiente repositorio: https://gitlab.etsit.urjc.es/grex/X-Serv-15.8-CmsUsersPut-resuelto del GitLab de la ETSIT. Nota: si partes del repositorio, nota que el superusuario será "grex" y la contraseña "pakito"; esto es así, porque los usuarios se guardan en la base de datos (que viene incluida en el repo).

Retomando las plantillas

En el Guión de la práctica 3 ya hablamos de plantillas. Al final del todo, si recordáis bien, teníamos la siguiente plantilla en cms/content.html:

cms/templates/cms/content.html
<h1>{{ contenido.valor }}</h1>
{% for comentario in contenido.comentario_set.all %}
    <hr>
    <h1>{{ comentario.titulo }}</h1><br>
        {{ comentario.cuerpo }}<br>
        Enviado el {{ comentario.fecha }}<br>
{% endfor %}

<hr>
<br>
<form action="" method="POST">
  Introduce el (nuevo) contenido para esta página: 
  <input type="text" name="valor">
  <input type="submit" name="action" value="Enviar Contenido">
</form>

<br>
<form action="" method="POST">
  Introduce un nuevo comentario para esta página: 
  <br>Título: <input type="text" name="titulo">
  <br>Cuerpo: <input type="text" name="cuerpo">
  <input type="submit" name="action" value="Enviar Comentario">
</form>

En esta plantilla, primero mostrábamos el contenido, luego los comentarios asociados y finalmente ofrecíamos dos formularios: uno para modificar/agregar un nuevo contenido y otro para añadir comentarios. Vamos a realizar una serie de modificaciones en nuestra plantilla para que quede todo un poco más presentable. Así, la nueva plantilla content.html va a tener la siguiente pinta:

cms/templates/cms/content.html
{% load static %}
<html>
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="{% static 'cms/bootstrap.min.css' %}">

    <title>CMS de CursosWeb</title>
  </head>
  <body>
    <div class="jumbotron">
      <h1 class="display-4">Llave: {{ contenido.clave }}</h1>
        <p class="lead"> {{ contenido.valor }} </p>
      <hr class="my-4">
        <div  class="content container">
            <div  class="row">
                <div  class="col-md-8">
                {% for comentario in contenido.comentario_set.all %}
                    <div  class="comentario">
                        <div  class="date">
                            {{ comentario.fecha }}
                        </div>
                        <h2>{{ comentario.titulo }}</h2>
                        <p>{{ comentario.cuerpo|linebreaksbr }}</p>
                    </div>
                {% endfor %}
                </div>
            </div>
        </div>
     </div>
    </body>
</html>

Podrás observar que hemos añadido referencias a hojas de estilo CSS (que veremos pronto en detalle), que seguimos mostrando el contenido y los comentarios, pero que hemos eliminado los formularios. Para que todo funcione bien, te tendrás que descargar el archivos bootstrap.min.css y almacenarlo en el subdirectorio /static/cms/ dentro de la aplicación cms (donde en el guión #4 guardamos la imagen example.jpg), porque como vimos en la práctica anterior, lo queremo servir de manera estática.

Prueba a lanzar el servidor y pedir algún recurso, como http://localhost:8000/cms/nabucco. Nuestra plantilla es algo más sofisticada que lo que habíamos hecho hasta ahora, por las hojas de estilo.

Extendiendo plantillas

Una funcionalidad muy interesante que tiene Django que es la extensión de plantillas. Significa que puedes reutilizar partes del HTML para diferentes páginas del sitio web. Las plantillas son útiles cuando quieres utilizar la misma información o el mismo diseño en más de un lugar. No tienes que repetirte a ti misma en cada archivo. Y si quieres cambiar algo, no tienes que hacerlo en cada plantilla, solamente en una!

Una plantilla base es la plantilla más básica que extiendes en cada página de tu sitio web. Nosotros utilizaremos como base la plantilla content.html que creamos en prácticas anteriores, y que está en cms/templates/cms/. Para tal fin, en content.html reemplaza por completo tu (todo lo que haya entre <body> y </body>) con esto:

cms/templates/cms/content.html

[...]
  <body>
    <div  class="jumbotron">
        {% block contenido %}
        {% endblock %}
     </div>
  </body>
</html>

Lo que hemos hecho ha sido incluir un bloque. Por eso ha desaparecido todo lo referente al contenido y lo que había entre {% for comentario in ccontenido.comentario_set.all %} y {% endfor %} para mostrar los comentarios. La etiqueta de plantilla {% block %} se usa para crear un área en la que se insertará HTML. Ese HTML vendrá de otra plantilla que extiende esta (content.html). Enseguida te enseñamos cómo se hace.

Guarda content.html y crea cms/templates/cms/contenido.html. Debemos incluir en este fichero todo lo que corresponde al bloque que queremos rellenar. Y como queremos utilizar este fichero como parte de nuestra plantilla, incluyendo el bloque de contneido. Así que es hora de añadir etiquetas de bloque en este archivo. Tu etiqueta de bloque debe ser la misma que la etiqueta del archivo content.html. También querrás que incluya todo el código que va en los bloques de contenido. Para ello, pon todo entre {% block comentarios %} y {% endblock %}. Algo como esto:

cms/templates/cms/contenido.html

    {% block contenido %}
        <h1  class="display-4">Llave: {{ contenido.clave }}</h1>
          <p  class="lead"> {{ contenido.valor }} </p>
          <hr  class="my-4">
            <div  class="content container">
                <div  class="row">
                    <div  class="col-md-8">
                    {% for comentario in contenido.comentario_set.all %}
                        <div  class="comentario">
                            <div  class="date">
                                {{ comentario.fecha }}
                            </div>
                            <h2>{{ comentario.titulo }}</h2>
                            <p>{{ comentario.cuerpo|linebreaksbr }}</p>
                        </div>
                    {% endfor %}
                    </div>
                </div>
        </div>
    {% endblock %}

Solo falta conectar las dos plantillas. Esto precisamente es lo que significa extender plantillas. Para eso tenemos que añadir una etiqueta "extends" al comienzo del archivo. Así:

cms/templates/cms/contenido.html

{% extends 'cms/content.html' %}

    {% block contenido %} 
        <h1  class="display-4">Llave: {{ contenido.clave }}</h1>
          <p  class="lead"> {{ contenido.valor }} </p>
          <hr  class="my-4">
            <div  class="content container">
                <div  class="row">
                    <div  class="col-md-8">
                    {% for comentario in contenido.comentario_set.all %} 
                        <div  class="comentario">
                            <div  class="date">
                                {{ comentario.fecha }}
                            </div>
                            <h2>{{ comentario.titulo }}</h2>
                            <p>{{ comentario.cuerpo|linebreaksbr }}</p>
                        </div>
                    {% endfor %} 
                    </div>
                </div>
        </div>
    {% endblock %} 

Finalmente, tendríamos que modificar la vista para que quede de la siguiente manera:

cms/views.py
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render
from django.views.decorators.csrf import csrf_exempt
from django.utils import timezone
from django.template import loader

from .models import Contenido, Comentario


@csrf_exempt
def get_content(request, llave):
    if request.method == "PUT":
        valor = request.body.decode('utf-8')           
    elif request.method == "POST":
        action = request.POST['action']
        if action == "Enviar Contenido":
            valor = request.POST['valor']                
    if request.method == "PUT" or (request.method == "POST" and action == "Enviar Contenido"):
        try:
            c = Contenido.objects.get(clave=llave) 
            c.valor = valor
        except Contenido.DoesNotExist:
            c = Contenido(clave=llave, valor=valor)
        c.save()
    if request.method == "POST" and action == "Enviar Comentario":
            c = get_object_or_404(Contenido, clave=llave) 
            titulo = request.POST['titulo']
            cuerpo = request.POST['cuerpo']
            q = Comentario(contenido=c, titulo=titulo, cuerpo=cuerpo, fecha=timezone.now())
            q.save()

    contenido = get_object_or_404(Contenido, clave=llave)
    context = {'contenido': contenido}
    return render(request, 'cms/contenido.html', context)
    
    [...]

Nota la última línea, donde ahora a render le pasamso "cms/contenido.html" en vez de "cms/content.html". ¡Y ya está! Guarda el fichero y comprueba que el sitio web sigue funcionando como antes. :)

Nota: Si te sale el error TemplateDoesNotExist, que significa que no hay ningún archivo cms/content.html y tienes runserver corriendo en la consola. Intenta pararlo, pulsando Ctrl+C (teclas Control y C a la vez) en la consola y reiniciarlo con el comando python manage.py runserver.

Si has llegado hasta aquí, pon en el chat de Teams: "Ya sé extender plantillas" para que sepamos los profes por dónde vas. Esto nos ayuda a seguir el transcurso de la clase y dimensionar las prácticas.

Trabajando con los forms de Django

Lo siguiente que haremos en nuestro sitio web será crear una forma agradable de agregar y editar contenidos en nuestra aplicación cms. El interfaz de admin de Django está bien, pero es bastante difícil de personalizar y hacerlo bonito. Con forms tendremos un poder absoluto sobre nuestra interfaz; ¡podemos hacer casi cualquier cosa que podamos imaginar!

Lo bueno de los formularios de Django es que podemos definirlos desde cero o crear un ModelForm, el cual guardará el resultado del formulario en el modelo. Esto es exactamente lo que queremos hacer: crearemos un formulario para poder hacer peticiones POST.

Como cada parte importante de Django, los formularios tienen su propio archivo: forms.py. Necesitamos crear un archivo con este nombre en el directorio de la aplicación.

cms/forms.py
from django import forms

from .models import Contenido

class ContenidoForm(forms.ModelForm):

    class Meta:
        model = Contenido
        fields = ('clave', 'valor',)
        

Lo primero, necesitamos importar Django forms (from django import forms) y nuestro modelo Contenido (from .models import Contenido). ContenidoForm, como probablemente sospechas, es el nombre de nuestro formulario. Necesitamos decirle a Django que este formulario es un ModelForm (así Django hará algo de magia por nosotros) - forms.ModelForm es responsable de ello.

Luego, tenemos class Meta, donde le decimos a Django qué modelo debe ser utilizado para crear este formulario (model = Contenido). Finalmente, podemos decir qué campo(s) deberían estar en nuestro formulario. En este escenario sólo queremos clave y valor para ser mostrados. ¡Y eso es todo! Todo lo que necesitamos hacer ahora es usar el formulario en una view y mostrarla en una plantilla.

Una vez más vamos a crear: un enlace a la página, una dirección URL, una vista y una plantilla. Para ello, toca abrir el fichero cms/templates/cms/content.html en el editor. Vamos a añadir un enlace en el div llamado page-header:

cms/templates/cms/content.html
 
<button type="button" class="btn btn-light"><a href="{% url 'cms_new' %}">Añadir un nuevo contenido</span></a></button>

Después de agregar la línea, tu archivo HTML debería lucir de esta forma:

cms/templates/cms/content.html

[...]
  <body>
    <div  class="jumbotron">
        {%  block contenido %} 
        {% endblock %} 
     </div>
     <button type="button"  class="btn btn-light"><a href="{% url 'cms_new' %}">Añadir un nuevo contenido</span></a></button>
  </body>
</html>

Abrimos cms/urls.py en el editor para añadir una línea con una nueva URLConf:

cms/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('new/', views.cms_new, name='cms_new'),
    [...]
]

Ahora abre el fichero cms/views.py en el editor y añade estas líneas con el resto de imports from:

cms/views.py
from .forms import ContenidoForm

[...]

def cms_new(request):
    form = ContenidoForm()
    return render(request, 'cms/cms_edit.html', {'form': form})

Para crear un nuevo formulario Contenido, tenemos que llamar a ContenidoForm() y pasarlo a la plantilla. Volveremos a esta vista pero, por ahora, vamos a crear rápidamente una plantilla para el formulario. Para ello, tenemos que crear un fichero cms_edit.html el el directorio cms/templates/cms, y abrirlo en el editor de código. Para hacer que un formulario funcione necesitamos varias cosas:

  • Tenemos que mostrar el formulario. Podemos hacerlo, por ejemplo, con un sencillo {{ form.as_p }}.
  • La línea anterior tiene que estar dentro de una etiqueta de formuLario HTML: <form method="POST">...</form>.
  • Necesitamos un botón Guardar. Lo hacemos con un botón HTML: <button type='submit'>Save</button>.
  • Finalmente justo después de abrir la etiqueta <form ...> tenemos que añadir {% csrf_token %} . ¡Esto es muy importante ya que hace que tus formularios sean seguros! Si olvidas este pedazo, Django se molestará cuando intentes guardar el formulario:

Bueno, miremos lo que se debería ver el HTML en cms_edit.html

cms/templates/cms/cms_edit.html

{% extends 'cms/content.html' %} 

{% block contenido %} 
    <h2>New content</h2>
    <form method="POST"  class="cms-form">{% csrf_token %} 
        {{ form.as_p }}
        <button type="submit" class="save btn btn-info">Guardar</button>
    </form>
{% endblock %} 

¡Es hora de actualizar! ¡Sí! ¡Tu formulario se muestra!

Pero, ¡un momento! Si escribes algo en los campos clave y valor, y tratas de guardar los cambios - ¿qué pasará? ¡Nada! Una vez más estamos en la misma página y el texto se ha ido... no se añade ningún contenido nuevo. Entonces, ¿qué ha ido mal? La respuesta es: nada. Tenemos que trabajar un poco más en nuestra vista.

Guardando el formulario

Abre cms/views.py de nuevo en el editor. De momento todo lo que tenemos en la vista cms_new es lo siguiente:

cms/views.py

def cms_new(request):
    form = ContenidoForm()
    return render(request, 'cms/cms_edit.html', {'form': form})

Cuando enviamos el formulario somos redirigidos a la misma vista, pero esta vez tenemos algunos datos adicionales en request, más específicamente en request.POST. ¿Recuerdas que en el archivo HTML la definición de <form> tenía la variable method="POST"? Todos los campos del formulario estan ahora en request.POST.

En nuestra vista tenemos dos situaciones distintas que manejar: primero, cuando accedemos a la página por primera vez y queremos un formulario vacío, y segundo, cuando regresamos a la vista con los datos del formulario que acabamos de ingresar. Así que tenemos que añadir una condición (utilizaremos if para eso):

cms/views.py

if request.method == "POST":
    [...]
else:
    form = ContenidoForm()

Es hora de rellenar los puntos [...]. Si el método es POST entonces querremos construir el ContenidoForm con datos del formulario, cierto? Lo haremos de la siguiente manera:

cms/views.py

form = ContenidoForm(request.POST)

Lo siguiente es verificar si el formulario está correcto (si todos los campos necesarios están definidos y no hay valores incorrectos). Lo hacemos con form.is_valid(). Comprobamos que el formulario es válido y, si es así, ¡lo podemos salvar!

cms/views.py

if form.is_valid():
    contenido = form.save()

De esta forma, guardamos el formulario, con form.save.

Por último, sería genial si podemos inmediatamente ir a la página get_content del nuevo Contenido, ¿no? Para hacerlo necesitamos importar algo más:

cms/views.py
from django.shortcuts import redirect

Agrégalo al principio del archivo. Y ahora podemos decir: "ve a la página get_content del Contenido recién creado":

cms/views.py

return redirect('get_content', llave=contenido.clave)

Abrimos cms/urls.py en el editor para modificar la línea que llama a la vista get_content para darle un nombre con el parámetro name. De esta manera, podremos llamarla directamente con el redirect:

cms/urls.py
from django.urls import path
from . import views

urlpatterns = [
    [...]
    path('<str:llave>', views.get_content, name="get_content"),
]

get_content pasa a ser el nombre de la vista a la que queremos ir. ¿Recuerdas que esta view requiere una variable clave? Para pasarlo a las vistas utilizamos clave=contenido.clave, donde contenido es el contenido recién creado! Bien, hablamos mucho, pero probablemente queremos ver como se ve la vista, ¿verdad?

cms/views.py

def cms_new(request):
    if request.method == "POST":
        form = ContenidoForm(request.POST)
        if form.is_valid():
            contenido = form.save()
            return redirect('get_content', llave=contenido.clave)
    else:
        form = ContenidoForm()
    return render(request, 'cms/cms_edit.html', {'form': form})

Veamos si funciona. Ve a la página http://127.0.0.1:8000/cms/new/, añade un title y un text, guárdalo... y voilá! Se ha añadido el nuevo contenido al cms y somos redirigidos a la página de get_content!

Ahora, vamos a enseñarte qué tan bueno es Django forms. Un contenido del cms debe tener los campos clave y valor. En nuestro modelo Contenido no dijimos que estos campos no son necesarios, así que Django, por defecto, espera que estén definidos. Trata de guardar el formulario sin clave y valor. ¡Adivina qué pasará!

Django se encarga de validar que todos los campos en el formulario estén correctos. ¿No es genial?

Si has llegado hasta aquí, pon en el chat de Teams: "He guardado el formulario con forms." para que sepamos los profes por dónde vas. Gracias.

Editando formularios

Ahora sabemos cómo agregar un nuevo formulario. Pero, ¿qué pasa si queremos editar uno existente? Es muy similar a lo que acabamos de hacer. Creemos rápidamente algunas cosas importantes. Abre cms/templates/cms/contenido.html y escribe:

cms/templates/cms/contenido.html

{%  extends 'cms/content.html' %} 
    {%  block contenido %} 
        <h1  class="display-4">Llave: {{ contenido.clave }}</h1>
          <p  class="lead"> {{ contenido.valor }} </p>
          <button type="button"  class="btn btn-light"><a href="{% url 'cms_modify' llave=contenido.clave %}">Modificar contenido</span></a></button>
          <hr  class="my-4">
            <div  class="content container">
                <div  class="row">
                    <div  class="col-md-8">
                    {%  for comentario in contenido.comentario_set.all %} 
                        <div  class="comentario">
                            <div  class="date">
                                {{ comentario.fecha }}
                            </div>
                            <h2>{{  comentario.titulo }}</h2>
                            <p>{{  comentario.cuerpo|linebreaksbr }}</p>
                        </div>
                    {% endfor %} 
                    </div>
                </div>
        </div>
    {% endblock %} 

Abre cms/urls.py en el editor y añade esta línea:

cms/urls.py


    path('modify/<str:llave>', views.cms_modify, name="cms_modify"),

Vamos a reusar la plantilla cms/templates/cms/cms_modify.html, así que lo último que nos falta es crear la vista. Abre cms/views.py en el editor de código y añade esto al final del todo:

cms/views.py

def cms_modify(request, llave):
    contenido = get_object_or_404(Contenido, clave=llave)
    if request.method == "POST":
        form = ContenidoForm(request.POST, instance=contenido)
        if form.is_valid():
            contenido = form.save()
            return redirect('get_content', llave=contenido.clave)
    else:
        form = ContenidoForm(instance=contenido)
    return render(request, 'cms/cms_edit.html', {'form': form})

Esto se ve casi exactamente igual a nuestra view cms_new, ¿no? Pero no del todo. Por un lado, pasamos un parámetro llave adicional en urls. Segundo: obtenemos el modelo Contenido que queremos editar con get_object_or_404(Contenido, clave=llave) y después, al crear el formulario pasamos este contenido como una instancia tanto al guardar el formulario...

cms/views.py

form = ContenidoForm(request.POST, instance=Contenido)

... y justo cuando abrimos un formulario con este contenido para editarlo:

cms/views.py

form = ContenidoForm(instance=contenido)

Ok, ¡vamos a probar si funciona! Dirígete a la página http://localhost:8000/cms/nabucco. Ahí debe haber un botón para editar, justo debajo del contenido:

¡Siéntete libre de cambiar el título o el texto y guarda los cambios! ¡Felicitaciones! ¡Tu aplicación está cada vez más completa! Si necesitas más información sobre los formularios de Django, lee la documentación: https://docs.djangoproject.com/en/3.0/topics/forms/

¡Poder crear nuevas publicaciones haciendo click en un enlace es genial! Pero, ahora mismo, cualquiera que visite tu página podría publicar un nuevo contenido y seguro que eso no es lo que quieres. Vamos a hacer que el botón sea visible para ti pero no para nadie más.

Abre cms/templates/cms/content.html en el editor, busca el div page-header y la etiqueta del enlace (anchor) que pusimos antes. Debería ser algo así:

cms/templates/cms/content.html

<button type="button" class="btn btn-light"><a href="{% url 'cms_new' %}">Añadir un nuevo contenido</span></a></button>

Vamos a añadir otra etiqueta {% if %} que hará que el enlace sólo parezca para los usuarios que hayan iniciado sesión en el admin. Ahora mismo, ¡eres sólo tú! Cambia la etiqueta <a> para que se parezca a esto:

cms/templates/cms/content.html

 {%  if user.is_authenticated %} 
     <button type="button" class="btn btn-light"><a href="{% url 'cms_new' %}">Añadir un nuevo contenido</span></a></button>
 {%  endif %} 

Este {% if %} hará que el enlace sólo se envíe al navegador si el usuario que solicita la página ha iniciado sesión. Esto no protege completamente la creación de nuevos contenidos, pero es un buen primer paso. ¿Recuerdas el icono de "editar" que acabamos de añadir a nuestra página de detalles? También queremos añadir lo mismo aquí, así otras personas no podrán editar contenidos existentes.

Abre cms/templates/cms/contenido.html en el editor y busca esta línea:

cms/templates/cms/contenido.html

<button type="button" class="btn btn-light"><a href="{% url 'cms_modify' llave=contenido.clave %}">Modificar contenido</span></a></button>

Cámbiala a lo siguiente:

cms/templates/cms/contenido.html

 {%  if user.is_authenticated %} 
    <button type="button" class="btn btn-light"><a href="{% url 'cms_modify' llave=contenido.clave %}">Modificar contenido</span></a></button>
 {%  endif %} 

Dado que es probable que estés conectado, si actualizas la página, no verás nada diferente. Carga la página en un navegador diferente o en una ventana en modo incógnito (o "privado" en Windows Edge) y verás que el link no aparece, y el icono tampoco!

Si has llegado hasta aquí, has acabado la práctica. ¡Enhorabuena! Por favor, pon en el chat de Teams: "He terminado la práctdica" para que los profes también lo sepamos. Gracias.

Si tienes todavía energía, puedes crear el formulario para los comentarios con Forms.