一、用戶信息字段html
app/models.pypython
新添加的字段保存用戶的真實姓名,所在地,自我介紹,註冊日期和最後的訪問日期。shell
about_me字段的類型是db.Text()數據庫
db.Text和db.String的區別在於後者不須要指定最大長度數組
兩個時間戳的默認值都是當前時間。注意,datetime.utcnow後面沒有(),由於db.Column()的default參數能夠接受函數做爲默認值,因此每次須要生成默認值時,db.Column()都會調用指定的函數。member_since字段只須要默認值緩存
last_seen字段建立時的初始值也是當前時間,但用戶每次訪問網站後,這個值都會被刷新session
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)
二、刷新用戶的最後訪問時間app
app/models.py 刷新用戶的最後訪問時間編輯器
每次收到用戶請求時,都要調用ping方法,因爲auth藍本中的before_app_request處理程序會在每次請求前運行ide
class User(UserMixin, db.Model): #....... #刷新用戶的最後訪問時間 def ping(self): self.last_seen = datetime.utcnow() db.session.add(self)
三、更新已登陸用戶的訪問時間
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'))
四、用戶資料頁面
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)
五、用戶資料頁面的模板
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 %}
六、在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 %}
七、用戶級別的資料編輯器
用戶資料分爲兩種狀況:
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
八、資料編輯的路由以下
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)
九、資料編輯的連接
app/templates/user.html 資料編輯的連接
<p> {% if user == current_user %} <a class="btn btn-default" href="{{ url_for('.edit_profile') }}">Edit Profile</a> {% endif %} </p>
連接外層的條件語句能確保只有當用戶查看本身的資料頁面時纔會顯示這個連接
十、管理員級別的資料編輯器
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.')
十一、管理員的資料編輯路由,上面表單 的後臺邏輯
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 屬性值是整數
十二、爲銜接到這個頁面,還需在用戶資料頁面中添加一個連接按鈕
{% 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 樣式進行渲染。這裏使用的條件語句確保只當登陸用戶爲管理員時才顯示按鈕
1三、用戶頭像
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)
1四、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
1五、 在資料頁面中添加頭像
app/templates/user.html 資料頁面中的頭像
gravatar()方法可在Jinja2模板中調用,下面表示在資料頁面添加一個大小爲256像素的頭像
... <img class = "img-rounded profile-thumbnail" src="{{ user.gravatar(size=256)}}"> ...
1六、緩存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)