Hướng dẫn lập trình Flask – Phần 6: Hồ sơ cá nhân và ảnh đại diện

flask_tutorial_1

Trong phần này, chúng ta sẽ tìm hiểu cách tạo trang hồ sơ cá nhân.

Để giúp cho bạn dễ theo dõi, sau đây là danh sách các bài viết trong loạt bài hướng dẫn này:

Bạn có thể truy cập mã nguồn cho phần này tại GitHub.

Phần này sẽ đi sâu vào cách tạo trang hồ sơ cá nhân cho ứng dụng. Trang hồ sơ cá nhân sẽ trình bày thông tin về user, chủ yếu là do chính user nhập vào. Chúng ta sẽ học cách để tạo ra các trang hồ sơ cho mỗi cá nhân tự động. Cuối cùng, chúng ta sẽ tạo ra một trình soạn thảo hồ sơ nhỏ để user có thể nhập vào các thông tin về họ.

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

Lần này, cách chúng ta sử dụng decorator @app.route hơi khác so với các lần trước. Ở đây chúng ta có sử dụng một thành phần thay đổi được đại diện bởi chuỗi trong URL và nằm giữa hai dấu ngoặc nhọn (<>). Khi một bộ định tuyến có thành phần thay đổi như trên, Flask sẽ chấp nhận bất kỳ chuỗi nào trong vị trí này của URL và sẽ gọi hàm hiển thị với giá trị của chuỗi như là tham số. Ví dụ như nếu browser của user yêu cầu URL /user/thai, hàm hiển thị sẽ được gọi với tham số username được gán giá trị ‘thai’. Hàm hiển thị này chỉ có thể được gọi bởi các user đã đăng nhập bởi vì chúng ta có sử dụng decorator @login_required từ Flask-Login.

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

Đến đây chúng ta đã hoàn tât trang hồ sơ cá nhân, nhưng chúng ta vẫn chưa tạo liên kết đến trang này trong ứng dụng. Để user có thể dễ dàng truy cập hồ sơ cá nhân của họ, chúng ta sẽ thêm một liên kết đến trang này vào thanh định hướng ở đầu trang:

app/templates/base.html: liên kết đến trang hồ sơ cá nhân

Để ý rằng cách chúng ta sử dụng hàm url_for() cho liên kết đến trang hồ sơ cá nhân đã thay đổi so với cách chúng ta hay dùng. Bởi vì hàm hiển thị cho trang hồ sơ cá nhân có một tham số động, hàm url_for() sẽ nhận vào một tham số thứ hai. Và do đây là một liên kết đến một user đã đăng nhập, chúng ta có thể sử dụng biến current_user của thư viện Flask-Login để tạo ra URL cần có.

Flask_Tutorial_Chapter6_ProfilePage

Bây giờ bạn hãy chạy thử chương trình. Khi bấm vào liên kết Profile ở đầu trang, bạn sẽ thấy trang hồ sơ cá nhân của mình. Hiện tại chúng ta còn chưa có liên kết đến trang hồ sơ cá nhân cho các user khác, nhưng nếu bạn muốn thử truy nhập các trang này, bạn có thể nhập trực tiếp vào thanh địa chỉ của trình duyệt. Ví dụ như nếu bạn biết rằng có một user tên là “nguyen” đã đăng ký với ứng dụng, bạn có thể vào trang hồ sơ cá nhân của user này bằng cách đánh địa chỉ http://localhost:5000/user/nguyen vào thanh địa chỉ của trình duyệt.

Ả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:

Nếu bạn muốn tìm một ảnh đại diện có thật, bạn có thể sử dụng URL của Gravatar của tôi là: https://www.gravatar.com/avatar/ade78574217d64f1b8013a276c7e0417 .Đây là kết quả khi bạn dùng URL trên:

Theo mặc định, kích thước của ảnh là 80x80 pixel. Nhưng bạn có thể yêu cầu các kích thước khác bằng cách sử dụng tham số s trong địa chỉ URL. Ví dụ như để lấy ảnh đại diện của tôi với kích thước 128x128 pixel, chúng ta dùng URL : https://www.gravatar.com/avatar/ade78574217d64f1b8013a276c7e0417?s=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

