Using Slugify with Django blog to create slug or url

In this article we will have a look at how to use Django utility to create slug field automatically and most importantly, you can create slugs in languages other than English too.

For creating blog application, please refer to post how to create Blog app using Django.

Once you create blog application, you admin screen will look something like below.

Here if you see carefully, slug is user input field, now we want to make it auto created field by making some changes into models.py

from django.db import models
from django.contrib.auth.models import User
from django.utils.text import slugify                                # add this

class Post(models.Model):
    title = models.CharField(max_length=200, unique=True)
    slug = models.SlugField(max_length=200, unique=True,editable=False)  # Note the changes here, editable is false.
    author_local = models.ForeignKey(User, on_delete= models.CASCADE,related_name='blog_posts',default="admin")
    updated_on = models.DateTimeField(auto_now= True)
    content = models.TextField()
    created_on = models.DateTimeField(auto_now_add=True)
    status = models.IntegerField(choices=STATUS, default=0)
    post_type = models.CharField(max_length=15,choices=POST_CHOICES,default=None,blank=True)
    category = models.CharField(max_length=50,default=None,blank=True)
    featured_image = models.ImageField(upload_to='img', blank=True, null=True)

    class Meta:
        ordering = ['-created_on']

    def save(self, *args, **kwargs):                                  # add this
        self.slug = slugify(self.title, allow_unicode=True)           # add this
        super().save(*args, **kwargs)                                 # add this

    def __str__(self):
        return self.title

Since you are making changes in models.py, you need to run following commands

 

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

 

Now access admin again, it will look like below and once you save the post, slug is automatically created.

Since we are using Unicode, it works for Devanagari script (Marathi and Hindi), I did not test for other languages but it would work for other languages too.

 

You might be wondering why content text field has options like WordPress editor in my screenshot. Please refer to post How to add Summernote WYSIWYG Editor in Django.

How to add Summernote WYSIWYG Editor in Django

After I created a blog application, biggest hurdle from using it is its admin editor. Anyone who has every used wordpress or any otehr blogging platform will not be OK to use simple text editor. I was searching for options to make the text field something like wordpress. I came across two options tinymce editor and Summernote.

I looked at install steps of tinymce and it looked complicated, I did not try and see if this works. When I looked at Summernote, it look simple and straghtforward and I decided to give it a try. Its very simple and it works perfectly fine.

Here are the steps for installtion

Step#1 Install Summernote package.

Install it just like any other python package. If you are using virtualenv, please make sure you install it in virtualenv

$pip install django-summernote
python -m pip install Pillow

Step#2 Changes in urls.py

Add this to main urls.py file

urlpatterns = [
..
..
    path('summernote/', include('django_summernote.urls')),
..
]
..
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)  #add this for media

Step#3 Changes in settings.py

Add django_summernote in installed apps in settings.py file.

INSTALLED_APPS = [
    'blog.apps.BlogConfig', # add this statement
    'ifsccode.apps.IfsccodeConfig', # add this statement
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # 'django.contrib.sites',
    'django.contrib.sitemaps',
    'django_summernote',          # This is added
]

 

Also add media folder settings at the end of settings.py

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

X_FRAME_OPTIONS = 'SAMEORIGIN'

Step#4 Changes in admin.py

Changes in admin.py

from django.contrib import admin
from django_summernote.admin import SummernoteModelAdmin

from .models import Post, Category, Comment, Tag


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

...

class PostAdmin(SummernoteModelAdmin):    #added this
    summernote_fields = ('content',)      #added this

admin.site.register(Post, PostAdmin)

Please note that in above table, I have content field under Post

Step#5 Changes in Templates

Finally and most importantly, when you are displaying the field, mark it as safe.

<div id="postdetails">{{post.content|safe}}</div>

once this is done

Django admin CSS or static file not working [Solved]

You might have came across a situation where your application works perfectly fine on local machine but when deployed to production, everything works except admin section, technically it functions as expected but it does not have CSS. Why this happens ?

If you want to recreate this problem, just following in settings.py from True to False.

DEBUG = False
This happens because you application is not able to load css files for admin  functionality. To address this problem, please follow below mentioned steps.

Step#1 collectstatic

First you need to create a folder called static in your project root directory, then run below command
$python3.6 manage.py collectstatic.
This will move admin css files under static folder. You can see new folder created under static folder called admin. This folder will have admin view related css, js , fonts, img.
You might wonder where were these files before ? these were server from site-packages folder of Django. You dont need to dig details here, but if you want you can.

