📜 ⬆️ ⬇️

Flask Mega-Tutorial, Part IX: Pagination (Edition 2018)

Miguel grinberg




<<< previous next >>>


This is the ninth edition of the Mega-Tutorial Flask series, in which I will tell you how to split lists in a database.


For reference, below is a list of articles in this series.



Note 1: If you are looking for old versions of this course, this is here .


Note 2: If suddenly you would like to speak in support of my (Miguel) work on this blog, or simply do not have the patience to wait for a week of the article, I (Miguel Greenberg) offer a full version of this guide a packed e-book or video. For more information, visit learn.miguelgrinberg.com .


In Chapter 8, I made a few changes to the database needed to support the follower paradigm (subscriber), which is so popular on social networks. With this functionality, I’m ready to delete the last entries I created for the demonstration. This is a fake message.


In this chapter, the application will start accepting blog posts from users, and also delivers them to the index page and profile pages.


GitHub links for this chapter: Browse , Zip , Diff .


Blog posts


Let's start with something simple. The homepage should have a form in which users can enter new messages. First I create a form class:


class PostForm(FlaskForm): post = TextAreaField('Say something', validators=[ DataRequired(), Length(min=1, max=140)]) submit = SubmitField('Submit') 

Now you can add this form to the template for the main page of the application:


 {% extends "base.html" %} {% block content %} <h1>Hi, {{ current_user.username }}!</h1> <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.post.label }}<br> {{ form.post(cols=32, rows=4) }}<br> {% for error in form.post.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% for post in posts %} <p> {{ post.author.username }} says: <b>{{ post.body }}</b> </p> {% endfor %} {% endblock %} 

Changes in this template are similar to those made in other forms. In conclusion, add the form creation and processing in the view function:


 from app.forms import PostForm from app.models import 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, author=current_user) db.session.add(post) db.session.commit() flash('Your post is now live!') return redirect(url_for('index')) posts = [ { 'author': {'username': 'John'}, 'body': 'Beautiful day in Portland!' }, { 'author': {'username': 'Susan'}, 'body': 'The Avengers movie was so cool!' } ] return render_template("index.html", title='Home Page', form=form, posts=posts) 

Let's take a look at the changes in this view function in order:



Before proceeding, I would like to draw attention to a number of important points related to the processing of web forms. Please note that after processing the form data, I complete the request by sending redirect to the main index page. I could easily skip the redirection and allow the function to continue working in the template rendering part, since this is already a function of viewing the index .


So why redirect?



The standard practice is to respond to the ( request ) POST request created when the web form is sent with redirection. This will help somehow to avoid bouts of irritation when using the update command in web browsers. After all, when you click the refresh button, the web browser will display the last request. If a POST request with a form submission returns a regular response, the update will re-send the form. Since this is always unexpected, the browser will ask the user to confirm the resubmission, but most users will not understand what the browser needs.


But if a redirect responds to a POST request, the browser will be instructed to send a GET request to capture the page specified in the redirect, so now the last request is no longer a POST request, and the update command works in a more predictable way.


This simple trick is nothing but the Post / Redirect / Get pattern . . It avoids the insertion of duplicate messages when the user inadvertently refreshes the page after sending the web form.


View blog posts


If you remember, I created a couple of blog posts that I had been showing on the home page for a long time. These fake objects are explicitly created in the index function in the form of a simple Python list:


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

But now I have the followed_posts() method in the User model, which returns messages that this user would like to see. So, now I can replace temporary messages with real ones:


 @app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): # ... posts = current_user.followed_posts().all() return render_template("index.html", title='Home Page', form=form, posts=posts) 

The followed_posts method of the User class returns an SQLAlchemy query object that is configured to capture messages that the user has subscribed to from the database. Calling all() on this query starts its execution, and the return value is a list with all the results.


Thus, I get a structure that is very similar to the one that formed the temporary messages that I have used so far. It is so much like that the template doesn't even need to be changed.


Facilitate the search for users


I hope that you noticed, how the application is working at the moment is not very convenient to use, allowing users to find other users. In fact, there is actually no way to see what other users are there at all. I'm going to fix this with a few simple changes.


It would be necessary to create a new page, which I am going to call the “Explore” page. This page will work as a home page, but instead of showing only messages from the following users, it will show a global flow of messages from all users. Here is a new browsing feature:


 @app.route('/explore') @login_required def explore(): posts = Post.query.order_by(Post.timestamp.desc()).all() return render_template('index.html', title='Explore', posts=posts) 

