Blog Blueprint

Utilizarás las mismas técnicas que aprendiste al escribir el Blueprint de autenticación para escribir el Blueprint del blog. El blog debe listar todas las entradas, permitir a los usuarios registrados crear entradas, y permitir al autor de una entrada editarla o eliminarla.

A medida que implementes cada vista, mantén el servidor de desarrollo en funcionamiento. A medida que guarde los cambios, intente ir a la URL en su navegador y probarlos.

El Blueprint

Define el blueprint y regístralo en la fábrica de aplicaciones.

flaskr/blog.py
from flask import (
    Blueprint, flash, g, redirect, render_template, request, url_for
)
from werkzeug.exceptions import abort

from flaskr.auth import login_required
from flaskr.db import get_db

bp = Blueprint('blog', __name__)

Importa y registra el blueprint de la fábrica usando app.register_blueprint(). Coloca el nuevo código al final de la función de fábrica antes de devolver la app.

flaskr/__init__.py
def create_app():
    app = ...
    # existing code omitted

    from . import blog
    app.register_blueprint(blog.bp)
    app.add_url_rule('/', endpoint='index')

    return app

A diferencia del plano de autenticidad, el blueprint del blog no tiene una url_prefix. Así que la vista index estará en /, la vista create en /create, y así sucesivamente. El blog es la característica principal de Flaskr, así que tiene sentido que el índice del blog sea el índice principal.

Sin embargo, el endpoint para la vista index definida a continuación será blog.index. Algunas de las vistas de autenticación hacían referencia a un punto final simple index. app.add_url_rule() asocia el nombre del punto final 'index con la url / para que url_for('index') o url_for('blog.index') funcionen ambos, generando la misma URL / de cualquier manera.

En otra aplicación podrías dar al blueprint del blog un url_prefix y definir una vista index separada en la fábrica de la aplicación, similar a la vista hello. Entonces los endpoints y las URL de index y blog.index serían diferentes.

Índice

El índice mostrará todos los mensajes, el más reciente primero. Se utiliza un JOIN para que la información del autor de la tabla user esté disponible en el resultado.

flaskr/blog.py
@bp.route('/')
def index():
    db = get_db()
    posts = db.execute(
        'SELECT p.id, title, body, created, author_id, username'
        ' FROM post p JOIN user u ON p.author_id = u.id'
        ' ORDER BY created DESC'
    ).fetchall()
    return render_template('blog/index.html', posts=posts)
