为应用添加个人主页。个人主页用来展示用户的相关信息,其个人信息由本人录入。 我将为你展示如何动态地生成每个用户的主页,并提供一个编辑页面给他们来更新个人信息。
个人主页
第一步,让我们为其URL /user/ 新建一个对应的视图函数。
app/main/routers.py:
@app.route('/user/<username>')
@login_required
def user(username):
user = User.query.filter_by(username=username).first_or_404()
posts = [
{'author': user, 'body': 'Test post #1'},
{'author': user, 'body': 'Test post #2'}
]
return render_template('user.html', user=user, posts=posts)
为这个用户初始化一个虚拟的用户动态列表,最后用传入的用户对象和用户动态列表渲染一个新的user.html模板。
{% extends "base.html" %}
{% block app_content %}
<h1>User: {{ user.username }}</h1>
<hr>
{% for post in posts %}
<p>
{{ post.author.username }} says: <b>{{ post.body }}</b>
</p>
{% endfor %}
{% endblock %}
在顶部的导航栏中添加这个入口链接,以便用户可以轻松查看自己的个人资料:
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li><a href="{{ url_for('main.index') }}">Home</a></li>
<li><a href="#">Explore</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_anonymous %}
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
{% else %}
<li><a href="{{ url_for('main.user', username=current_user.username) }}">Profile</a></li>
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
{% endif %}
</ul>
</div>
这里唯一有趣的变化是用来生成链接到个人主页的url_for()调用。 由于个人主页视图函数接受一个动态参数,所以url_for()函数接收一个值作为关键字参数。 由于这是一个指向当前登录个人主页的链接,我可以使用Flask-Login的current_user对象来生成正确的URL。
个人头像
Miguel Grinberg在头像服务中使用了Gravatar为所有用户提供图片服务,但遗憾的是在国内我们无法使用这个服务,而且在国内也没有找到类似的可以替代的服务。为了使用个人头像,参考类似项目做了一下变更。
- 用户模型
对用户模型进行变更,增加_avatar,about_me,last_seen,并定义save(),set_avatar(),avatar()方法,用来上传自我简介,上次登录时间以及文件头像文件并做处理。
app/models/user.py:
from datetime import datetime
from app import db,login
from werkzeug.security import generate_password_hash,check_password_hash
from flask_login import UserMixin
class User(UserMixin,db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
about_me=db.Column(db.String(140))
last_seen=db.Column(db.DateTime,default=datetime.utcnow)
_avatar=db.Column(db.String(100))
posts = db.relationship('Post', backref='author', lazy='dynamic')
def __repr__(self):
return '<User {}>'.format(self.username)
def set_password(self,password):
self.password_hash=generate_password_hash(password)
def check_password(self,password):
return check_password_hash(self.password_hash,password)
def save(self):
db.session.add(self)
db.session.commit()
def set_avatar(self, avatar):
self._avatar = avatar
def avatar(self,size=128):
if self._avatar and size==128:
return '/static/uploads/images/' + self._avatar
elif self._avatar:
name = self._avatar.split('.',1)
new_name=name[0]+str(size)+'.'+name[1]
return '/static/uploads/images/' + new_name
return '/static/image/lijuan.jpg'
@login.user_loader
def load_user(id):
return User.query.get(int(id))
每次修改完成后,不要忘记使用flask db migrate以及flask db upgrade来更新数据库迁移
- 将头像图片插入到个人主义模板当中。使用User类来返回头像的好处是,如果使用其他的头像服务,可以重写avatar()方法来返回头像图片,所有的模板将自动显示新的头像。
个人主页的顶部有一个大头像,不止如此,底下的所有用户动态都会有一个小头像。 对于个人主页而言,所有的头像当然都是对应用户的。主页面上实现每个用户动态都用其作者的头像来装饰。
app/templates/user.html:
{% extends "base.html" %}
{% block content %}
<table>
<tr valign="top">
<td><img src="{{ user.avatar(128) }}"></td>
<td><h1>User: {{ user.username }}</h1></td>
</tr>
</table>
<hr>
{% for post in posts %}
<table>
<tr valign="top">
<td><img src="{{ post.author.avatar(36) }}"></td>
<td>{{ post.author.username }} says:<br>{{ post.body }}</td>
</tr>
</table>
{% endfor %}
{% endblock %}
- Jinja子模板
个人主页使用头像和文字组合的方式来展示了用户动态。 现在要在主页也使用类似的风格来布局。 我可以复制/粘贴来处理用户动态渲染的模板部分,但这实际上并不理想,因为之后如果想要对此布局进行更改,将不得不记住要更新两个模板。
取而代之,创建一个只渲染一条用户动态的子模板,然后在user.html和index.html模板中引用它。 将其命名为app/templates/_post.html, _前缀只是一个命名约定,可以帮助识别哪些模板文件是子模板。
<table>
<tr valign="top">
<td><img src="{{ post.author.avatar(36) }}"></td>
<td>{{ post.author.username }} says:<br>{{ post.body }}</td>
</tr>
</table>
在user.html模板中使用了Jinja2的include语句来调用该子模板:
{% extends "base.html" %}
{% block app_content %}
<table>
<tr valign="top">
<td><img src="{{ user.avatar(128) }}"></td>
<td>
<h1>User: {{ user.username }}</h1>
{% if user.about_me %}
<p>{{ user.about_me }}</p>
{% endif %}
{% if user.last_seen %}
<p>Last seen on: {{ user.last_seen }}</p>
{% endif %}
{% if user == current_user %}
<p><a href="{{ url_for('auth.edit_profile') }}">Edit your profile</a></p>
{% endif %}
</td>
</tr>
</table>
<hr>
{% for post in posts %}
{% include '_post.html' %}
{% endfor %}
{% endblock %}
4、需要给用户一个表单,让他们输入一些个人资料。 表单将允许用户更改他们的用户名,写一些个人介绍,以存储在新的about_me字段中,并且提供图片文件上传按钮,保存头像文件。 开始写一个表单类:
app/forms/auth.py:
# .......
from wtforms.validators import DataRequired, ValidationError, Email, EqualTo,Length
from app.models import User
# .........
class EditProfileForm(FlaskForm):
username=StringField('Username',validators=[DataRequired()])
about_me=TextAreaField('About me',validators=[Length(min=0,max=140)])
avatar=FileField('Avatar')
submit=SubmitField('Submit')
对于“about_me”字段,使用TextAreaField,这是一个多行输入文本框,用户可以在其中输入文本。 为了验证这个字段的长度,使用了Length,它将确保输入的文本在0到140个字符之间,因为这是我为数据库中的相应字段分配的空间。avatar使用了FileField,这是一个文件输入框,用户可以通过它来进行文件上传。
对表单进行渲染,app/templates/edit_profile.html:
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Edit Profile</h1>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}
通过bootstrap/wtf进行渲染,是不是特简单?
- 视图函数,模型及表单准备好,html文件也准备好了,用视图函数连接起来!
from datetime import datetime
from app.utils import handle_upload,resize_avatar
# ...
auth=Blueprint('auth',__name__)
"""
在视图函数处理请求之前执行一段简单的代码,
一旦某个用户向服务器发送请求,就将当前时间写入到这个字段。
"""
@auth.before_request
def before_request():
if current_user.is_authenticated:
current_user.last_seen = datetime.utcnow()
db.session.commit()
@auth.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
user=current_user
form = EditProfileForm()
errors=[]
if form.validate_on_submit():
current_user.username = form.username.data
current_user.about_me = form.about_me.data
if request.files.get('avatar'):
avatar = request.files['avatar']
"""
通过自定义的handle_upload以及resize_avatar函数
来对上传的头像存储及处理,考虑hash函数对名称进行定义,并回传不同像素的图像
"""
ok,info = handle_upload(avatar,'image')
if ok:
size128=resize_avatar(info,128)
size36=resize_avatar(info,36)
current_user.set_avatar(size128)
else:
errors.append("Avatar upload failed")
current_user.save()
flash('Your changes have been saved.')
return redirect(url_for('auth.edit_profile'))
elif request.method == 'GET':
form.username.data = current_user.username
form.about_me.data = current_user.about_me
return render_template('edit_profile.html', title='Edit Profile',
form=form)
在这里我们用到了两个工具函数,handle_upload(avatar,type)以及resize_avatar(info,size),我们把它定义在了工具函数中,app/utils.py:
from datetime import datetime
from random import randint
from hashlib import sha256,md5
from app import app
import os
from PIL import Image
def hash_name(name):
degest=md5(name.lower().encode('utf-8')).hexdigest()
return degest
def allowed_file(filename,type):
return '.' in filename and \
filename.rsplit('.', 1)[1] in app.config['ALLOWED_EXTENSIONS'][type]
def handle_upload(file,type):
''' type is the file type,for example:image.
more file type to be added in the future.'''
if file and allowed_file(file.filename,type):
old_filename = file.filename
file_suffix = old_filename.split('.')[-1]
new_filename = hash_name(old_filename) + '.' + file_suffix
print(new_filename)
try:
upload_path = os.path.join(app.config['UPLOAD_FOLDER'],type+'s/')
print(upload_path)
file.save(os.path.join(upload_path, new_filename))
except FileNotFoundError:
os.makedirs(upload_path)
file.save(os.path.join(upload_path, new_filename))
except Exception as e:
return False,e
# img = ImageStore(old_filename,new_filename)
# img.save()
return True,new_filename
return False,"File type disallowd!"
def resize_avatar(old_file,size):
upload_base = os.path.join(app.config['UPLOAD_FOLDER'],'image'+'s/')
with Image.open(os.path.join(upload_base, old_file)) as img:
image_width, image_height = img.size
thumbnail_width = size
thumbnail_height = size
if image_width <= thumbnail_width and image_height <= thumbnail_height:
return old_file
# generate thumbnail if the avatar is too large
if size==128:
new_filename = hash_name(old_file) + '.png'
else:
new_filename=hash_name(old_file)+str(size)+'.png'
try:
img.thumbnail((thumbnail_width, thumbnail_height), Image.ANTIALIAS)
img.save(os.path.join(upload_base, new_filename), "PNG")
except IOError:
print("Failed to create thumbnail from '" + old_file + "' to '" + new_filename + "'")
return old_file
return new_filename
return old_file
现在把程序运行起来看一看吧!