Step#2 Change server config.

When you are running, runserver, you are running development server and it does a lot of things for you, including serving of static files but in production, Django expects that the static files will be served by your serer.

This is not difficult at all, just have below changes done to your nginx server config.

server {
    server_name domainname.com www.domainname.com;

    location / {
        include uwsgi_params;
        uwsgi_pass unix:/run/uwsgi/domainname.sock;
    }

    location /static {                        # add this
       alias /var/www/domainname/static;      # add this
    }                                         # add this

}

if you are hosting django application using uwsgi and NGINX, this will work for you. I am not sure about gunicorn server. You can check post deploying django with uwsgi and nginx for deployment of your application to server.

First make sure you application is working on production server and then work on to make admin successfully.  Deploying Django to server has its own challenges, don’t make it more complicated by adding this change too. Attack problems one at a time.

Please let me know if you faced any issue while getting this working.

Create html or xml sitempa for Django app

If you are creating any content based website, it is utmost important to have a sitemap.   At first it looks like sitemap creation is complex using Django but if you understand the process its fairly simple. You can follow below steps to create simple sitemap.xml

Step#1 Config changes.

Add 'django.contrib.sitemaps', in INSTALLED_APPS section of settings.py
Also ensure following settings for TEMPLATES

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            BASE_DIR + '/templates/', # add this line
        ],
        'APP_DIRS': True,

Step#2 Create sitemap.py

Now cd to subapp directory and create file sitemaps.py as below

from django.contrib.sitemaps import Sitemap
from .models import Post
 
 
class PostSitemap(Sitemap):    
    changefreq = "monthly"
    priority = 0.9
 
    def items(self):
        return Post.objects.all()
 
    def lastmod(self, obj):
        return obj.created_on

changefreq can be hourly, monthly, never etc.

Most important point to be noted here. Whichever data model you are using, make sure it has function get_absolute_url()  defined.
for example

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 get_absolute_url(self): #this is added for sitemap
        return '/'+self.slug
    def __str__(self):
        return self.title

Now make changes into blog/urls.py

from django.urls import path
from . import views
from django.contrib.sitemaps.views import sitemap
from .sitemaps import PostSitemap

sitemaps = {
    'posts': PostSitemap
}

app_name = 'blog'
urlpatterns = [
    path('', views.index, name='index'),  
    path('sitemap.xml', sitemap, {'sitemaps' : sitemaps } , name='sitemap'),  
]

Now if you try to access sitemap.xml, you can see it

http://127.0.0.1:8000/sitemap.xml/

This XML file does not appear to have any style information associated with it. The document tree is shown below.
<urlset>
  <url>
    <loc>https://www.fintrekking.com/finance-is-everything</loc>
    <lastmod>2020-04-18</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.9</priority>
  </url>
</urlset>

Please notethat this siitemap is accessible from subapp. Either you need to make this subapp accessible on main url or this sitempa will be available at url something like below

http://127.0.0.1:8000/blog/sitemap.xml/

Please let me know if this works for you or if you faced any issue while making this work.

Deploy Django Application using NGINX, uWSGI

After developing couple of django application and testing them using inbuild development server (runserver), I was ready to deploy my Django application to VPS but littile did I know, deployment of django application is a big task itself.

I first started using official tutorial from Digital Ocean How To Set Up Django with Postgres, Nginx, and Gunicorn on Ubuntu 16.04 but it turned out to be two complex. I spent almost a day but I ended up nowhere. I was searching for otherway of deployment thats when I came across uWSGI. You can find official documentation here. Setting up Django using uWSGI was not cakewalk either but I found it easy as compared to Gunicorn. In this effort, i spent almost two days (not whole days, may be  hours in total) but finally I got it working. I am documenting step by step actions for you to help.

Step#1 Prepare environment

$sudo apt-get update
$sudo apt-get upgrade
$sudo python3.6 -m pip install virtualenv

Now let us create a django application on server. If you have already developed your application, you can deploy it to server using git as explained here or copy code using FileZilla or some other ways. For simplicity, I am creating an application.

