Chức năng tạo bài viết
Trước hết, chúng ta cần tạo một form đơn giản để các user có thể nhập vào các bài viết. Chúng ta sẽ tạo một form như sau:- app/forms.py: form đăng bài viết
class PostForm(FlaskForm): post = TextAreaField('Your post', validators=[ DataRequired(), Length(min=1, max=140)]) submit = SubmitField('Submit')
- app/templates/index.html: Tích hợp form đăng bài viết vào trang chủ
{% extends "base.html" %} {% block content %} <h1>Hi, {{ current_user.username }}</h1> <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.post.label }}<br> {{ form.post(cols=32, rows=4) }}<br> {% for error in form.post.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% for post in posts %} <p> {{ post.author.username }}: <b>{{ post.body }}</b> </p> {% endfor %} {% endblock %}
- app/routes.py: hàm hiển thị cho form để đăng bài viết
from app.forms import PostForm from app.models import Post @app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): form = PostForm() if form.validate_on_submit(): post = Post(body=form.post.data, author=current_user) db.session.add(post) db.session.commit() flash('Your post is now live!') return redirect(url_for('index')) posts = [ { 'author': {'username': 'cuong'}, 'body': 'Chúng ta đang học phần 9!' }, { 'author': {'username': 'phu'}, 'body': 'Flask đâu có khó!' } ] return render_template("index.html", title='Home Page', form=form, posts=posts)
- Thêm tham chiếu đến các lớp Post và PostForm
- Cho phép hàm hiển thị chấp nhận cả các yêu cầu POST thay vì chỉ có GET như trước đây để có thể dùng các dữ liệu về bài viết được user gởi đến trong form.
- Lưu các bài viết vào cơ sở dữ liệu.
- Thêm tham số form vào phương thức render_template() để nó có thể hiển thị trường nhập liệu văn bản.
Trong thực tế, đây là một cách xử lý rất phổ biến cho các yêu cầu POST. Lý do là để tránh các rắc rối với chức năng “refresh” (làm mới trang Web) trong các trình duyệt. Khi bạn bấm vào nút “Refresh” trên trình duyệt, nó sẽ phát lại yêu cầu cuối cùng đã được gởi tới máy chủ. Do đó, nếu yêu cầu cuối cùng là yêu cầu dạng POST và được máy chủ trả lời như bình thường, chức năng “Refresh” trên trình duyệt sẽ gởi lại yêu cầu POST đó tới máy chủ. Bởi vì điều này bất thường, cho nên trình duyệt sẽ hỏi user để xác nhận rằng đây là một yêu cầu trùng lặp, nhưng phần lớn các user sẽ không hiểu (hoặc không để ý ý nghĩa của câu hỏi này) và cho phép trình duyệt gởi đi và hậu quả là sẽ tạo ra các nội dung trùng lặp trong ứng dụng. Nhưng nếu máy chủ chuyển hướng (redirect) sau khi một yêu cầu POST được thực hiện thành công, trình duyệt phải gởi một yêu cầu GET đến trang đã được chỉ định trong lệnh chuyến hướng ngay sau đó. Vì vậy, trong trường hợp này, yêu cầu cuối cùng được thực hiện bởi trình duyệt là GET chứ không phải POST. Vì thế, nếu có refresh thì user sẽ không bị ảnh hưởng bởi yêu cầu POST trước đó.
Mẹo nhỏ này được gọi là mẫu thiết kế (pattern) Post/Redirect/Get. Nó giúp chúng ta tránh được tác dụng phụ khi có lệnh refresh sau khi trình duyệt vừa mới gởi đi một yêu cầu POST để gởi dữ liệu về máy chủ.
Hiển thị các bài viết
Bạn có nhớ là chúng ta tạo ra các bài viết giả và vẫn còn sử dụng chúng cho đến thời điểm này hay không? Chúng ta đã tạo ra các bài viết giả này dưới dạng một danh sách đơn giản trong mã nguồn của hàm hiển thị cho trang chủ:posts = [ { 'author': {'username': 'cuong'}, 'body': 'Chúng ta đang học phần 9!' }, { 'author': {'username': 'phu'}, 'body': 'Flask đâu có khó!' } ]
- app/routes.py: Hiển thị các bài viết thật trên trang chủ
@app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): ... posts = current_user.followed_posts().all() return render_template("index.html", title='Home Page', form=form, posts=posts)
Tính năng tìm kiếm user
Chắc chắn là bạn cũng thấy được rằng cho đến thời điểm này, các user sẽ gặp khó khăn khi muốn theo dõi các user khác vì ứng dụng không có chức năng hỗ trợ để trả về danh sách các user đã đăng ký. Vì vậy, chúng ta sẽ thực hiện một số thay đổi để tạo ra tính năng này.Chúng ta sẽ tạo ra một trang mới trong ứng dụng gọi là trang “Explore”. Trang này sẽ hoạt động tương tự như trang chủ nhưng nó sẽ hiển thị danh sách các bài viết từ tất cả các user trong hệt thống thay vì danh sách các bài viết của các user được theo dõi. Sau đây là hàm hiển thị cho chức năng này:
- app/routes.py: Hàm hiển thị cho trang Explore
{% extends "base.html" %} {% block content %} <h1>Hi, {{ current_user.username }}!</h1> {% if form %} <form action="" method="post"> ... </form> {% endif %} ... {% endblock %}
- app/templates/base.html: liên kết đến trang explore từ thanh định hướng
<a href="{{ url_for('index') }}">Home</a> <a href="{{ url_for('explore') }}">Explore</a>
- app/templates/_post.html: Hiển thị liên kết đến tác giả các bài viết
<table> <tr valign="top"> <td><img src="{{ post.author.avatar(36) }}"></td> <td> <a href="{{ url_for('user', username=post.author.username) }}"> {{ post.author.username }} </a> :<br>{{ post.body }} </td> </tr> </table>
- app/templates/index.html: sử dụng template _post.html
... {% for post in posts %} {% include '_post.html' %} {% endfor %} ...
Với các thay đổi nhỏ này, tính khả dụng của ứng dụng được nâng cao đáng kể. Các user có thể đến trang Explore để đọc các bài viết từ các user bất kỳ trong ứng dụng. Nếu cần theo dõi một user nào từ trang này, họ chỉ cần bấm vào username để mở trang hồ sơ cá nhân tương ứng và kích hoạt chức năng theo dõi từ đó.
Bạn có thể chạy ứng dụng và làm quen với các chức năng mới mà chúng ta vừa thêm vào:
Chức năng phân trang các bài viết
Ứng dụng của chúng ta đã tương đối tốt. Tuy vậy, việc hiển thị tất cả các bài viết được theo dõi trên trang chủ có thể sớm trở thành một vấn đề quan trọng. Điều gì sẽ xảy ra nếu một user có một nghìn hay một triệu bài viết trong danh sách theo dõi? Việc quản lý một danh sách lớn sẽ làm ứng dụng chạy chậm một cách đáng kể và không hiệu quả.Để giải quyết vấn đề này, chúng ta sẽ tiến hành phân trang cho các bài viết. Nói cách khác, chúng ta sẽ chỉ hiển thị một phần nhỏ các bài viết tại một thời điểm và cung cấp liên kết để truy cập toàn bộ các bài viết còn lại. Flask-SQLAlchemy hỗ trợ cho tác vụ này thông qua hàm truy vấn có sẵn là paginate(). Ví dụ như nếu chúng ta chỉ muốn lấy danh sách hai mươi bài viết đầu tiên của user, chúng ta sẽ thay thế hàm all() ở cuối truy vấn như sau:
>>> user.followed_posts().paginate(1, 20, False).items
- Số thứ tự của trang, bắt đầu từ 1
- Số đơn vị trên một trang
- Cờ hiệu lỗi (error flag). Nếu là True, hàm này sẽ trả về lỗi 404 nếu trang được yêu cầu không tồn tại (out of range). Nếu là False, nó sẽ trả về một danh sách rỗng nếu trang được yêu cầu không tồn tại.
Tiếp theo, chúng ta sẽ tìm cách để tạo chức năng phân trang trong hàm hiển thị index(). Đầu tiên, chúng ta cần thiết lập giá trị để cho biết có bao nhiêu phần tử trong mỗi trang:
- config.py: tham số để thiết lập cho số bài viết trong mỗi trang
class Config(object): ... POSTS_PER_PAGE = 3
Tiếp theo, chúng ta cần quyết định sử dụng số thứ tự của trang như thế nào trong URL của ứng dụng. Cách phổ biến là dùng các tham số trong địa chỉ (query string) để chỉ định số thứ tự trang với giá trị mặc định là 1 nếu không có nó. Sau đây là một số ví dụ về các URL theo định dạng mà chúng ta sẽ sử dụng:
- Trang 1, ngầm định: http://localhost:5000/index
- Trang 1, chỉ định: http://localhost:5000/index?page=1
- Trang 3: http://localhost:5000/index?page=3
Chúng ta sẽ cập nhật hàm hiển thị cho trang chủ và trang explore để sử dụng chức năng phân trang như sau:
- app/routes.py: tạo phân trang cho trang chủ và Explore
@app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): ... page = request.args.get('page', 1, type=int) posts = current_user.followed_posts().paginate( page, app.config['POSTS_PER_PAGE'], False) return render_template('index.html', title='Home', form=form, posts=posts.items) @app.route('/explore') @login_required def explore(): page = request.args.get('page', 1, type=int) posts = Post.query.order_by(Post.timestamp.desc()).paginate( page, app.config['POSTS_PER_PAGE'], False) return render_template("index.html", title='Explore', posts=posts.items)
Nếu để ý, bạn sẽ thấy các thay đổi của chúng ta không gây ra các hiệu ứng phụ và ảnh hưởng đến các phần khác ngoài ý muốn. Đó là vì chúng ta đã thiết kế và xây dựng mỗi thành phần tương đối độc lập với nhau và không dựa trên giả định các thành phần khác hoạt động như thế nào. Phương pháp này cho phép chúng ta tạo ra các thành phần và ứng dụng dễ mở rộng và kiểm tra, ít gây lỗi hoặc bị trục trặc trong quá trình vận hành.
Bạn hãy chạy thử ứng dụng và thử chức năng phân trang. Trước hết, hãy chắc rằng bạn tạo ra nhiều hơn ba bài viết. Điều này rất dễ kiểm tra vì trang Explore có hiển thị bài viết từ tất cả user. Nếu bạn vào trang này lần đầu tiên, bạn sẽ chỉ thấy ba bài viết mới nhất, nếu bạn muốn thấy ba bài viết tiếp theo, hãy nhập vào URL: http://localhost:5000/explore?page=2 trong thanh địa chỉ của trình duyệt.
Di chuyển giữa các trang
Thay đổi tiếp theo mà chúng ta sẽ làm là thêm các liên kết vào cuối danh sách các bài viết để user có thể di chuyển đến trang tiếp theo hoặc trang trước đó. Bạn có nhớ rằng giá trị trả về của hàm paginate() là một đối tượng thuộc lớp Pagination trong thư viện Flask-SQLAlchemy? Hiện giờ, chúng ta đang sử dụng thuộc tính items của đối tượng này, và như đã nói ở trên, nó chứa một danh sách các phần tử trong trang được chọn. Tuy vậy, đối tượng này còn có vài thuộc tính hữu ích khác mà chúng ta có thể sử dụng để tạo ra các liên kết cho việc phân trang:- has_next: có giá trị True nếu có ít nhất một trang phía sau trang hiện tại
- has_prev: có giá trị True nếu có ít nhất một trang phía trước trang hiện tại
- next_num: số thứ tự của trang tiếp theo
- prev_num: số thứ tự của trang trước đó
- app/routes.py: Liên kết đến các trang trước và sau
@app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): ... page = request.args.get('page', 1, type=int) posts = current_user.followed_posts().paginate( page, app.config['POSTS_PER_PAGE'], False) next_url = url_for('index', page=posts.next_num) \ if posts.has_next else None prev_url = url_for('index', page=posts.prev_num) \ if posts.has_prev else None return render_template('index.html', title='Home', form=form, posts=posts.items, next_url=next_url, prev_url=prev_url) @app.route('/explore') @login_required def explore(): page = request.args.get('page', 1, type=int) posts = Post.query.order_by(Post.timestamp.desc()).paginate( page, app.config['POSTS_PER_PAGE'], False) next_url = url_for('explore', page=posts.next_num) \ if posts.has_next else None prev_url = url_for('explore', page=posts.prev_num) \ if posts.has_prev else None return render_template("index.html", title='Explore', posts=posts.items, next_url=next_url, prev_url=prev_url)
Có một đáng lưu ý về hàm url_for() là nó cho phép bạn sử dụng các tham số bất kỳ dưới dạng các từ khóa (ví dụ như tham số page trong đoạn mã trên) và nếu các tham số này không được dùng trực tiếp trong URL, Flask sẽ thêm các tham số này vào phần query string trong URL (phần tham số phía sau dấu ? trong URL)
Sau khi các liên kết phân trang đã được khởi tạo với các giá trị thích hợp, chúng ta cần hiển thị chúng trong trang chủ ở phía dưới danh sách các bài viết:
- app/templates/index.html: Hiển thị các liên kết phân trang
... {% for post in posts %} {% include '_post.html' %} {% endfor %} {% if prev_url %} <a href="{{ prev_url }}">Newer posts</a> {% endif %} {% if next_url %} <a href="{{ next_url }}">Older posts</a> {% endif %} ...
Phân trang trong trang hồ sơ cá nhân
Chúng ta đã hoàn tất việc cập nhật trang chủ. Tuy nhiên, trang hồ sơ cá nhân cũng có một danh sách các bài viết của user đang đăng nhập. Vì vậy, chúng ta cũng cần thêm chức năng phân trang trong hồ sơ cá nhân để để bảo đảm tính nhất quán với trang chủ.Trước hết, chúng ta cần cập nhật trang hồ sơ cá nhân để sử dụng các bài viết thật thay vì dùng danh sách các bài viết giả mà chúng ta đã dùng trước đây:
- app/routes.py: Chức năng phân trang trong trang hồ sơ cá nhân
@app.route('/user/<username>') @login_required def user(username): user = User.query.filter_by(username=username).first_or_404() page = request.args.get('page', 1, type=int) posts = user.posts.order_by(Post.timestamp.desc()).paginate( page, app.config['POSTS_PER_PAGE'], False) next_url = url_for('user', username=user.username, page=posts.next_num) \ if posts.has_next else None prev_url = url_for('user', username=user.username, page=posts.prev_num) \ if posts.has_prev else None return render_template('user.html', user=user, posts=posts.items, next_url=next_url, prev_url=prev_url)
Đến đây, chúng ta có thể cập nhật template user.html như trong trang chủ:
- app/templates/user.html: Phân trang trong trang hồ sơ cá nhân
... {% for post in posts %} {% include '_post.html' %} {% endfor %} {% if prev_url %} <a href="{{ prev_url }}">Newer posts</a> {% endif %} {% if next_url %} <a href="{{ next_url }}">Older posts</a> {% endif %}
- config.py: Số bài viết trong một trang
class Config(object): ... POSTS_PER_PAGE = 25