Phương thức avatar() trong lớp User sẽ trả về URL của ảnh đại diện user theo kích thước được yêu cầu (trong biến size). Nếu user không đăng ký với dịch vụ Gravatar và không có ảnh đại diện, một ảnh “identicon” sẽ được tạo ra. Để tạo ra mã hash MD5 từ địa chỉ email, chúng ta sẽ đổi các ký tự trong địa chỉ email thành chữ thường theo tiêu chuẩn của dịch vụ Gravatar. Sau đó, chúng ta cần chuyển các ký tự này thành dạng bytes trước khi truyền nó vào hàm hash vì thư viện MD5 trong Python chỉ hỗ trợ dữ liệu dạng byte.

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

Bằng cách đặt phương thức avatar() để sinh ra URL cho avatar trong lớp User, chúng ta có thể dễ dàng thay đổi mã của phương thức này để dùng các URL từ các dịch vụ khác. Khi đó, các template sẽ tự động hiển thị các ảnh đại diện khác.

Đế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.

Flask_Tutorial_Chapter6_ProfilePageWithAvatar

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

Để sử dụng template con này trong template user.html, chúng ta sẽ dùng biểu thức include của Jinja2:

app/templates/user.html: Ảnh đại diện của user trong các bài viết

Trang chính của ứng dụng cần thêm một vài thay đổi trước khi chúng ta cập nhật nó để sử dụng template con _post.html.

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

Mỗi lần chúng ta thay đổi cơ sở dữ liệu, chúng ta cần tạo ra mã chuyển đổi cơ sở dữ liệu. Trong Phần 4, chúng ta đã học cách để thiết lập ứng dụng theo dõi các thay đổi của cơ sở dữ liệu thông qua cơ sở dữ liệu. Vì lần này chúng ta thêm hai trường mới, việc đầu tiên cần làm là tạo ra mã kịch bản chuyển đổi:

Khi chạy, lệnh migrate phát hiện ra có hai trường mới trong lớp User và tạo ra mã kịch bản chuyển đổi cho cơ sở dữ liệu. Chúng ta cần thi hành mã này với cơ sở dữ liệu:

Mã chuyển đổi sẽ thay đổi cấu trúc dữ liệu mà không làm ảnh hưởng đến các dữ liệu sẵn có trong đó. Sau khi mã này được thi hành, các thông tin về user hiện có trong cơ sở dữ liệu sẽ không bị mất đi.

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

Để ý rằng hai trường này được đặt bên trong các lệnh điều kiện của Jinja2. Lý do là vì chúng ta chỉ muốn hiển thị các trường này khi chúng có giá trị kèm theo. Tại thời điểm này, các trường này chưa được gán giá trị nào, vì vậy chúng ta sẽ tạm thời không thấy chúng.

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

Trong đoạn mã trên, decorator @before_request sẽ báo cho Flask biết cần phải thự hiện hàm này này trước khi gọi hàm hiển thị. Nhờ phương pháp này, chúng ta có thể chạy các đoạn mã mà chúng ta muốn thực hiện trước khi các hàm hiển thị được thi hành và đặt chúng vào cùng một nơi. Trong trường hợp này, chúng ta sẽ kiểm tra là user hiện tại (current_user) có đăng nhập hay không, và nếu có thì gán thời điểm hiện tại vào biến last_seen. Trước đây chúng ta đã nói qua là các ứng dụng từ máy chủ phải bảo đảm tính nhất quán về thời gian, đó là lý do tại sao chúng ta sử dụng giờ UTC. Dùng giờ địa phương không phải là giải pháp hay vì nó sẽ làm cho giá trị thời gian mà chúng ta lưu trữ trong cơ sở dữ liệu phụ thuộc vào vị trí của người sử dụng. Và bước cuối cùng là gọi hàm commit() để lưu giá trị này vào cơ sở dữ liệu. Nếu bạn thắc mắc là tại sao đoạn mã trên không gọi hàm db.session.add() trước lệnh commit, hãy nhớ lại rằng khi bạn dùng biến current_user, Flask-Login sẽ gọi hàm tải dữ liệu người dùng (hàm load_user() trong app/models.py mà chúng ta đã viết trong Phần 5), và hàm này sẽ chạy một truy vấn để thêm mô hình dữ liệu user tương ứng vào phiên làm việc hiện tại của database. Vì thế, bạn vẫn có thể gọi hàm db.session.add() lần nữa ở đây, nhưng điều đó không cần thiết.

