Tutorial on implementing the paging function in the Python Flask framework
This article mainly introduces how to implement the paging function in the Python Flask framework. The example in this article is implemented based on a blog. For more information, see
Submission of Blog Posts
Let's start with something simple. A user must submit a new post form on the homepage.
First, we define a single-domain form object (fileapp/forms. py ):
?
1 2 |
Class PostForm (Form ): Post = TextField ('post', validators = [Required ()]) |
Next, we add this form to the template (fileapp/templates/index.html ):
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<! -- Extend base layout --> {% Extends "base.html" %} {% Block content %} <H1> Hi, {g. user. nickname }}! </H1> <Form action = "" method = "post" name = "post"> {Form. hidden_tag ()}} <Table> <Tr> <Td> Say something: </td> <Td >{{ form. post (size = 30, maxlength = 140) }}</td> <Td> {% For error in form. errors. post %} <Span style = "color: red;"> [{error}] </span> <br> {% Endfor %} </Td> </Tr> <Tr> <Td> </td> <Td> <input type = "submit" value = "Post! "> </Td> <Td> </td> </Tr> </Table> </Form> {% For post in posts %} <P> {Post. author. nickname} says: <B >{{ post. body }}</B> </P> {% Endfor %} {% Endblock %} |
Nothing new so far. You can see that we just added another form, as we did last time.
Finally, the function tries to link everything together and be extended to process the form (fileapp/views. py ):
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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': 'beauul ul day in Portland! ' }, { 'Author': {'nickname': 'use '}, 'Body': 'The Avengers movie was so cool! ' } ] Return render_template('index.html ', Title = 'home ', Form = form, Posts = posts) |
Next let's review the changes in this function one by one:
We imported the Post and PostForm classes.
We received the POST request from the index and view in two paths, because that is how we receive the submitted request.
When we submit the form to the function view, we will input the new Post record into the database. Then, as previously done, access it through regular GET requests.
Templat receives an additional content-form, so it is submitted to the text field.
Before we proceed, we have a last note: note how to add a new Post request to the database:
?
1 |
Return redirect (url_for ('index ')) |
We can easily skip redirection and allow it to jump to the template rendering part, which is more efficient. Because all the redirection is returned to the same function view after the web browser.
So why choose redirection? After a user writes a post request to a blog, the user only needs to submit the request and click the refresh button in the browser. What can the "Refresh" command do? The browser resends the last published request as the result of a "Refresh" command. (Translator's note: due to the limited personal level, please correct me if you find that the translation is different from the original article. Thank you !)
If there is no redirection, the POST request is submitted to the form, so a "Refresh Action" will re-submit the form, this will cause the second submitted post record to be the same as the one written to the database for the first time. Such behavior is Not so good.
If there is a redirection, we can force the browser to send another request after the form is submitted, it captures the redirected page. This is a simple "GET" request, so the "Refresh" Action repeats the "GET" request instead of submitting the form again.
This simple trick prevents users from repeatedly writing post requests after submitting a blog post request and accidentally refreshing the page.
Display blog post requests
Next let's talk about something interesting. We need to capture the blog post request from the database and the request is not displayed.
If you recall some previous articles, we have created many "fake" requests and displayed them on the homepage for a long time. These "fake" objects are created as Python lists in the index view.
?
1 2 3 4 5 6 7 8 9 10 |
Posts = [ { 'Author': {'nickname': 'john '}, 'Body': 'beauul ul day in Portland! ' }, { 'Author': {'nickname': 'use '}, 'Body': 'The Avengers movie was so cool! ' } ] |
However, in the previous article, the query statement we created allows us to retrieve all requests from the "followers, so we can use the following statement to replace the above (fileapp/views. py ):
?
1 |
Posts = g. user. followed_posts (). all () |
Then, when you run this application, you will see the bolg post request captured in the database.
The followed_posts method of the User class returns an SQL query statement that captures the requests we are interested in. In this query statement, Callingall () is used to retrieve all requests to a list. Therefore, we end with the structure of the "false" request that we have been following so far. They are so similar that even the template has not noticed.
In this case, you can freely use this application. You can create multiple users so that they can follow others, and then publish some information to see how each user sees its bolg post request data stream.
Paging
Our program is becoming more and more decent, but we are faced with another problem. All followed posts are displayed on the homepage. What happens if a user has thousands of followed posts? Or 1 million? As we can imagine, capturing and processing such a large list of objects is very inefficient.
We can display such a large number of post groups or display them on pages.
Flask-SQLAlchemy supports paging. For example, you can use the following method to easily obtain the followed posts of the first three articles of a user:
?
1 |
Posts = g. user. followed_posts (). paginate (1, 3, False). items |
The paging method can be called by any query object. It accepts three parameters:
Page number, starting from 1
Number of records displayed per page
Error mark. If it is True, if the page request exceeds the record range is received, the 404 error request will be automatically reported to the client browser. If it is False, an empty list will be returned without displaying an error.
The return value of paginate is a Pagination object. The members in this object include the record list on the request page. Later, we will discuss some other useful things in the Pagination object.
Now let's think about how to implement paging in our view function. We can add some configuration information to our application first, including how many records (fileconfig. py) should be displayed on each page ):
?
1 2 |
# Pagination POSTS_PER_PAGE = 3 |
Using the global configuration file to change our application is a good idea, because we only need to go to a place to modify all the configurations.
In the final application, we certainly use numbers greater than 3, but as a test, using small numbers is more effective.
Next, let's take a look at how URLs judges different pages for different requests. We already know that Flask routes can accept parameters, so we can add a Suffix in the URL to specify the page we want:
?
1 2 3 4 |
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 |
We can easily implement the URLs format and add a new route to our view function (fileapp/views. py ):
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
From config import POSTS_PER_PAGE @ App. route ('/', methods = ['get', 'post']) @ App. route ('/Index', methods = ['get', 'post']) @ App. route ('/index/<int: page>', 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 accepts paging parameters and defines the parameters as integers. We also need to add paging parameters to the index function and give them a default value. because two of the three route have no paging parameters, these route must use the default value.
Now we have obtained a page number parameter, which can be easily applied to our followed_post query. There is also a previously defined configuration constant: POSTS_PER_PAGE.
We can notice how simple these changes are, and when we change a page, few code will be affected. When we try to write each part of the application, we will not assume how the other part works. This will allow us to write modular and robust applications, making testing easier, and fewer error opportunities or defects.
Now you can try to enter the page number in the URL bar of your browser. Make sure that you have more than three pots so that you can see multiple pages.
Page navigation
Now we need to add links so that our users can view the previous/next page. Fortunately, this function is easy to implement and Flask-SQLAlchemy has done most of the work for us.
Now let's change a small part of the code for the view function. In the paginate method, modify:
?
1 |
Posts = g. user. followed_posts (). paginate (page, POSTS_PER_PAGE, False). items |
We only keep the item member of the Pagination object returned by the paginate method. However, this object contains some useful information, so we changed it to retaining the Pagination object (fileapp/views. py ):
?
1 |
Posts = g. user. followed_posts (). paginate (page, POSTS_PER_PAGE, False) |
To complete this modification, we also need to modify the template (fileapp/templates/index.html ):
?
1 2 3 4 5 6 |
<! -- Posts is a Paginate object --> {% For post in posts. items %} <P> {Post. author. nickname} says: <B >{{ post. body }}</B> </P> {% Endfor %} |
What we do is to apply a full set of paging objects to our template. The following members are used for paging objects:
Has_next: if there is more than one page after this page, True is returned.
Has_prev: returns True if more than one page exists before the current page.
Next_num: returns the page number of the next page.
Prev_num: returns the page number of the previous page.
To apply these Members, we can generate the following code (app/templates/index.html ):
?
1 2 3 4 5 6 7 8 |
<! -- Posts is a Paginate object --> {% For post in posts. items %} <P> {Post. author. nickname} says: <B >{{ post. body }}</B> </P> {% Endfor %} {% If posts. has_prev %} <a href = "{url_for ('index', page = posts. prev_num) }}" >< <Newer posts </a >{% else % }< <Newer posts {% endif %} | {% If posts. has_next %} <a href = "{url_for ('index', page = posts. next_num) }}"> Older posts >></a >{% else %} Older posts >>{% endif %} |
Now we have two hyperlinks. The first one is called "Newer posts", which can be linked to the previous page (Remember, we put the latest post at the top, so the previous page post is a new post ). Conversely, "Older posts" points to the next page.
When we are on the first page, we do not want to display the link of the previous page because it does not exist. We can easily detect it, because posts. has_prev returns False. In this case, we can only display the text of the "Previous Page" without the link attribute. The same applies to the next page.
Implement the Post subtemplate
Back to the article we added the Avatar image, we used HTML to define a subtemplate for a separate post. The reason is that we can use it in multiple places only once to avoid repeated code. Now we apply this subtemplate to our index page. Just like what we do today, this is very simple (app/templates/index.html ):
?
1 2 3 4 |
<! -- Posts is a Paginate object --> {% For post in posts. items %} {% Include 'post.html '%} {% Endfor %} |
Magic? We just replaced the old rendering code with the sub-template. Now we get the post page that contains the user's avatar image.
User's personal page
We have completed the development of the index page. However, we also introduced posts on the user's personal page, which only lists the post published by the user. Correspondingly, the user's personal page needs to be changed according to the index design.
Similar to the changes to the index page, the following are what we need to do:
Add a route that accepts the page number.
Add a page number parameter to the view function. The default value is 1.
Replace the post list with appropriate database query and paging code
Update a template using a paging object
The following is the updated view function (app/views. py ):
?
1 2 3 4 5 6 7 8 9 10 11 12 |
@ App. route ('/user/<nickname> ') @ App. route ('/user/<nickname>/<int: page> ') @ 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) |
We noticed that this function already contains a parameter (the nickname of the user), so we add the page number to the second parameter.
It is also quite simple to change the template (app/templates/user.html ):
?
1 2 3 4 5 6 |
<! -- Posts is a Paginate object --> {% For post in posts. items %} {% Include 'post.html '%} {% Endfor %} {% If posts. has_prev %} <a href = "{url_for ('user', nickname = user. nickname, page = posts. prev_num) }}" >< <Newer posts </a >{% else % }< <Newer posts {% endif %} | {% If posts. has_next %} <a href = "{url_for ('user', nickname = user. nickname, page = posts. next_num) }}"> Older posts >></a >{% else %} Older posts >>{% endif %} |