Lesson 5B

A Social Blogging Application — Part 4: Blog Posts

Share Tweet

Contents

  1. Resources
  2. Part 4: Blog Posts
    1. Updating the database
    2. Show Blog Posts on User Profile Pages
    3. Pagination
    4. Rich-text format with Markdown syntax
    5. Permanent links to blog posts
    6. Blog post editor
  3. Exercises
  4. Further Reading

Resources


Part 4: Blog Posts

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:

  • Modifying database structure to support blog posts;
  • Implementing functions to support reading and writing blog posts;
  • Paginating the data and rendering it in chunks;
  • Markdown syntax and rich-text preview of blog posts;
  • Managing permanent links to blog posts;
  • Editing blog posts.

1/ Updating the database

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 User model.

Fig 1. app/models.py: Insert new data model to support blog posts
Fig 1. app/models.py: Insert new data model to support blog posts

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.

Fig 2. app/main/forms.py: implementing blog post form
Fig 2. app/main/forms.py: implementing 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.

Fig 3. app/main/views.py: rendering blog post form and handling form data
Fig 3. app/main/views.py: rendering blog post form and handling form data

The 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.

Fig 4. app/templates: modifying the index.html template
Fig 4. app/templates: modifying the index.html template

You should note that to properly render the blog posts on the homepage, you need to check the following prerequisites:

  • Import the style.css to the project directory;
  • Update the link to the style.css in the base.html file, as shown in Fig. 5;
  • Implement the Gravatar API as mentioned in the previous lesson or use a static image for all authors, as shown in Fig. 4.
Fig 5. app/templates: modifying the base.html template
Fig 5. app/templates: modifying the base.html template

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.

Fig 6. The new home page with a text box and a list of blog posts
Fig 6. The new home page with a text box and a list of blog posts

2/ Show Blog Posts on User Profile Pages

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:

  • Update the 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.
  • Since both the home page and User Profile pages also do render the list of blog posts, we move the code of rendering the list to one place and then include it into corresponding templates.

Fig 7. app/main/views.py: Update user(...) function to inclulde a list of blog posts
Fig 7. app/main/views.py: Update user(...) function to inclulde a list of blog posts

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.

Fig 8. app/templates/_posts.html: A macro to render a list of blog posts
Fig 8. app/templates/_posts.html: A macro to render a list of blog posts
Fig 9. app/templates/index.html & app/templates/user.html: Include the defined macro to HTML templates
Fig 9. index.html, user.html: Include the defined macro to HTML templates

3/ Pagination

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:

  • Generating fake data to demonstrate the pagination of blog posts;
  • Rendering data in pages in back-end;
  • Showing paginated data on front-end.

3.1/ Generating fake data

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.

Fig 10. app/fake.py: Implement functions to generate fake user accounts and blog posts
Fig 10. app/fake.py: Implement functions to generate fake user accounts and blog posts

And then, you need to execute the following commands in the flask shell environment to generate and insert the fake data to the database.

  • flask shell
  • >>> from app import fake
  • >>> fake.users()
  • >>> fake.posts()
  • >>> quit()

3.2/ Rendering blog posts in pages

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.

Fig 11. app/main/views.py: Implement pagination mechanism
Fig 11. app/main/views.py: Implement pagination mechanism
3.3/ Showing the pagination widget

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.

Fig 12. app/templates/_macros.html: Implement the pagination widget macro
Fig 12. app/templates/_macros.html: Implement the pagination widget macro
Fig 13. app/templates/index.html: Include the pagination widget to the home page
Fig 13. app/templates/index.html: Include the pagination widget to the home page

4/ Rich-text format with Markdown syntax

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.

4.1/ Composing blog posts in Markdown syntax

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.

Fig 14. app/__init__.py: Initialize the Flask-Markdown extension
Fig 14. app/__init__.py: Initialize the Flask-Markdown extension

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 15. app/main/forms.py: Changing TextAreaField to PageDownField
Fig 15. app/main/forms.py: Changing TextAreaField to PageDownField
Fig 16. Declare the Markdown preview area
Fig 16. Declare the Markdown preview area

Fig. 17 shows the result of the new home page that contains the new Markdown-based text box and the preview area.

Fig 17. Markdown text box and the preview area
Fig 17. Markdown text box and the preview area
4.2/ Storing Markdown contents in database

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.

Fig 18. app/models.py: Update the Posts model
Fig 18. app/models.py: Update the Posts model

The 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 <a> tag.

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 with post.body_html in the template when available.

Fig 19. app/templates/_posts.html: Update _posts.html to render Markdown contents
Fig 19. app/templates/_posts.html: Update _posts.html to render Markdown contents

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.

Fig 20. app/main/views.py: Define a new route and view function to support permanent links for posts
Fig 20. app/main/views.py: Define a new route and view function to support permanent links for posts

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.

Fig 21. app/templates/_posts.html: Include permanent links for blog posts
Fig 21. app/templates/_posts.html: Include permanent links for blog posts

Then, we define a HTML template for rendering a specific blog post given post's id, the implementation is as shown in below figure.

Fig 22. app/templates/post.html: Define new HTML template to render a specific blog post
Fig 22. app/templates/post.html: Define new HTML template to render a specific blog post

6/ Blog post editor

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.

Fig 23. app/main/views.py: Implement the view function of blog editor
Fig 23. app/main/views.py: Implement the view function of blog editor

In Fig. 24, we implement the corresponding HTML template for the blog post editor view function.

Fig 24. app/templates/edit_post.html: HTML template to render blog post editor form
Fig 24. app/templates/edit_post.html: HTML template to render blog post editor form

Then, we include the link to the blog post editor for each blog post on the home page, as shown in Fig. 25.

Fig 25. app/templates/_post.html: Include the links to blog post editor for author and administrator
Fig 25. app/templates/_post.html: Include the links to blog post editor for author and administrator

Fig. 26 shows the new version of the home page that now includes the links to the blog post editor.

Fig 26. The new version of the application's homepage
Fig 26. The new version of the application's homepage

Exercises

  1. Taking advantage of the procedure to get a list of blog posts as shown in Fig. 3, the user profile page should show a list of blog posts authored by the user. However, instead of retrieving all blog posts from the database as implemented in Fig. 3, we just need to query blog posts of the current user by using the posts = user.posts.order_by(Post.timestamp.desc()).all() expression.
  2. When the number of posts increases, it will make the process of retrieving and rendering blog posts on homepage and user profile page become slow and impractical. In this exercise, you should refer to the textbook to implement the pagination mechanism to organize blog posts to pages.
  3. Instead of entering text in plain-text format, in this exercise, you should upgrade your application to support the Markdown syntax and present a rich-text preview of the post.
  4. In order to allow users to be able share links to specific blog posts, in this exercise, each post will be assigned with a unique URL. Permanent links can be assigned by unique IDs (integer numbers) or a slug which is a unique string that is based on the title or first few words of the post.

Further Reading

  1. WYSIWYG (What You See Is What You Get)

Next Lesson

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.

Lesson 6: A Social Blogging Application — Part 5: Followers


Relaxing 🎧