Đế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.

Flask_Tutorial_Chapter6_ProfilePageWithLastSeenTime

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

Trong form này, chúng ta sử dụng một trường kiểu mới là TextAreaField cho trường “About”. TextArea là một trường cho phép nhập nhiều dòng văn bản (thay vì một dòng duy nhất như trong trường kiểu Text). Và để kiểm tra giá trị của trường này, chúng ta cũng dùng một biến kiểm tra dữ liệu mới là Length. Nó chỉ cho phép các dòng văn bản dài tối đa 140 ký tự - vừa đúng với kích thước quy định của trường này trong cơ sở dữ liệu.

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

Và cuối cùng là hàm hiển thị để kết nối template và form:

app/routes.py: Hàm hiển thị cho trình soạn thảo hồ sơ cá nhân

Hàm hiển thị này hơi khác với các hàm hiển thị mà chúng ta đã tạo ra để xử lý form. Nếu giá trị của validate_on_submit() là True, chúng ta sẽ sao chép các dữ liệu từ form vào đối tượng user và ghi vào cơ sở dữ liệu. Nhưng khi validate_on_submit() False, có thể có hai nguyên nhân khác nhau. Nguyên nhân thứ nhất có thể là do trình duyệt gởi một yêu cầu dạng GET đến máy chủ thay vì POST, và trong trường hợp này, nó có ý nghĩa là user chỉ truy cập vào trang này mà chưa sẵn sàng để submit form, vì vậy chúng ta cần hiển thị form cho họ chứ không phải xử lý dữ liệu nhập. Nguyên nhân thứ hai là form được gởi đi với yêu cầu dạng POST nhưng chứa dữ liệu không hợp lệ. Chúng ta phải xử lý hai trường hợp này khác nhau. Đối với yêu cầu dạng GET, chúng ta sẽ gởi lại form để trình duyệt hiển thị và hiển thị các thông tin về user trên các trường nhập liệu (các thông tin này được lấy ra từ biến current_user và có các thông tin về user từ cơ sở dữ liệu). Vì vậy chúng ta sẽ phải tiến hành quá trình đảo ngược so với quá trình submit form: sao chép các thông tin từ biến current_user vào các trường tương ứng trên form. Trong trường hợp chúng ta nhận được yêu cầu dạng POST nhưng với dữ liệu không hợp lệ, chúng ta không cần viết mã để in ra thông báo lỗi vì các thông báo lỗi này sẽ được WTForm xử lý và in ra. Để phân biệt hai trường hợp có thể xảy ra trong điều kiện validate_on_submission()False, chúng ta sẽ kiểm tra giá trị của biến request.method, biến này sẽ có giá trị GET khi user vào trang này và POST nếu user submit form với dữ liệu không hợp lệ.

Flask_Tutorial_Chapter6_Editing_User_Profile

Để dễ truy cập trình soạn thảo này, chúng ta sẽ thêm liên kết đến trang này vào trang hồ sơ cá nhân:

app/templates/user.html: Liên kết đến trang soạn thảo hồ sơ cá nhân

Để ý đến điều kiện được đặt trong template: nó sẽ bảo đảm rằng liên kết này chỉ xuất hiện khi bạn đang ở trong trang hồ sơ cá nhân của chính bạn. Nếu bạn đang truy cập trang hồ sơ cá nhân của user khác, bạn sẽ không thấy liên kết này.

Flask_Tutorial_Chapter6_Final_Profile_Page

Vì phần này đã khá dài, chúng ta sẽ kết thúc ở đây. Hẹn gặp bạn trong phần tiếp theo.

Leave a Reply

Your email address will not be published. Required fields are marked *