flaskr/templates/blog/index.html
{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Posts{% endblock %}</h1>
  {% if g.user %}
    <a class="action" href="{{ url_for('blog.create') }}">New</a>
  {% endif %}
{% endblock %}

{% block content %}
  {% for post in posts %}
    <article class="post">
      <header>
        <div>
          <h1>{{ post['title'] }}</h1>
          <div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div>
        </div>
        {% if g.user['id'] == post['author_id'] %}
          <a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a>
        {% endif %}
      </header>
      <p class="body">{{ post['body'] }}</p>
    </article>
    {% if not loop.last %}
      <hr>
    {% endif %}
  {% endfor %}
{% endblock %}

Cuando un usuario está conectado, el bloque header añade un enlace a la vista create. Cuando el usuario es el autor de una entrada, verá un enlace «Edit» a la vista update para esa entrada. loop.last es una variable especial disponible dentro de Jinja para bucles. Se utiliza para mostrar una línea después de cada entrada excepto la última, para separarlas visualmente.

Crear

La vista create funciona igual que la vista auth register. O bien se muestra el formulario, o bien se validan los datos publicados y se añaden a la base de datos o se muestra un error.

El decorador login_required que escribiste antes se utiliza en las vistas del blog. Un usuario debe estar conectado para visitar estas vistas, de lo contrario será redirigido a la página de inicio de sesión.

flaskr/blog.py
@bp.route('/create', methods=('GET', 'POST'))
@login_required
def create():
    if request.method == 'POST':
        title = request.form['title']
        body = request.form['body']
        error = None

        if not title:
            error = 'Title is required.'

        if error is not None:
            flash(error)
        else:
            db = get_db()
            db.execute(
                'INSERT INTO post (title, body, author_id)'
                ' VALUES (?, ?, ?)',
                (title, body, g.user['id'])
            )
            db.commit()
            return redirect(url_for('blog.index'))

    return render_template('blog/create.html')
flaskr/templates/blog/create.html
{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}New Post{% endblock %}</h1>
{% endblock %}

{% block content %}
  <form method="post">
    <label for="title">Title</label>
    <input name="title" id="title" value="{{ request.form['title'] }}" required>
    <label for="body">Body</label>
    <textarea name="body" id="body">{{ request.form['body'] }}</textarea>
    <input type="submit" value="Save">
  </form>
{% endblock %}

Actualización

Tanto la vista update como la vista delete necesitarán obtener un post por id y comprobar si el autor coincide con el usuario conectado. Para evitar duplicar el código, puedes escribir una función para obtener el post y llamarla desde cada vista.

flaskr/blog.py
def get_post(id, check_author=True):
    post = get_db().execute(
        'SELECT p.id, title, body, created, author_id, username'
        ' FROM post p JOIN user u ON p.author_id = u.id'
        ' WHERE p.id = ?',
        (id,)
    ).fetchone()

    if post is None:
        abort(404, f"Post id {id} doesn't exist.")

    if check_author and post['author_id'] != g.user['id']:
        abort(403)

    return post

abort() lanzará una excepción especial que devuelve un código de estado HTTP. Toma un mensaje opcional para mostrar con el error, de lo contrario se utiliza un mensaje por defecto. 404 significa «Not Found», y 403 significa «Forbidden». (401 significa «Unauthorized», pero redirige a la página de acceso en lugar de devolver ese estado).

El argumento check_author se define para que la función pueda ser utilizada para obtener un post sin comprobar el autor. Esto sería útil si escribes una vista para mostrar un post individual en una página, donde el usuario no importa porque no está modificando el post.

flaskr/blog.py
@bp.route('/<int:id>/update', methods=('GET', 'POST'))
@login_required
def update(id):
    post = get_post(id)

    if request.method == 'POST':
        title = request.form['title']
        body = request.form['body']
        error = None

        if not title:
            error = 'Title is required.'

        if error is not None:
            flash(error)
        else:
            db = get_db()
            db.execute(
                'UPDATE post SET title = ?, body = ?'
                ' WHERE id = ?',
                (title, body, id)
            )
            db.commit()
            return redirect(url_for('blog.index'))

    return render_template('blog/update.html', post=post)

A diferencia de las vistas que has escrito hasta ahora, la función update toma un argumento, id. Este corresponde al <int:id>` de la ruta. Una URL real será como ``/1/update. Flask capturará el 1, se asegurará de que es un int, y lo pasará como el argumento id. Si no especificas int: y en su lugar haces <id>, será una cadena. Para generar una URL a la página de actualización, a url_for() hay que pasarle el id para que sepa qué rellenar: url_for('blog.update', id=post['id']). Esto también está en el archivo index.html de arriba.

Las vistas create y update son muy similares. La principal diferencia es que la vista update utiliza un objeto post y una consulta UPDATE en lugar de un INSERT. Con un poco de refactorización inteligente, podrías usar una vista y una plantilla para ambas acciones, pero para el tutorial es más claro mantenerlas separadas.

flaskr/templates/blog/update.html
{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
{% endblock %}

{% block content %}
  <form method="post">
    <label for="title">Title</label>
    <input name="title" id="title"
      value="{{ request.form['title'] or post['title'] }}" required>
    <label for="body">Body</label>
    <textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>
    <input type="submit" value="Save">
  </form>
  <hr>
  <form action="{{ url_for('blog.delete', id=post['id']) }}" method="post">
    <input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');">
  </form>
{% endblock %}

Esta plantilla tiene dos formas. El primero publica los datos editados en la página actual (/<id>/update). El otro formulario contiene sólo un botón y especifica un atributo action que envía a la vista de borrado. El botón utiliza algo de JavaScript para mostrar un diálogo de confirmación antes de enviarlo.

El patrón {{ request.form['title'] o post['title'] }}` se utiliza para elegir qué datos aparecen en el formulario. Cuando el formulario no se ha enviado, aparecen los datos originales de ``post, pero si se han publicado datos de formulario no válidos, se desea mostrarlos para que el usuario pueda solucionar el error, por lo que se utiliza request.form en su lugar. request es otra variable que está disponible automáticamente en las plantillas.

Borrar

La vista de borrado no tiene su propia plantilla, el botón de borrado es parte de update.html y publica en la URL /<id>/delete. Como no hay plantilla, sólo manejará el método POST y luego redirigirá a la vista index.

flaskr/blog.py
@bp.route('/<int:id>/delete', methods=('POST',))
@login_required
def delete(id):
    get_post(id)
    db = get_db()
    db.execute('DELETE FROM post WHERE id = ?', (id,))
    db.commit()
    return redirect(url_for('blog.index'))

Enhorabuena, ya has terminado de escribir tu aplicación. Tómate un tiempo para probarlo todo en el navegador. Sin embargo, todavía hay más cosas que hacer antes de que el proyecto esté completo.

Continúa con Hacer que el proyecto sea instalable.