План блога

Для создания блога вы будете использовать те же приемы, о которых вы узнали при написании чертежа аутентификации. Блог должен содержать список всех записей, позволять пользователям, вошедшим в систему, создавать записи и позволять автору записи редактировать или удалять ее.

По мере внедрения каждого представления сохраняйте сервер разработки запущенным. Сохранив изменения, попробуйте перейти по URL-адресу в браузере и протестировать их.

План

Определите чертеж и зарегистрируйте его в фабрике приложений.

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

Импортируйте и зарегистрируйте blueprint из фабрики с помощью app.register_blueprint(). Поместите новый код в конец функции фабрики перед возвратом приложения.

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

В отличие от чертежа auth, чертеж blog не имеет url_prefix. Поэтому представление index будет находиться по адресу /, представление create по адресу /create и так далее. Блог является главной особенностью Flaskr, поэтому логично, что индекс блога будет главным индексом.

Однако конечная точка для представления index, определенного ниже, будет blog.index. Некоторые представления аутентификации ссылались на обычную конечную точку index. app.add_url_rule() связывает имя конечной точки 'index' с урлом /, так что url_for('index') или url_for('blog.index') будут работать оба, генерируя один и тот же урл / в любом случае.

В другом приложении вы могли бы дать чертежу блога значение url_prefix и определить отдельное представление index в фабрике приложения, аналогичное представлению hello. Тогда конечные точки index и blog.index и URL будут разными.

Индекс

В индексе будут показаны все сообщения, сначала самые последние. JOIN используется для того, чтобы информация об авторе из таблицы user была доступна в результате.

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

Когда пользователь вошел в систему, блок header добавляет ссылку на представление create. Когда пользователь является автором сообщения, он увидит ссылку «Редактировать» на представление update для этого сообщения. loop.last - это специальная переменная, доступная внутри Jinja for loops. Она используется для отображения строки после каждого сообщения, кроме последнего, чтобы визуально разделить их.

Создать

Представление create работает так же, как и представление auth register. Либо отображается форма, либо проверяются размещенные данные и пост добавляется в базу данных, либо показывается ошибка.

Декоратор login_required, который вы написали ранее, используется в представлениях блога. Пользователь должен войти в систему, чтобы посетить эти представления, иначе он будет перенаправлен на страницу входа.

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 и delete должны получить post по id и проверить, соответствует ли автор вошедшему пользователю. Чтобы избежать дублирования кода, можно написать функцию для получения post и вызывать ее из каждого представления.

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() вызовет специальное исключение, которое вернет код состояния HTTP. Оно принимает необязательное сообщение для отображения ошибки, в противном случае используется сообщение по умолчанию. 404 означает «Не найдено», а 403 означает «Запрещено». (401 означает «Unauthorized», но вы перенаправляете на страницу входа в систему вместо того, чтобы возвращать этот статус).

Аргумент check_author определен для того, чтобы функцию можно было использовать для получения 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)

В отличие от представлений, которые вы написали до сих пор, функция update принимает аргумент id. Это соответствует <int:id> в маршруте. Настоящий URL будет выглядеть как /1/update. Flask перехватит 1, убедится, что это int, и передаст его в качестве аргумента id. Если вы не укажете int: и вместо этого сделаете <id>, это будет строка. Чтобы сгенерировать URL для страницы обновления, url_for() нужно передать id, чтобы он знал, что заполнять: url_for('blog.update', id=post['id']). Это также находится в файле index.html выше.

Представления create и update выглядят очень похоже. Основное различие заключается в том, что представление update использует объект post и запрос UPDATE вместо запроса INSERT. С помощью грамотного рефакторинга можно было бы использовать одно представление и шаблон для обоих действий, но для учебного пособия понятнее держать их отдельно.

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

Этот шаблон имеет две формы. Первая размещает отредактированные данные на текущей странице (/<id>/update). Другая форма содержит только кнопку и указывает атрибут action, который вместо этого постит данные в представление удаления. Кнопка использует JavaScript для отображения диалога подтверждения перед отправкой.

Шаблон {{ request.form['title'] or post['title'] }} используется для выбора того, какие данные появляются в форме. Когда форма не была отправлена, отображаются исходные данные post, но если были опубликованы недопустимые данные формы, вы хотите отобразить их, чтобы пользователь мог исправить ошибку, поэтому вместо них используется request.form. request - еще одна переменная, которая автоматически доступна в шаблонах.

Удалить

Представление удаления не имеет собственного шаблона, кнопка удаления является частью update.html и размещается на URL /<id>/delete. Поскольку шаблона нет, он будет обрабатывать только метод POST, а затем перенаправлять на представление 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'))

Поздравляем, вы закончили написание приложения! Потратьте немного времени, чтобы опробовать все в браузере. Однако до завершения проекта еще многое предстоит сделать.

Продолжить Сделать проект устанавливаемым.

Вернуться на верх