Building Blog application using Django

In this post we will build a blog application using Django, before going thru this application, you should have basic knowledge of Django. If you want to review basics, please go thru following posts.

Let us create project

$django-admin startproject blogapp
$cd blogapp
/blogapp$ python3.6 manage.py runserver

Now our basic project  is running. Let us create an app now.

$python3.6 manage.py startapp blog

urls.py is not created inside app directory, we need to create blog/urls.py manually.

from django.urls import path
from . import views

app_name = 'blog'
urlpatterns = [
    path('', views.index, name='index'),  
]

To have application urls accessible from main project, we need to add this urls.py with main project

from django.contrib import admin
from django.urls import include, path # Add include here

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('todo.urls')),# Add this line
]

before doing further changes let us create sample view in blog/views.py we will edit this file later for details but we need to create this view to avoid any error while running intermediate command.

from django.shortcuts import render, get_object_or_404, redirect

def index(request):
    context = "temp"
    return render(request, 'blog/index.html',{'context':context})  

Changes  in settings.py file

Add application in settings file

INSTALLED_APPS = [
    'blog.apps.BlogConfig', # add this statement
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Now let us define templates directory as below

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            BASE_DIR + '/templates/', # add this line
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

and finally define static directory at the end of settings.py file.

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),
]
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

We need to manually create static and templates directories. Directory structure created looks as below:

├── static
│   ├── css
│   ├── img
│   └── js
├── templates
│   ├── components
│   └── blog
├── media
│   ├── img

Creating model

Let us first create todoapp database and database user

$sudo su - postgres
[sudo] password for conquistadorjd: 
postgres@inspiron-3542:~$ psql
psql (10.10 (Ubuntu 10.10-0ubuntu0.18.04.1))
Type "help" for help.
postgres=# CREATE DATABASE blogapp;
CREATE DATABASE
postgres=# CREATE USER blogappuser WITH PASSWORD 'password';  
CREATE ROLE
postgres=# ALTER ROLE blogappuser SET client_encoding TO 'utf8';
ALTER ROLE
postgres=# ALTER ROLE blogappuser SET default_transaction_isolation TO 'read committed';
ALTER ROLE
postgres=# ALTER ROLE blogappuser SET default_transaction_isolation TO 'read committed';
ALTER ROLE
postgres=# ALTER ROLE blogappuser SET timezone TO 'UTC';
ALTER ROLE
postgres=# GRANT ALL PRIVILEGES ON DATABASE blogapp TO blogappuser;
GRANT
postgres=# \q
postgres@inspiron-3542:~$ exit
logout

Now we need to configure this database in our application

DATABASES = {
    'default': {
        # 'ENGINE': 'django.db.backends.sqlite3',
        # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'blogapp',
        'USER': 'blogappuser',
        'PASSWORD': 'password',
        'HOST': 'localhost',
        'PORT': '',              
    }
}

Now let us create a model

from django.db import models
from django.contrib.auth.models import User

STATUS = (
    (0,"Draft"),
    (1,"Publish")
)

class Category(models.Model):
    # created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created at")
    # updated_at = models.DateTimeField(auto_now=True, verbose_name="Updated at")
    title = models.CharField(max_length=255, verbose_name="Title")
    # slug = models.CharField(max_length=20, unique=True)

    class Meta:
        verbose_name = "Category"
        verbose_name_plural = "Categories"
        ordering = ['title']

    def __str__(self):
        return self.title

class Tag(models.Model):
    name = models.CharField(max_length=20, unique=True)
    # slug = models.CharField(max_length=40, unique=True)

    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField(max_length=200, unique=True)
    slug = models.SlugField(max_length=200, unique=True)
    author = models.ForeignKey(User, on_delete= models.CASCADE,related_name='blog_posts')
    content = models.TextField()
    created_on = models.DateTimeField(auto_now_add=True)
    status = models.IntegerField(choices=STATUS, default=0)
    category = models.ForeignKey(Category, on_delete = models.CASCADE,verbose_name="Category",default=None,blank=True)
    updated_on = models.DateTimeField(auto_now= True)
    tags = models.ManyToManyField(Tag, related_name='rel_posts',default=None,blank=True)
    featured_image = models.ImageField(upload_to='img', blank=True, null=True)

    class Meta:
        ordering = ['-created_on']

    def __str__(self):
        return self.title