Have you noticed something weird in this feature? The call to render_template() refers to the index.html template that I use on the main page of the application. Since this page will be very similar to the main page, I decided to reuse the template.


But one difference from the main page is that on the Explore page I don’t want to have a form to write blog posts, so in this view function I did not include the form argument in the template call.


To prevent the index.html template from failing, when I try to display a web form that does not exist, I will add a condition that displays the form only if it is defined:


 {% extends "base.html" %} {% block content %} <h1>Hi, {{ current_user.username }}!</h1> {% if form %} <form action="" method="post"> ... </form> {% endif %} ... {% endblock %} 

I will also add a link to this new page in the navigation bar:


 <a href="{{ url_for('explore') }}">Explore</a> 

Remember the _post.html , in chapter 6 , to display blog posts on a user profile page? This is a small template that was extracted from the user profile page template and became separate so that it can be used from other templates. I will now make a small improvement that will allow to show the username of the blog post as a link:


 <table> <tr valign="top"> <td><img src="{{ post.author.avatar(36) }}"></td> <td> <a href="{{ url_for('user', username=post.author.username) }}"> {{ post.author.username }} </a> says:<br>{{ post.body }} </td> </tr> </table> 

Now I can use this sub-template to visualize and study the blog on the home page:


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

The nested template expects a variable named post to exist, and that is the name of the loop variable in the index template, so this works fine.


Thanks to these small changes, the usability of the application has improved significantly. Now the user can visit the page to read blog posts from unknown users and find new ones on the basis of these messages, to add a subscription, which is very easy to do by clicking on the username to access the profile page. Amazing, right?


At this stage, I suggest you try the application again, so that you yourself would experience these latest UI improvements.



Divide blog entries


The app looks better than it was, but displaying all the entries on the home page will become a problem much earlier than you can imagine. What happens if a user has a thousand entries? Or a million? Managing such a large list of messages will be extremely slow and inefficient.


To solve this problem, I'm going to break the list of messages. This means that initially I will only show a limited number of messages at a time and include links to navigate the rest of the list of messages. Flask-SQLAlchemy supports paginate() query paging paginate() . If, for example, I need to get the first twenty user records, I can replace the all() call at the end of the query:


 >>> user.followed_posts().paginate(1, 20, False).items 

The paginate method can be called for any query object from Flask-SQLAlchemy. This requires three arguments:



The return value from paginate is a Pagination object. The items attribute of this object contains a list of items on the requested page. There are more useful things in the Pagination object, which I will discuss later.


Now let's think about how pagination could be implemented in the index() view function. You can start by adding a configuration item to your application that determines how many items will be displayed on the page.


 class Config(object): # ... POSTS_PER_PAGE = 3 

It is a good idea that these “knobs” for the entire application can influence its behavior from the configuration file, because then I can make all the adjustments in one place. As a result, I, of course, will use more than three elements on the page, but for testing it is useful to work with small numbers.


Next, I need to decide how the page number will be included in the application URLs. A fairly common way is to use the query string argument to specify an optional page number, the default on page 1 if it is not specified. Here are some examples of URLs that show how I will implement this:



To access the arguments specified in the query string, I can use the object Flask's request.args object. You already saw this in Chapter 5 , where I injected the URLs for the user to log in from Flask-Login, which may include the query string argument.


The following example demonstrates how I added a breakdown of the home page into several and explored browsing features:


 @app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): # ... page = request.args.get('page', 1, type=int) posts = current_user.followed_posts().paginate( page, app.config['POSTS_PER_PAGE'], False) return render_template('index.html', title='Home', form=form, posts=posts.items) @app.route('/explore') @login_required def explore(): page = request.args.get('page', 1, type=int) posts = Post.query.order_by(Post.timestamp.desc()).paginate( page, app.config['POSTS_PER_PAGE'], False) return render_template("index.html", title='Explore', posts=posts.items) 

In these changes, both of the two routes determine the page number to display, either from the page argument of the page request, or the default is 1. Then the paginate() method is used to retrieve only the desired page with the results. The POSTS_PER_PAGE configuration POSTS_PER_PAGE , which defines the page size, is accessible via the app.config object.


Notice how easy these changes are, and how little they affect every piece of code. I try to write each part, abstracting from the work of other parts, and this allows me to write modular and reliable applications that are easier to expand and test. In this case, the probability of getting a fatal or minor error is substantially small.


