| 
									
										
										
										
											2018-02-10 06:39:05 +08:00
										 |  |  | .. currentmodule:: flask
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Blog Blueprint
 | 
					
						
							|  |  |  | ==============
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | You'll use the same techniques you learned about when writing the
 | 
					
						
							|  |  |  | authentication blueprint to write the blog blueprint. The blog should
 | 
					
						
							|  |  |  | list all posts, allow logged in users to create posts, and allow the
 | 
					
						
							|  |  |  | author of a post to edit or delete it.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | As you implement each view, keep the development server running. As you
 | 
					
						
							|  |  |  | save your changes, try going to the URL in your browser and testing them
 | 
					
						
							|  |  |  | out.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The Blueprint
 | 
					
						
							|  |  |  | -------------
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Define the blueprint and register it in the application factory.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. code-block:: python
 | 
					
						
							|  |  |  |     :caption: ``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__)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Import and register the blueprint from the factory using
 | 
					
						
							|  |  |  | :meth:`app.register_blueprint() <Flask.register_blueprint>`. Place the
 | 
					
						
							|  |  |  | new code at the end of the factory function before returning the app.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. code-block:: python
 | 
					
						
							|  |  |  |     :caption: ``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
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Unlike the auth blueprint, the blog blueprint does not have a
 | 
					
						
							|  |  |  | ``url_prefix``. So the ``index`` view will be at ``/``, the ``create``
 | 
					
						
							|  |  |  | view at ``/create``, and so on. The blog is the main feature of Flaskr,
 | 
					
						
							|  |  |  | so it makes sense that the blog index will be the main index.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | However, the endpoint for the ``index`` view defined below will be
 | 
					
						
							|  |  |  | ``blog.index``. Some of the authentication views referred to a plain
 | 
					
						
							|  |  |  | ``index`` endpoint. :meth:`app.add_url_rule() <Flask.add_url_rule>`
 | 
					
						
							|  |  |  | associates the endpoint name ``'index'`` with the ``/`` url so that
 | 
					
						
							|  |  |  | ``url_for('index')`` or ``url_for('blog.index')`` will both work,
 | 
					
						
							|  |  |  | generating the same ``/`` URL either way.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | In another application you might give the blog blueprint a
 | 
					
						
							|  |  |  | ``url_prefix`` and define a separate ``index`` view in the application
 | 
					
						
							|  |  |  | factory, similar to the ``hello`` view. Then the ``index`` and
 | 
					
						
							|  |  |  | ``blog.index`` endpoints and URLs would be different.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Index
 | 
					
						
							|  |  |  | -----
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The index will show all of the posts, most recent first. A ``JOIN`` is
 | 
					
						
							|  |  |  | used so that the author information from the ``user`` table is
 | 
					
						
							|  |  |  | available in the result.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. code-block:: python
 | 
					
						
							|  |  |  |     :caption: ``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)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. code-block:: html+jinja
 | 
					
						
							|  |  |  |     :caption: ``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 %}
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | When a user is logged in, the ``header`` block adds a link to the
 | 
					
						
							|  |  |  | ``create`` view. When the user is the author of a post, they'll see an
 | 
					
						
							|  |  |  | "Edit" link to the ``update`` view for that post. ``loop.last`` is a
 | 
					
						
							|  |  |  | special variable available inside `Jinja for loops`_. It's used to
 | 
					
						
							|  |  |  | display a line after each post except the last one, to visually separate
 | 
					
						
							|  |  |  | them.
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-15 12:27:44 +08:00
										 |  |  | .. _Jinja for loops: https://jinja.palletsprojects.com/templates/#for
 | 
					
						
							| 
									
										
										
										
											2018-02-10 06:39:05 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Create
 | 
					
						
							|  |  |  | ------
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The ``create`` view works the same as the auth ``register`` view. Either
 | 
					
						
							|  |  |  | the form is displayed, or the posted data is validated and the post is
 | 
					
						
							|  |  |  | added to the database or an error is shown.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The ``login_required`` decorator you wrote earlier is used on the blog
 | 
					
						
							|  |  |  | views. A user must be logged in to visit these views, otherwise they
 | 
					
						
							|  |  |  | will be redirected to the login page.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. code-block:: python
 | 
					
						
							|  |  |  |     :caption: ``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')
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. code-block:: html+jinja
 | 
					
						
							|  |  |  |     :caption: ``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
 | 
					
						
							|  |  |  | ------
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Both the ``update`` and ``delete`` views will need to fetch a ``post``
 | 
					
						
							|  |  |  | by ``id`` and check if the author matches the logged in user. To avoid
 | 
					
						
							|  |  |  | duplicating code, you can write a function to get the ``post`` and call
 | 
					
						
							|  |  |  | it from each view.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. code-block:: python
 | 
					
						
							|  |  |  |     :caption: ``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:
 | 
					
						
							| 
									
										
										
										
											2020-04-05 02:39:03 +08:00
										 |  |  |             abort(404, f"Post id {id} doesn't exist.")
 | 
					
						
							| 
									
										
										
										
											2018-02-10 06:39:05 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if check_author and post['author_id'] != g.user['id']:
 | 
					
						
							|  |  |  |             abort(403)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return post
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :func:`abort` will raise a special exception that returns an HTTP status
 | 
					
						
							|  |  |  | code. It takes an optional message to show with the error, otherwise a
 | 
					
						
							|  |  |  | default message is used. ``404`` means "Not Found", and ``403`` means
 | 
					
						
							|  |  |  | "Forbidden". (``401`` means "Unauthorized", but you redirect to the
 | 
					
						
							|  |  |  | login page instead of returning that status.)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The ``check_author`` argument is defined so that the function can be
 | 
					
						
							|  |  |  | used to get a ``post`` without checking the author. This would be useful
 | 
					
						
							|  |  |  | if you wrote a view to show an individual post on a page, where the user
 | 
					
						
							|  |  |  | doesn't matter because they're not modifying the post.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. code-block:: python
 | 
					
						
							|  |  |  |     :caption: ``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)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Unlike the views you've written so far, the ``update`` function takes
 | 
					
						
							|  |  |  | an argument, ``id``. That corresponds to the ``<int:id>`` in the route.
 | 
					
						
							|  |  |  | A real URL will look like ``/1/update``. Flask will capture the ``1``,
 | 
					
						
							|  |  |  | ensure it's an :class:`int`, and pass it as the ``id`` argument. If you
 | 
					
						
							|  |  |  | don't specify ``int:`` and instead do ``<id>``, it will be a string.
 | 
					
						
							|  |  |  | To generate a URL to the update page, :func:`url_for` needs to be passed
 | 
					
						
							|  |  |  | the ``id`` so it knows what to fill in:
 | 
					
						
							|  |  |  | ``url_for('blog.update', id=post['id'])``. This is also in the
 | 
					
						
							|  |  |  | ``index.html`` file above.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The ``create`` and ``update`` views look very similar. The main
 | 
					
						
							|  |  |  | difference is that the ``update`` view uses a ``post`` object and an
 | 
					
						
							|  |  |  | ``UPDATE`` query instead of an ``INSERT``. With some clever refactoring,
 | 
					
						
							|  |  |  | you could use one view and template for both actions, but for the
 | 
					
						
							|  |  |  | tutorial it's clearer to keep them separate.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. code-block:: html+jinja
 | 
					
						
							|  |  |  |     :caption: ``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 %}
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | This template has two forms. The first posts the edited data to the
 | 
					
						
							|  |  |  | current page (``/<id>/update``). The other form contains only a button
 | 
					
						
							|  |  |  | and specifies an ``action`` attribute that posts to the delete view
 | 
					
						
							|  |  |  | instead. The button uses some JavaScript to show a confirmation dialog
 | 
					
						
							|  |  |  | before submitting.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The pattern ``{{ request.form['title'] or post['title'] }}`` is used to
 | 
					
						
							|  |  |  | choose what data appears in the form. When the form hasn't been
 | 
					
						
							|  |  |  | submitted, the original ``post`` data appears, but if invalid form data
 | 
					
						
							|  |  |  | was posted you want to display that so the user can fix the error, so
 | 
					
						
							|  |  |  | ``request.form`` is used instead. :data:`request` is another variable
 | 
					
						
							|  |  |  | that's automatically available in templates.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Delete
 | 
					
						
							|  |  |  | ------
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The delete view doesn't have its own template, the delete button is part
 | 
					
						
							|  |  |  | of ``update.html`` and posts to the ``/<id>/delete`` URL. Since there
 | 
					
						
							| 
									
										
										
										
											2018-12-15 00:59:16 +08:00
										 |  |  | is no template, it will only handle the ``POST`` method and then redirect
 | 
					
						
							| 
									
										
										
										
											2018-02-10 06:39:05 +08:00
										 |  |  | to the ``index`` view.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. code-block:: python
 | 
					
						
							|  |  |  |     :caption: ``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'))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Congratulations, you've now finished writing your application! Take some
 | 
					
						
							|  |  |  | time to try out everything in the browser. However, there's still more
 | 
					
						
							|  |  |  | to do before the project is complete.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Continue to :doc:`install`.
 |