英文博客地址:http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-vi-profile-page-and-avatars
中文翻译地址:http://www.pythondoc.com/flask-mega-tutorial/profile.html
开源中国社区:http://www.oschina.net/translate/the-flask-mega-tutorial-part-vi-profile-page-and-avatars
备注:我是三个一起看的,有些部分的中文翻译太拗口而且还有错,因此我选择是比较清晰的中文解释,而有些部分是直接翻译英文博客。
上一部分:Flask学习之五 用户登录
一、用户资料页面
创建一个用户信息不需要引入新的概念。我们只要创建一个新的视图函数以及与它配套的 HTML 模版。
这里就是视图函数(文件 app/views.py):
@app.route(‘/user/<nickname>‘) @login_required def user(nickname): user = User.query.filter_by(nickname = nickname).first() if user == None: flash(‘User ‘ + nickname + ‘ not found.‘) return redirect(url_for(‘index‘)) posts = [ { ‘author‘: user, ‘body‘: ‘Test post #1‘ }, { ‘author‘: user, ‘body‘: ‘Test post #2‘ } ] return render_template(‘user.html‘, user = user, posts = posts)
这里的@app.route和之前有点不一样,有了一个参数 <nickname> 。
在函数里面它会转化成跟它同名的参数,当用户有请求的时候,例如这样的一个URL:URL/user/miguel,次视图函数就会识别为有一个名为nickname值为‘miguel‘的参数,即nickname = ‘miguel‘。
上面的实现过程很简单,首先把转化后的 nickname 参数作为条件,尝试着从数据库里载入用户。如果没有查询到数据,就给用户一个错误的提示并且重定向到主页去。
一旦找到了该用户,就把它传入到 render_template 调用,并且传入一些伪造的 blog。注意在用户信息页上只会显示该用户的 blog,因此伪造的 blog 的 author 域必须正确。
这部分最初的视图模版是十分简单的(文件 app/templates/user.html):
<!-- extend base layout --> {% extends "base.html" %} {% block content %} <h1>User: {{user.nickname}}!</h1> <hr> {% for post in posts %} <p> {{post.author.nickname}} says: <b>{{post.body}}</b> </p> {% endfor %} {% endblock %}
用户信息页现在已经完成了,但是缺少对它的链接。为了让用户很容易地检查他的或者她的信息,我们直接把用户信息页的链接放在导航栏中(文件 app/templates/base.html):
<div>Microblog: <a href="{{ url_for(‘index‘) }}">Home</a> {% if g.user.is_authenticated() %} | <a href="{{ url_for(‘user‘, nickname=g.user.nickname) }}">Your Profile</a> | <a href="{{ url_for(‘logout‘) }}">Logout</a> {% endif %} </div>
现在可以运行试一下,当然,记住得先登录
因为我们还没有到任何用户的信息页的链接,因此你必须手动键入你想要看到的用户信息的 URL。比如,你可以键入 http://localhost:5000/user/miguel,查看 miguel 用户信息。因为我们的blog是伪造的,所以不管哪个用户,你看到的话都是一样的。而如果那个用户也不在数据库中,那么就会看见重定向回到了index页面。
二、头像
不需要在我们自己的服务器处理大量的上传图片,我们依赖 Gravatar 服务为我们提供用户头像。(Gravatar好像已经被墙,反正我上不去)
因为返回一个头像是与用户相关的任务,我们把它放在 User 类(文件 app/models.py):
from hashlib import md5 # ... class User(db.Model): # ... def avatar(self, size): return ‘http://www.gravatar.com/avatar/%s?d=mm&s=%d‘ % (md5(self.email.encode(‘utf-8‘)).hexdigest(), size)
User 的方法 avatar 返回用户图片的 URL,以像素为单位缩放成要求的尺寸。
有了 Gravatar 服务的协助,很容易处理头像。你只需要创建一个用户邮箱的 MD5 哈希,然后将其加入 URL中,像上面你看见的。在邮箱 MD5 后,你还需要提供一个定制头像尺寸的数字。d=mm 决定什么样的图片占位符当用户没有 Gravatar 账户。mm 选项将会返回一个“神秘人”图片,一个人灰色的轮廓。s=N 选项要求头像按照以像素为单位的给定尺寸缩放。
Gravatar 官方文档 对 avatar URL 有着更加详细的解释。
现在我们的 User 类知道怎样返回一个头像图片,我们把它融入到用户信息页的布局中(文件 app/templates/user.html):
<!-- extend base layout --> {% extends "base.html" %} {% block content %} <table> <tr valign="top"> <td><img src="{{ user.avatar(128) }}"></td> <td><h1>User: {{ user.nickname }}</h1></td> </tr> </table> <hr> {% for post in posts %} <p> {{ post.author.nickname }} says: <b>{{ post.body }}</b> </p> {% endfor %} {% endblock %}
User 类负责返回头像是一个很巧妙的事情,如果有一天决定不想要 Gravatar 头像,我们只要重构 avatar 返回不同的 URLs(即使指向我们自己的服务器,如果我们想要自己的头像服务器),所有我们的模版将会自动地开始显示新的头像。
我的邮箱没在Gravatar上注册过,所以返回的就是默认头像啦。
备注:如果你们和我一样懒得FQ,就修改URL,将Gravatar头像的服务器变为国内的,放在国内知名公司的服务器上或者专业的CDN服务器上。
User 类(文件 app/models.py):
from hashlib import md5 # ... class User(db.Model): # ... def avatar(self, size): return ‘http://gravatar.duoshuo.com/avatar/%s?d=mm&s=%d‘ % (md5(self.email.encode(‘utf-8‘)).hexdigest(), size)
为什么用这个URL,见这篇文章 Gravatar头像不显示完美解决方案
三、在子模板中重用
我们已经实现了用户信息页,它能够显示用户的 blog。我们的首页也应该显示任何一个用户这个时候的 blog 。这样我们有两个页需要显示用户的 blog。当然我们可以直接拷贝和复制处理渲染 blog 的模板,但这不是最理想的。因为当我们决定要修改 blog 的布局的时候,我们要更新所有使用它的模板。
相反,我们将要制作一个渲染 blog 的子模板,我们在使用它的模板中包含这个子模板。
我们创建一个 blog 的子模板,这是一个再普通不过的模板(文件 /app/templates/post.html):
<table> <tr valign="top"> <td><img src="{{ post.author.avatar(50) }}"></td> <td><i>{{ post.author.nickname }} says:</i><br>{{ post.body }}</td> </tr> </table>
接着我们使用 Jinja2 的 include 命令在我们的用户模板中调用这个子模板(文件 app/templates/user.html):
<!-- extend base layout --> {% extends "base.html" %} {% block content %} <table> <tr valign="top"> <td><img src="{{ user.avatar(128) }}"></td> <td><h1>User: {{ user.nickname }}</h1></td> </tr> </table> <hr> {% for post in posts %} {% include ‘post.html‘ %} {% endfor %} {% endblock %}
一旦有了完整的页面我们就可以按照上面的方法去调用下子模板来显示文章,不过现在还不急。
四、更多有趣的信息
用户资料页面应该有更多的东西,例如用户自我介绍,每个用户访问页面的最后一次的时间。
为了增加这些,我们必须开始修改数据库。更具体地说,我们必须在我们的 User 类上增加两个字段(文件 app/models.py):
class User(db.Model): id = db.Column(db.Integer, primary_key=True) nickname = db.Column(db.String(64), index=True, unique=True) email = db.Column(db.String(120), index=True, unique=True) posts = db.relationship(‘Post‘, backref=‘author‘, lazy=‘dynamic‘) about_me = db.Column(db.String(140)) last_seen = db.Column(db.DateTime)
为了增加这两个新字段到数据库,需要运行升级脚本:
./db_migrate.py
接着,让我们修改用户信息页模板来展示这些字段(文件 app/templates/user.html):
<!-- extend base layout --> {% extends "base.html" %} {% block content %} <table> <tr valign="top"> <td><img src="{{user.avatar(128)}}"></td> <td> <h1>User: {{user.nickname}}</h1> {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %} {% if user.last_seen %}<p><i>Last seen on: {{ user.last_seen }}</i></p>{% endif %} </td> </tr> </table> <hr> {% for post in posts %} {% include ‘post.html‘ %} {% endfor %} {% endblock %}
注意:利用 Jinja2 的条件语句来显示这些字段,只有当它们被设置的时候才会显示出来。
最后显示的字段(last_seen)就特别的好处理。记得在之前的章节中,我们创建了一个 before_request 函数,用来注册登录的用户到全局变量 flask.g 中。这个函数可以用来在数据库中更新用户最后一次的访问时间(文件 app/views.py):
from datetime import datetime # ... @app.before_request def before_request(): g.user = current_user if g.user.is_authenticated(): g.user.last_seen = datetime.utcnow() db.session.add(g.user) db.session.commit()
如果你登录到你的信息页,最后出现时间会显示出来。每次刷新页面,最后出现时间都会更新,因为每次浏览器在发送请求之前,before_request 函数都会在数据库中更新时间。
注意的是我们是以标准的 UTC 时区写入时间。我们在之前的章节中讨论过这个问题,因此我们将会以 UTC 格式写入所有时间内容以保证它们的一致性。这种时间形式在前台显示,看起来会很别扭。我们将会在后面的章节中修正这种显示问题。
要显示用户的关于我的信息,我们必须给他们编辑的地方,就是“编辑个人信息”页面。
五、编辑用户信息
新增一个用户信息表单是相当容易的。我们开始创建网页表单(文件 app/forms.py):
from flask.ext.wtf import Form from wtforms import StringField, BooleanField, TextAreaField from wtforms.validators import DataRequired, Length class EditForm(Form): nickname = StringField(‘nickname‘, validators=[DataRequired()]) about_me = TextAreaField(‘about_me‘, validators=[Length(min=0, max=140)])
视图模板(文件 app/templates/edit.html):
<!-- extend base layout --> {% extends "base.html" %} {% block content %} <h1>Edit Your Profile</h1> <form action="" method="post" name="edit"> {{form.hidden_tag()}} <table> <tr> <td>Your nickname:</td> <td>{{ form.nickname(size=24) }}</td> </tr> <tr> <td>About yourself:</td> <td>{{ form.about_me(cols=32, rows=4) }}</td> </tr> <tr> <td></td> <td><input type="submit" value="Save Changes"></td> </tr> </table> </form> {% endblock %}
视图函数(文件 app/views.py):
from forms import LoginForm, EditForm @app.route(‘/edit‘, methods=[‘GET‘, ‘POST‘]) @login_required def edit(): form = EditForm() if form.validate_on_submit(): g.user.nickname = form.nickname.data g.user.about_me = form.about_me.data db.session.add(g.user) db.session.commit() flash(‘Your changes have been saved.‘) return redirect(url_for(‘edit‘)) else: form.nickname.data = g.user.nickname form.about_me.data = g.user.about_me return render_template(‘edit.html‘, form=form)
方便用户编辑,我们需要在用户的个人资料页面添加一个到此页面的链接地址(文件 app/templates/user.html):
<!-- extend base layout --> {% extends "base.html" %} {% block content %} <table> <tr valign="top"> <td><img src="{{ user.avatar(128) }}"></td> <td> <h1>User: {{user.nickname}}</h1> {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %} {% if user.last_seen %}<p><i>Last seen on: {{ user.last_seen }}</i></p>{% endif %} {% if user.id == g.user.id %}<p><a href="{{ url_for(‘edit‘) }}">Edit</a></p>{% endif %} </td> </tr> </table> <hr> {% for post in posts %} {% include ‘post.html‘ %} {% endfor %} {% endblock %}
编辑保存后:
再看回资料页: