13、用户资料
1、用户信息字段
app/models.py
新添加的字段保存用户的真实姓名,所在地,自我介绍,注册日期和最后的访问日期。
about_me字段的类型是db.Text()
db.Text和db.String的区别在于后者不需要指定最大长度
两个时间戳的默认值都是当前时间。注意,datetime.utcnow后面没有(),因为db.Column()的default参数可以接受函数作为默认值,所以每次需要生成默认值时,db.Column()都会调用指定的函数。member_since字段只需要默认值
last_seen字段创建时的初始值也是当前时间,但用户每次访问网站后,这个值都会被刷新
class User(UserMixin, db.Model): #...... #添加用户字段 name = db.Column(db.String(64)) location = db.Column(db.String(64)) about_me = db.Column(db.Text()) member_since = db.Column(db.DateTime(), default=datetime.utcnow)
last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
2、刷新用户的最后访问时间
app/models.py 刷新用户的最后访问时间
每次收到用户请求时,都要调用ping方法,由于auth蓝本中的before_app_request处理程序会在每次请求前运行
class User(UserMixin, db.Model): #....... #刷新用户的最后访问时间 def ping(self): self.last_seen = datetime.utcnow() db.session.add(self)
3、更新已登录用户的访问时间
app/auth/view.py
#过滤未确认用户 @auth.before_app_request def before_request(): if current_user.is_authenticated(): current_user.ping() if not current_user.confirmed and request.endpoint[:5] != 'auth.' and request.endpoint != 'static': return redirect(url_for('auth.unconfirmed'))
4、用户资料页面
app/main/views.py 资料页面的路由
这个路由在main蓝本中添加,对于名为john的用户,其资料页面的地址是http://localhost:5000/user/john
这个视图函数会在数据库中搜索URL中指定的用户名,如果找到,则渲染模板user.html,并把用户名作为参数传入模板,如果用户不村子返回404错误
user.html模板应该渲染保存在用户对象中的信息
@auth.route('/user/<username>') def use(username): user = User.query.filter_by(username=username).first() if user is None: abort(404) return render_template('user.html', user=user)
5、用户资料页面的模板
app/templates/user.html
模板说明:
name和location字段在同一个<p>元素中渲染。只有至少定义了这两个字段中的一个时,<p>元素才会创建
用户的location字段被渲染成指向谷歌地图的查询链接
如果登录用户时管理员,那么就显示用户的电子邮件地址,且渲染成mailto链接
{% extends "base.html" %} {% block title %}Flasky{% endblock %} { % block page_content %} <div class="page-header"> <h1>Hello, {{ name }}!</h1> {% if user.name or user.location %} <p> {% if user.name %} {{ user.name }}{% endif %} {% if user.location %} From <a href="http://maps.google.com/?q={{ user.location }}"> {{ user.location }} </a> {% endif %} </p> {% endif %} {% if current_user.is_administrator()%} <p><a href="mailto:{{ user.email }}">{{ user.email }}</a></p> {% endif %} {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %} <p> Member since {{ moment(user.member_since).format('L')}}. Last seen {{ moment(user.last_seen).fromNow() }}. </p> </div> {% endblock %}
6、在base.html中添加访问自己资料页面的链接
app/templates/base.html
{% if current_user.is_authenticated %} <li><a href="{{ url_for('auth.logout') }}">Sign Out</a></li> <li><a href="{{ url_for('main.user', username=current_user.username) }}">Profile</a> {% else %} <li><a href="{{ url_for('auth.login') }}">Sign In</a></li> {% endif %}
7、用户级别的资料编辑器
用户资料分为两种情况:
a)用户要进入一个页面并在其中输入自己的资料
b)管理员能够编辑用户的资料
普通用户的资料编辑表单如下:
app/main/forms.py 资料编辑表单
#普通用户的资料编辑表单 class EditProfileFrom(Form): name = StringField('Real name', validators=[Length(0, 64)]) location = StringField('Location', validators=[Length(0, 64)]) about_me = TextAreaField('About me') submit = SubmitField('Submit')
这个表单中的所有字段都是可选的,因此长度验证函数允许长度为0
8、资料编辑的路由如下
app/main/views.py
显示表单之前,所有字段都有初始值,对于所有给定的值,这个工作都是通过把初始值赋值给form.<field-name>.data完成,当form.validate_on_submit()返回false时,表单中的3个字段都是使用current_user中保存的初始值。提交表单后,表单字段的data属性中保存有更新后的值,因此可以将其赋值给用户对象中的各字段,然后再把用户对象添加到数据库会话中去。
@main.route('/edit-profile', methods=['GET', 'POST']) @login_required def edit_profile(): form = EditProfileFrom() if form.validate_on_submit(): current_user.name = form.name.data current_user.location = form.location.data current_user.about_me = form.about_me.data db.session.add(current_user) flash('Your profile has been updated') return redirect(url_for('.user', username=current_user.username)) form.name.data = current_user.name form.location.data = current_user.location form.about_me.data = current_user.about_me return render_template('edit_profile.html', form=form)
9、资料编辑的链接
app/templates/user.html 资料编辑的链接
<p> {% if user == current_user %} <a class="btn btn-default" href="{{ url_for('.edit_profile') }}">Edit Profile</a> {% endif %} </p>
链接外层的条件语句能确保只有当用户查看自己的资料页面时才会显示这个链接
10、管理员级别的资料编辑器
app/main/forms.py 管理员使用的资料编辑表单
WTForms对HTML表单控件<select>进行SelectField包装,从而实现下拉列表,用来在这个表单中选择用户角色。SelectFileld实例必须在其choices属性中设置各选项,选项必须是一个有元祖组成的列表,各元祖都包含两个元素:选项的表示符合显示在控件中的文本字符串
#管理员使用的资料编辑表单 class EditProfileAdminForm(Form): email = StringField('Email', validators=[Required(), Length(1, 64), Email()]) username = StringField('Username', validators=[Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, 'Usernames must have only letters', 'numbers, dots or underscores')]) confirmed = BooleanField('Confirmed') role = SelectField('Role', coerce=int) name = StringField('Role name', validators=[Length(0, 64)]) location = StringField('Location', validators=[Length(0, 64)]) about_me = TextAreaField('About me') submit = SubmitField('Submit') def __init__(self, user, *args, **kwargs): super(EditProfileAdminForm, self).__init__(*args, **kwargs) self.role.choices = [(role.id, role.name) for role in Role.query.order_by(Role.name).all()] self.user = user def validate_email(self, field): if field.data != self.user.email and User.query.filter_by(email=field.data).first(): raise ValidationError('Email already registered.') def validate_username(self, field): if field.data != self.user.username and User.query.filter_by(username=field.data).first(): raise ValidationError('Username already in use.')
11、管理员的资料编辑路由,上面表单 的后台逻辑
app/main/views.py 管理员的资料编辑路由
get_or_404由Flask-SQLAlchemy 提供,如果id提供的不正确,则会返回404错误
#管理员的资料编辑路由 @main.route('/edit-profile/<int:id>', methods=['GET', 'POST']) @login_required @admin_required def edit_profile_admin(id): user = User.query.get_or_404(id) form = EditProfileAdminForm(user=user) if form.validate_on_submit(): user.email = form.email.data user.username = form.username.data user.confirmed = form.confirmed.data user.role = Role.query.get(form.role.data) user.name = form.name.data user.location = form.name.data user.about_me = form.location.data db.session.add(user) flash('The profile has been updated.') return redirect(url_for('.user', username=user.username)) form.email.data = user.email form.username.data = user.username form.confirmed.data = user.confirmed form.role.data = user.role_id form.name.data = user.name form.location.data = user.location form.about_me.data = user.about_me return render_template('edit_profile.html', form=form, user=user)
我们还需要再探讨一下用于选择用户角色的 SelectField。设定这个字段的初始值时,role_id 被赋值给了 field.role.data,这么做的原因在于 choices 属性中设置的元组列表使用数字标识符表示各选项。 表单提交后, id 从字段的 data 属性中提取,并且查询时会使用提取出来的 id 值加载角色对象。表单中声明 SelectField 时使用 coerce=int 参数,其作用是保证这个字段的 data 属性值是整数
12、为衔接到这个页面,还需在用户资料页面中添加一个链接按钮
{% if current_user.is_administrator() %} <a class="btn btn-danger" href="{{ url_for('.edit_profile_admin', id=user.id) }}">Edit Profile [Admin]</a> {% endif %}
为了醒目,这个按钮使用了不同的 Bootstrap 样式进行渲染。这里使用的条件语句确保只当登录用户为管理员时才显示按钮
13、用户头像
app/models.py
头像的URL由URL基,用户电子邮件地址的MD5散列值和参数组成,而且各参数都设定了默认值
#生成Gravatar URL def gravatar(self, size=100, default='identicon', rating='g'): if request.is_secure: url = 'https://secure.gravatar.com/avatar' else: url = 'https://www.gravatar.com/avatar' hash = hashlib.md5(self.email.encode('utf-8')).hexdigest() return '{url}/{hask}?s={size}&d={default}&r={rating}'.format(url=url, hash=hash, size=size, default=default, rating=rating)
14、python shell中生成头像的URL
(venv) python manage.py shell >>> u = User(email='john@example.com') >>>u.gravatar() 'http://www.gravatar.com/avatar/d4c74594d84113932869575bd6?s=100&d=identicon&r=g' >>> u.gravatar(size=256) 'http://www.gravatar.com/avatar/d4c74594d84113932869575bd6?s=256&d=identicon&r=g
15、 在资料页面中添加头像
app/templates/user.html 资料页面中的头像
gravatar()方法可在Jinja2模板中调用,下面表示在资料页面添加一个大小为256像素的头像
... <img class = "img-rounded profile-thumbnail" src="{{ user.gravatar(size=256)}}"> ...
16、缓存MD5散列值,修改User模型
app/models.py 使用缓存的MD5散列值生成Gravatar URL
class User(UserMixin, db.Model): #...... avatar_hash = db.Column(db.String(32)) #添加用户字段 name = db.Column(db.String(64)) location = db.Column(db.String(64)) about_me = db.Column(db.Text()) member_since = db.Column(db.DateTime(), default=datetime.utcnow) last_seen = db.Column(db.DateTime(), default=datetime.utcnow) def change_email(self, token): s = Serializer(current_app.config['SECRET_KEY']) try: data = s.loads(token.encode('utf-8')) except: return False if data.get('change_email') != self.id: return False new_email = data.get('new_email') if new_email is None: return False if self.query.filter_by(email=new_email).first() is not None: return False self.email = new_email self.avatar_hash = self.gravatar_hash() db.session.add(self) return True #生成Gravatar URL def gravatar(self, size=100, default='identicon', rating='g'): if request.is_secure: url = 'https://secure.gravatar.com/avatar' else: url = 'https://www.gravatar.com/avatar' hash = self.avatar_hash or hashlib.md5(self.email.encode('utf-8')).hexdigest() return '{url}/{hask}?s={size}&d={default}&r={rating}'.format(url=url, hash=hash, size=size, default=default, rating=rating)
#生成Gravatar URL
def gravatar(self, size=100, default='identicon', rating='g'):
if request.is_secure:
url = 'https://secure.gravatar.com/avatar'
else:
url = 'https://www.gravatar.com/avatar'
hash = hashlib.md5(self.email.encode('utf-8')).hexdigest()
return '{url}/{hask}?s={size}&d={default}&r={rating}'.format(url=url, hash=hash, size=size, default=default, rating=rating)