$django-admin startproject yourapp
$cd yourapp
$python3.6 manage.py startapp blog
$virtualenv venvft
created virtual environment CPython3.6.4.final.0-64 in 453ms
creator CPython3Posix(dest=/var/www/yourapp/venvft, clear=False, global=False)
seeder FromAppData(download=False, pip=latest, setuptools=latest, wheel=latest, via=copy, app_data_dir=/home/user/.local/share/virtualenv/seed-app-data/v1.0.1)
activators BashActivator,CShellActivator,FishActivator,PowerShellActivator,PythonActivator,XonshActivator

$source venvft/bin/activate
$pip install django 
$pip install psycopg2
deactivate

Please note the packages are installed inside virtual environment using pip install but outside it using command $sudo python3.6 -m pip install psycopg2. This command will not work inside virtual environment. Please don’t ask me why, I only know how. You can deactivate virtual environment at this stage.

Step#2 Django Application set up.

You already know what you need to do to set up django application. Changes to settings.py etc. For more details, you can refer to this post Building blog application using django.

Once basic setup is done, you need to run following commands

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

Before we test the application, please update ALLOWED_HOSTS in settings.py. Here ‘NNN.NN.NN.NNN’ stands for your server ip

ALLOWED_HOSTS = ['NNN.NN.NN.NNN','www.domain.com','domain.com','localhost','127.0.0.1','0.0.0.1']

Once done, let us test our application using our old development server. This is to ensure, our application is working fine. If you face any issue at this stage, it has nothing to do with server setup, tourbleshoot any Django related issue here. Only if you can run your application on development server, proceed to next step

python3.6 manage.oy runserver

You can open new command prompt and test this using following

curl http://127.0.0.1:8000/

Step#3 Run using uWSGI server

Now lets run it using uwsgi using following command

sudo uwsgi --http 0.0.0.0:8000 --home /var/www/yourapp/venvft --chdir /var/www/yourapp/yourapp --wsgi-file /var/www/yourapp/yourapp/wsgi.py

Toubleshooting. There is possibility that you might get below error

ModuleNotFoundError: No module named 'yourapp'
unable to load app 0 (mountpoint='') (callable not found or import error)
*** no app loaded. going in full dynamic mode ***
uWSGI running as root, you can use --uid/--gid/--chroot options
*** WARNING: you are running uWSGI as root !!! (use the --uid flag) *** 
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI worker 1 (and the only) (pid: 28959, cores: 

In this case, you need to make few changes to yourapp/yourapp/uwsgi.py file

import os
import sys #added this

from django.core.wsgi import get_wsgi_application

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) #added this
sys.path.append(BASE_DIR) #added this

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yourapp.settings')

application = get_wsgi_application()

I have added comments about which line needs to be added. Now test again and it should work file.

Step# 4 uWSGI setup

Create a yourapp.ini file @ location /home/username/uwsgi/sites as below

[uwsgi]
home = /var/www/yourapp/venvft
chdir = /var/www/yourapp/yourapp
wsgi-file = /var/www/yourapp/yourapp/wsgi.py

#http = 0.0.0.0:8000

socket = /run/uwsgi/yourapp.sock
vacuum = true
chown-socket = username:www-data
chmod-socket = 666

Please change yourapp and username in this file as pr your names

Now cd /etc/systemd/system/ and sudo nano uwsgi.service

[Unit]
#Description=uWSGI Emperor service
Description=uWSGI Emperor

[Service]
ExecStartPre=/bin/bash -c 'mkdir -p /run/uwsgi; chown username:www-data /run/uwsgi'
ExecStart=/usr/local/bin/uwsgi --emperor /home/username/uwsgi/sites
Restart=always
KillSignal=SIGQUIT
Type=notify
NotifyAccess=all

[Install]
WantedBy=multi-user.target

Here also. change the username to match yours.

After this is done cd /var/www/ and change ownership of project folder

sudo chown www-data:www-data -R yourapp

Step#5 NGINX setup

This is simplest of all. Simply use following code.

server {
    server_name yourdomain.com www.yourdomain.com;

    location / {
        include uwsgi_params;
        uwsgi_pass unix:/run/uwsgi/yourapp.sock;
        }
}

Now run following commands

sudo service nginx stop
sudo service uwsgi stop
sudo service nginx start
sudo service uwsgi start

Restart can also work but I prefer to stop and start.

Now before you check your application on your browser cd /run/uwsgi
and check if this folder has any files

If there is no error you will see filename yourapp.sock. Now you are all set to access your application using its domain name.

Following links were very helpful for me. If you need any clarification or facing error, you can let me know or check below links

 

