2018-01-30T18:07:32Z

The Flask Mega-Tutorial Part IX: Pagination

This 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.

Note 1: If you are looking for the legacy version of this tutorial, it's here.

Note 2: If you would like to support my work on this blog, or just don't have patience to wait for weekly articles, I am offering the complete version of this tutorial packaged as an ebook or a set of videos. For more information, visit courses.miguelgrinberg.com.

In Chapter 8 I have made several database changes 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.

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 and PostForm classes
  • I accept POST requests in both routes associated with the index view function in addition to GET 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.

Blog Posts

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(1, 20, 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. If False, 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, 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)

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 one
  • has_prev: True if there is at least one more page before the current one
  • next_num: page number for the next page
  • prev_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, 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)

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

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, 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
    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

112 comments

  • #76 Miguel Grinberg said 2019-07-14T10:40:39Z

    @Juan: I don't see any problem with doing it that way. It achieves the same result.

  • #77 Freeman said 2019-08-06T13:18:51Z

    Hi. This is the error am getting. 2019-08-06 13:42:17,409 ERROR: Exception on /explore [GET] [in c:\project\venv\lib\site-packages\flask\app.py:1560] Traceback (most recent call last): File "c:\project\venv\lib\site-packages\flask\app.py", line 1982, in wsgi_app response = self.full_dispatch_request() File "c:\project\venv\lib\site-packages\flask\app.py", line 1614, in full_dispatch_request rv = self.handle_user_exception(e) File "c:\project\venv\lib\site-packages\flask\app.py", line 1517, in handle_user_exception reraise(exc_type, exc_value, tb) File "c:\project\venv\lib\site-packages\flask_compat.py", line 33, in reraise raise value File "c:\project\venv\lib\site-packages\flask\app.py", line 1612, in full_dispatch_request rv = self.dispatch_request() File "c:\project\venv\lib\site-packages\flask\app.py", line 1598, in dispatch_request return self.view_functionsrule.endpoint File "c:\project\venv\lib\site-packages\flask_login\utils.py", line 261, in decorated_view return func(*args, kwargs) File "C:\project\app\routes.py", line 52, in explore next_url=next_url, prev_url=prev_url) File "c:\project\venv\lib\site-packages\flask\templating.py", line 134, in render_template context, ctx.app) File "c:\project\venv\lib\site-packages\flask\templating.py", line 116, in _render rv = template.render(context) File "c:\project\venv\lib\site-packages\jinja2\asyncsupport.py", line 76, in render return original_render(self, *args, kwargs) File "c:\project\venv\lib\site-packages\jinja2\environment.py", line 1008, in render return self.environment.handle_exception(exc_info, True) File "c:\project\venv\lib\site-packages\jinja2\environment.py", line 780, in handle_exception reraise(exc_type, exc_value, tb) File "c:\project\venv\lib\site-packages\jinja2_compat.py", line 37, in reraise raise value.with_traceback(tb) File "C:\project\app\templates\index.html", line 1, in top-level template code {% extends "base.html" %} File "C:\project\app\templates\base.html", line 31, in top-level template code {% block content %}{% endblock %} File "C:\project\app\templates\index.html", line 19, in block "content" {% include '_post.html' %} File "C:\project\app\templates_post.html", line 8, in top-level template code says:{{ post.body }} File "c:\project\venv\lib\site-packages\markupsafe_native.py", line 22, in escape return Markup(text_type(s) File "c:\project\venv\lib\site-packages\sqlalchemy\sql\elements.py", line 452, in str return str(self.compile()) File "", line 1, in File "c:\project\venv\lib\site-packages\sqlalchemy\sql\elements.py", line 442, in compile return self._compiler(dialect, bind=bind, kw) File "c:\project\venv\lib\site-packages\sqlalchemy\sql\elements.py", line 448, in _compiler return dialect.statement_compiler(dialect, self, kw) File "c:\project\venv\lib\site-packages\sqlalchemy\sql\compiler.py", line 453, in init Compiled.init(self, dialect, statement, kwargs) File "c:\project\venv\lib\site-packages\sqlalchemy\sql\compiler.py", line 219, in __init__ self.string = self.process(self.statement, compile_kwargs) File "c:\project\venv\lib\site-packages\sqlalchemy\sql\compiler.py", line 245, in process return obj._compiler_dispatch(self, kwargs) File "c:\project\venv\lib\site-packages\sqlalchemy\sql\visitors.py", line 81, in _compiler_dispatch return meth(self, kw) File "c:\project\venv\lib\site-packages\sqlalchemy\sql\compiler.py", line 716, in visit_column name = self.preparer.quote(name) File "c:\project\venv\lib\site-packages\sqlalchemy\sql\compiler.py", line 3097, in quote if self._requires_quotes(ident): File "c:\project\venv\lib\site-packages\sqlalchemy\sql\compiler.py", line 3068, in _requires_quotes lc_value = value.lower() AttributeError: 'String' object has no attribute 'lower'

  • #78 Miguel Grinberg said 2019-08-06T16:15:01Z

    @Freeman: can't tell you exactly where the problem is, but based on the error, it seems you are passing something that is not a string as an argument to the template. This is in routes.py, around line 52 (again, based on the stack trace). You may want to compare the code in that location against my version on GitHub.

  • #79 Hara Prasad said 2019-09-30T05:51:19Z

    Thank you so much for this amazing tutorial. This is just superb work!

    I was wondering how we can type emojis in the post. I see john's post has a sad smiley.

  • #80 Miguel Grinberg said 2019-10-02T10:33:25Z

    @Hara: emojis are unicode characters, you have to use whatever options your operating system provides to enter characters that are not in your keyboard.

  • #81 Hazel said 2019-10-26T16:58:57Z

    Hi, I've changed where I'm displaying posts a bit. I successfully got it so if I logged-in user is on their own profile, they see their posts and the posts of the people they follow. If they are viewing someone else's page, they only see that user's posts. But I'm having trouble with pagination. I tried this code:

    next_url = url_for('user/', username = username, page = posts.next_num) if posts.has_next else None prev_url = url_for('user/', username = username, page = posts.prev_num) if posts.has_prev else None

    But I get this error: werkzeug.routing.BuildError: Could not build url for endpoint 'user/' with values ['page', 'username']. Did you mean 'user' instead?

    Any help would be great :)

  • #82 Miguel Grinberg said 2019-10-27T09:39:45Z

    @Hazel: the first argument to url_for is the endpoint name of the route, not the URL. Use the function name there instead of the URL and you should be fine.

  • #83 Sean Heber said 2020-01-05T19:38:30Z

    I think I've identified an improvement to the pagination implementation that eliminates a fair bit of code. Instead of doing the URL generation in the view function, I pass the Pagination object into the template and do the logic there. So my my view function's code follows this pattern:

    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.jinja', posts=posts, form=form)

    And in the index template, I can do this instead:

    {% for post in posts.items %} {% include "_post.jinja" %} {% endfor %} {% if posts.has_prev %}<a href="{{ url_for(request.endpoint, page=posts.prev_num) }}">Newer</a>{% endif %} {% if posts.has_next %}<a href="{{ url_for(request.endpoint, page=posts.next_num) }}">Older</a>{% endif %}

    So by using request.endpoint in the template, the index template can still be reused for both index and explore (as you have done) while still allowing pagination to work without needing to compute those urls in each view function.

  • #84 Stefan said 2020-03-23T10:40:21Z

    Could you help me implementing pagination into my app ? Trying to make it work, but get some errors like ModuleNotFoundError: No module named 'flask_paginate'

  • #85 Miguel Grinberg said 2020-03-23T11:09:22Z

    @Stefan: you don't seem to be following this tutorial, you are doing this in a different way. Maybe you did not install the flask-paginate extension?

  • #86 Sergio said 2020-04-05T06:23:55Z

    Hi Miguel, First of all congratulations for your this tutorial. I am learning a lot.

    I tried : posts = Post.query.order_by(Post.timestamp.desc()).paginate( page, app.config['POSTS_PER_PAGE'], False)

    and an error was found --> TypeError: 'Pagination' object is not iterable

    I solved : posts = Post.query.order_by(Post.timestamp.desc()).paginate( page, app.config['POSTS_PER_PAGE'], False).items

    as said in: https://flask-sqlalchemy.palletsprojects.com/en/2.x/api/#flask_sqlalchemy.Pagination

    Thanks and take care all of you for this period of COVID 19. Sergio

  • #87 Miguel Grinberg said 2020-04-06T22:28:21Z

    @Sergio: your solution basically destroys the pagination object and replaces it with the items. If you look at my solution, I store the pagination object in posts, and then pass posts.items to the template, which is better, because it preserves the pagination object, which you will need to implement the pagination links later on.

  • #88 Justice said 2020-05-14T00:19:54Z

    Best comprehensive flask series. Thank you for this series

  • #89 Gerben said 2020-06-09T15:22:11Z

    Hi Miguel, Very nice tutorial! Thanks for posting this! I got stuck in this chapter with the explore page, and even when downloading your github version and copied it over mine, I still get 404 when clicking on "Explore":

    127.0.0.1 - - [09/Jun/2020 17:20:31] "GET /explore/ HTTP/1.1" 404 -

    Index page is working, but without pagination. What am I doing wrong? Or where to look for the mistake? Thanks!

  • #90 RR said 2020-06-10T09:52:11Z

    Have a strange situation. I have both regular login and google login on the site.

    The explore page seems to function differently for both of them.

    If I log in with an email and a password, my explore page correctly shows me posts from all users of the app.

    If I log in with google, my explore page only shows me posts from other users that logged in with google oauth.

    Where would I begin to troubleshoot this?

    I'm not sure why the explore page would render anything differently depending on how the login happened. The logic there doesn't seem to touch on any users at all, just pulls from the posts table.

    Thoughts?

  • #91 Miguel Grinberg said 2020-06-10T22:47:20Z

    @Gerben: there is an extra "/" after the word "explore" in the URL. It seems you typed the wrong URL in your browser, try removing that trailing slash character.

  • #92 Miguel Grinberg said 2020-06-10T22:53:59Z

    @RR: it's hard for me to say, since I haven't seen your Google auth implementation. I would debug the database query that gets the posts, to see why it returns a different set of results.

  • #93 rr said 2020-06-11T06:19:17Z

    I can't seem to find any difference in my explore page query compared to yours. It seems the same and does work as expected when doing a login with email and password.

    Here's where I add google users to the db

    newuser = User(googid=unique_id, email=users_email, username=users_name) user = User.query.filter_by(googid=unique_id).first() if user is None: db.session.add(newuser) db.session.commit() user = User.query.filter_by(googid=unique_id).first() login_user(user) if user is not None: login_user(user) return redirect(url_for("index"))

    But I'm just confused because the Post table only references the users. The query Post.query.order_by ... should pull all the posts from the db as long as there isn't any additional filter query.

    I'm not sure where else the issue could be but it's clearly something with the different kinds of users. But i'm not sure how to see what's different.

    If I manually go to a users profile page from a google logged in user by changing the url, then i can follow those users and then they do appear both in explore and in the main index page.

  • #94 Miguel Grinberg said 2020-06-11T09:43:21Z

    @RR: As I said before, I cannot comment on your project as I haven't seen it. Some of the things you say do not make sense to me, but of course this can be because I don't know how you coded your project. For example, why do you have to manually go to your users profile page? Once the user is logged in, you should be able to click on the Profile link, regardless of how the user logged in. Clearly there are things that I'm missing, and one of those things is likely causing your bug.

  • #95 MareksNo said 2020-06-14T07:13:53Z

    How could I do something like this but instead of switching to the next page, have a show more button and show also the previous results?

  • #96 Miguel Grinberg said 2020-06-15T15:20:05Z

    @MareksNo: That is harder to do, because you have to use Ajax. The topic is covered later in the tutorial.

  • #97 Filipe Bezerra said 2020-06-27T13:21:19Z

    I was reading though the WTForms validators documentation page about the DataRequired validator, there's this note:

    NOTE this validator used to be called Required but the way it behaved (requiring coerced data, not input data) meant it functioned in a way which was not symmetric to the Optional validator and furthermore caused confusion with certain fields which coerced data to ‘falsey’ values like 0, Decimal(0), time(0) etc. Unless a very specific reason exists, we recommend using the InputRequired instead.

    So I ask why we're using DataRequired instead of InputRequired? Could you elaborate real uses cases demonstrating which cases we should use DataRequired and InputRequired?

    Thank you so much Miguel, for all your effort here budy.

  • #98 Miguel Grinberg said 2020-06-27T22:45:42Z

    @Filipe: the WTForms docs are confusing on this. For a string field these two validators are almost identical. The only difference is that if you add custom filters to your field, DataRequired validates the data after it was filtered and inputRequired validates it before the filters. Filters is a rarely used feature in WTForms, so in general the two behave in the same way.

    For a field that uses a type other than a string, InputRequired will validate the string representation of the field as sent by the client, before it was converted to the field type, while DataRequired will validate the converted data. In this case DataRequired may not work as intended. For example, for a number field if you enter 0 the field will not validate because the number 0 is considered a False value in Python.

  • #99 Robin Kohrs said 2020-06-30T15:05:18Z

    I'm so sorry for asking again, but I just don't know what I'm doing wrong:/ I have a superlong traceback and don't even know where to start. I'd love to continue with the tutorial, but I guess I don't know how...

    /home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask_sqlalchemy/__init__.py:834: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future. Set it to True or False to suppress this warning. 'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and ' [2020-06-30 17:01:11,083] INFO in __init__: Microblog startup * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) [2020-06-30 17:01:14,701] ERROR in app: Exception on / [GET] Traceback (most recent call last): File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/app.py", line 2447, in wsgi_app response = self.full_dispatch_request() File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/app.py", line 1952, in full_dispatch_request rv = self.handle_user_exception(e) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/app.py", line 1821, in handle_user_exception reraise(exc_type, exc_value, tb) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/_compat.py", line 39, in reraise raise value File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/app.py", line 1950, in full_dispatch_request rv = self.dispatch_request() File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/app.py", line 1936, in dispatch_request return self.view_functions[rule.endpoint](**req.view_args) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask_login/utils.py", line 272, in decorated_view return func(*args, **kwargs) File "/home/robin/www/rmdb/app/routes.py", line 37, in index return render_template('index.html', title='Home', form = form, posts=posts) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/templating.py", line 138, in render_template ctx.app.jinja_env.get_or_select_template(template_name_or_list), File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/jinja2/environment.py", line 930, in get_or_select_template return self.get_template(template_name_or_list, parent, globals) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/jinja2/environment.py", line 883, in get_template return self._load_template(name, self.make_globals(globals)) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/jinja2/environment.py", line 857, in _load_template template = self.loader.load(self, name, globals) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/jinja2/loaders.py", line 127, in load code = environment.compile(source, name, filename) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/jinja2/environment.py", line 638, in compile self.handle_exception(source=source_hint) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/jinja2/environment.py", line 832, in handle_exception reraise(*rewrite_traceback_stack(source=source)) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/jinja2/_compat.py", line 28, in reraise raise value.with_traceback(tb) File "/home/robin/www/rmdb/app/templates/index.html", line 21, in template {% endblock %} jinja2.exceptions.TemplateSyntaxError: Encountered unknown tag 'endblock'. You probably made a nesting mistake. Jinja is expecting this tag, but currently looking for 'elif' or 'else' or 'endif'. The innermost block that needs to be closed is 'if'. --- Logging error --- Traceback (most recent call last): File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/app.py", line 2447, in wsgi_app response = self.full_dispatch_request() File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/app.py", line 1952, in full_dispatch_request rv = self.handle_user_exception(e) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/app.py", line 1821, in handle_user_exception reraise(exc_type, exc_value, tb) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/_compat.py", line 39, in reraise raise value File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/app.py", line 1950, in full_dispatch_request rv = self.dispatch_request() File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/app.py", line 1936, in dispatch_request return self.view_functions[rule.endpoint](**req.view_args) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask_login/utils.py", line 272, in decorated_view return func(*args, **kwargs) File "/home/robin/www/rmdb/app/routes.py", line 37, in index return render_template('index.html', title='Home', form = form, posts=posts) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/templating.py", line 138, in render_template ctx.app.jinja_env.get_or_select_template(template_name_or_list), File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/jinja2/environment.py", line 930, in get_or_select_template return self.get_template(template_name_or_list, parent, globals) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/jinja2/environment.py", line 883, in get_template return self._load_template(name, self.make_globals(globals)) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/jinja2/environment.py", line 857, in _load_template template = self.loader.load(self, name, globals) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/jinja2/loaders.py", line 127, in load code = environment.compile(source, name, filename) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/jinja2/environment.py", line 638, in compile self.handle_exception(source=source_hint) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/jinja2/environment.py", line 832, in handle_exception reraise(*rewrite_traceback_stack(source=source)) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/jinja2/_compat.py", line 28, in reraise raise value.with_traceback(tb) File "/home/robin/www/rmdb/app/templates/index.html", line 21, in template {% endblock %} jinja2.exceptions.TemplateSyntaxError: Encountered unknown tag 'endblock'. You probably made a nesting mistake. Jinja is expecting this tag, but currently looking for 'elif' or 'else' or 'endif'. The innermost block that needs to be closed is 'if'. During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/home/robin/miniconda3/lib/python3.7/logging/handlers.py", line 1021, in emit smtp.send_message(msg) File "/home/robin/miniconda3/lib/python3.7/smtplib.py", line 967, in send_message rcpt_options) File "/home/robin/miniconda3/lib/python3.7/smtplib.py", line 867, in sendmail raise SMTPSenderRefused(code, resp, from_addr) smtplib.SMTPSenderRefused: (550, b'Requested action not taken: mailbox unavailable\nSender address is not allowed.', 'no-reply@mail.gmx.net') Call stack: File "/home/robin/miniconda3/lib/python3.7/threading.py", line 890, in _bootstrap self._bootstrap_inner() File "/home/robin/miniconda3/lib/python3.7/threading.py", line 926, in _bootstrap_inner self.run() File "/home/robin/miniconda3/lib/python3.7/threading.py", line 870, in run self._target(*self._args, **self._kwargs) File "/home/robin/miniconda3/lib/python3.7/socketserver.py", line 650, in process_request_thread self.finish_request(request, client_address) File "/home/robin/miniconda3/lib/python3.7/socketserver.py", line 360, in finish_request self.RequestHandlerClass(request, client_address, self) File "/home/robin/miniconda3/lib/python3.7/socketserver.py", line 720, in __init__ self.handle() File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/werkzeug/serving.py", line 345, in handle BaseHTTPRequestHandler.handle(self) File "/home/robin/miniconda3/lib/python3.7/http/server.py", line 426, in handle self.handle_one_request() File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/werkzeug/serving.py", line 379, in handle_one_request return self.run_wsgi() File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/werkzeug/serving.py", line 323, in run_wsgi execute(self.server.app) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/werkzeug/serving.py", line 312, in execute application_iter = app(environ, start_response) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/cli.py", line 337, in __call__ return self._app(environ, start_response) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/app.py", line 2464, in __call__ return self.wsgi_app(environ, start_response) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/app.py", line 2450, in wsgi_app response = self.handle_exception(e) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/app.py", line 1871, in handle_exception self.log_exception((exc_type, exc_value, tb)) File "/home/robin/www/rmdb/venv/lib/python3.7/site-packages/flask/app.py", line 1892, in log_exception "Exception on %s [%s]" % (request.path, request.method), exc_info=exc_info Message: 'Exception on / [GET]' Arguments: ()
  • #100 Miguel Grinberg said 2020-06-30T21:58:41Z

    @Robin: You have a bug in your index.html template. Here is the important part of the stack trace:

    File "/home/robin/www/rmdb/app/templates/index.html", line 21, in template {% endblock %} jinja2.exceptions.TemplateSyntaxError: Encountered unknown tag 'endblock'. You probably made a nesting mistake. Jinja is expecting this tag, but currently looking for 'elif' or 'else' or 'endif'. The innermost block that needs to be closed is 'if'.

Leave a Comment