The Flask Mega-Tutorial Part IX: Pagination
Posted by
on underThis is the ninth installment of the Flask Mega-Tutorial series, in which I'm going to tell you how to paginate lists of database entries.
For your reference, below is a list of the articles in this series.
- Chapter 1: Hello, World!
- Chapter 2: Templates
- Chapter 3: Web Forms
- Chapter 4: Database
- Chapter 5: User Logins
- Chapter 6: Profile Page and Avatars
- Chapter 7: Error Handling
- Chapter 8: Followers
- Chapter 9: Pagination (this article)
- Chapter 10: Email Support
- Chapter 11: Facelift
- Chapter 12: Dates and Times
- Chapter 13: I18n and L10n
- Chapter 14: Ajax
- Chapter 15: A Better Application Structure
- Chapter 16: Full-Text Search
- Chapter 17: Deployment on Linux
- Chapter 18: Deployment on Heroku
- Chapter 19: Deployment on Docker Containers
- Chapter 20: Some JavaScript Magic
- Chapter 21: User Notifications
- Chapter 22: Background Jobs
- Chapter 23: Application Programming Interfaces (APIs)
In Chapter 8 I have made several database changes that were necessary to support the "follower" paradigm that is so popular with social networks. With that functionality in place, I'm ready to remove the last piece of scaffolding that I have put in place in the beginning, the fake posts. In this chapter the application will start accepting blog posts from users, and also deliver them in the home and profile pages as a paginated list.
The GitHub links for this chapter are: Browse, Zip, Diff.
Submission of Blog Posts
Let's start with something simple. The home page needs to have a form in which users can type new posts. First I create a form class:
app/forms.py: Blog submission form.
class PostForm(FlaskForm):
post = TextAreaField('Say something', validators=[
DataRequired(), Length(min=1, max=140)])
submit = SubmitField('Submit')
Next, I can add this form to the template for the main page of the application:
app/templates/index.html: Post submission form in index template
{% 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 %}
The changes in this template are similar to how previous forms were handled. The final part is to add the form creation and handling in the view function:
app/routes.py: Post submission form in index 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 review the changes in this view function one by one:
- I'm now importing the
Post
andPostForm
classes - I accept
POST
requests in both routes associated with theindex
view function in addition toGET
requests, since this view function will now receive form data. - The form processing logic inserts a new
Post
record into the database. - The template receives the
form
object as an additional argument, so that it can render the text field.
Before I continue, I wanted to mention something important related to processing of web forms. Notice how after I process the form data, I end the request by issuing a redirect to the home page. I could have easily skipped the redirect and allowed the function to continue down into the template rendering part, since this is already the index view function.
So, why the redirect? It is a standard practice to respond to a POST
request generated by a web form submission with a redirect. This helps mitigate an annoyance with how the refresh command is implemented in web browsers. All the web browser does when you hit the refresh key is to re-issue the last request. If a POST
request with a form submission returns a regular response, then a refresh will re-submit the form. Because this is unexpected, the browser is going to ask the user to confirm the duplicate submission, but most users will not understand what the browser is asking them. But if a POST
request is answered with a redirect, the browser is now instructed to send a GET
request to grab the page indicated in the redirect, so now the last request is not a POST
request anymore, and the refresh command works in a more predictable way.
This simple trick is called the Post/Redirect/Get pattern. It avoids inserting duplicate posts when a user inadvertently refreshes the page after submitting a web form.
Displaying Blog Posts
If you recall, I created a couple of fake blog posts that I've been displaying in the home page for a long time. These fake objects are created explicitly in the index
view function as 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 that returns a query for the posts that a given user wants to see. So now I can replace the fake posts with real posts:
app/routes.py: Display real posts in home page.
@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 a SQLAlchemy query object that is configured to grab the posts the user is interested in from the database. Calling all()
on this query triggers its execution, with the return value being a list with all the results. So I end up with a structure that is very much alike the one with fake posts that I have been using until now. It's so close that the template does not even need to change.
Making It Easier to Find Users to Follow
As I'm sure you noticed, the application as it is does not do a great job at letting users find other users to follow. In fact, there is actually no way to see what other users are there at all. I'm going to address that with a few simple changes.
I'm going to create a new page that I'm going to call the "Explore" page. This page will work like the home page, but instead of only showing posts from followed users, it will show a global post stream from all users. Here is the new explore view function:
app/routes.py: Explore view function.
@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)
Did you notice something odd in this view function? The render_template()
call references the index.html template, which I'm using in the main page of the application. Since this page is going to be very similar to the main page, I decided to reuse the template. But one difference with the main page is that in the explore page I do not 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 crashing when it tries to render a web form that does not exist, I'm going to add a conditional that only renders the form if it is defined:
app/templates/index.html: Make the blog post submission form optional.
{% extends "base.html" %}
{% block content %}
<h1>Hi, {{ current_user.username }}!</h1>
{% if form %}
<form action="" method="post">
...
</form>
{% endif %}
...
{% endblock %}
I'm also going to add a link to this new page in the navigation bar:
app/templates/base.html: Link to explore page in navigation bar.
<a href="{{ url_for('explore') }}">Explore</a>
Remember the _post.html sub-template that I have introduced in Chapter 6 to render blog posts in the user profile page? This was a small template that was included from the user profile page template, and was separate so that it can also be used from other templates. I'm now going to make a small improvement to it, which is to show the username of the blog post author as a link:
app/templates/_post.html: Show link to author in blog posts.
<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>
I can now use this sub-template to render blog posts in the home and explore pages:
app/templates/index.html: Use blog post sub-template.
...
{% for post in posts %}
{% include '_post.html' %}
{% endfor %}
...
The sub-template expects a variable named post
to exist, and that is how the loop variable in the index template is named, so that works perfectly.
With these small changes, the usability of the application has improved considerably. Now a user can visit the explore page to read blog posts from unknown users and based on those posts find new users to follow, which can be done by simply clicking on a username to access the profile page. Amazing, right?
At this point I suggest you try the application once again, so that you experience these last user interface improvements.
Pagination of Blog Posts
The application is looking better than ever, but showing all of the followed posts in the home page is going to become a problem sooner rather than later. What happens if a user has a thousand followed posts? Or a million? As you can imagine, managing such a large list of posts will be extremely slow and inefficient.
To address that problem, I'm going to paginate the post list. This means that initially I'm going to show just a limited number of posts at a time, and include links to navigate through the entire list of posts. Flask-SQLAlchemy supports pagination natively with the paginate()
query method. If for example, I want to get the first twenty followed posts of the user, I can replace the all()
call that terminates the query with:
>>> user.followed_posts().paginate(page=1, per_page=20, error_out=False).items
The paginate
method can be called on any query object from Flask-SQLAlchemy. It takes three arguments:
- the page number, starting from 1
- the number of items per page
- an error flag. If
True
, when an out of range page is requested a 404 error will be automatically returned to the client. IfFalse
, an empty list will be returned for out of range pages.
The return value from paginate
is a Pagination
object. The items
attribute of this object contains the list of items in the requested page. There are other useful things in the Pagination
object that I will discuss later.
Now let's think about how I can implement pagination in the index()
view function. I can start by adding a configuration item to the application that determines how many items will be displayed per page.
config.py: Posts per page configuration.
class Config(object):
# ...
POSTS_PER_PAGE = 3
It is a good idea to have these application-wide "knobs" that can change behaviors in the configuration file, because then I can go to a single place to make adjustments. In the final application I will of course use a larger number than three items per page, but for testing it is useful to work with small numbers.
Next, I need to decide how the page number is going to be incorporated into application URLs. A fairly common way is to use a query string argument to specify an optional page number, defaulting to page 1 if it is not given. Here are some example URLs that show how I'm going to implement this:
- Page 1, implicit: http://localhost:5000/index
- Page 1, explicit: http://localhost:5000/index?page=1
- Page 3: http://localhost:5000/index?page=3
To access arguments given in the query string, I can use the Flask's request.args
object. You have seen this already in Chapter 5, where I implemented user login URLs from Flask-Login that can include a next
query string argument.
Below you can see how I added pagination to the home and explore view functions:
app/routes.py: Followers association table
@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=page, per_page=app.config['POSTS_PER_PAGE'], error_out=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=page, per_page=app.config['POSTS_PER_PAGE'], error_out=False)
return render_template("index.html", title='Explore', posts=posts.items)
With these changes, the two routes determine the page number to display, either from the page
query string argument or a default of 1, and then use the paginate()
method to retrieve only the desired page of results. The POSTS_PER_PAGE
configuration item that determines the page size is accessed through the app.config
object.
Note how easy these changes are, and how little code is affected each time a change is made. I am trying to write each part of the application without making any assumptions about how the other parts work, and this enables me to write modular and robust applications that are easier to extend and to test, and are less likely to fail or have bugs.
Go ahead and try the pagination support. First make sure you have more than three blog posts. This is easier to see in the explore page, which shows posts from all users. You are now going to see just the three most recent posts. If you want to see the next three, type http://localhost:5000/explore?page=2 in your browser's address bar.
Page Navigation
The next change is to add links at the bottom of the blog post list that allow users to navigate to the next and/or previous pages. Remember that I mentioned that the return value from a paginate()
call is an object of a Pagination
class from Flask-SQLAlchemy? So far, I have used the items
attribute of this object, which contains the list of items retrieved for the selected page. But this object has a few other attributes that are useful when building pagination links:
has_next
: True if there is at least one more page after the current onehas_prev
: True if there is at least one more page before the current onenext_num
: page number for the next pageprev_num
: page number for the previous page
With these four elements, I can generate next and previous page links and pass them down to the templates for rendering:
app/routes.py: Next and previous page links.
@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=page, per_page=app.config['POSTS_PER_PAGE'], error_out=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=page, per_page=app.config['POSTS_PER_PAGE'], error_out=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)
The next_url
and prev_url
in these two view functions are going to be set to a URL returned by url_for()
only if there is a page in that direction. If the current page is at one of the ends of the collection of posts, then the has_next
or has_prev
attributes of the Pagination
object will be False
, and in that case the link in that direction will be set to None
.
One interesting aspect of the url_for()
function that I haven't discussed before is that you can add any keyword arguments to it, and if the names of those arguments are not referenced in the URL directly, then Flask will include them in the URL as query arguments.
The pagination links are being set to the index.html template, so now let's render them on the page, right below the post list:
app/templates/index.html: Render pagination links on the template.
...
{% 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 change adds two links below the post list on both the index and explore pages. The first link is labeled "Newer posts", and it points to the previous page (keep in mind I'm showing posts sorted by newest first, so the first page is the one with the newest content). The second link is labeled "Older posts" and points to the next page of posts. If any of these two links is None
, then it is omitted from the page, through a conditional.
Pagination in the User Profile Page
The changes for the index page are sufficient for now. However, there is also a list of posts in the user profile page, which shows only posts from the owner of the profile. To be consistent, the user profile page should be changed to match the pagination style of the index page.
I begin by updating the user profile view function, which still had a list of fake post objects in it.
app/routes.py: Pagination in the user profile view function.
@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=page, per_page=app.config['POSTS_PER_PAGE'], error_out=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
form = EmptyForm()
return render_template('user.html', user=user, posts=posts.items,
next_url=next_url, prev_url=prev_url, form=form)
To get the list of posts from the user, I take advantage of the fact that the user.posts
relationship is a query that is already set up by SQLAlchemy as a result of the db.relationship()
definition in the User
model. I take this query and add a order_by()
clause so that I get the newest posts first, and then do the pagination exactly like I did for the posts in the index and explore pages. Note that the pagination links that are generated by the url_for()
function need the extra username
argument, because they are pointing back at the user profile page, which has this username as a dynamic component of the URL.
Finally, the changes to the user.html template are identical to those I made on the index page:
app/templates/user.html: Pagination links in the user profile template.
...
{% 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 are done experiment with the pagination feature, you can set the POSTS_PER_PAGE
configuration item to a more reasonable value:
config.py: Posts per page configuration.
class Config(object):
# ...
POSTS_PER_PAGE = 25
-
#126 Ryan said
Hey Miguel, thanks for this very detailed guide and i am able to move forward to this chapter.
However, since i am really new to programming, i might not be able to understand a lot of concept or syntax that you used.Can you help me with the two questions below:-
First Question
In this code below, you've used a slash at the end of the next_url. What is the purpose and if you can point me to some documentation of this syntax? On top of that, throughout the tutorial, you've always been using IF statement before some condition/statement, but this example is using IF with indents and it is written after the next_url variable, is this some sort of syntax and if you can point me to more reading material to understand it?next_url = url_for('explore', page=posts.next_num) \
if posts.has_next else NoneSecond Question
In the user/<username> route, you've used the following with additional "username" argument.next_url = url_for('user', username=user.username, page=posts.next_num)
I am trying to understand the concept when it is appropriate to use current_user? As some of the view function I see you are using current_user but some you are calling the the User class directly.
For the case above, could it work if it is written as below?
next_url = url_for('user', username=current_user.username, page=posts.next_num)
-
#127 Miguel Grinberg said
@Ryan: 1) The \ at the end of a line tells Python that the statement continues in the next line. documentation.
Theif
statement at the end of a statement is also a Python construct. documentation2) the
current_user
variable refers to the logged in user. Theuser
variable refers to any user that the logged in user is viewing. When a user is viewing the posts for another user, theuser
variable is used to retrieve the second user from the database. If you replaceuser
withcurrent_user
in those instances, you will only be able to see your own blog posts. -
#128 Ryan said
Thanks for the documentation info.
All good on current_user, now i am super clear! -
#129 Ann said
Hi! Thank you for such cool tutorial.
I have a mistake, can you please help me with it?
ERROR in app: Exception on /index [GET]
Traceback (most recent call last):
File "c:\users\vera\microblog\venv\lib\site-packages\flask\app.py", line 2070, in wsgi_app
response = self.full_dispatch_request()
File "c:\users\vera\microblog\venv\lib\site-packages\flask\app.py", line 1515, in full_dispatch_request
rv = self.handle_user_exception(e)
File "c:\users\vera\microblog\venv\lib\site-packages\flask\app.py", line 1513, in full_dispatch_request
rv = self.dispatch_request()
File "c:\users\vera\microblog\venv\lib\site-packages\flask\app.py", line 1499, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(req.view_args)
File "c:\users\vera\microblog\venv\lib\site-packages\flask_login\utils.py", line 272, in decorated_view
return func(*args, kwargs)
File "C:\Users\Vera\microblog\app\routes.py", line 33, in index
return render_template('index.html', title='Home', form=form, posts=posts.items, next_url=next_url, prev_url=prev_url)
File "c:\users\vera\microblog\venv\lib\site-packages\flask\templating.py", line 147, in render_template
return _render(
File "c:\users\vera\microblog\venv\lib\site-packages\flask\templating.py", line 128, in _render
rv = template.render(context)
File "c:\users\vera\microblog\venv\lib\site-packages\jinja2\environment.py", line 1304, in render
self.environment.handle_exception()
File "c:\users\vera\microblog\venv\lib\site-packages\jinja2\environment.py", line 925, in handle_exception
raise rewrite_traceback_stack(source=source)
File "C:\Users\Vera\microblog\app\templates\index.html", line 1, in top-level template code
{% extends "base.html" %}
File "C:\Users\Vera\microblog\app\templates\base.html", line 32, in top-level template code
{% block content %}{% endblock %}
File "C:\Users\Vera\microblog\app\templates\index.html", line 28, in block 'content'
{{ post.author.username }} says: {{ post.body }}
File "c:\users\vera\microblog\venv\lib\site-packages\jinja2\environment.py", line 474, in getattr
return getattr(obj, attribute)
jinja2.exceptions.UndefinedError: 'post' is undefined -
#130 Miguel Grinberg said
@Ann: your template does not recognize the
post
identifier. You must have forgotten to include this argument in therender_template()
call for this route. -
#131 Hamid Allaoui said
Thank you so much for this great tutorial.
-
#132 Arturo said
Hi Miguel
I'm following the Flask Mega Tutorial and I haven't had any problem to run all the code but in this article I do have a question.
At the end, when we update the user view function. Why it is necessary to call the Empty Form. I understood in the previous chapter that we needed it because the user is going to click in the button "follow"/"unfollow".
Thank you
-
#133 Miguel Grinberg said
@Arturo: the empty form in the user view is needed so that the template can render the forms that implement the buttons. This is explained in the "Integrating Followers with the Application" section in the Followers chapter.
-
#134 Simba Pfaira said
I might be late to comment, but I just wanna say thank you for this tutorial. I am following it right now and I am enjoying myself
-
#135 Eugene said
Hi Miguel,
Many thanks for such a thorough tutorial and especially for your focus on the best practices.
I've tried refactoring out the pagination logic into a Post class method, but it ended up looking relatively ugly - mainly because I need to pass along the endpoint name and some other parameters for the "url_for" function (namely, the "username" from the /user/<username> endpoint).
Here is my code:
<... Post class above ... >
@classmethod
def get_posts_paginated(cls, query, page: int, dst_page: str, order=None, params):
if order is None:
order = cls.timestamp.desc()
posts = query.order_by(order).paginate(
page, app.config['POSTS_PER_PAGE'], False)
next_url = url_for(dst_page, page=posts.next_num, params) \
if posts.has_next else None
prev_url = url_for(dst_page, page=posts.prev_num, **params) \
if posts.has_prev else Nonereturn posts, prev_url, next_url
And here is how I'm calling it:
@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, prev_url, next_url = Post.get_posts_paginated(
user.posts,
page, 'user',
**{'username': user.username},
)
form = EmptyForm()
return render_template(
'user.html',
user=user,
posts=posts.items,
prev_url=prev_url,
next_url=next_url,
form=form
)I feel like there must be a better way. Is there any? Or is it even a sane thing to do?
-
#136 Miguel Grinberg said
@Eugene: Yeah, I don't think you simplify things much if you do this. You may be able to improve your code a little bit, such as getting the endpoint from the request object instead of having to pass it as an argument, but overall you are not going to make a big improvement over doing the pagination directly in the endpoint.
-
#137 ea_ucop said
SQLAlchemy Query.paginate() was changed to not take position arguments, but keyword args instead.
https://flask-sqlalchemy.palletsprojects.com/en/2.x/api/#flask_sqlalchemy.BaseQuery.paginate
posts = current_user.followed_posts().paginate(
page, app.config["POSTS_PER_PAGE"], False
)Becomes:
posts = current_user.followed_posts().paginate(
page=page, per_page=app.config["POSTS_PER_PAGE"], error_out=False
)Had me scratching my head for about an hour. Replaced all my typed code with the code from the .zip at the top of the article and was still getting the problem and found no mention of the issue in the comments, so! Hope this helps someone in the future.
Love the tutorial otherwise, thank you very much!
-
#138 Miguel Grinberg said
@ea_ucop: Looks like this started a few days ago with the release of Flask-SQLAlchemy 3.x. Thanks for the alert, I will update this page accordingly.
-
#139 Ian said
Hi Miguel,
I am creating a blog using Flask. I have it working pretty well so far but something occurred to me and I thought you might know how to handle it:
I currently have the newest posts displaying at the top of the page as desired. There are 5 posts per page. As is, when the blog is used, older posts will be be pushed to pages with higher and higher numbers, while the most recent posts will always be on Page 1.
I would like to go ahead and reverse this. I still want the newest posts on the home page, but I would like the very oldest posts to be (and always be) on a page called 1. And newer posts will be displayed on the newest pages. So when new posts are made, new pages are made to have them. That way five posts will always "belong" on whatever page they happened to be created on in the first place.
So I would like to reverse the pagination somehow. And the display, so that at the bottom of the page the newest (highest) page numbers will be listed on the left and the oldest (1) on the right.
@main.route("/")
@main.route("/home")
def home():
page = request.args.get('page', 1, type=int)
posts = Post.query.order_by(Post.date_posted.desc()).paginate(page=page, per_page=5)
return render_template('main/home.html', posts=posts)~~~
{% for page_num in posts.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=1) %}
{% if page_num %}
{% if posts.page == page_num %}
{{ page_num }}
{% else %}
{{ page_num }}
{% endif %}
{% else %}
...
{% endif %}
{% endfor %}This is how it is currently. I like how the posts are ordered (desc). Now I want to also reverse the pagination.
Can you please help?
Ian -
#140 Miguel Grinberg said
@Ian: I don't think your idea can be implemented in practice. With 5 posts per page, what do you show in the front page when there are 6 blog posts in the system? Posts 1 to 5 are in page 1, and post 6 is in page 2. Are you say you want to show page 2 with just one post?
-
#141 Niklas Katter said
Hi Miguel, thanks again for the amazing tutorial!
I'm currently building my own project as a variation of your microblog app.
One feature I'd like to add is the ability to delete blog posts (as in: users should be able to delete their own posts).
Is there a simple way to add such a feature?
Thanks a ton,
Niklas -
#142 Miguel Grinberg said
@Niklas: you should try to add the delete feature yourself. It does not require any new concepts. If you need to review how to delete from the database, see Chapter 4 which covers this topic.
-
#143 Paul said
Hi Miguel, thanks for the amazing tutorial!
I am having an issue with displaying Newer and Older posts, I've compared my code with yours and I don't see any mistakes. Any idea what is causing this?
Best,
Paul
-
#144 Miguel Grinberg said
@Paul: hard to know. I suggest you not only compare my code and your code, you should download my code and try it directly to confirm if you see the same problem or not.
-
#145 Paul said
I figured out the mistake, I actually did not have enough posts to show the Newer and Older posts.
Thanks!