Moving on! And you should experience paging functionality. First make sure that you have more than three blog posts. This is easier to see on the search page (explore page), where messages from all users are displayed. Now you only see the last three messages. If you want to see the following three, enter http://localhost:5000/explore?page=2 in the address bar of the browser.



The next change is to add links at the bottom of the list of blog posts that allow users to go to the next and / or previous pages. Remember that I mentioned that the return value from the paginate() call is an object of the Pagination class from Flask-SQLAlchemy? So far I have used the items attribute of this object,
which contains a list of items received for the selected page. But this object has several other attributes that are useful when creating links to pages:



Using these four elements, I can create links to pages (the following and previous ones) and pass them on to templates for display:


 @app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): # ... page = request.args.get('page', 1, type=int) posts = current_user.followed_posts().paginate( page, app.config['POSTS_PER_PAGE'], False) next_url = url_for('index', page=posts.next_num) \ if posts.has_next else None prev_url = url_for('index', page=posts.prev_num) \ if posts.has_prev else None return render_template('index.html', title='Home', form=form, posts=posts.items, next_url=next_url, prev_url=prev_url) @app.route('/explore') @login_required def explore(): page = request.args.get('page', 1, type=int) posts = Post.query.order_by(Post.timestamp.desc()).paginate( page, app.config['POSTS_PER_PAGE'], False) next_url = url_for('explore', page=posts.next_num) \ if posts.has_next else None prev_url = url_for('explore', page=posts.prev_num) \ if posts.has_prev else None return render_template("index.html", title='Explore', posts=posts.items, next_url=next_url, prev_url=prev_url) 

In next_url and prev_url in these two functions, the URL returned by url_for() will be used only if there is a page in this direction. If the current page is on one of the ends of the message collection, the has_prev object has_next or has_prev Pagination will be False , in which case the link in this direction will be set to None .


One interesting aspect of the url_for() function, which I have mentioned before, is that you can add any keyword arguments to it, and if the names of these arguments are not directly indicated in the URL, then Flask will include them in the URL- address as request arguments.


Links to pages are set in the index.html template, so now let's display them on the page, right below the list of messages:


 ... {% for post in posts %} {% include '_post.html' %} {% endfor %} {% if prev_url %} <a href="{{ prev_url }}">Newer posts</a> {% endif %} {% if next_url %} <a href="{{ next_url }}">Older posts</a> {% endif %} ... 

This addition adds a link below the list of messages for both the main index page and the pages that are being examined. The first link is marked as “Newer posts”, and it points to the previous page (keep in mind that I show posts sorted by the latest data, so the first page is with the latest content).


The second link is marked as "Older posts" ("Older posts") and points to the next page of posts.


If either of these two links is None , then the link will not be shown on the page through a conditional expression.



Split user profile page


At the moment, the changes for the index page are enough. However, there should also be a list of messages on the user profile page, in which only messages from the profile owner are displayed. To be consistent, the user profile page should be modified in the same way as the index page.


I start by updating the user profile viewer function, which still has a list of temporary messages.


 @app.route('/user/<username>') @login_required def user(username): user = User.query.filter_by(username=username).first_or_404() page = request.args.get('page', 1, type=int) posts = user.posts.order_by(Post.timestamp.desc()).paginate( page, app.config['POSTS_PER_PAGE'], False) next_url = url_for('user', username=user.username, page=posts.next_num) \ if posts.has_next else None prev_url = url_for('user', username=user.username, page=posts.prev_num) \ if posts.has_prev else None return render_template('user.html', user=user, posts=posts.items, next_url=next_url, prev_url=prev_url) 

To get a list of messages from a user, I’ll use the fact that the user.posts relation is a query that SQLAlchemy has already configured as a result of defining db.relationship() in the User model. I'll take this query and add order_by() to get the most recent posts first, and then do the pagination just like I did for the posts in index and explore. Note that links to pages that are generated by the url_for() function need an additional username argument, because they point to a user profile page that has this username as a dynamic component of the URL.


Finally , the changes in the user.html template are identical to those I made on the index page:


 ... {% for post in posts %} {% include '_post.html' %} {% endfor %} {% if prev_url %} <a href="{{ prev_url }}">Newer posts</a> {% endif %} {% if next_url %} <a href="{{ next_url }}">Older posts</a> {% endif %} 

After you finish experimenting with the pagination function, you can set a more reasonable value for the POSTS_PER_PAGE configuration POSTS_PER_PAGE :


 class Config(object): # ... POSTS_PER_PAGE = 25 

<<< previous next >>>


')

Source: https://habr.com/ru/post/347926/


All Articles