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.

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__)

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.

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

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.

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 %}

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.

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 %}

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.

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, "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.

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)

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.

url_for('blog.update', id=post['id'])
{% 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.

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'))

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.