In this chapter, we will implement a feature of our demonstration application which allows users to read and write blog posts. This chapter will cover the following topics:
In Fig. 1, we present an implementation of a new database model to support blog posts. A blog post is represented by a body, timestamp and a one-to-many relationship from the
We modify the main homepage of our application to allow users to immediately write a blog post. This form just contains a text area for blog posts and a submit button to save the post to the database. Fig. 2 presents the implementation of a blog post form.
In Fig. 3, the view function passes both the form and the complete list of blog posts ordered by timestamp (in descending order) to the template. Besides, we not only validate the submitted form's data but also verify whether the current user has the WRITE permission. You should note that instead of assigning
current_user to the author attribute, we use the expression
current_user._get_current_object() to obtain a real object, since
current_user just returns a thin wrapper that contains the actual user object inside. As we have modified the view function, we also need to update the corresponding HTML template, as shown in Fig. 4.
current_user.can(...) expression, in Fig. 4, is used to skip the blog posts for users who don't have the
WRITE permission in their role. We place all blog posts in an HTML unordered list with a small avatar of the author on the left side and an embedded link to the user's profile page.
You should note that to properly render the blog posts on the homepage, you need to check the following prerequisites:
Fig. 6 shows the result of the new index page of the application, it now contains the text field to compose a new blog post (for users with
WRITE permission) and a list of blog posts.
After finishing the previous chapter, the User Profile page has been implemented to show the user profile image obtained from Gravatar, along with the basic information of the user such as name, location and bio. In this chapter, we will enhance the User Profile pages by showing the blog posts authored by the users. To implement this functionality, we will perform the following tasks:
user(...)function in app/main/views.py to query and return a list of blog posts authored by the user, as shown in Fig. 7.
We move the HTML code to render a list of blog posts to a new file named app/templates/_posts.html, as shown in Fig. 8. By doing so, we can reuse the HTML code in multiple templates, and broadcast the changes to those ones. In Fig. 9, we present the change to the app/templates/index.html and app/templates/user.html files to include the new _posts.html file.
When there are many blog posts stored in the database, the process of querying and rendering those ones is time-consuming and impractical. In this section, we will present how to paginate the data and render it in chunks. This section will cover the following tasks:
Instead of manually inserting data to the database for demonstration, we can take advantage of the Faker extension which can be used to generate fake information. To install Faker, you can execute the following command in your current Python [virtual] environment:
$ pip install faker
To generate fake data, we implement a small program named app/fake.py, as shown in Fig. 10, consisting of two functions to generate fake user accounts and blog posts, respectively. Although Faker totally generates data such as email addresses and usernames in random fashion, we need to make sure that they must be unique in the database. If we commit duplicate records to the database, it will raise an
IntegrityError exception; therefore, we handle this error by rolling back the session to cancel that duplicate user account.
And then, you need to execute the following commands in the flask shell environment to generate and insert the fake data to the database.
>>> from app import fake
In Fig. 11, we show the changes to the main route in app/main/views.py to support the pagination. The page number can be obtained from the
request.args. If a specific page number is not given, the default value which is the first page is used. To load a single page that contains a range of blog posts instead of all of them, we replace the
all() expression with Flask-SQLAlchemy's
paginate() . We make the value of
per_page configurable by presenting it as an environment variable, named
FLASKY_POSTS_PER_PAGE, which is defined in config.py. You should modify the config.py to include the definition of the
FLASKY_POSTS_PER_PAGE=<number> environment variable to your desired number or leave it as the default one which is
20. The URL for the homepage will now have ?page=<integer_number> query string, for example, ?page=2, to indicate the current page number.
The HTML template that shows a list of blog posts will be displayed with a pagination widget at the bottom of the list. We first define the pagination template macro, as shown in Fig. 12. And then, we embed this macro to the pages that we want to display a list of paginated blog posts, for example, Fig. 13 shows the update to the appl/templates/index.html template.
In this section, we will present how to attach Markdown syntax to the application to allow users to compose blog posts in rich-text format, instead of using the current plain-text. In general, the procedure to include Markdown syntax to the application consists of two main steps; first, we need to migrate the current WTForms's
TextAreaField object to the corresponding Markdown one; and second, we need to render the composed Markdown contents to the HTML page.
Before beginning implementation, we need to install a few new packages, as follows:
$ pip install flask-pagedown markdown bleach
The Flask-PageDown provides a
PageDownField class that extends Flask-WTF with a specialized text area field that renders an HTML preview of the Markdown text on the fly as you type. The goal of Markdown package is to maintain a Python library (with an optional CLI wrapper) suited to use in web server environments (never raise an exception, never write to stdout, etc.) as an implementation of the markdown parser that follows the syntax rules and the behavior of the original Markdown implementation as reasonably as possible. And Bleach is an HTML sanitizer implemented in Python.
After installing the requirement packages, we can now modify the application to upgrade the textbox from plain-text format to rich-text one based on Markdown syntax. First, we need to initialize the Flask-Markdown extension at the application-level, as shown in Fig. 14.
Next, we change the WTForms's
TextAreaField in app/main/forms.py to the Markdown text box -
PageDownField, as shown in Fig. 15. The Markdown preview can be generated in real time when users are entering their contents by including Flask-PageDown template macro, as shown in Fig. 16.
Fig. 17 shows the result of the new home page that contains the new Markdown-based text box and the preview area.
When users create blog posts with Markdown syntax, there will be a preview in HTML format under the text box. And when users submit the blog posts, only the Markdown source texts are submitted to the server, and the preview HTML texts are ignored. By doing so, we can avoid any risks that users construct HTML contents that do not match Markdown syntax and submit them to the server.
When the server receives the Markdown source texts, it will convert them to HTML format and store them in the database. This process can save a lot of time on the client side, as posts will not have to be converted every time they are rendered on the web page. Fig. 18 shows the updates to the
Post model to fulfill our strategy.
on_changed_body() method is registered as a listener, so it will be invoked whenever there is a change on the body attribute. The value parameter which is the Markdown source text will be converted to HTML format along with the list of approved HTML tags. The
linkify() function converts all URL from plain text to proper HTML
In Fig. 19, we show the changes to the _posts.html macro to include either the plain text format of blog posts or the HTML one. In particular, we replace
post.body_html in the template when available.
By default, Jinja2 escapes all template variables including HTML tags as a security measure. The HTML tags in this case are generated from server-side, they are thus
safe to render directly on the web page.
In this section, we will assign each blog post with a unique URL that allows users to be able to share a specific blog post. We first define a new route and view function to support permanent links for posts, as shown in Fig. 20.
Second, we update the _posts.html macro to include permanent links for blog posts. The implementation of the change is shown in Fig. 21. Note that we also update the style.css to include some new HTML classes.
Then, we define a HTML template for rendering a specific blog post given post's id, the implementation is as shown in below figure.
The last feature that is related to blog post functionality is post editor which allows users to edit their submitted post. This functionality has two options; the first one is to allow the post's authors to edit the content by themselves, and the second one is to allow the administrator to edit all of the blog posts. As usual, this functionality consists of a form and its corresponding view functions, along with a HTML template.
In Fig. 23, we show the implementation of the corresponding view function blog editor form. In this case, we do not need to implement the new form class, we just reuse the one (
PostForm class) that was implemented to render a Markdown text box in the home page.
In Fig. 24, we implement the corresponding HTML template for the blog post editor view function.
Then, we include the link to the blog post editor for each blog post on the home page, as shown in Fig. 25.
Fig. 26 shows the new version of the home page that now includes the links to the blog post editor.
posts = user.posts.order_by(Post.timestamp.desc()).all()expression.
In the next lessons, we will first present how to implement a follower feature in the demo application. This feature allows our users to connect with other users which is often called relationships such as followers, friends, contacts, connections. In the second topic, we will present the User Comments functionality to allow users to leave comments on blog posts.