Profile Information

User表单添加相关条目

app/models.py

from datetime import datetime

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    # ...
    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)

db.Stringdb.Text的区别在于db.Text没有长度上限
两个时间字段的默认值为datetime.utcnow,注意这里没有(),这是因为db.Column接受函数作为默认值
member_since字段只需要初始化一次即可,但last_seen字段需要在用户每次登录的时候更新
app/models.py中添加函数

class User(UserMixin, db.Model):
    # ...

    def ping(self):
        self.last_seen = datetime.utcnow()
        db.session.add(self)

在用户每次放松请求时都需要调用ping()函数,所以可以将其放在before_app_request中调用
app/auth/views.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'))

User Profile Page

app/main/views.py

@main.route('/user/<username>')
def user(username):
    user = User.query.filter_by(username=username).first_or_404()
    return render_template('user.html', user=user)

添加模版app/templates/user.html


{% extends "base.html" %}

{% block title %}Flasky - {{ user.username}}{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>{{ user.username }}</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 %}

在导航条中添加到用户信息页面的链接

app/templates/base.html


{% if current_user.is_authenticated %}
  <li>
    <a href="{{ url_for('main.user', username=current_user.username) }}">
      Profile
    </a>
  </li>
{% endif %}

更新数据库模型

python manage.py db migrate -m "user_info"
python manage.py db upgrade

Profile Editor

修改用户信息有两种情况,一种是用户自己填写自己的信息,另一种是管理员修改所有用户信息

User-Level Profile Editor

app/main/forms.py中添加Profile编辑表格

class EditProfileForm(Form):
    name = StringField('Real name', validators=[Length(0, 64)])
    location = StringField('Location' validators=[Length(0, 64)])
    about_me = TextAreaFeild('About me')
    submit = SubmitField('Submit')

app/main/views.py中添加route

@main.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
    form = EditProfileForm()
    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 = current_user.about_me
    return render_template('edit_profile', form=form)

app/templates/user.html中添加到edit_profile的链接


{% if user == current_user %}
  <a class="btn btn-default" href="{{ url_for('.edit_profile') }}">
    Edit Profile
  </a>
{% endif %}

添加模版app/templates/edit_profile.html


{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Edit Profile{% endblock %}

{% block page_content %}
<div class="page-header">
  <h1>Edit your profile</h1>
</div>
<div class="col-md-4">
  {{ wtf.quick_form(form) }}
</div>
{% endblock %}

Administrator-Level Profile Editor

app/main/forms.py中添加管理员修改form

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('Real name', validators=[Length(0, 64)])
    location = StringField('Location', validators=[Length(0, 64)])
    about_me = TextAreaFeild('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.')

app/main/views.py中添加route

@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.location.data
        user.about_me = form.about_me.data
        db.session.add(user)
        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 = user.about_me
    return render_template('edit_profile.html', form=form, user=user)

app/templates/user.html中添加到编辑页面的链接


{% if current_user.is_administrator() %}
  <a class="btn btn-danger"
      href="{{ url_for('.edit_profile_admin', id=user.id) }}">
    Edit Profile [Admin]
  </a>
{% endif %}

User Avatar

本节需要使用Gravatar

Gravatar查询参数

Argument name Description
s image size, in pixels
r image rating. Options are “g”, “pg”, “r”, and “x”
d the default image generator for users who have no avatar registered with Gravatar Service. Opthions are “404” to return a 404 error, a URL that points to a default image, or one of the following image generators: “mm”, “identicon”, “monsterid”, “wavatar”, “retro” or “blank”
fd force the use of default avatar

可以将产生 Gravatar URL 的方式放在app/models.py

import hashlib
from flask import request

class User(UserMixin, db.Model):
    # ...
    def gravatar(self, size=100, default='identicon', rating='g'):
        if request.is_secure:
            url = 'https://secure.gravatar.com/avatar'
        else:
            url = 'http://www.gravatar.com/avatar'
        hash = hashlib.md5(self.email.encode('utf-8')).hexdigest()
        return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
                url=url, hash=hash, size=size, default=default, rating=rating)

添加css文件app/static/styles.css

.profile-thumbnail{
  position: absolute;
}

.profile-header{
  min-height: 260px;
  margin-left: 280px;
}

Jinja2模版中可以直接调用gravatar()函数

app/templates/user.html


<img class="img-rounded profile-thumbnail" src="{{ user.gravatar(size=256)}}">

app/templates/base.html导航条中添加头像缩略图


<link rel="stylesheet" type="text/css"
  href="{{ url_for('static', filename='styles.css')}}">

<ul class="nav navbar-nav navbar-right">
  {% if current_user.is_authenticated %}
    <li class="dropdown">
      <a href="#" class="dropdown-toggle" data-toggle="dropdown">
        <img src="{{ current_user.gravatar(size=18) }}"
        Account <b class="caret"></b>
      </a>
      <ul class="dropdown-menu">
        <li><a href="{{ url_for('auth.logout') }}">Log out</a></li>
      </ul>
    </li>
  {% else %}
    <li><a href="{{ url_for('auth.login') }}">Log in</a></li>
  {% endif%}
</ul>

生成md5比较占用资源,但是可以提前生成放入数据库中缓存

app/models.py

class User(Usermixin, db.Model):
    # ...
    avatar_hash = db.Column(db.String(32))

    def __init__(self, **kwargs):
        #...
        if self.email is not None and self.avatar_hash is None:
            self.avatar_hash = hashlib.md5(
                                  self.email.encode('utf-8')).hexdigest()

    def gravatar(self, size=100, default='identicon', rating='g'):
        if request.is_secure:
            url = 'https://secure.gravatar.com/avatar'
        else:
            url = 'http://www.gravatar.com/avatar'
        hash = self.avatar_hash or hashlib.md5(
                                      self.email.encode('utf-8')).hexdigest()
        return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
                url=url, hash=hash, size=size, default=default, rating=rating)