zoukankan      html  css  js  c++  java
  • 14.flask博客项目实战九之分页功能

    配套视频教程

    本文B站配套视频教程

    这章将学习如何对数据库中条目列表进行分页。

    在上一节,进行了一些必要的数据库更改,以支持社交网络大受欢迎的“关注”功能。有了这个功能,我们准备删除最后一块脚手架,那就是一开始放置的“假”帖子。在这一章,应用程序将开始接受用户的博客帖子,并在 /index页面和个人资料页面展示它们。

    提交博客帖子

    主页需要一个 表单,用户在此可以写新帖子。首先,建立一个表单类: app/forms.py:博客提交表单

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

    接着,将上述表单添加到应用程序主页的模板中:
    app/templates/index.html:index模板中帖子提交表单

    {% extends "base.html" %}
    
    {% block content %}
    	<h1>Hello,{{ 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 %}
    
    

    这个模板的更改与以前的表单处理方式类似。

    最后,在视图函数 index() 中添加上述表单的创建和处理:修改代码
    app/routes.py:在视图函数中的帖子提交表单

    #...
    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)
    #...
    
    

    逐一查看视图函数中的更改:

    1. 导入 PostPostForm类;
    2. GET请求外,关联到两个路由的视图函数index()都接受POST请求,因为这个视图函数现在将接收表单数据;
    3. 表单处理逻辑将一个新Post记录 插入数据库;
    4. 模板接收 form对象作为一个附加参数,以便它呈现文本字段。

    在继续之前,我想提一些处理Web表单相关的重要事项。注意,在我处理表单数据后,通过发出一个重定向到主页来结束请求。我可以轻松地跳过重定向,并允许函数继续向下进入模板渲染部分,因为这已经是index()视图函数的功能了。

    所以,为什么要重定向?标准做法是通过重定向来响应一个由Web表单提交生成的POST请求。这有助于缓解一个在Web浏览器如何实现刷新命令的烦恼。当点击 刷新键时,所有Web浏览器都会重新发出最后一个请求。如果带有表单提交的POST请求返回常规响应,那么刷新将重新提交表单。因为这是意料之外的,浏览器将要求用户确认重复提交,但大多数用户将无法理解浏览器询问的内容。但是,如果POST请求用重定向来回答请求,则浏览器现在指示发送一个GET请求以获取重定向中指示的页面,因此,现在最后一个请求不再是POST请求,刷新命令可以更可预测的方式工作。

    这个简单的技巧称为:Post/Redirect/Get模式。当用户在提交Web表单后,无意中刷新页面时,它可以避免插入重复的帖子。

    显示博客帖子

    应该记得,之前创建了一些虚假博客帖子,在主页上显示了很长时间了。这些虚拟对象在index()视图函数中显示创建为一个简单的Python列表:

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

    但是,现在User模型中我有followed_posts()方法,它返回给定用户想看的帖子的查询。所以,现在可用真正的帖子替换“假”帖子:
    app/routes.py:在主页显示真实的帖子

    @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)
    
    

    User类followed_posts()方法返回一个SQLAlchemy查询对象,这个对象配置为 从数据库中获取用户感兴趣的帖子。在这个查询中调用all()方法会触发其执行,返回值为一个所有结果集的列表。所以最终得到的结构 跟之前使用的“假”帖子非常相似。正因如此,模板就无须更改了。
    flask run 命令运行程序,效果:

    C:UsersAdministrator>d:
    
    D:>cd D:microblogvenvScripts
    
    D:microblogvenvScripts>activate
    (venv) D:microblogvenvScripts>cd D:microblog
    
    (venv) D:microblog>flask run
     * Environment: production
       WARNING: Do not use the development server in a production environment.
       Use a production WSGI server instead.
     * Debug mode: off
    [2018-08-17 09:59:26,504] INFO in __init__: Microblog startup
     * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
    127.0.0.1 - - [17/Aug/2018 09:59:36] "GET /index HTTP/1.1" 200 -
    
    

    补充 提交帖子后的效果。(导航 Explore链接是后续添加的),图略

    数据库查看刚才刚才添加的帖子:

    (venv) D:microblog>sqlite3 app.db
    SQLite version 3.16.2 2017-01-06 16:32:41
    Enter ".help" for usage hints.
    sqlite> select * from post;
    1|哈哈哈 测试一下  我是susan2018|2018-08-17 02:03:36.912898|1
    sqlite> .quit
    
    (venv) D:microblog>
    
    

    轻松地找到用户和关注

    如上述上一小节效果所示,应用程序不能方便地让用户找到其他用户、自己的帖子。事实上,目前为止还没有办法查看其他用户在哪里。接下来,将通过简单的更改解决这个问题。

    创建一个新页面,称之为 “Explore”页面。这个页面像主页一样工作,但不会仅显示来自所关注用户的帖子,而是显示来自所有用户的全局帖子流。下方是新的 explore()视图函数:
    app/routes.py

    #...
    def index():
        #...
    
    @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)
    #...
    
    

    可注意到,这个视图函数有些奇怪。render_template()调用引用了index.html模板,它是应用程序的主页使用的模板。由于这个页面 跟主页面非常相似,因此重用index.html模块。不过,与主页面有一个区别是 在/explore页面中不需要一个写博客帖子的表单,所以在这个视图函数中,没有在模板中调用 包含form的参数。

    为了防止index.html模板在呈现不存在的Web表单时崩溃,将添加一个条件,只有在定义时才呈现表单(即传入表单参数后才会呈现):
    app/templates/index.html:让博客帖子提交表单 可选

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

    还需在导航栏中添加指向这个新页面的链接:
    app/templates/base.html

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

    _post.html子模板,用于在用户个人资料页面中呈现博客帖子。这是一个包含在用户个人资料页面模板中的小模板,是独立的,因此也可以从其他模板中引用。现在对它进行一些改进,即 将博客帖子作者的用户名显示为链接:
    app/templates/_post.html:在博客帖子中显示作者的链接

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

    现在,就可以使用子模板在主页/explore页面中呈现博客帖子:
    app/templates/index.html:使用博客帖子 子模板

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

    子模板需要一个存在的名为 post 的变量,才能完美地工作,这是在 index模板中 循环变量的命名方式。

    通过上述这些微小的变化,应用程序的可用性得到显著改善。现在,用户可访问 /explore页 阅读来自未知用户的博客帖子,并根据这些帖子找到要关注的新用户,只需单击用户名就可访问 个人资料页面。

    flask run 运行程序,登录susan2018、belen、john发几个帖子,效果:
    image.png

    博客帖子分页

    现在应用程序看起来比以前更好,但是在主页显示所有关注的帖子很快将变成一个问题。如果用户有1000个关注帖子会怎么样?如果是100万呢?可以想象,管理如此庞大的帖子列表将很缓慢且效率低下。

    为解决这个问题,我将对帖子列表进行分页。这意味着,最初将一次只显示有限数量的帖子,并包含用于浏览整个帖子列表的链接。Flask-SQLAlchemy本身支持使用paginate()查询方法进行分页。例如,如果想获得用户的前20个帖子,可以用如下代码替换all()终止查询:

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

    paginate()方法可以在Flask_SQLAlchemy的任何查询对象上调用。它有3个参数:

    1. 页码,从1开始;
    2. 每页的项目数;
    3. 错误标志。若为True,当请求超出范围的页面时,404错误将自动返回给客户端。若为False,超出范围的页面将返回一个空列表。
      paginate()方法的返回值是一个Pagination对象。这个对象的items属性包含所请求页面的项目列表。在Pagination对象中还有其他有用的东西,稍后讨论。

    现在让我们考虑如何在index()视图函数实现分页。首先,在应用程序config.py中添加一个配置项,以确定每页显示多少个项目数。
    microblog/config.py:每页配置的帖子

    #...
    class Config:
    	#...
    	ADMINS = ['your-email@example.com']
    	POSTS_PER_PAGE = 3
    
    

    这些应用程序范围内的 旋钮能改变配置文件中的行为,因为我能够去一个简单的地方做调整。在最终的应用程序中,当然会使用每页大于3个项目的数字,但是对于测试,使用小数字就有用了。

    接下来,我需要决定如何将页码合并到应用程序的URL中。一种常见的方法是使用查询字符串参数来指定可选的页码,如果没有给出,就默认第1页。下方是一些示例网址,展示将如何实现这一点:

    下方将可以看到如何向/index/explore视图函数中添加分页:
    app/routes.py:关注者关联表

    #...
    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 Page', 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)
    #...
    
    

    通过上述更改,两个路由确定要显示的页码,可以是page查询字符串参数,或 默认值1,然后使用paginate()方法取得所需结果的页面。通过app.config对象的POSTS_PER_PAGE 配置项 决定了要访问页码的大小。

    注意,这些更改很容易,以及 每次更改代码的影响程度如何。我正尝试编写应用程序的每个部分,而不对其他部分如何工作做任何假设,这使我能够编写更易于扩展和测试的模块化、健壮的应用程序,并且不太可能出现故障 或bug。

    运行程序,测试上述所编写的分页支持。首先,确保3篇以上的帖子。这在 /explore页面很容易看到,这个页面显示所有用户的帖子。目前将只会看到最近的3篇帖子。若要查询下一个3篇帖子,可在浏览器地址栏输入:http://localhost:5000/explore?page=2

    页面导航

    下一个更改是 在博客帖子列表底部添加链接,允许用户导航到下一页 或上一页。还记得 调用paginate()方法的返回值是一个Flask-SQLAlchemyPagination类的一个对象?目前为止,我们已经使用了这个对象的items属性,它包含为所选页面检索的项目列表。但是 这个对象还具有一些在构建分页链接时有用的前提属性:

    1. has_next:如果当前页面后面至少还有一页,则为True;
    2. has_prev:如果在当前页面之前至少还有一页,则为True;
    3. next_num:下一页的页码;
    4. prev_num:上一页的页码。
      通过上述4个属性,可生成下一个和上一个页面链接,并将它们传递给模板进行渲染:
      app/routes.py:下一页和上一页链接
    #...
    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 Page', 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)
    
    

    上述两个视图函数的next_url 和prev_url 只有在该方向上有页面时,才会设置为由url_for()返回的的一个URL。如果当前页面位于帖子集合的一端,则Pagination对象的has_nexthas_prev属性将是False,并在这种情况下,该方向上的链接将被设置为None

    url_for()函数有一个有趣的方面(之前未讨论过)是 你能够向它添加任何关键字参数,如果这些参数的名字没有直接在URL中引用,那么Flask会将它们作为查询参数包含在URL中。

    分页链接被设置在 index.html模板中,所以现在帖子列表的正下方渲染它们:
    app/templates/index.html:在模板中渲染分页链接

        ...
        {% 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 %}
        ...
    
    

    这个更改会在 /index页、/explore页上的帖子列表下添加链接。第一个链接标记为“Newer posts”,指向上一页(注意,显示帖子按最新排序,因此第一页是具有最新内容的页面)。第二个链接标记为“Older posts”,指向帖子的下一页。如果这两个链接中的任何一个是None,则通过条件从页面中省略它。

    运行程序,效果:
    image.png

    在用户个人资料页面分页

    /index 页面的更改现在已足够。但是,用户个人资料页面中还有一个帖子列表,其仅显示来自个人资料所有者的帖子。为了保持一致,应更改用户个人资料页面以匹配 /index页面的分页样式。

    首先,更新用户个人资料的视图函数,其中仍然有一个“假”帖子的对象列表。
    app/routes.py:用户个人资料页面视图中的分页

    #...
    @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)
    #...
    
    

    为了取得用户的帖子列表,我利用了一个事实,即 user.posts 关系是一个通过SQLAlchemy</b已经建立的查询,它是在User模型中由db.relationship()定义的结果。我们接受这个查询,并添加一个order_by()子句,以便首先取得最新的帖子,然后像对 /index和 /explore页中那样完成分页。注意,url_for()函数生成的分页链接 需要额外的 username参数,因为它们指向用户个人资料页面,这个页面具有这个用户名作为URL的动态组件。

    最后,对user.html模板的更改 与在/index页面上所做 的更改相同:
    app/templates/user.html:用户个人资料页面模板的分页链接

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

    完成分页功能的实验后,可将POSTS_PER_PAGE配置项设置为更合理的值:
    microblog/config.py:每个页面配置 帖子

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

    目前为止,项目结构

    microblog/
        app/
            templates/
    	        _post.html
    	        404.html
    	        500.html
                base.html
                edit_profile.html
                index.html
                login.html
                register.html
                user.html
            __init__.py
            errors.py
            forms.py
            models.py
            routes.py
        logs/
            microblog.log
        migrations/
        venv/
        app.db
        config.py
        microblog.py
        tests.py
    
    

    参考
    https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-ix-pagination

  • 相关阅读:
    css之position
    js之循环语句
    js之条件判断
    js之字典操作
    js之获取html标签的值
    5.15 牛客挑战赛40 C 小V和字符串 数位dp 计数问题
    5.21 省选模拟赛 luogu P4297 [NOI2006]网络收费 树形dp
    luogu P4525 自适应辛普森法1
    luogu P1784 数独 dfs 舞蹈链 DXL
    5.21 省选模拟赛 luogu P4207 [NOI2005]月下柠檬树 解析几何 自适应辛普森积分法
  • 原文地址:https://www.cnblogs.com/songboriceboy/p/13852047.html
Copyright © 2011-2022 走看看