Building Blog The First Steps

We now know how to create Model, Templates and Views in Django, lets put this knowledge to use and create some pages for our site - The Great Django Blog. In this lesson we will create the following pages.

  • Post List Page - To display a list of blog post
  • Post Detail Page - To display post in detail
  • Category Page - Displays a list of posts under a particular category
  • Tag Page - Displays a list of posts tagged with a particular tag

Creating Post List Page #

Open views.py in the blog app and delete today_is() view function. Then add a new view function called post_list() below the index() view as follows:

TGDB/django_project/blog/views.py

from django.http import HttpResponse
from django.shortcuts import render
from .models import Author, Tag, Category, Post


def index(request):
    return HttpResponse("Hello Django")


# view function to display a list of posts
def post_list(request):
    posts = Post.objects.all()
    return render(request, 'blog/post_list.html', {'posts': posts})

Lets step through the changes:

  1. In line 3, we are importing Author, Category, Tag and Post models from models.py file.

  2. In line 12, we are using all() method of the objects manager to fetch all the Post objects from the database.

  3. In line 13, we are using render() method to return a response to the user.

Lets create a new base template named base.html for our blog app inside blog/templates/blog directory and add the following code to it.

TGDB/django_project/blog/templates/blog/base.html

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}The Great Django Blog{% endblock %}</title>        
</head>
<body>

    <div class="navbar">
        <div class="section-inner clearfix">
            <nav>
                <a href="/">The Great Django Blog</a>
                <a href="#">Blog</a>
                <a href="#">Contact</a>
                <a href="#">Career</a>
            </nav>
        </div>
    </div>

    {% block content %}

    {% endblock %}

    {% block footer %}

    <div class="footer">
        <div class="section-inner clearfix">
            <hr>
            <p>&copy; The Great Django Footer</p>
        </div>
    </div>

    {% endblock %}

</body>
</html>

Create another template called post_list.html in the blog app (i.e in the blog/templates/blog directory) and add the following code to it.

TGDB/django_project/blog/templates/blog/post_list.html

{% extends "blog/base.html"  %}

{% block title %}
    Blogg - {{ block.super }}
{% endblock %}

{% block content %}    

    <div class="content">
        <div class="section-inner clearfix">        

        {% for post in posts %}
            <h3>                        
                <a href="http://127.0.0.1:8000/{{ post.pk }}/">{{ post.title|capfirst }}</a>
            </h3>
            <p class="post-info">
                Date: {{ post.pub_date }} |
                Category: <a href="http://127.0.0.1:8000/category/{{ post.category.slug }}">{{ post.category.name }}</a> |
                Tag:
                {% for tag in post.tags.all %}
                      <a href="http://127.0.0.1:8000/tag/{{ tag.slug }}">{{ tag.name }}</a>
                {% empty %}
                      None
                {% endfor %}

                | Author: {{ post.author.name }}
            </p>
        {% empty %}
            <p>There are no posts</p>
        {% endfor %}    

        </div>
    </div>

{% endblock %}

Here are few things to note about this template:

  • The outer for tag loops through all the post and inner for tag loops through all the post's tags.
  • We are creating the following three links in the template:

    1. First link points to the post detail page (line 14). Here is how it works:

      When user visits http://127.0.0.1:8000/1/ display content of the post whose id is 1.
      Similarly, when user visits http://127.0.0.1:8000/2/ display the post whose id is 2 and so on.

    2. The second link points to Post by Category page (line 18). Here is how it works:

      When user visits http://127.0.0.1:8000/category/python/ display all posts under category python.
      Similarly, when user visits http://127.0.0.1:8000/category/java/ display all posts under category java and so on.

    3. The third link point to Posts by Tag page (line 21). Here is how it works:

      When user visits http://127.0.0.1:8000/tag/django/ display all posts under tag django.
      Similarly, when user visits http://127.0.0.1:8000/tag/flask/ display all posts under tag flask and so on.

