Flask Web DEvelopment翻译10

作者: 易木成华 | 来源:发表于2016-07-27 10:13 被阅读392次

    第十一章 博客文章

    本章将实现Flasky博客的核心功能,允许用户读取和撰写文章。你将学到一些关于模板重用、长项目列表的分页和富文本等新知识。

    提交和显示文章

    我们需要先准备好一个新的数据库模型来支持文章发布。这个模型设计如例子11-1所示:

    Example 11-1. app/models.py: Post model
    class Post(db.Model):
        __tablename__ = 'posts'
        id = db.Column(db.Integer, primary_key=True)
        body = db.Column(db.Text)
        timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
        author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    class User(UserMixin, db.Model):
        # ...
        posts = db.relationship('Post', backref='author', lazy='dynamic')
    

    文章由body,timestamp和一用户对多文章的关系组成。body字段类型为db.Text——没有长度限制。
      撰写文章的表单将被显示在程序的主页面上。这个表单也很简单,仅包含了一个输入文字的文本块和一个提交按钮。表单的定义如例子11-2所示:

    Example 11-2. app/main/forms.py: Blog post form
    class PostForm(Form):
        body = TextAreaField("What's on your mind?", validators=[Required()])
        submit = SubmitField('Submit')
    

    视图函数index()处理这个表单请求并将旧的文章列表传递给模板,如例子11-3:

    Example 11-3. app/main/views.py: Home page route with a blog post
    @main.route('/', methods=['GET', 'POST'])
    def index():
        form = PostForm()
        if current_user.can(Permission.WRITE_ARTICLES) and form.validate_on_submit():
            post = Post(body=form.body.data, author=current_user._get_current_object())
            db.session.add(post)
            return redirect(url_for('.index'))
        posts = Post.query.order_by(Post.timestamp.desc()).all()
        return render_template('index.html', form=form, posts=posts)
    

    视图函数把表单和完整的文章列表(list格式)传递给模板。文章列表根据文章的发表时间倒序排列。表单使用传统方式进行处理:接收到提交请求就会创建一个新的Post实例。在允许创建新post前会检查当前用户的撰写文章的权限。
      注意,新的文章对象的author属性被设置为current_user._get_current_object()。来自于flask-login的current_user变量就像所有的上下文变量一样,作为一个线程本地代理对象存在。这个对象表现地像一个用户对象,实际上也真的是一个内部包含了一个真实用户对象的包装器。数据库需要一个真实用户对象,通过调用_get_current_object()来取得。(译注:这一段不理解
      表单在index.html模板的欢迎信息下方显示,接下来是文章列表。文章列表把数据库里所有的文章按照从新到旧的顺序依次排列,创建一个文章时间线。这部分模板变化请看例子11-4:

    Example 11-4. app/templates/index.html: Home page template with blog posts
    {% extends "base.html" %}
    {% import "bootstrap/wtf.html" as wtf %}
    ...
    <div>
        {% if current_user.can(Permission.WRITE_ARTICLES) %}
        {{ wtf.quick_form(form) }}
        {% endif %}
    </div>
    <ul class="posts">
        {% for post in posts %}
        <li class="post">
            <div class="profile-thumbnail">
                <a href="{{ url_for('.user', username=post.author.username) }}">
                    <img class="img-rounded profile-thumbnail" src="{{ post.author.gravatar(size=40) }}">
                </a>
            </div>
            <div class="post-date">{{ moment(post.timestamp).fromNow() }}</div>
            <div class="post-author">
                <a href="{{ url_for('.user', username=post.author.username) }}">
                    {{ post.author.username }}
                </a>
            </div>
            <div class="post-body">{{ post.body }}</div>
        </li>
        {% endfor %}
    </ul>
    ... 
    

    注意,我们调用User.can()方法用来对不具备WRITE_ARTICLES权限的用户隐藏表单。文章列表使用了css样式表美化,显示为HTML无序列表格式。作者的小头像显示在左侧,头像及作者名字都是连接到用户档案页面的链接。我们使用的CSS样式表保存在程序static文件夹下。你可以在GIthub仓库中找到它。

    译注:css文件也需要做相应修改,可自行斟酌。

    图11-1是浏览器中显示的带有表单和文章列表的首页。

    Paste_Image.png

    档案页面的文章

    我们来改进一下用户档案页面,让它显示该用户的所有文章列表。例子11-5显示了对该视图函数的更改来获取文章列表:

    Example 11-5. app/main/views.py: Profile page route with blog posts
    @main.route('/user/<username>')
    def user(username):
        user = User.query.filter_by(username=username).first()
        if user is None:
            abort(404)
        posts = user.posts.order_by(Post.timestamp.desc()).all()
        return render_template('user.html', user=user, posts=posts)
    

    某用户的文章列表可以依赖User.posts关系来获取,这一关系是一个查询对象,我们可以使用类似于order_by()的过滤器器来进行筛选。
      user.html模板也需要<ul>HTML列表结构来显示文章列表,类似于index.html中那样——所以,一个HTML列表片段存在两个副本并不是一个好主意,So,Jinja2的include()指令就正好派上用场了。User.html模板可以从一个外部文件当中include这个列表,如例子11-6所示。

    Example 11-6. app/templates/user.html: Profile page template with blog posts
    ...
    <h3>Posts by {{ user.username }}</h3>
    {% include '_posts.html' %}
    ...
    

    为了完成这一重构,<ul>树形结构被从index.html中挪到了新的模板_post.html中,代之以include()指令。注意,_post.html模板命名中的前缀下划线的使用并不是必须的,这样只是一个区分独立模板和局部模板的默认约定。

    译注:在template文件夹下,新建_posts.html。输入如下代码保存:

    <ul class="posts">
            {% for post in posts %}
            <li class="post">
                <div class="profile-thumbnail">
                    <a href="{{ url_for('.user', username=post.author.username) }}">
                        <img class="img-rounded profile-thumbnail" src="{{ post.author.gravatar(size=40) }}">
                    </a>
                </div>
                <div class="post-date">{{ moment(post.timestamp).fromNow() }}</div>
                <div class="post-author">
                    <a href="{{ url_for('.user', username=post.author.username) }}">
                        {{ post.author.username }}
                    </a>
                </div>
                <div class="post-body">{{ post.body }}</div>
            </li>
            {% endfor %}
        </ul>
    

    准备长文章列表

    随着站点和文章数量的不断增长,程序响应速度会变慢,要想在档案页面和首页显示所有的文章列表就显得不切实际了。大页面需要花费更多的时间生成、下载然后在浏览器中渲染显示,所以随着页面变大用户体验质量就会下降。解决办法就是对数据分页(paginate)并按块(chunks)显示。

    创建模拟文章数据

    为了显示多页文章,我们需要先有一个大量数据的测试数据库。手工添加新数据即耗时又乏味,我们需要一个自动方案。有好几个Python包可以用来生成模拟数据信息,比较完整的一个就是ForgeryPy,可以用pip安装:

        (venv) $ pip install forgerypy
    

    严格说起来,ForgeryPy并不是一个程序依赖包,因为它只是在开发过程中才用到。为了将开发依赖和生产依赖区分开,我们把requirements.txt替换为requirements文件夹,分别存储不同环境下的依赖。这个文件夹里用dev.txt和prod.txt分别存储开发环境依赖和生产环境依赖。由于二者会有大部分依赖都一样,所以把共同的部分独立成common.txt,而dev.txt和prod.txt分别使用-r 前缀包含common.txt,例子11-7展示了dev.txt:

    Example 11-7. requirements/dev.txt: Development requirements file
    -r common.txt
    ForgeryPy==0.1
    

    例子11-8,通过分别给user和post添加类方法后,user和Post模型就可以生成模拟数据了:

    Example 11-8. app/models.py: Generate fake users and blog posts
    class User(UserMixin, db.Model):
        # ...
        @staticmethod
        def generate_fake(count=100):
            from sqlalchemy.exc import IntegrityError
            from random import seed
            import forgery_py
        
            seed()
            for i in range(count):
                u = User(email=forgery_py.internet.email_address(),username=forgery_py.internet.user_name(True),password=forgery_py.lorem_ipsum.word(),confirmed=True,name=forgery_py.name.full_name(),
                location=forgery_py.address.city(),about_me=forgery_py.lorem_ipsum.sentence(),member_since=forgery_py.date.date(True))
                db.session.add(u)
                try:
                    db.session.commit()
                except IntegrityError:
                    db.session.rollback()
    class Post(db.Model):
        # ...
        @staticmethod
        def generate_fake(count=100):
            from random import seed, randint
            import forgery_py
            
            seed()
            user_count = User.query.count()
            for i in range(count):
                u = User.query.offset(randint(0, user_count - 1)).first()
                p = Post(body=forgery_py.lorem_ipsum.sentences(randint(1, 3)),timestamp=forgery_py.date.date(True),author=u)
            db.session.add(p)
            db.session.commit()
    

    我们使用ForgeryPy的随机信息生成器生成了很多模拟对象属性,这一生成器可以生成逼真的names,email,setences和更多属性。
      email地址和用户名必须唯一,但由于ForgeryPy完全使用随机生成,所以有一定的几率会出现重复。一旦出现这种意外,数据库会话提交时就会抛出一个IntegrityError例外错误。这个错误将导致会话回滚,因此这一产生重复的操作就不会将用户写入数据库,所以模拟用户数就可能会比我们指定的数量要少。
      随机生成文章时需要为每篇文章指定一个随机的用户。此处使用了offset()查询过滤器。这个过滤器只保留指定数量的结果,通过设置一个随机偏移值然后再调用first(),我们就可以每次取得一个随机用户。这个新方法使得我们从shell中生成大量的模拟用户和文章变得非常轻松:

    (venv) $ python manage.py shell
    >>> User.generate_fake(100)
    >>> Post.generate_fake(100)
    

    译注:要运行shell命令,你需要补充相关引用导入——只要shell中报错未定义xxx,你就需要检查了。这里,在manage.py中修改如下:

    from app.models import User,Role,Permission,Post
    #...
    def make_shell_context():
        return dict(app=app,db=db,User=User,Role=Role,Permission=Permission,Post=Post)
    
    在页面中显示数据

    例子11-9展示了首页路由为了显示分页数据所做的更改:

    Example 11-9. app/main/views.py: Paginate the blog post list
    @main.route('/', methods=['GET', 'POST'])
    def index():
        # ...
        page = request.args.get('page', 1, type=int)
        pagination = Post.query.order_by(Post.timestamp.desc()).paginate(page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],error_out=False)
        posts = pagination.items
        return render_template('index.html', form=form, posts=posts,pagination=pagination)
    

    显示的页码来自于请求查询字符串,可以从request.args中获取。如果没有有效的页码,就会使用默认值1(第一页)。type=int参数确保了一旦参数不能转换成整数,则返回一个默认值。
      为了载入一页记录,应该用Flask-SQLAlchemy的paginate()替换all()方法。paginate()方法把当前页码作为第一个且必要的一个参数。可选参数per_page用来确定每页的记录数,如果没有指定,则默认每页显示20条。另外一个可选参数是error_out默认设置为True,它用来触发404错误,如果请求的页码超过有效范围就会触发该错误。如果error_out被设置为False,超出范围的页码请求将返回一个空列表。为了配置每页显示数,我们在程序配置中单独指定了FLASK_POSTS_PER_PAGE,可以从这里读取它。
      经过这些更改,首页上的文章列表将以一个指定数量显示。如果希望看第二页,可以在浏览器地址栏URL中添加?page=2回车。

    添加一个Pagination插件

    paginate()的返回值是一个Paginatin类的对象。这个对象包含了一些很有用的特性,可以用来在模板中生成链接,所以它应该以参数的形式传递给模板。pagination对象的特性列表如表11-1所示:

    属性      说明
    items     当前页面上的记录
    query     用来分页的源查询
    page      当前页码
    prev_num  上一页页码
    next_num  下一页页码
    has_next  如果有下一页则为True
    has_prev  如果有上一页则为True
    pages     查询得到的所有页数
    per_page  每页记录数
    total     查询结果总数
    

    pagination()的方法列表如下,表11-2:

    方法                        说明
    iter_pages(left_edge=2,     一个迭代器,用来返回分页插件中要显示的页码序列
      left_current=2,           这个列表中的参数分别如下:left_edge,左侧2页码数,left_current 当前页码向左偏移两页,
      right_current=5,          right_current 当前页码向右偏移五页right_edge 右侧边界2页的页码
      right_edge=2)             例如:对于第50页(当前页),共100页,这个迭代器按照左侧的默认配置将返回
                                如下页: 1, 2,None , 48, 49, 50, 51, 52, 53, 54, 55,  None , 
                                99, 100。 None值界定了页码序列的前后缺口(在模板中就显示为...)。     
    prev()  上一页的分页对象.
    next()  下一页的分页对象
    

    武装上这个强大对象和bootstrap样式表类,就可以轻松创建一个分页导航。例子11-10显示了可复用的Jinja2宏格式的实现:

    Example 11-10. app/templates/_macros.html: Pagination template macro
    {% macro pagination_widget(pagination, endpoint) %}
    <ul class="pagination">
        <li{% if not pagination.has_prev %} class="disabled"{% endif %}>
            <a href="{% if pagination.has_prev %}{{ url_for(endpoint,page = pagination.page - 1, **kwargs) }}{% else %}#{% endif %}">
            «
            </a>
        </li>
        {% for p in pagination.iter_pages() %}
            {% if p %}
                {% if p == pagination.page %}
                <li class="active">
                    <a href="{{ url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a>
                </li>
                {% else %}
                <li>
                    <a href="{{ url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a>
                </li>
                {% endif %}
            {% else %}
            <li class="disabled"><a href="#">…</a></li>
            {% endif %}
        {% endfor %}
        <li{% if not pagination.has_next %} class="disabled"{% endif %}>
            <a href="{% if pagination.has_next %}{{ url_for(endpoint,page = pagination.page + 1, **kwargs) }}{% else %}#{% endif %}">
                »
            </a>
        </li>
    </ul>
    {% endmacro %}
    

    这个宏创建了一个Bootstrap无序列表风格的分页元素。其内部的定义链接如下:

    • "Previous Page"上一页,如果当前页是第一页的话这个链接就会被设置为CSS 的disable 类(译注:呈现禁用状态)。
    • 链接到分页对象的iter_pages()迭代器返回来的所有页面。这些页面作为明确页码名称的链接,通过传递给url_for()明确的页码生成。当前页使用activecss类来高亮显示,而页码序列的"缺口"(Gaps in the sequence of pages)则用省略号显示。
    • "Next page"下一页链接,如果当前是最后一页则这个链接不可用(disable css类)

    Jinja2宏总是接收键值参数,不带有必须包含**kwargs的参数列表(?译注:不太理解这一句:Jinja2 macros always receive keyword arguments without having to include **kwargs in the argument list)。Pagination宏把所有接收到的键值参数都传递给url_for()来生成分页导航链接。这个方法可以被用在路由上,如档案页面的动态部分。
      pagination_widget宏可以被添加在index.html和user.html中,位于被引用的_post.html模板下方。例子11-11展示了首页中它的用法:

    Example 11-11. app/templates/index.html: Pagination footer for blog post lists
    {% extends "base.html" %}
    {% import "bootstrap/wtf.html" as wtf %}
    {% import "_macros.html" as macros %}
    ...
    {% include '_posts.html' %}
    <div class="pagination">
        {{ macros.pagination_widget(pagination, '.index') }}
    </div>
    {% endif %}
    

    图11-2就是分页链接在页面中的样子:

    Paste_Image.png

    使用Markdown和Flask-PageDown的富文本文章

    对于短信息和状态更新,纯文本就足够了,但对于希望撰写长篇文字的用户来说就缺乏格式支持非常不便。本节中,我们将升级文本域,来支持Markdown语法,并且提供文章的富文本预览。
      为了实现这一要求,我们需要几个新的包:

    • PageDown,客户端Markdown-HTML转换的js实现
    • Flask-PageDown,PageDown与Flask-WTF集成后的封装
    • Markdown ,服务器端的Markdown-HTML转换的Python实现
    • Bleach, HTML清除器的Python实现

    安装命令(同时安装多个扩展)如下:

    (venv) $ pip install flask-pagedown markdown bleach
    
    使用Flask-pagedown

    Flask-pagedown扩展定义了一个pagedownField字段类,其外观与WTForms的TextAreField一样。在使用这个字段之前,我们需要先初始化这个扩展,如例子11-12:

    Example 11-12. app/__init__.py: Flask-PageDown initialization
    from flask.ext.pagedown import PageDown
    # ...
    pagedown = PageDown()
    # ...
    def create_app(config_name):
        # ...
        pagedown.init_app(app)
        # ...
    

    为了把首页中的文本域(textarea)转换成Markdown 富文本编辑器,我们需要先把PostForm中的body字段转成pagedownField,如例子11-13:

    Example 11-13. app/main/forms.py: Markdown-enabled post form
    from flask.ext.pagedown.fields import PageDownField
    class PostForm(Form):
        body = PageDownField("What's on your mind?", validators=[Required()])
        submit = SubmitField('Submit')
    

    在pageDown库的帮助下生成Markdown预览,这也需要添加到模板中。Flask-pagedown简化了这一任务,它通过CDN提供了一个包含必要文件的宏模板,如例子11-14所示:

    Example 11-14. app/index.html: Flask-PageDown template declaration
    {% block scripts %}
    {{ super() }}
    {{ pagedown.include_pagedown() }}
    {% endblock %}
    

    现在,我们实现了在文本域中录入Markdown格式的文字,立即就可以在预览区看到HTML格式的显示。图11-3显示了带有富文本的博客提交表单:

    Paste_Image.png
    在服务器端处理富文本

    客户端提交表单时,将通过POST请求发送Markdown的源文本——页面上显示的HTML预览是被忽略的。通过表单传递HTML预览具有很大的安全风险,因为通过构造不符合markdown源文本的HTML代码片段,攻击者能很容易地向服务器非法提交内容。为了防止风险,我们只提交markdown源文本,在服务器端使用Markdown包(python编写的markdown-html转换器)转成html,再使用Bleach清理这一html结构,确保其中只使用了我们允许的html标记。
      Markdown转HTML这一动作也可以在_post.html模板中完成,但非常没有效率,因为每次显示页面时都需要对文章进行转换。为了解决这一问题,我们只在创建文章时执行一次转换——把文章的HTML代码格式缓存在一个Post模型的新字段里,这样模板可以直接访问。Markdown原格式文章被保存在数据库里,以备修改之用。例子11-15显示了models.py中Post模型的变化:

    Example 11-15. app/models.py: Markdown text handling in the Post model#原文误写models/post.py
    
    from markdown import markdown
    import bleach
    
    class Post(db.Model):
        # ...
        body_html = db.Column(db.Text)
        # ...
        @staticmethod
        def on_changed_body(target, value, oldvalue, initiator):
            allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code','em', 'i', 'li', 'ol', 'pre', 'strong', 'ul','h1', 'h2', 'h3', 'p']
            target.body_html = bleach.linkify(bleach.clean(markdown(value, output_format='html'),tags=allowed_tags, strip=True))
    db.event.listen(Post.body, 'set', Post.on_changed_body)
    

    on_change_body函数被注册为监听SQLAlchemy对body字段的"set"事件,也就是说,一旦这个类任意实例中的body字段被设置为新值,这个函数都会被自动调用。这个函数渲染html格式的body并把它存储在body_html中,有效的自动完成从markdown到html的格式转换。
      实际上转换有三个步骤。首先,markdown()函数完成初步转换。然后,结果传递给clean(),只允许指定的html标记。clean()函数将清除所有不在白名单中的标记。最终转换由Bleach提供的linkify()函数完成,它会把纯文本格式的URL转换成正确的<a>连接。最后一步是必要的,因为markdown本身没有提供自动连接转换。PageDown作为一个扩展进行了支持,所以linkify()必须在服务器端进行匹配。
      最后一个改变是在模板中用post.body_html替换了post.body,如例子11-16所示:

    Example 11-16. app/templates/_posts.html: Use the HTML version of the post bodies in the template
    ...
    <div class="post-body">
    {% if post.body_html %}
    {{ post.body_html | safe }}
    {% else %}
    {{ post.body }}
    {% endif %}
    </div>
    ...
    

    |safe后缀用来在渲染显示html内容是告诉Jinja2不要对这一html元素进行转义——Jinja2默认会对所有模板变量进行转义以确保安全。由于markdown转html是在服务器端完成的,所以可以安全的显示。

    译注:效果上一条是markdown格式,下一条未写任何格式(markdown将仅添加一个<p>标志)。

    Paste_Image.png

    博客文章的永久性连接

    用户可能希望在社交网络上和朋友分享某篇文章,因此,我们需要为每一篇文章分配一个可以引用的唯一URL。例子11-17展示了支持永久链接的路由和视图函数代码:

    Example 11-17. app/main/views.py: Permanent links to posts
    @main.route('/post/<int:id>')
    def post(id):
        post = Post.query.get_or_404(id)
        return render_template('post.html', posts=[post])
    

    这个URL是由文章的主键id(向数据库插入文章时自动生成,唯一)构成的。

    提醒:对于某些类型的程序,使用一个可读的URL而不是数字id来创建永久链接会更好一些。作为数字id方案的另外方案,(可读的URL)实际上就是给每一篇文章分配一个slug(块?)——指向这篇文章的唯一字符串。

    注意,post.html模板接收并用来显示的是一个列表(list),它实际上只包含指定的这篇文章。这是因为,我们在post.html中引用的_post.html模板要用到的变量是一个文章列表(如同index.html和user.html中一样,它们都是传递了文章列表给_post.html)。
      我们把永久链接添加在_post.html通用模板中每一篇文章的底部,如例子11-18所示:

    Example 11-18. app/templates/_posts.html: Permanent links to posts
    <ul class="posts">
        {% for post in posts %}
        <li class="post">
            ...
            <div class="post-content">
                ...
                <div class="post-footer">
                    <a href="{{ url_for('.post', id=post.id) }}">
                        <span class="label label-default">Permalink</span>
                    </a>
                </div>
            </div>
        </li>
        {% endfor %}
    </ul>
    

    新的带有永久链接的post.html模板如例子 11-19所示,它包含了上例模板。

    Example 11-19. app/templates/post.html: Permanent link template
    {% extends "base.html" %}
    {% block title %}Flasky - Post{% endblock %}
    {% block page_content %}
    {% include '_posts.html' %}
    {% endblock %}
    

    文章编辑器

    关于博客文章的最后一个功能就是创建文章编辑器,允许用户修改自己文章。这个编辑器位于一个独立页面,在页面顶部,显示当前文章的版本,接下来是markdown编辑器——用来修改markdown源文本。这个编辑器基于Flask-PageDown,所以页面底部将会显示一个文章预览。edit_post.html模板如例子11-20所示。

        Example 11-20. app/templates/edit_post.html: Edit blog post template
        {% extends "base.html" %}
    {% import "bootstrap/wtf.html" as wtf %}
    {% block title %}Flasky - Edit Post{% endblock %}
    {% block page_content %}
    <div class="page-header">
        <h1>Edit Post</h1>
    </div>
    <div>
        {{ wtf.quick_form(form) }}
    </div>
    {% endblock %}
    {% block scripts %}
    {{ super() }}
    {{ pagedown.include_pagedown() }}
    {% endblock %}
    

    相关的路由实现如例子11-21所示,

    Example 11-21. app/main/views.py: Edit blog post route
    @main.route('/edit/<int:id>', methods=['GET', 'POST'])
    @login_required
    def edit(id):
        post = Post.query.get_or_404(id)
        if current_user != post.author and not current_user.can(Permission.ADMINISTER):
            abort(403)
        form = PostForm()
        if form.validate_on_submit():
            post.body = form.body.data
            db.session.add(post)
            flash('The post has been updated.')
            return redirect(url_for('.post', id=post.id))
        form.body.data = post.body
        return render_template('edit_post.html', form=form)
    

    视图函数只允许作者修改自己的文章,管理员除外——他可以修改所有人的文章。如果用户试图修改别人的文章,视图函数将返回一个403错误代码的响应。这里使用的PostForm表单类跟首页中引用的那个是一样的。
      最后,我们给每篇文章底下在永久链接后面再添加一个编辑器链接,这个功能就完工了。请看例子11-22:

    Example 11-22. app/templates/_posts.html: Edit blog post links
    <ul class="posts">
        {% for post in posts %}
        <li class="post">
            ...
            <div class="post-content">
                ...
                <div class="post-footer">
                    ...
                    {% if current_user == post.author %}
                    <a href="{{ url_for('.edit', id=post.id) }}">
                        <span class="label label-primary">Edit</span>
                    </a>
                    {% elif current_user.is_administrator() %}
                    <a href="{{ url_for('.edit', id=post.id) }}">
                        <span class="label label-danger">Edit [Admin]</span>
                    </a>
                    {% endif %}
                </div>
            </div>
        </li>
    {% endfor %}
    </ul>
    

    这里增加了一个到当前用户所编写的任意文章的"Edit"链接。对于管理员,这个链接在所有文章上都会显示,并且具有特殊的风格,从视觉上提醒这是一个管理员功能。图11-4展示了edit和永久链接在浏览器里的样子:

    Paste_Image.png
    Over,下章见。
    <<第十章 用户资料 第十二章 关注者>>

    相关文章

      网友评论

        本文标题:Flask Web DEvelopment翻译10

        本文链接:https://www.haomeiwen.com/subject/uuqwjttx.html