The Flask Mega-Tutorial, Part 9: Pagination

Original author: Miguel
  • Transfer
  • Tutorial
This is the ninth article in a series where I describe my experience writing a Python web application using the Flask microframework.

The purpose of this guide is to develop a fairly functional microblogging application, which I decided to name for a complete lack of originalitymicroblog.



Brief repetition



In the previous article, we made all the necessary changes to the database to support the “subscribers” paradigm, in which users can track posts of other users.

Today, based on what we did last time, we will bring our application to mind so that it receives and delivers real content for our users. Today we say goodbye to the last of our fake objects!

Submitting Blog Posts



Let's start with something simple. The home page should have a custom form for new posts.

First we define a form object with one field (файл app/forms.py):

class PostForm(Form):
    post = TextField('post', validators = [Required()])


Next, we will add the form to the templates (файл app/templates/index.html):


{% extends "base.html" %}
{% block content %}

Hi, {{g.user.nickname}}!

{{form.hidden_tag()}}
Say something:{{ form.post(size = 30, maxlength = 140) }} {% for error in form.errors.post %} [{{error}}]
{% endfor %}
{% for post in posts %}

{{post.author.nickname}} says: {{post.body}}

{% endfor %} {% endblock %}


Nothing supernatural, as you may notice. We just add another shape, just as we did before.

And finally, a presentation function that ties everything together (файл app/views.py):

from forms import LoginForm, EditForm, PostForm
from models import User, ROLE_USER, ROLE_ADMIN, Post
@app.route('/', methods = ['GET', 'POST'])
@app.route('/index', methods = ['GET', 'POST'])
@login_required
def index():
    form = PostForm()
    if form.validate_on_submit():
        post = Post(body = form.post.data, timestamp = datetime.utcnow(), author = g.user)
        db.session.add(post)
        db.session.commit()
        flash('Your post is now live!')
        return redirect(url_for('index'))
    posts = [
        { 
            'author': { 'nickname': 'John' }, 
            'body': 'Beautiful day in Portland!' 
        },
        { 
            'author': { 'nickname': 'Susan' }, 
            'body': 'The Avengers movie was so cool!' 
        }
    ]
    return render_template('index.html',
        title = 'Home',
        form = form,
        posts = posts)


Let's look at the changes we made in this function, step by step:

  • First we import the classes PostandPostForm
  • We accept POST requests from both routes associated with the presentation function index(), here we will accept new posts.
  • Before the post is saved to the database, it falls into this function. When we get into this function through GET, everything happens as before.
  • The template now receives an additional argument, the form that it renders into text fields.


And finally, before we continue. Please note that before writing a new post to the database, we do this:

return redirect(url_for('index'))


We can easily skip this redirect and move on to rendering. Perhaps it will even be more effective. Because all that this redirect does is, in fact, it brings us back to the same function.

So why a redirect? Consider what happens when a user writes a post to a blog, publishes it and presses the Update button. What does the refresh command do? The browser resends the previous request.

Without a redirect, the last one was a POST request that sent the form, so the “Update” action will re-send this request, resulting in a second post that is identical to the first. This is bad.

But with a redirect we will force the browser to issue another request after submitting the form. This is a simple GET request, and now the Refresh button will just load the page again, instead of sending the form again.

This simple trick avoids inserting duplicate posts if the user accidentally refreshes the page after posting.

Display Blog Posts



We pass to the most interesting. We are going to pull posts from the database and display them.

If you remember a few articles ago, we made a couple of fake posts and we have been displaying them on our homepage for a long time. These fake objects were created explicitly in the Python list view function:

    posts = [
        { 
            'author': { 'nickname': 'John' }, 
            'body': 'Beautiful day in Portland!' 
        },
        { 
            'author': { 'nickname': 'Susan' }, 
            'body': 'The Avengers movie was so cool!' 
        }
    ]


But in the last article, we created a request that allows us to receive all messages from the people the user is subscribed to, so that we can simply replace the above lines (файл app/views.py):

 posts = g.user.followed_posts().all()


Now, when you start the application, you will see posts from the database!

The followed_postclass method Userreturns an querysqlalchemy object that is configured to pull out the messages of interest to us. By calling the method all()on this object, we get all the posts in the form of a sheet, so that in the end we will work with the structure we are familiar with. They are so similar that the template will not notice anything.

Now you can play with our application. Do not be shy. You can create several users, subscribe them to other users, and finally publish several messages to see how each user sees his own feed.

Pagination



Our application looks better than ever, but there is a problem. We show all messages on the home page. What happens if a user is subscribed to several thousand people? Or a million? As you can imagine, fetching and processing such a large list will be extremely inefficient.

Instead, we will display a potentially large number of paginated posts.

Flask-SQLAlchemy comes with very good pagination support. If, for example, we want to get the first three posts from tracked users, we can do this:

 posts = g.user.followed_posts().paginate(1, 3, False).items


The method paginateis called on any object query. It takes three arguments:

  • Page number starting at 1
  • number of elements per page,
  • and the error flag. If the flag is set to True, when the list is exceeded, a 404 error is returned to the client.
  • Otherwise, an empty list will be returned instead of an error.


The method paginatereturns a Pagination object. Members of this object contain a list of elements of the requested page. There are other useful things in the Pagination object, but we will discuss them a bit later.

Let's think about how we can implement pagination in our index () view function. We can start by adding a configuration element to our application, which determines how many elements on the page we will show.

# pagination
POSTS_PER_PAGE = 3


It is a good idea to store global application settings that can influence behavior in one place.

In the final application, we, of course, will use a number greater than 3, but for testing it is more convenient to work with a small amount.

Next, let's decide now what the URL with the page request will look like. We saw earlier that Flask allows you to accept arguments in routes, so that we can add a suffix that will point to the desired page:

http://localhost:5000/         <-- page #1 (default)
http://localhost:5000/index    <-- page #1 (default)
http://localhost:5000/index/1  <-- page #1
http://localhost:5000/index/2  <-- page #2


This URL format can be easily implemented using an additional route in our view (файл app/views.py):

from config import POSTS_PER_PAGE
@app.route('/', methods = ['GET', 'POST'])
@app.route('/index', methods = ['GET', 'POST'])
@app.route('/index/', methods = ['GET', 'POST'])
@login_required
def index(page = 1):
    form = PostForm()
    if form.validate_on_submit():
        post = Post(body = form.post.data, timestamp = datetime.utcnow(), author = g.user)
        db.session.add(post)
        db.session.commit()
        flash('Your post is now live!')
        return redirect(url_for('index'))
    posts = g.user.followed_posts().paginate(page, POSTS_PER_PAGE, False).items
    return render_template('index.html',
        title = 'Home',
        form = form,
        posts = posts)


Our new route yanks the argument with the page number and declares it as an integer. We also need to add this argument to the function index()and set the default value, because two of the three routes do not use this argument, and for them it will be used with the default value.

Now that we have the page number, we can easily connect it to our request followed_posts, along with the variable POSTS_PER_PAGEthat we defined earlier.

Notice how easy these changes go, and how little the code changes. We try to write every part of the application, not trying to guess how the other parts work, which allows us to build modular and reliable applications that are easy to test.

Now you can experience pagination by entering URLs with different line numbers in the address bar of your browser. Make sure that there are more than three posts that you see on the page.

Page navigation



Now we need to add links with which the user can navigate to the next / previous page, and fortunately for us Flask-SQLAlchemy will do most of the work.

We are going to start by making small changes to the presentation function. In our current version, we use pagination as follows:

posts = g.user.followed_posts().paginate(page, POSTS_PER_PAGE, False).items


By doing this, we only save the elements of the Pagination object returned by the paginate method. But this object provides some very useful features, so we will store the entire object as a whole (файл app/views.py):

posts = g.user.followed_posts().paginate(page, POSTS_PER_PAGE, False)


To copy this change, we need to change the template (файл app/templates/index.html):


{% for post in posts.items %}

{{post.author.nickname}} says: {{post.body}}

{% endfor %}


What gives us the Paginate object in the template. Here are the methods of the object that we will use:

  • has_next: True there is at least one page after the current
  • has_prev: True if there is at least one page before the current
  • next_num: next page number
  • prev_num: previous page number


With their help, we can do the following (файл app/templates/index.html):


{% for post in posts.items %}

{{post.author.nickname}} says: {{post.body}}

{% endfor %} {% if posts.has_prev %}<< Newer posts{% else %}<< Newer posts{% endif %} | {% if posts.has_next %}Older posts >>{% else %}Older posts >>{% endif %}


Now we have two links. First, we show the “Newer posts” link, which will send us to the previous page, to new posts. On the other hand, Older posts will send us to the next page, to older posts.

But, when we are on the first page, we do not need a link to the previous page. Such a case is easy to track using the posts.has_prev method, which will return False. In this case, we display the link text, but without the link itself. Link not the next page is processed in the same way.

Post Subpattern Implementation



Earlier, in the article in which we added the avatar, we will define a subpattern with HTML code for rendering a single post. We made this template to get rid of code duplication if we wanted to render posts on different pages.

It's time to implement this subpattern on the main page. It will be as simple as most of the things we do today (файл app/templates/index.html):


{% for post in posts.items %}
    {% include 'post.html' %}
{% endfor %}


Amazing right? We just replaced the old rendering code with the include of our template. This way we get a better version of the rendering, with the user avatar.

Here is a screenshot of the main page in its current state:

image

User Profile Page



Now we are done with the main page. We also included messages in the user profile, but not all, but only the owner of the profile. To be consistent, we must change the profile page to fit the main page.

The changes are similar to those we made on the main page. Here is a short list of what we need to do:

  • add an extra route that takes page number
  • add the argument with the page number to the view function and assign it 1
  • replace fake posts with real ones from the database and break them into pages
  • update the template to use the pagination object


These are updates to the view function (файл app/views.py):

@app.route('/user/')
@app.route('/user//')
@login_required
def user(nickname, page = 1):
    user = User.query.filter_by(nickname = nickname).first()
    if user == None:
        flash('User ' + nickname + ' not found.')
        return redirect(url_for('index'))
    posts = user.posts.paginate(page, POSTS_PER_PAGE, False)
    return render_template('user.html',
        user = user,
        posts = posts)


Обратите внимание, что эта функция уже имеет аргумент (никнейм пользователя), поэтому мы добавим номер страницы как второй аргумент.

Changes to the template are also quite simple (файл app/templates/user.html):


{% for post in posts.items %}
    {% include 'post.html' %}
{% endfor %}
{% if posts.has_prev %}<< Newer posts{% else %}<< Newer posts{% endif %} | 
{% if posts.has_next %}Older posts >>{% else %}Older posts >>{% endif %}


Final words



Below I post the updated version of the application microblogin all the changes made in this article.

Download microblog-0.9.zip.

As always, there is no database, you must create it yourself. If you follow this series of articles, you know how to do it. If not, then return to the database article to find out.

As always, I thank you for following me. I hope to see you in the next article.

Miguel

Also popular now: