Django for Beginners #4

You can download the source code for this tutorial here.

Finally, it is time for us to create our blog application using Django. In our sample homepage, we created an admin system on our own, where the user could perform CRUD operations on the Home model. However, creating an entire admin system might be too challenging for beginners. So, in this article, we are going to use the built-in Django admin system instead.

Designing database structure

Let’s start by designing our database structure. For a simple blogging system, we need at least 4 database tables: UsersCategoriesTags, and Posts. If you want other functions for your blog, comments, for example, you can add other tables yourself. To keep this tutorial short and easy to understand, these four are all we need.

Users Table

key type info
id integer auto increment
name string cannot be empty
email string unique, cannot be empty
password string

The users table is already included in Django and we don’t need to do anything about it. In fact, Django comes with full user authentication and permission system, however, it is a little confusing for beginners, so I’m not going to talk about it here. But I will try to create another tutorial on this subject in the future.

Categories Table

key type info
name string cannot be empty
slug string unique, cannot be empty
description text can be empty

Tags Table

key type info
name string cannot be empty
slug string unique, cannot be empty
description text can be empty

Posts Table

key type info
title string cannot be empty
slug string unique, cannot be empty
featured_image string or text can be empty
content text cannot be empty
is_published boolean

And of course, we need another table that stores the basic information of our entire website, such as name, description and logo. Since we’ve discussed this topic in detail in the previous article, I will not repeat it here.

Designing relationships between database tables

For our blog website project. There are six relationships we need to take care of.

  • Each user has multiple posts
  • Each category has many posts
  • Each tag belongs to many posts
  • Each post belongs to one user
  • Each post belongs to one category
  • Each post belongs to many tags

Creating the corresponding models

Now, it’s time for us to implement our design. First, we need a Site model.

1
2
3
4
5
6
7
class Site(models.Model):
    name = models.CharField(max_length=200)
    description = models.TextField()
    logo = models.ImageField(upload_to='logo/')

    def __str__(self):
        return self.name