Simple Git Auto Deployment to Digital Ocean or any other VPS

I have been using Digital Ocean VPS for couple of years now. I am always editing my websites even their technology stack has changed back and forth multiple times. I have always used FileZilla to transfer my code from local to remote repository. This is not efficient way and while searching for better way of autodeployment I came across an article by Digital Ocean itself for using git for deployment. This article recommended using beta directory for testing and then deploying to your primary directory. I thought it is not required for small and single developer websites so I thought to create a simple structure.

Here we will have only two repositories

  • Local repository where I will be making changes
  • Remote repository on Digital Ocean droplet from where my live website will run

Step#1 Install git on local and remote machine.

Installing git

$sudo apt-get install git

Step#2 Initiate Git repo

On Local machine cd to folder having your code and then initiate git repo as below

$git init

On remote server, cd to directory which will host your code and then initiate git repo

$git init --bare

Please note the difference : A bare repository is a git repository without a working copy, therefore the content of .git is top-level for that directory.

Use a non-bare repository to work locally and a bare repository as a central server/hub For example, when you create a repository on github.com, it is created as a bare repository.

To conclude, the repository on the server side is going to get commits via pull and push, and not by you editing files and then committing them in the server machine, therefore it is a bare repository.

You can push your changes from local to remote by using following command

Step#3 Add remote repository

You can add remote repository using below command.

$git remote add live ssh://user@domain.com/var/www/foldername

Here instead of domain.com you can use your droplets ip as well.

Once this is added you can check your remotes using following command

$ git remote -v
live ssh://user@mydomain.com/var/www/foldername (fetch)
live ssh://user@mydomain.com/var/www/foldername (push)

Step#4 Deploying the changes.

Once your changes are ready, you can deploy it using following commands

git add .  
git commit -m "1st commit"  
git push live master  

Step#5 Verify the changes

You can log in to your droplet and check if your code is updated. You can do this by simply verifying the files.

Troubleshooting

I faced an issue that my local repo was pushing files successfully but remote git repo was not updated.  To check if git changes were actually pushed or not run following command

$git log

This will show list of changes pushed to remote. If you see your changes are pushed but still not reflected, you need to run following manually

GIT_WORK_TREE=/var/www/foldername git checkout -f

Obviously, you should not run above command manually every time you make the change as this will loose the whole purpose of deployment automation.

cd to hooks folder, create post-receive file and add above line into file

cd hooks/
sudo nano post-receive

Once file is created, change the file permissions as below


sudo chmod a+x post-receive

And now you are all set. Going forward, any change that you push will be reflected on your remote repository.

Important Note: Depending on your changes and the type of application that you are hosting, you might have to restart the server to have these changes reflected on your website.

Django : ProgrammingError: column “id” does not exist

Whichever database you use as your backend, django needs every table to have a primary key.

If you have specified primary key in your model definition, you will never get this error.

If you have not defined primary key then django automatically creates a column named id (auto increment) and treat this as primary key.

If django takes care of both the situation, why are you getting this error ?

Well, most likely you have not created table using django migrate command. Your table already existed and you want to use this in django,

How to fix this issue ?

Simple, add a primary key to table and update your model to reflect the same.

How to handle media files in django application

Unlike static files, media files are the files (images , pdf or any other documents etc) uploaded by user.

For managing media files, you need to setup MEDIA_ROOT and MEDIA_URL .

MEDIA_ROOT

Media root is the directory where media files are stored

MEDIA_URL

URL that handles the media served from MEDIA_ROOT, used for managing stored files. It must end in a slash if set to a non-empty value.

Step#1

Add following lines at the end of settings.py

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

Step#2

Create a folder called as media at the base directory. Directory structure looks like as below
├── static
│   ├── css
│   ├── img
│   └── js
├── templates
│   ├── components
│   └── blog
├── media

Step#3

we need to configure main urls.py file as below

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

from . import settings                                              #add this for media
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

Step#4

You also need to define a field which will accept media file.

class Post(models.Model):
.
.
.

    featured_image = models.ImageField(upload_to='img', blank=True, null=True)
.
.

 

Please note the “upload_to” option. Here, “img” folder will get created (you dont have to create it manually) and files will be stored and served from here.

Once these changes are done , don’t forget to run following commands

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

Now start the server using following command and start using media files.

python3.6 manage.py runserver

If you run into any issue, please let me know

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.