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.
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.
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.
@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)
{% 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.
@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')
{% 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.
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.
@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.
{% 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
.
@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.