The category model

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Category(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField()
    description = models.TextField()

    class Meta:
        verbose_name_plural = "categories"

    def __str__(self):
        return self.name 

The first four lines of code should be easy to understand. What I want to talk about is the Meta class inside our Category model. This is how we add metadata to our models.

Recall that model metadata is “anything that’s not a field”, such as ordering options, database table name etc. In this case, we use verbose_name_plural to define the plural form of the word category. Unfortunately, Django is not as “smart” as Laravel in this particular aspect, if we do not give Django the correct plural form, it will use categorys instead.

And the __str__(self) function defines what field Django is going to use when refering to a particular category, in our case, we are using the name field. It will become clear why this is necessary when we get to Django Admin.

Tag Model

1
2
3
4
5
6
7
class Tag(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField()
    description = models.TextField()

    def __str__(self):
        return self.name 

This model should be quite easy to understand for you.

Post Model

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from ckeditor.fields import RichTextField

class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField()
    content = RichTextField()
    featured_image = models.ImageField(upload_to='images/')
    is_published = models.BooleanField(default=False)
    is_featured = models.BooleanField(default=False)
    created_at = models.DateField(auto_now=True)

    def __str__(self):
        return self.title 

Line 1, if you just copy and pasted this code, your editor will tell you that it cannot find the RichTextField and ckeditor. That is because it is not included in the Django framework.

The rich text editor or WYSIWYG HTML editor allows you to edit HTML pages directly without writing the code. In this tutorial, I am using the CKEditor as an example.

CKEditor

To install CKEditor, run the following command:

1
pip install django-ckeditor

After that, register ckeditor in settings.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
    'ckeditor',
] 

Setting up relationships

Now, we add relationships to our models. We only need to add three lines of code in the Post model, which is a lot easier compared to Laravel.

1
2
3
category = models.ForeignKey(Category, on_delete=models.CASCADE)
tag = models.ManyToManyField(Tag)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

And since we are using the built-in User model (settings.AUTH_USER_MODEL), remember to import the settings module.

1
from django.conf import settings

Last but not least, we need to generate the migration files and apply them to our database.

1
2
python manage.py makemigrations
python manage.py migrate

Setting up Django admin panel

Our next step would be to set up the admin panel. Django comes with a built-in admin system, to use it, all we need to do is just register a superuser. Run the following command:

1
python manage.py createsuperuser

django create superuser

And then, you can access the admin panel by going to http://127.0.0.1:8000/admin/.

django admin panel login

Django admin panel

Right now, the admin panel is still empty, there is only an authentication tab that you can use to assign different roles to different users. This is a rather complicated topic that requires another tutorial series, so we are not going to cover that right now. Our focus would be on how to connect our blog app to the admin system.

Inside our blog app, you should find a file called admin.py. Add the following code to it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.contrib import admin

# Register your models here.
from .models import Site, Category, Tag, Post


class CategoryAdmin(admin.ModelAdmin):
    prepopulated_fields = {"slug": ("name",)}


class TagAdmin(admin.ModelAdmin):
    prepopulated_fields = {"slug": ("name",)}


class PostAdmin(admin.ModelAdmin):
    prepopulated_fields = {"slug": ("title",)}


admin.site.register(Site)
admin.site.register(Category, CategoryAdmin)
admin.site.register(Tag, TagAdmin)
admin.site.register(Post, PostAdmin)

On line 4, we imported the models that we just created. And then, we simply register the imported model using admin.site.register(). However, notice that when we register the Category model, there is something extra called CategoryAdmin, which is a class that we defined on line 7. This is how we can pass some extra information to the Django admin system.

Here we use prepopulated_fields to generate slugs for all categories, tags and posts. The value of the slug will be depended on the name. Let’s test it by creating a new category.

Go to http://127.0.0.1:8000/admin/. Click on “Categories”, and add a new one. Remember we defined the plural form of Category in our model? This is why it is necessary, if we don’t do that, Django will simply use Categorys.

django admin homepage

category page

Notice that the slug will be automatically generated as you type in the name. Try adding some dummy data, everything should work smoothly.

Optional configurations

However, our work is not done completely. Open the category panel, you will notice that we can access categories from the post page but there is no way to access corresponding posts from the category page. If you don’t think that’s necessary, you can jump to the next article. If you want to solve this problem, we need to use InlineModelAdmin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class PostInlineCategory(admin.StackedInline):
    model = Post
    max_num = 2


class CategoryAdmin(admin.ModelAdmin):
    prepopulated_fields = {"slug": ("name",)}
    inlines = [
        PostInlineCategory
    ] 

We first create a PostInlineCategory, and then use it in the CategoryAdminmax_num = 2 means only two posts will be shown on the category page. This is how it looks:

image-17

Next, we do something similar to the TagAdmin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class PostInlineTag(admin.TabularInline):
    model = Post.tag.through
    max_num = 5


class TagAdmin(admin.ModelAdmin):
    prepopulated_fields = {"slug": ("name",)}
    inlines = [
        PostInlineTag
    ] 

The code is very similar, but notice the model is not just Post, it is Post.tag.through. This is because the relation between Post and Tag is a many-to-many relationship. This is the final result.

image-18

In the previous article, we mainly focused on the backend and admin section of our Django application. Now, it is time for us to focus on the frontend, the part that the users can see. For the templates, instead of writing our own HTML and CSS code, we are going to use a Bootstrap template instead, since HTML and CSS are not really our focus here. You can download the source code directly:

Blog Home

Blog Post

Creating views and setting up URLs

For our blog application, since we have the admin panel setup, we don’t need to build the full CRUD operations on our own, we only need to worry about how to retrieve information from the database. We are designing four pages, home, category, tag and post, and we’ll need one view function for each of them.

Homepage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from .models import Site, Category, Tag, Post

def home(request):
    site = Site.objects.first()
    posts = Post.objects.all().filter(is_published=True)
    categories = Category.objects.all()
    tags = Tag.objects.all()

    return render(request, 'home.html', {
        'site': site,
        'posts': posts,
        'categories': categories,
        'tags': tags,
    })

Line 1, here we import the models we created in the previous article.

Line 4, site contains the basic information of our website, and we are always retrieving the first record in the database.

Line 5, filter(is_published=True) makes sure that only published articles will be displayed.

Next, don’t forget the corresponding URL dispatcher.

1
path('', views.home, name='home'),

Category

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def category(request, slug):
    site = Site.objects.first()
    posts = Post.objects.filter(category__slug=slug).filter(is_published=True)
    requested_category = Category.objects.get(slug=slug)
    categories = Category.objects.all()
    tags = Tag.objects.all()

    return render(request, 'category.html', {
        'site': site,
        'posts': posts,
        'category': requested_category,
        'categories': categories,
        'tags': tags,
    })
1
path('category/<slug:slug>', views.category, name='category'),

Here we passed an extra variable, slug, from the URL to the view function, and on lines 3 and 4, we used that variable to find the correct category and posts.

Tag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

def tag(request, slug):
    site = Site.objects.first()
    posts = Post.objects.filter(tag__slug=slug).filter(is_published=True)
    requested_tag = Tag.objects.get(slug=slug)
    categories = Category.objects.all()
    tags = Tag.objects.all()

    return render(request, 'tag.html', {
        'site': site,
        'posts': posts,
        'tag': requested_tag,
        'categories': categories,
        'tags': tags,
    })
1
path('tag/<slug:slug>', views.tag, name='tag'),

Post

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def post(request, slug):
    site = Site.objects.first()
    requested_post = Post.objects.get(slug=slug)
    categories = Category.objects.all()
    tags = Tag.objects.all()

    return render(request, 'post.html', {
        'site': site,
        'post': requested_post,
        'categories': categories,
        'tags': tags,
    })
1
path('post/<slug:slug>', views.post, name='post'),

Building the template system

As for the template system, this is the structure I’m going with.

template system

The layout.html contains the header and the footer, and it is where we import the CSS and JavaScript files. The homecategorytag and post are the templates that our view functions point to, and they all extends to the layout.And finally, inside the vendor directory are the components that will appear multiple times in different templates, and we can import them with the include tag.

Layout

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<!DOCTYPE html>
<html lang="en">

<head>
  {% block meta %}{% endblock meta %}

  <!-- Core Bootstrap -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" />

  {% load static %}
  <!-- Custom Styles -->
  <link href="{% static "css/style.css" %}" rel="stylesheet">
</head>

<body>
  <!-- Responsive navbar -->

  <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
    <div class="container">
      <a class="navbar-brand" href="/">{{ site.name }}</a>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
        aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav ms-auto mb-2 mb-lg-0">
          <li class="nav-item"><a class="nav-link" href="/">Home</a></li>
          <li class="nav-item"><a class="nav-link" href="#">About</a></li>
          <li class="nav-item">
            <a class="nav-link" href="/admin">Admin</a>
          </li>
        </ul>
      </div>
    </div>
  </nav>

  {% block header %}{% endblock header %}

  <!-- Page content -->

  <div class="container mt-5">
    <div class="row">{% block content %}{% endblock content %}</div>
  </div>

  <!-- Footer-->

  <footer class="py-5 bg-dark">
    <div class="container">
      <p class="m-0 text-center text-white">
        Copyright &copy; {{ site.name }} 2021
      </p>
    </div>
  </footer>

  <!-- Bootstrap core JS-->

  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
    crossorigin="anonymous"></script>

</body>

</html>

static files

There is one thing we need to talk about in this file. Notice line 11 to 13, this is how we can import static files (CSS and JavaScript files) in Django. Of course, we are not using CSS in this tutorial, but I’d like to talk about how it can be done if you do need to import extra CSS files.

By default, Django will search for static files in individual app folders. In our project, Django will go to /blog and search for a folder called static, and then inside that static folder, Django will look for css/style.css, as defined in our template.

Home

Home page

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{% extends 'layout.html' %}

{% block meta %}
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="description" content="{{ site.description }}" />
    <meta name="author" content="Eric Hu" />
    <title>{{ site.name }}</title>
{% endblock meta %}

{% block header %}
<!-- Page header with logo and tagline -->

<header class="py-5 bg-light border-bottom mb-4">
    <div class="container">
        <div class="text-center my-5">
            <h1 class="fw-bolder">Welcome to {{ site.name }}!</h1>
            <p class="lead mb-0">{{ site.description }}</p>
        </div>
    </div>
</header>

{% endblock header %}

{% block content %}
    {% include 'vendor/post-list.html' %}
    {% include 'vendor/sidebar.html' %}
{% endblock content %}

Notice that instead of hardcoding the sidebar and the list of posts, we separated them, since we are going to use the same components in the category and the tag page.

Post-list

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!-- Blog entries -->

<div class="col-lg-8">
    <!-- Nested row for non-featured blog posts -->
    <div class="row">
        {% for post in posts %}
            <!-- Blog post-->
            <div class="col-md-6">
                <div class="card mb-4 px-md-0">
                    <a href="{% url 'post' post.slug %}"><img class="card-img-top"
                            src="{{ post.featured_image.url }}" alt="..." /></a>
                    <div class="card-body">
                        <div class="small text-muted">{{ post.created_at|date:"M d, Y" }}
                        </div>
                        <h2 class="card-title h4">{{ post.title }}</h2>
                        <p class="card-text">{{ post.content|striptags|slice:150 }}</p>
                        <a class="btn btn-primary" href="{% url 'post' post.slug %}">Read more
                            →</a>
                    </div>
                </div>
            </div>
        {% endfor %}
    </div>
    <!-- Pagination-->
    <nav aria-label="Pagination">
        <hr class="my-0" />
        <ul class="pagination justify-content-center my-4">
            <li class="page-item disabled"><a class="page-link" href="#" tabindex="-1"
                    aria-disabled="true">Newer</a></li>
            <li class="page-item active" aria-current="page"><a class="page-link" href="#!">1</a></li>
            <li class="page-item"><a class="page-link" href="#!">2</a></li>
            <li class="page-item"><a class="page-link" href="#!">3</a></li>
            <li class="page-item disabled"><a class="page-link" href="#!">...</a></li>
            <li class="page-item"><a class="page-link" href="#!">15</a></li>
            <li class="page-item"><a class="page-link" href="#!">Older</a></li>
        </ul>
    </nav>
</div>

From line 6 to 22, recall that we passed a variable posts from the view to the template. The posts contains a collection of posts, and here, inside the template, we iterate over every item in that collection using a for loop.

Line 10, recall that we created a URL dispatcher like this:

1
path('post/<slug:slug>', views.post, name='post'),

In our template, {% url 'post' post.slug %} will find the URL dispatcher with the name 'posts', and assign the value of post.slug to the variable <slug:slug>, which will then be passed to the corresponding view function.

Line 13, the date filter will format the date data that is passed to the template since the default value is not exactly user-friendly. You can find other date formats here.

Line 16, here we chained two filters to post.content. The first one removes the HTML tags, and the second one takes the first 150 characters and slices the rest.

Sidebar

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!-- Side widgets-->

<div class="col-lg-4">
    <!-- Search widget-->
    <div class="card mb-4">
        <div class="card-header">Search</div>
        <div class="card-body">
            <div class="input-group">
                <input class="form-control" type="text" placeholder="Enter search term..."
                    aria-label="Enter search term..." aria-describedby="button-search" />
                <button class="btn btn-primary" id="button-search" type="button">Go!</button>
            </div>
        </div>
    </div>
    <!-- Categories widget-->
    <div class="card mb-4">
        <div class="card-header">Categories</div>
        <div class="card-body">
            <ul class="list-unstyled mb-0 row">
                {% for category in categories %}
                    <li class="col-sm-6"><a
                            href="{% url 'category' category.slug %}">{{ category.name }}</a>
                    </li>
                {% endfor %}
            </ul>
        </div>
    </div>
    <!-- Tags widget-->
    <div class="card mb-4">
        <div class="card-header">Tags</div>
        <div class="card-body">
            <ul class="list-unstyled mb-0 row">
                {% for tag in tags %}
                    <li class="col-sm-6"><a
                            href="{% url 'tag' tag.slug %}">{{ tag.name }}</a>
                    </li>
                {% endfor %}
            </ul>
        </div>
    </div>
    <!-- Side widget-->
    <div class="card mb-4">
        <div class="card-header">Side Widget</div>
        <div class="card-body">You can put anything you want inside of these side widgets. They are easy to
            use, and feature the Bootstrap 5 card component!</div>
    </div>
</div>

Category

category page

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{% extends 'layout.html' %}

{% block meta %}
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="description" content="{{ site.description }}" />
    <meta name="author" content="Eric Hu" />
    <title>{{ category.name }} - {{ site.name }}</title>
{% endblock meta %}

{% block header %}
    <!-- Page header with logo and tagline -->
    <header class="py-5 bg-light border-bottom mb-4">
        <div class="container">
            <div class="text-center my-5">
                <h1 class="fw-bolder">Category: {{ category.name }}!</h1>
                <p class="lead mb-0">{{ category.description }}</p>
            </div>
        </div>
    </header>
{% endblock header %}

{% block content %}
    {% include 'vendor/post-list.html' %}
    {% include 'vendor/sidebar.html' %}
{% endblock content %}

Tag

tag page

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{% extends 'layout.html' %}

{% block meta %}
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="description" content="{{ site.description }}" />
    <meta name="author" content="Eric Hu" />
    <title>{{ tag.name }} - {{ site.name }}</title>
{% endblock meta %}

{% block header %}
    <!-- Page header with logo and tagline -->
    <header class="py-5 bg-light border-bottom mb-4">
        <div class="container">
            <div class="text-center my-5">
                <h1 class="fw-bolder">Tag: {{ tag.name }}!</h1>
                <p class="lead mb-0">{{ tag.description }}</p>
            </div>
        </div>
    </header>
{% endblock header %}

{% block content %}
    {% include 'vendor/post-list.html' %}
    {% include 'vendor/sidebar.html' %}
{% endblock content %}

Post

post page

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
{% extends 'layout.html' %}

{% block meta %}
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="description" content="{{ site.description }}" />
    <meta name="author" content="Eric Hu" />
    <title>{{ post.title }} - {{ site.name }}</title>
{% endblock meta %}

{% block header %}{% endblock header %}

{% block content %}
    <div class="col-lg-8">
        <!-- Post content-->
        <article>
            <!-- Post header-->
            <header class="mb-4">
                <!-- Post title-->
                <h1 class="fw-bolder mb-1">{{ post.title }}</h1>
                <!-- Post meta content-->
                <div class="text-muted fst-italic mb-2">Posted on
                    {{ post.created_at|date:"F d, Y" }} by {{ post.user.name }}</div>
                <!-- Post Tags-->
                {% for tag in post.tags %}
                    <a class="badge bg-secondary text-decoration-none link-light"
                        href="{% url 'tag' tag.slug %}">{{ tag.name }}</a>
                {% endfor %}
            </header>
            <!-- Preview image figure-->
            <figure class="mb-4"><img class="img-fluid rounded" src="{{ post.featured_image.url }}"
                    alt="..." /></figure>
            <!-- Post content-->
            <section class="mb-5">
                {{ post.content|safe }}
            </section>
        </article>

        <!-- Comments section-->
        <section class="mb-5">
            <div class="card bg-light">
                <div class="card-body">
                    <!-- Comment form-->
                    <form class="mb-4"><textarea class="form-control" rows="3"
                            placeholder="Join the discussion and leave a comment!"></textarea></form>
                    <!-- Comment with nested comments-->
                    <div class="d-flex mb-4">
                        <!-- Parent comment-->
                        <div class="flex-shrink-0"><img class="rounded-circle"
                                src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
                        <div class="ms-3">
                            <div class="fw-bold">Commenter Name</div>
                            If you're going to lead a space frontier, it has to be government; it'll never be
                            private enterprise. Because the space frontier is dangerous, and it's expensive, and
                            it has unquantified risks.
                            <!-- Child comment 1-->
                            <div class="d-flex mt-4">
                                <div class="flex-shrink-0"><img class="rounded-circle"
                                        src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
                                <div class="ms-3">
                                    <div class="fw-bold">Commenter Name</div>
                                    And under those conditions, you cannot establish a capital-market evaluation
                                    of that enterprise. You can't get investors.
                                </div>
                            </div>
                            <!-- Child comment 2-->
                            <div class="d-flex mt-4">
                                <div class="flex-shrink-0"><img class="rounded-circle"
                                        src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
                                <div class="ms-3">
                                    <div class="fw-bold">Commenter Name</div>
                                    When you put money directly to a problem, it makes a good headline.
                                </div>
                            </div>
                        </div>
                    </div>
                    <!-- Single comment-->
                    <div class="d-flex">
                        <div class="flex-shrink-0"><img class="rounded-circle"
                                src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
                        <div class="ms-3">
                            <div class="fw-bold">Commenter Name</div>
                            When I look at the universe and all the ways the universe wants to kill us, I find
                            it hard to reconcile that with statements of beneficence.
                        </div>
                    </div>
                </div>
            </div>
        </section>
    </div>
    {% include 'vendor/sidebar.html' %}

{% endblock content %}

One last thing we need to talk about is line 35, notice we added a safe filter. That is because by default, Django will render HTML code as plain texts for security reasons, we have to tell Django that it is OK to render HTML codes as HTML.


comments powered by Disqus