Trang hồ sơ cá nhân
Để tạo một trang hồ sơ cá nhân, trước tiên chúng ta hãy tạo một hàm hiển thị tại URL /user/.- app/routes.py: Hàm hiển thị hồ sơ cá nhân
@app.route('/user/<username>') @login_required def user(username): user = User.query.filter_by(username=username).first_or_404() posts = [ {'author': user, 'body': 'Bài viết #1'}, {'author': user, 'body': 'Bài viết #2'} ] return render_template('user.html', user=user, posts=posts)
Quá trình xây dựng hàm này tương đối đơn giản. Đầu tiên, chúng ta sẽ tải thông tin về user từ cơ sở dữ liệu qua truy vấn bằng username. Trước đây chúng ta đã thấy trường hợp truy vấn sử dụng hàm all() nếu chúng ta muốn lấy tất cả kết quả, hoặc first() nếu chúng ta chỉ muốn lấy kết quả đầu tiên hoặc None nếu không có kết quả nào. Trong hàm hiển thị này, chúng ta sẽ dùng một biến thể khác của first() gọi là first_or_404(). Hàm này sẽ hoạt động y hệt như first() nếu có kết quả tìm kiếm từ cơ sở dữ liệu, nhưng nếu không có kết quả, nó sẽ tự động gởi lỗi 404 về trình duyệt của user. Với cách dùng này, chúng ta không cần phải kiểm tra nếu kết quả trả về có thông tin về user hay không, vì nếu không có thông tin tương ứng với username đang tìm, hàm sẽ thoát với ngoại lệ 404 thay vì kết thúc.
Nếu truy vấn dữ liệu không tạo ra lỗi 404, điều đó có nghĩa là có một user tương ứng với username mà chúng ta đã đưa vào. Tiếp theo đó, chúng ta sẽ tạo ra một danh sách các bài viết giả, và cuối cùng hiển thị một template mới là user.html với các tham số là đối tượng user và danh sách các bài viết.
Template user.html có mã như sau:
- app/templates/user.html: template cho trang hồ sơ cá nhân
{% extends "base.html" %} {% block content %} <h1>User: {{ user.username }}</h1> <hr> {% for post in posts %} <p> {{ post.author.username }} : <b>{{ post.body }}</b> </p> {% endfor %} {% endblock %}
- app/templates/base.html: liên kết đến trang hồ sơ cá nhân
<div> Myblog: <a href="{{ url_for('index') }}">Home</a> {% if current_user.is_anonymous %} <a href="{{ url_for('login') }}">Login</a> {% else %} <a href="{{ url_for('user', username=current_user.username) }}">Profile</a> <a href="{{ url_for('logout') }}">Logout</a> {% endif %} </div>
Ảnh đại diện (avatar)
Chắc chắn rằng bạn đang cảm thấy trang hồ sơ cá nhân hiện tại thật tẻ nhạt. Để làm cho nó hấp dẫn hơn một chút, chúng ta sẽ thêm ảnh đại diện (avatar) vào trang. Nhưng thay vì phải tải lên các ảnh này vào máy chủ, chúng ta sẽ sử dụng dịch vụ Gravatar để cung cấp ảnh đại diện cho các user.Cách sử dụng dịch vụ Gravatar rất đơn giản. Để lấy ảnh đại diện của các user có đăng ký với Gravatar, chúng ta chỉ cần dùng URL https://www.gravatar.com/avatar/ với là mã hash của email của user theo chuẩn MD5. Sau đây là ví dụ để tìm URL của user có địa chỉ email là thai@example.com từ Gravatar:
>>> from hashlib import md5 >>> 'https://www.gravatar.com/avatar/' + md5(b'thai@example.com').hexdigest() 'https://www.gravatar.com/avatar/XXXXXXXXXXXXXXXXXXX'
https://www.gravatar.com/avatar/XXXXXXXXXXXXXXXXXXXXX=128
Một tham số đặc biệt khác chúng ta có thể sử dụng trong URL của Gravatar là d. Tham số này sẽ báo cho Gravatar biết sẽ trả về ảnh đại diện nào khi user không có đăng ký với dịch vụ Gravatar. Tùy theo giá trị của tham số này, Gravatar sẽ trả về các kết quả khác nhau. Chúng ta sẽ sử dụng một loại ảnh đại diện gọi là “identicon” trong trường hợp này như ví dụ dưới đây:Các identicon là các ảnh đối xứng và sẽ được trả về khi chúng ta sử dụng tham số d với giá trị “identicon”. Thật là đẹp phải không? Tuy nhiên, một số các các mở rộng (extension) của trình duyệt như là Ghostery sẽ chặn các ảnh đại diện từ Gravatar bởi vì Automattic (công ty mẹ của dịch vụ Gravatar) có thể phát hiện các địa chỉ Web mà bạn truy cập tùy theo yêu cầu Gravatar mà họ nhận được. Vì vậy nếu bạn không thấy ảnh đại diện Gravatar trong trình duyệt của bạn, vấn đề có thể là do một trong các extension mà bạn đã cài đặt trên trình duyệt.
Bởi vì các ảnh đại diện gắn liền với các user, chúng ta sẽ đặt mã tạo URL cho ảnh đại diện vào trong mô hình dữ liệu của user.
- app/models.py: URL cho ảnh đại diện của user
from hashlib import md5 ... class User(UserMixin, db.Model): ... def avatar(self, size): digest = md5(self.email.lower().encode('utf-8')).hexdigest() return 'https://www.gravatar.com/avatar/{}?d=identicon&s={}'.format( digest, size)
Nếu bạn muốn tìm hiểu về dịch vụ Gravatar, bạn có thể tham khảo các tài liệu tại Web site của Gravatar.
Tiếp theo, chúng ta cần đặt ảnh đại diện này vào template hồ sơ cá nhân:
- app/templates/user.html: ảnh đại diện của user trong template
{% 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 %} <p> {{ post.author.username }}: <b>{{ post.body }}</b> </p> {% endfor %} {% endblock %}
Đến đây chúng ta đã hiển thị ảnh đại diện lớn ở đầu trang hồ sơ cá nhân, nhưng chúng ta không ngừng lại ở đó. Chúng ta có một vài bài viết từ user này ở cuối trang và chúng ta cũng muốn đặt một ảnh đại diện nhỏ hơn ở mỗi bài viết. Trong trang hồ sơ cá nhân các ảnh đại diện đều giống nhau, nhưng chúng ta có thể xây dựng chức năng tương tự trong trang chủ để mỗi bài viết đều có ảnh đại diện của tác giả.
Để hiển thị ảnh đại diện trước mỗi bài viết chúng ta chỉ cần thay đổi một chút trong template user.html:
- app/templates/user.html: Hiển thị ảnh đại diện trước các bài viết.
{% 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 }} :<br>{{ post.body }}</td> </tr> </table> {% endfor %} {% endblock %}
Sử dụng template con (sub-template) trong Jinja2
Trang hồ sơ cá nhân được thết kế để hiển thị ảnh đại diện của user trước các bài viết của họ. Chúng ta cũng muốn làm giống như vậy trong trang chính. Chúng ta có thể sao chép phần template để hiển thị bài viết sang template của trang chính nhưng đó không phải là một ý hay bởi vì nếu chúng ta cần thay đổi cách sắp xếp trang, chúng ta phải nhớ để thay đổi cả hai template.Thay vào đó, chúng ta sẽ dùng template con (sub-template) để hiển thị một bài viết đơn, và sau đó chúng ta sẽ sử dụng template con này trong cả hai template user.html và index.html. Để bắt đầu, chúng ta sẽ tạo template con với các thẻ HTML cho một bài viết đơn. Chúng ta sẽ đặt tên template này là app/templates/_post.html. Ký tự _ ở đầu chỉ là quy ước để chúng ta nhớ template nào là template con.
- app/templates/_post.html: template con cho các bài viết đơn
<table> <tr valign="top"> <td><img src="{{ post.author.avatar(36) }}"></td> <td>{{ post.author.username }} says:<br>{{ post.body }}</td> </tr> </table>
- app/templates/user.html: Ảnh đại diện của user trong các bài viết
{% 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 %} {% include '_post.html' %} {% endfor %} {% endblock %}
Bổ túc cho trang hồ sơ cá nhân
Cho đến giờ, trang hồ sơ cá nhân của chúng ta tương đối tẻ nhạt và không có nhiều thông tin trên đó. Các user thường muốn viết về họ trên trang này, vì vậy chúng ta sẽ cho họ viết về mình và hiển thị ở đây. Chúng ta cũng sẽ lưu lại lần cuối cùng user truy cập ứng dụng và hiển thị trên trang hồ sơ cá nhân.Điều đầu tiên cần làm để thêm các thông tin này là thêm hai trường nữa vào bảng (table) Users trong cơ sở dữ liệu:
- app/models.py: các trường mới trong mô hình dữ liệu user
class User(UserMixin, db.Model): ... about_me = db.Column(db.String(140)) last_seen = db.Column(db.DateTime, default=datetime.utcnow)
(myenv) $ flask db migrate -m "new fields in user model" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added column 'user.about_me' INFO [alembic.autogenerate.compare] Detected added column 'user.last_seen' Generating /home/thaipt/Works/Flask/myblog/migrations/versions/18e92e3cb029_new_fields_in_user_model.py ... done
(myenv) $ flask db upgrade INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running upgrade f0db34c99253 -> 18e92e3cb029, new fields in user model
Bước tiếp theo là thêm các trường này vào template hồ sơ cá nhân:
- app/templates/user.html: Hiển thị thông tin về user trong trang hồ sơ cá nhân
{% extends "base.html" %} {% block 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 %} </td> </tr> </table> ... {% endblock %}
Lưu và hiển thị thời gian user truy cập lần cuối
Chúng ta cũng muốn hiển thị thời gian user truy cập ứng dụng lần cuối. Để làm điều này, mỗi lần trình duyệt của user gởi một yêu cầu đến máy chủ của ứng dụng (đồng nghĩa user đang truy cập ứng dụng) chúng ta sẽ cập nhật thời điểm máy chủ nhân được yêu cầu vào biến last_seen. Nhưng vấn đề là chúng ta sẽ thêm mã cập nhật này vào đâu?Điều đầu tiên chúng ta nghĩ đến là thêm mã này vào mỗi hàm hiển thị. Mỗi khi một hàm hiển thị được gọi, chúng ta sẽ gán thời gian hiện tại vào biến last_seen. Tuy nhiên, điều này không thực tế vì chúng ta sẽ phải sao chép đoạn mã này vào mỗi hàm hiển thị và điều này vi phạm một trong những nguyên tắc cơ bản của lập trình là không lặp lại mã. Thật may là trong các ứng dụng Web, tình huống cần phải thực thi một số lệnh nhất định trước khi gọi hàm hiển thị rất phổ biến. Vì vậy Flask có cung cấp sẵn một phương pháp rất tiện lợi để làm việc này như sau:
- app/routes.py: Lưu lại thời điểm truy cập lần cuối
from datetime import datetime @app.before_request def before_request(): if current_user.is_authenticated: current_user.last_seen = datetime.utcnow() db.session.commit()
Đến đây, nếu bạn chạy ứng dụng và vào trang hồ sơ cá nhân, bạn sẽ thấy dòng chữ “Last seen on” và thời điểm được in ra sẽ rất gần với thời điểm hiện tại. Và nếu bạn vào một trang khác và sau đó trở lại trang này, bạn sẽ thấy thời gian này sẽ liên tục được cập nhật.
Vì chúng ta lưu thời gian UTC trong cơ sở dữ liệu theo, thời gian hiển thị ở đây cũng là UTC. Và thêm nữa, định dạng của thời gian ở đây cũng không được như ý vì nó hiển thị theo cách trình bày của một đối tượng thời gian trong Python. Nhưng chúng ta cứ tạm chấp nhận như vậy và sẽ sửa sau.
Trình soạn thảo hồ sơ cá nhân
Chúng ta cũng cần cung cấp một form cho user để họ có thể soạn thảo và sửa đổi các thông tin trên trang hồ sơ cá nhân theo ý họ. Form này cho phép user sửa đổi họ tên và viết vài điều vê họ. Thông tin này sẽ được chứa trong biến about_me. Trước hết, hãy tạo một lớp cho form này:- app/forms.py: Form soạn thảo hồ sơ cá nhân
from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length ... class EditProfileForm(FlaskForm): username = StringField('Username', validators=[DataRequired()]) about_me = TextAreaField('About me', validators=[Length(min=0, max=140)]) submit = SubmitField('Submit')
Template cho form này như sau:
- app/templates/edit_profile.html: Template cho trình soạn thảo hồ sơ cá nhân
{% extends "base.html" %} {% block content %} <h1>Edit Profile</h1> <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.username.label }}<br> {{ form.username(size=32) }}<br> {% for error in form.username.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p> {{ form.about_me.label }}<br> {{ form.about_me(cols=50, rows=4) }}<br> {% for error in form.about_me.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% endblock %}
- app/routes.py: Hàm hiển thị cho trình soạn thảo hồ sơ cá nhân
from app.forms import EditProfileForm @app.route('/edit_profile', methods=['GET', 'POST']) @login_required def edit_profile(): form = EditProfileForm() if form.validate_on_submit(): current_user.username = form.username.data current_user.about_me = form.about_me.data db.session.commit() flash('Your changes have been saved.') return redirect(url_for('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)
- app/templates/user.html: Liên kết đến trang soạn thảo hồ sơ cá nhân
{% if user == current_user %} <p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p> {% endif %}