Le plan du blog¶
Vous utiliserez les techniques que nous avons vues pour écrire le plan d’authentification pour écrire le plan du blog. Le blog doit présenter tous les messages, permettre aux utilisateurs qui sont connectés de créer des messages et à l’auteur d’un message de l’éditer ou l’effacer.
Laissez le serveur de développement tourner pendant que vous implémentez chaque vue. Cela vous permettra d’aller directement sur l’URL correspondant sur votre navigateur et de le tester directement.
Le plan¶
Définissez le plan et enregistrez-le dans l’usine à applications.
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__)
Vous pouvez importer et enregistrer le plan de l’usine à applications en utilisant app.register_blueprint()
. Pour cela, il vous suffit de placer votre code à la fin de celui de l’usine à applications avant de retourner l’application.
def create_app():
app = ...
# existing code omitted
from . import blog
app.register_blueprint(blog.bp)
app.add_url_rule('/', endpoint='index')
return app
Contrairement au plan authentification, le plan du blog n’utilise pas d”url_prefix
. Pour cette raison, la vue index
sera à /
, la vue create
à /create
etc. Le blog est la fonctionnalité principale de Flaskr, et donc c’est logique que l’index du blog se trouve à la racine du site.
Cependant,le point final de la vue index
définie ci-dessous sera blog.index
. Certaines des vues d’authentification faisaient référence au point final index
. app.add_url_rule()
associe le nom du point final 'index'
avec l’URL /
de façon à ce que tant url_for('index')
que url_for('blog.index')
fonctionnent et génèrent toutes les deux l’URL /
.
Dans d’autres applications, vous pourriez devoir donner au plan blog un url_prefix
et définir une vue index
séparée dans l’usine à applications de la même façon que pour la vue hello
. De cette façon, les points finaux index
et blog.index
seraient différents.
Index¶
L’index va présenter tous les messages en commençant pas le plus récent. une requête SQL de type JOIN
est utilisée de façon à ce que l’information concernant l’auteur qui provient de la table user
soit incluse dans le résultat.
@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 %}
Lorsque l’utilisateur est authentifié, le bloc header
ajoute un lien dans la vue create
. Lorsque l’utilisateur est auteur d’un message, il verra le lien update
dans la vue relative à ce message. loop.last
est une variable spéciale de Jinja for loops. Elle est utilisée pour afficher une ligne après chaque message, sauf le dernier, de façon à les séparer visuellement.
Create¶
La vue Create
fonctionne de la même façon que la vue register
. Soit le formulaire est affiché, soit le message posté est validé et il ajouté à la base de données ou une erreur est affichée.
Le décorateur login_required
que vous avez écrit précédemment est utilisé dans les vues du blog. Un utilisateur doit être authentifié pour visiter ces vues, sinon il est redirigé vers la page de login.
@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 %}
Update¶
Les vues update
et delete
doivent récupérer un post
sur base de son id
et vérifier si l’auteur correspond à l’utilisateur connecté. Pour éviter de dupliquer du code, vous pouvez écrire une fonction pour récupérer un post
et l’appeler depuis chaque vue.
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, "Post id {0} doesn't exist.".format(id))
if check_author and post['author_id'] != g.user['id']:
abort(403)
return post
abort()
va lancer une exception spécial qui retourne un code de statut HTTP. Cette fonction prend en argument optionnel un message à présenter avec l’erreur, sinon un message par défaut est utilisé. 404
signifie que l’URL n’a pas été trouve (« Not Found ») et 403
signifie interdit (« Forbidden »). L’erreur 401
signifie non-autorisé (« Unauthorized »), mais dans ce cas vous redirigez déjà vers la page de login plutôt que de retourner le statut HTTP.
L’argument check_author
est défini de façon à ce que la fonction puisse être utilisée pour récupérer un post
dans devoir valider l’utilisateur. Cela peut être utile si vous écrivez une vue permettant de voir un message individuel dans une page où l’utilisateur n’est pas important car il ne peut pas modifier le message.
@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)
Contrairement aux vues que vous avez écrites jusque maintenant, la fonction update
prend un argument, id
. Cela correspond à <int:id>
dans la route. Un URL réel ressemblera à /1/update
. Flask capturera le 1
, vérifiera que c’est un int
et le passera via l’argument id
. Si vous ne spécifiez pas int:
et indiquez simplement <id>
, ce sera une chaîne de caractères. Pour générer un URL pour la page update, l’argument id
doit être passé à la fonction url_for()
pour qu’elle sache comment remplir url_for('blog.update', id=post['id'])
. C’est aussi le cas pour le fichier index.html
ci-dessus.
Les vues create
et update
sont très similaires. La différence principale est que la vue update
utilise un objet post``and une requête SQL ``UPDATE
plutôt qu’une requête INSERT
. Avec une bonne réorganisation du code, vous pourriez utiliser une vue et des templaces pour ces deux actions, mais pour le tutoriel, c’est plus simple de garder les deux séparés.
{% 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 %}
Ce template a deux formulaires. Le premier poste le message modifié sur la page courante (/<id>/update
). L’autre formulaire contient uniquement un bouton et spécifie un attribut action
qui poste sur la vue delete
à la place. Ce bouton utilise un peu de javascript pour montrer une boîte de dialogue qui demande à l’utilisateur de confirmer avant de soumettre.
Le motif (pattern) {{ request.form['title'] or post['title'] }}
est utilisé pour choisir entre quelle donné apparaît dans le formulaire. Tant que le formulaire n’a pas été soumis, c’est le post
original qui apparaît, mais si le formulaire contient une information invalide, vous voulez la présenter de façon à ce qu’il puisse corriger son erreur. Cela se fait en utilisant request.form
. request
est une autre variable qui est automatiquement disponible dans les templates.
Delete¶
La vue delete n’utilise pas de template spécifique, le bouton delete est inclus dans update.html
et poste sur l’URL /<id>/delete
. Comme il n’y pas pas de template, elle ne supporte que la méthode POST
et ensuite redirige vers la vue 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'))
Félicitations, vous avez fini d’écrire votre application! Prenez un peu de temps pour tout essayer dans votre navigateur. Votre travail n’est pas encore complètement terminé, il reste un peu à faire pour le finaliser.
Continuez à lire le document Rendre le projet installable.