Just like most blogging sites we will show a list of post on the homepage. Currently, the homepage of our site is shows a 404 error. Let's change that. Open urls.py inside the blog app. Delete todays_time and blog_index URL patterns and add post_list URL pattern as follows.

TGDB/django_project/blog/urls.py

from django.conf.urls import url
from blog import views

urlpatterns = [    
    url(r'^$', views.post_list, name='post_list'),
]

Open sitewide urls.py file at TGDB/django_project/django_project, it should look like this:

TGDB/django_project/django_project/urls.py

from django.conf.urls import url, include
from django.contrib import admin

urlpatterns = [
    url(r'^blog/', include('blog.urls')),
    url(r'^admin/', admin.site.urls),
]

Change the URL pattern in line 5 to:

url(r'', include('blog.urls')),

Now visit http://127.0.0.1:8000/, you should see a list of blog posts like this:

blog-list-page.png

Sure this design is not going to win any awards but atleast we are getting somewhere. We will add CSS later.

Right now the post detail URL is not mapped to any view function, so If you click on the post link you will get a HTTP 404 error.

post-detail-404-error.png

We are creating post detail page in the next section.

Creating Post Detail Page #

Our post detail page displays a post in detail. To fetch a particular blog post from the database we need to know the primary key of the post.

Lets add a new view function called post_detail() after post_list() as follows:

TGDB/django_project/blog/views.py

# view function to display a single post
def post_detail(request):    
    post = Post.objects.get(pk=pk)
    return render(request, 'blog/post_detail.html', {'post': post})

Next, create a new template called post_detail.html with the following code.

TGDB/django_project/blog/templates/blog/post_detail.html

{% extends "blog/base.html"  %}

{% block title %}
    {{ post.title|capfirst }} - {{ block.super }}
{% endblock %}

{% block content %}

    <div class="content">
        <div class="section-inner clearfix">

    <h1>{{ post.title|capfirst }}</h1>
    <p class="post-info">
        Date: {{ post.pub_date }} |
        Category: <a href="http://127.0.0.1:8000/category/{{ post.category.slug }}">{{ post.category.name }}</a> |
        Tag:
        {% for tag in post.tags.all %}
              <a href="http://127.0.0.1:8000/tag/{{ tag.slug }}">{{ tag.name }}</a>
        {% empty %}
              None
        {% endfor %}

        | Author: {{ post.author.name }}
    </p>
    <p>
        {{ post.content }}
    </p>

            </div>
    </div>

{% endblock %}