class Comment(models.Model):
    blog_post = models.ForeignKey(Post,on_delete = models.CASCADE,verbose_name = "blog_post",related_name="comments")
    comment_author = models.CharField(max_length = 50)
    comment_author_email = models.EmailField()
    comment_content = models.CharField(max_length = 200)
    comment_date = models.DateTimeField(auto_now_add=True)
    def __str__(self):
        return self.comment_content
    class Meta:
        ordering = ['-comment_date']

Once this is done, we need to run following two commands

$ python3.6 manage.py makemigrations blog
$ python3.6 manage.py migrate

Let us have this model accessible from admin and create some dummy data for development and unit testing.

from django.contrib import admin

from .models import Post,Comment,Category,Tag

admin.site.register(Post)
admin.site.register(Comment)
admin.site.register(Category)
admin.site.register(Tag)

Now create a admin user using following command

python3.6 manage.py createsuperuser

Let us run the server and login to admin from http://127.0.0.1:8000/admin and create some dummy data.

We had created url.py file under blogs directory in previous steps. Now update code as below:

from django.urls import path
from . import views

app_name = 'blog'
urlpatterns = [
    path('', views.index, name='index'),  
    path('<str:category>/<str:slug>/', views.details, name='details'),
]

We need to update main application urls.py file to enable media upload.

from django.contrib import admin
from django.urls import path, include

from . import settings
from django.contrib.staticfiles.urls import static                  #add this for media
from django.contrib.staticfiles.urls import staticfiles_urlpatterns #add this for media

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),# Add this line for new app
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)  #add this for media

blog/views.py

from django.shortcuts import render, get_object_or_404, redirect
from .models import Post

def index(request):
    context = Post.objects.all()
    return render(request, 'blog/index.html',{'context':context})      

def details(request, category, slug):
    blog_post = Post.objects.get(slug=slug)  
    comments = blog_post.comments.all()
    print('comments', comments)
    return render(request, 'blog/detail.html',{'context':blog_post,'comments':comments}) 

index.html

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

{% block content %}
<div class="container-fluid"/>
    <div class="row"/>
        <div class="col-1"/></div/>
        <div class="col-8"/> 
        {% if context %}
            <div class="list-group"/>
                {% for post  in context %}
                <a href="{{post.category}}/{{post.slug}}" class="list-group-item list-group-item-action flex-column align-items-start"/>
                    <div class="d-flex w-100 justify-content-between"/>
                      <h1 class="mb-1"/>{{post.created_on|date:"jS F Y" }} by {{post.title}} </h1/>
                      <small/>{{post.created_on|date:"jS F Y" }}</small/>
                    </div/>
                    <p class="mb-1"/>{{post.content|linebreaks|slice:":200"}}</p/>
                    <small/>more ...</small/>
                </a/><br/>                <br/><br/>
                {% endfor %}
            </div/>
        {% else %}
            <p/> Nothing here :) . Finally you found it</p/>
        {% endif %}
        </div/>
        <div class="col-3"/></div/>
    </div/>
</div/>
{% endblock %}

details.html

{% extends "components/base.html" %}
{% load static %}

{% block content %}
{% if context %}
<div class="container-fluid"/>
    <div class="row"/>
        <div class="col-1"/></div/>
        <div class="col-8"/>
            <h2/>{{context.title|linebreaks}}</h2/>
            <small/> {{context.created_on|date:"jS F Y"}} by {{context.author}}</small/><br/>
            
            <img src="{{context.featured_image.url}}" alt="Smiley face" class="rounded mx-auto d-block"  height="250" width="250"/> 
            <div id="postdetails"/>{{context.content|linebreaks}}</div/>
            <br/>
                {% if comments %}
                    <h2/>Comments</h2/>
                    <div class="list-group"/>
                        {% for comment in comments  %}
                            <!-- <a href="#" class="list-group-item list-group-item-action "/> --/>
                            <div class="d-flex w-100 justify-content-between"/>
                                <h6 class="mb-1"/>by {{comment.comment_author}}</h6/>
                                <small/>{{comment.comment_date|date:"jS F Y"}}</small/>
                            </div/>
                            <p class="mb-1"/>{{comment.comment_content}}</p/>

                        {% endfor %}
                    </div/>
                {% endif %}         
        </div/>
        <div class="col-3"/></div/>
    </div/>   
{% else %}
    <p/>No tasks are available.</p/>
{% endif %}
</div/> 
{% endblock %}

Here template files are very basic ones, you can update it as per your needs. Once you have core functionality ready, its up to you, how you want to display it.

If you run into any issue while working on this code, please do let me know.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.