We have two problems now:

  1. How we can get a single URL pattern to recognize both the http://127.0.0.1:8000/1/ and http://127.0.0.1:8000/2/ requests ?
  2. How do we access the primary key present in the URLs (like http://127.0.0.1:8000/2/) in our views ?

Lets tackle the first problem.

To match http://127.0.0.1:8000/1/ we can use the regular expression r'^1/$'. Similarly to match http://127.0.0.1:8000/2/ we can use r'^2/$'. At first you might think - We can solve this problem by creating a separate view function for each URL pattern.

urlpatterns = [
    url(r'^1/$', views.get_first_post),
    url(r'^2/$', views.get_second_post),
    url(r'^3/$', views.get_third_post),
]

The problem with this approach is that it doesn't scale very well. If you have 100 posts then you have to create 100 URLs patterns and 100 view functions for it.

The key to solve this problem is to use \d character. Special characters like \d are called Meta Characters in Regular Expression. These characters have some special meaning instead of their literal meaning. The \d matches a single digit (0-9).

If you want to match one or more digits just append + to \d. The regular expression \d+ matches 1, 100, 342, 1000 and so on.

So our new URL pattern r'^\d+/$' matches strings like 1/ , 409/, 99999/ and so on. It will not match URL patterns like a122/, 1024a/, 10a00/ and so on.

Our first problem is now solved. Lets now shift our attention to the second problem - How do we access primary key present in URL inside our view function ?

There are two ways to solve this problem. The first method consists of little bit of trickery and the second one is pretty easy.

Recall that the request parameter of a view function contains all the information about the current web request that has triggered a view. The request object has an attribute called path_info which returns the portion of the URL after the host name. For example, if the user requests http://127.0.0.1:8000/1/ then request.path_info would return /1/. Now, you just need to strip leading and trailing slashes which you can do so using some string methods.

The other solution is easy and thus recommended. To pass the primary key to the view function we use something called named regular-expression group or simply named group. We create named group using the following syntax:

(?P<name>pattern)

The name is the name of the group (whole thing inside parentheses) and pattern is the actual regular expression. What Django does behind the scenes is that it takes the named group and passes its value to the view function as keyword argument. In our case, let's assume the group name is pk and the pattern is \d+. So the named group becomes r'^(?P<pk>\d+)/$'. Open urls.py in the blog app and make the following changes.

TGDB/django_project/blog/urls.py

from django.conf.urls import url
from blog import views

urlpatterns = [
    url(r'^(?P<pk>\d+)/$', views.post_detail, name='post_detail'),
    url(r'^$', views.post_list, name='post_list'),
]

From now on, a request to URL like http://127.0.0.1:8000/1/ would call the view function post_detail() as :

post_detail(request, id=1)

Start the server if not already running and visit the http://127.0.0.1:8000/1/ and you will get the following error:

unexpected-keyword-argument-error.png

The error is telling us our post_detail() view is getting a keyword argument named pk. But, at this point post_detail() function accepts only one argument called request. To fix this error simply add a pk parameter to the post_detail() function as follows:

TGDB/django_project/blog/views.py

# view function to display a single post
def post_detail(request, pk):    
    post = Post.objects.get(pk=pk)
    return render(request, 'blog/post_detail.html', {'post': post})

Refresh the page and this time you should see a post detail page like this:

post-detail-page-EOAQCV.png

Creating Category and Tag page #

In this section we will create two new page - Category and Tag page.

  • Category Page - Displays a list of posts under a particular category
  • Tag Page - Displays a list of posts tagged with a particular tag

When the user visits http://127.0.0.1:8000/category/python, we will display a list of post published under category python. Similarly, When the user visits http://127.0.0.1:8000/tag/django, we will display a list of post tagged with django.

Just as in the above section we want a single URL pattern to recognize both /category/python/ and /category/machine-learning/ URLs.

Recall that "python" and "machine-learning" in the URLs are called slugs. A slug only consists of letters, numbers, underscores and hyphens only. So we can't use \d+ here because it only matches one or more numbers. We need something else.

In regular expression, \w metacharacter matches a single word character. So what's word character ?

Word character = alphanumeric(letters, digit, regardless of case) character + underscore(_)

So \w matches 1, a, z , R, _ and so on. Just as with \d you can append + to \w to match one or more instances of word characters.

\w+ matches kilo_word, 100doo, 771, abc and so on.

What about hyphen (-) ?

Regular expression doesn't provide any special character to match hyphen (-). So we use a literal hyphen (-) character to match a - character.

A slug is just a combination of one or more character which matches either \w or (-) or both. We use character classes to match only one of several characters. To create character class just simply list the characters you want to match inside square brackets []. So [\w-] matches a single character in the slug. In order words [\w-] matches either a word character or a hyphen (-). To match one or more word character or hyphen append + at the end of [\w-]. Therefore, the regular expression [\w-]+ matches string like "python", "machine-learning", "cat_123", "foo-bar-123" and so on.

Our next task is to pass category slug to the view function. We can do that easily using our named group.

(?P<name>pattern)

Here is the complete regular expression to map every category.

r'^category/(?P<category_slug>[\w-]+)/$'

Similarly, the regular expression to match every tag is as follows:

r'^tag/(?P<tag_slug>[\w-]+)/$'

Open urls.py in the blog app and add posts_by_category and posts_by_tag URL patterns to the urlpatterns list as follows:

TGDB/django_project/blog/urls.py

from django.conf.urls import url
from blog import views

urlpatterns = [
    url(r'^category/(?P<category_slug>[\w-]+)/$', views.posts_by_category, name='post_by_category'),
    url(r'^tag/(?P<tag_slug>[\w-]+)/$', views.posts_by_tag, name='post_by_tag'),
    url(r'^(?P<pk>\d+)/$', views.post_detail, name='post_detail'),
    url(r'^$', views.post_list, name='post_list'),
]

Open views.py and add posts_by_category() and posts_by_tag() view functions after post_detail() as follows:

TGDB/django_project/blog/views.py



# view function to display post by category
def post_by_category(request, category_slug):
    category = Category.objects.get(slug=category_slug)
    posts = Post.objects.filter(category__slug=category_slug)
    context = {
        'category': category,
        'posts': posts
    }
    print(category)
    return render(request, 'blog/post_by_category.html', context)


# view function to display post by tag
def post_by_tag(request, tag_slug):
    tag = Tag.objects.get(slug=tag_slug)
    posts = Post.objects.filter(tags__name=tag)
    context = {
        'tag': tag,
        'posts': posts
    }
    return render(request, 'blog/post_by_tag.html', context )

In the templates directory inside the blog app i.e TGDB/django_project/blog/templates and create two files post_by_category.html and post_by_tag.html as follows.

TGDB/django_project/blog/templates/blog/post_by_category.html

{% extends "blog/base.html"  %}

{% block title %}
    {{ category|title }} - {{ block.super }}
{% endblock %}

{% block content %}

    <div class="content">
        <div class="section-inner clearfix">

    <h2>All the posts under category - {{ category.name }}</h2>

    {% for post in posts %}
        <h3>
            <a href="http://127.0.0.1:8000/{{ post.pk }}/">{{ post.title|capfirst }}</a>
        </h3>
        <p class="post-info">
            Date: {{ post.pub_date }} |
            Category: <a href="http://127.0.0.1:8000/category/{{ post.category.slug }}">{{ post.category.name }}</a> |
            Tag:
            {% for tag in post.tags.all %}
                  <a href="http://127.0.0.1:8000/tag/{{ tag.slug }}">{{ tag.name }}</a>
            {% empty %}
                  None
            {% endfor %}

            | Author: {{ post.author.name }}
        </p>
    {% empty %}
        <p>There are not posts under {{ category }}</p>
    {% endfor %}

            </div>
    </div>

{% endblock %}

TGDB/django_project/blog/templates/blog/post_by_tag.html

{% extends "blog/base.html"  %}

{% block title %}
    {{ tag|title }} - {{ block.super }}
{% endblock %}

{% block content %}

    <div class="content">       
        <div class="section-inner clearfix">

    <h2>All the posts tagged with - {{ tag.name }}</h2>

    {% for post in posts %}
        <h3>
            <a href="http://127.0.0.1:8000/blog/{{ post.pk }}/">{{ post.title|capfirst }}</a>
        </h3>
        <p>
            Date: {{ post.pub_date }} |
            Category: <a href="http://127.0.0.1:8000/category/{{ post.category.slug }}">{{ post.category.name }}</a> |
            Tag:
            {% for tag in post.tags.all %}
                  <a href="http://127.0.0.1:8000/tag/{{ tag.slug }}">{{ tag.name }}</a>
            {% empty %}
                  None
            {% endfor %}

            | Author: {{ post.author.name }}
        </p>
    {% empty %}
        <p>There are not posts tagged with {{ tag }}</p>
    {% endfor %}

            </div>
    </div>

{% endblock %}

Open your browser and click on any category, you should see a page like this:

post-by-category-page.png

Similarly, a tag page should look like this:

post-by-tag-page.png

We have a created a very simple blog but it is far from complete. We have deliberately made some mistakes along the way. Did you find any ? I encourage you to find mistakes and make a list of them. In the upcoming lessons we will improve our blog app and make it look like a real world app.