Pythonメモ torinaブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

DjangoとBootstrap4で、ブログを作る

プログラミング関連 Bootstrap4 Django Bitbucketにソースあり
約184日前 2016年9月26日22:38
Bitbucketにソースをおきました。
https://bitbucket.org/toritoritorina/django-torina-blog

トップページ


右のカテゴリは、大カテゴリです。クリックするとPython...などの小カテゴリが開きます。


押すと、そのカテゴリでの記事一覧になります。タグも同様です。


上のナビバーにある検索フォームは、クイックサーチ的なものです。


続きを読むを押すと、記事の詳細画面に。カテゴリ、タグの最新5件なんかも表示されています。


とりあえずは、投稿はadminで行います。カテゴリの追加等も同様です。


↓にあるHTMLソースか?にチェックをするとHTMLを書くことができ


ちゃんと反映されますね。


Django1.10
Python3.5
プロジェクト名「blog」
アプリケーション名「torina_blog」
です。

blog/blog/settings.py
INSTALLERD_APPSの追加と、MEDIA_URL等の設定、
そしてcontext_processorsに自作のものを足します。
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'torina_blog',  # 足した
]
...
...
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        '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',
                'torina_blog.context_processors.common',  # 足した
            ],
        },
    },
]
...
...
# 足した
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = "/media/"


blog/blog/urls.py
開発環境での/media/の紐づけと、adminと作ったアプリのurls.pyを読み込む
from django.conf import settings
from django.conf.urls import url, include
from django.conf.urls.static import static
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^', include('torina_blog.urls', namespace='torina_blog')),
]
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL,
                          document_root=settings.MEDIA_ROOT)


blog/torina_blog/urls.py
カテゴリについて、
/category/大カテゴリ
/category/大カテゴリ/小カテゴリ
のようになります。大カテゴリの検索と、小カテゴリの検索ですね。
from django.conf.urls import url
from . import views


urlpatterns = [
    url(r'^$', views.PostIndexView.as_view(), name='index'),

    url(r'^detail/(?P<pk>[0-9]+)/$',
        views.PostDetailView.as_view(), name='detail'),

    url(r'^category/(?P<big>\w+)/(?P<small>\w+)/$',
        views.CategoryView.as_view(), name='category'),

    url(r'^category/(?P<big>\w+)/$',
        views.CategoryView.as_view(), name='category'),

    url(r'^tag/(?P<tag>\w+)/$',
        views.TagView.as_view(), name='tag'),
]


タグ名やカテゴリ名によっては、
Reverse for 'category' with arguments 

というようなエラーが出るかもしれません。その場合は、下記のように「.*」を使いましょう。
    url(r'^category/(?P<big>.*)/$',
        views.CategoryView.as_view(), name='category'),


blog/torina_blog/admin.py
adminで編集できるよう、追加します。
from django.contrib import admin
from .models import Post, SmallCategory, BigCategory, Tag

admin.site.register(Post)
admin.site.register(SmallCategory)
admin.site.register(BigCategory)
admin.site.register(Tag)



blog/torina_blog/context_processors.py
このアプリでは、どの画面に行っても大カテゴリの一覧とタグが存在しています。
このような場合、全てのviewでその処理を書くのではなく、context_processorを使います。
from .models import BigCategory, Tag


def common(request):
    context = {
        "big_categories": BigCategory.objects.all(),
        "tags": Tag.objects.all(),
    }
    return context


やってることは、単純に大カテゴリとタグを全て取得しているだけです。
これをsettings.pyのcontext_processorsに足すことで、どのtemplateからでも参照できます。


blog/torina_blog/forms.py
今回forms.pyは使いませんでした。

blog/torina_blog/models.py
from django.db import models


def _get_latest_post(queryset):
    """もらったクエリセットを、さらに絞り込んで返す。"""

    return queryset.filter(is_publick=True).order_by('-created_at')[:5]


class BigCategory(models.Model):
    """大カテゴリ"""

    name = models.CharField("大カテゴリ名", max_length=255)
    created_at = models.DateTimeField("作成日", auto_now_add=True)

    def __str__(self):
        return self.name

    def get_latest_post(self):
        queryset = Post.objects.filter(category__parent=self)
        return _get_latest_post(queryset)


class SmallCategory(models.Model):
    """小カテゴリー"""

    name = models.CharField("小カテゴリ名", max_length=255)
    parent = models.ForeignKey(BigCategory, verbose_name="大カテゴリ")
    created_at = models.DateTimeField("作成日", auto_now_add=True)

    def __str__(self):
        return self.name

    def get_latest_post(self):
        queryset = Post.objects.filter(category=self)
        return _get_latest_post(queryset)


class Tag(models.Model):
    """タグ"""

    name = models.CharField("タグ名", max_length=255)
    created_at = models.DateTimeField("作成日", auto_now_add=True)

    def __str__(self):
        return self.name

    def get_latest_post(self):
        queryset = Post.objects.filter(tag=self)
        return _get_latest_post(queryset)


class Post(models.Model):
    """ブログのポスト"""

    title = models.CharField("タイトル", max_length=255)
    text = models.TextField("本文")
    category = models.ForeignKey(SmallCategory, verbose_name="カテゴリ")
    tag = models.ManyToManyField(Tag, blank=True, verbose_name="タグ")
    thumbnail = models.ImageField("サムネイル", upload_to='thumnail/', blank=True)
    is_publick = models.BooleanField("公開可能か?", default=True)
    is_html = models.BooleanField("HTMLソースか?", default=False)
    created_at = models.DateTimeField("作成日", auto_now_add=True)
    updated_at = models.DateTimeField("更新日", auto_now=True)

    def __str__(self):
        return self.title


Django、ブログに使っているModel
https://torina.top/main/256/
の内容とほとんど同じです。

まずこれですが、ブログの記事にはis_publicというフィールドがあります。
また、記事の作成日降順で表示しますが、これらは共通です。
以下の関数は、その共通の部分だけを行うヘルパー関数です。
def _get_latest_post(queryset):
    """もらったクエリセットを、さらに絞り込んで返す。"""

    return queryset.filter(is_publick=True).order_by('-created_at')[:5]



記事は見たまんまです。
class Post(models.Model):
    """ブログのポスト"""

    title = models.CharField("タイトル", max_length=255)
    text = models.TextField("本文")
    category = models.ForeignKey(SmallCategory, verbose_name="カテゴリ")
    tag = models.ManyToManyField(Tag, blank=True, verbose_name="タグ")
    thumbnail = models.ImageField("サムネイル", upload_to='thumnail/', blank=True)
    is_publick = models.BooleanField("公開可能か?", default=True)
    is_html = models.BooleanField("HTMLソースか?", default=False)
    created_at = models.DateTimeField("作成日", auto_now_add=True)
    updated_at = models.DateTimeField("更新日", auto_now=True)

    def __str__(self):
        return self.title




get_latest_postは、そのタグやカテゴリでの最新5件を返すメソッドです。
templateから呼び出して使います。
カテゴリやタグ等の絞り込みまでは行い、後はis_public等の共通の処理を行う_get_lates_postに渡します。
class BigCategory(models.Model):
    """大カテゴリ"""

    name = models.CharField("大カテゴリ名", max_length=255)
    created_at = models.DateTimeField("作成日", auto_now_add=True)

    def __str__(self):
        return self.name

    def get_latest_post(self):
        queryset = Post.objects.filter(category__parent=self)
        return _get_latest_post(queryset)


class SmallCategory(models.Model):
    """小カテゴリー"""

    name = models.CharField("小カテゴリ名", max_length=255)
    parent = models.ForeignKey(BigCategory, verbose_name="大カテゴリ")
    created_at = models.DateTimeField("作成日", auto_now_add=True)

    def __str__(self):
        return self.name

    def get_latest_post(self):
        queryset = Post.objects.filter(category=self)
        return _get_latest_post(queryset)


class Tag(models.Model):
    """タグ"""

    name = models.CharField("タグ名", max_length=255)
    created_at = models.DateTimeField("作成日", auto_now_add=True)

    def __str__(self):
        return self.name

    def get_latest_post(self):
        queryset = Post.objects.filter(tag=self)
        return _get_latest_post(queryset)



blog/torina_blog/views.py
from django.core.urlresolvers import reverse_lazy
from django.db.models import Q
from django.views import generic
from .models import Post


class BaseListView(generic.ListView):
    paginate_by = 10

    def base_queryset(self):
        queryset = Post.objects.filter(
            is_publick=True).order_by('-created_at')
        return queryset


class PostIndexView(BaseListView):

    def get_queryset(self):
        queryset = self.base_queryset()
        keyword = self.request.GET.get("quick")
        if keyword:
            queryset = queryset.filter(
                Q(title__icontains=keyword) | Q(text__icontains=keyword))

        return queryset


class CategoryView(BaseListView):

    def get_queryset(self):
        queryset = self.base_queryset()
        category = self.kwargs.get("small")
        if category:
            queryset = queryset.filter(category__name=category)
        else:
            category = self.kwargs.get("big")
            queryset = queryset.filter(category__parent__name=category)

        return queryset


class TagView(BaseListView):

    def get_queryset(self):
        tag = self.kwargs["tag"]
        queryset = self.base_queryset().filter(tag__name=tag)
        return queryset


class PostDetailView(generic.DetailView):
    model = Post


paginate_by = 10は10件づつの表示、base_querysetはis_publicがTrue、作成日降順で絞り込みます。
一覧表示する他のViewはこいつを継承して使います。
class BaseListView(generic.ListView):
    paginate_by = 10

    def base_queryset(self):
        queryset = Post.objects.filter(
            is_publick=True).order_by('-created_at')
        return queryset


/でアクセスする一覧表示のビュー。上記のBaseListViewを継承しています。
class PostIndexView(BaseListView):

    def get_queryset(self):
        queryset = self.base_queryset()
        keyword = self.request.GET.get("quick")
        if keyword:
            queryset = queryset.filter(
                Q(title__icontains=keyword) | Q(text__icontains=keyword))

        return queryset


self.base_queryset()で表示する一覧データを作成しますが、ナビバーにあったクイックサーチに何か入力されていた場合は以下の処理を行います。
Qオブジェクトは、OR検索に使います。containsだと大文字小文字も区別し、icontainsは区別しません。
タイトルかテキストに、入力された文字(大文字小文字区別しない)が含まれているものを返します。
        keyword = self.request.GET.get("quick")
        if keyword:
            queryset = queryset.filter(
                Q(title__icontains=keyword) | Q(text__icontains=keyword))


カテゴリ検索です。小カテゴリ、大カテゴリ兼用です。
class CategoryView(BaseListView):

    def get_queryset(self):
        queryset = self.base_queryset()
        category = self.kwargs.get("small")
        if category:
            queryset = queryset.filter(category__name=category)
        else:
            category = self.kwargs.get("big")
            queryset = queryset.filter(category__parent__name=category)

        return queryset



こっちはタグ検索。
class TagView(BaseListView):

    def get_queryset(self):
        tag = self.kwargs["tag"]
        queryset = self.base_queryset().filter(tag__name=tag)
        return queryset


詳細画面。何も言うことはない。
class PostDetailView(generic.DetailView):
    model = Post



blog/torina_blog/templates/torina_blog/base.html
{% load static %}
<!DOCTYPE html>
<html lang="ja">
  <head>
    <!-- Required meta tags always come first -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="x-ua-compatible" content="ie=edge">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/css/bootstrap.min.css" integrity="2hfp1SzUoho7/TsGGGDaFdsuuDL0LX2hnUp6VkX3CUQ2K4K+xjboZdsXyp4oUHZj" crossorigin="anonymous">

    <!-- Custom CSS -->
    <link rel="stylesheet" href="{% static 'torina_blog/css/base.css'  %}">

  </head>
  <body>
    <nav class="navbar navbar-dark bg-primary">
      <button class="navbar-toggler hidden-sm-up" type="button" data-toggle="collapse" data-target="#exCollapsingNavbar2">
        &#9776;
      </button>
      <div class="collapse navbar-toggleable-xs" id="exCollapsingNavbar2">
        <div class="container">
          <a class="navbar-brand" href="#">Blog Name</a>
          <ul class="nav navbar-nav">
            <li class="nav-item {% block nav_home %}{% endblock %}">
              <a class="nav-link" href="{% url 'torina_blog:index' %}">Home <span class="sr-only">(current)</span></a>
            </li>
            {% if user.is_authenticated %}
              <li class="nav-item">
                <a class="nav-link" href="{% url 'admin:index' %}">Admin</a>
              </li>
            {% endif %}
          </ul>
          <form action="{% url 'torina_blog:index' %}" method="GET" class="form-inline pull-xs-right">
            <input class="form-control" type="text" name="quick" placeholder="Search">
            <button class="btn btn-outline-success" type="submit">Search</button>
          </form>
        </div>
      </div>
    </nav>

    <div class="jumbotron">
      <div class="container">
        <h1>Django + Bootstrap4 Blog</h1>
        <p class="lead text-muted">DjangoとBootstrap4で作成されました</p>
      </div>
    </div>

    <div class="container">
      <div class="row">
        <div class="col-xs-12 col-sm-8">
          {% block content %}{% endblock %}
        </div>
        <div class="col-xs-12 col-sm-3 offset-sm-1">
          {% include "torina_blog/side.html" %}
        </div>
      </div>
    </div>
    <!-- jQuery first, then Tether, then Bootstrap JS. -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js" integrity="sha384-THPy051/pYDQGanwU6poAc/hOdQxjnOEXzbT+OuUAFqNqFjL+4IGLBgCJC3ZOShY" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.2.0/js/tether.min.js" integrity="sha384-Plbmg8JY28KFelvJVai01l8WyZzrYWG825m+cZ0eDDS1f7d/js6ikvy1+X+guPIB" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/js/bootstrap.min.js" integrity="VjEeINv9OSwtWFLAtmc4JCtEJXXBub00gtSnszmspDLCtC0I4z4nqz7rEFbIZLLU" crossorigin="anonymous"></script>

    <!-- Custom JS -->
    <script src="{% static 'torina_blog/js/base.js' %}"></script>
  </body>
</html>


これはナビバーです。入力フォーム付きです。そしてレスポンシブです。
ログインしている場合は、管理画面へのリンクなんかもつけています。
    <nav class="navbar navbar-dark bg-primary">
      <button class="navbar-toggler hidden-sm-up" type="button" data-toggle="collapse" data-target="#exCollapsingNavbar2">
        &#9776;
      </button>
      <div class="collapse navbar-toggleable-xs" id="exCollapsingNavbar2">
        <div class="container">
          <a class="navbar-brand" href="#">Blog Name</a>
          <ul class="nav navbar-nav">
            <li class="nav-item {% block nav_home %}{% endblock %}">
              <a class="nav-link" href="{% url 'torina_blog:index' %}">Home <span class="sr-only">(current)</span></a>
            </li>
            {% if user.is_authenticated %}
              <li class="nav-item">
                <a class="nav-link" href="{% url 'admin:index' %}">Admin</a>
              </li>
            {% endif %}
          </ul>
          <form action="{% url 'torina_blog:index' %}" method="GET" class="form-inline pull-xs-right">
            <input class="form-control" type="text" name="quick" placeholder="Search">
            <button class="btn btn-outline-success" type="submit">Search</button>
          </form>
        </div>
      </div>
    </nav>


スマホでは、以下のような表示になります。


blog/torina_blog/static/torina_blog/base.css
カスタムcss、jsの読み込みなんか書いてましたが、jsは特に書くものはありませんでした。
cssもほとんど空で、内容は以下です。
jumbotronの背景を白くし、下側に線を引いただけです。
div.jumbotron {
	background-color: #fff;
	border-bottom: .05rem solid #eee;
}


Bootstrap3とは違い、col-sm-offsetではなくoffset-smになりました。
サイドバーは長くなったため、別HTMLにしました。
    <div class="container">
      <div class="row">
        <div class="col-xs-12 col-sm-8">
          {% block content %}{% endblock %}
        </div>
        <div class="col-xs-12 col-sm-3 offset-sm-1">
          {% include "torina_blog/side.html" %}
        </div>
      </div>
    </div>


blog/torina_blog/templates/torina_blog/side.html
<div class="list-group">
  <a href="#" class="list-group-item active text-xs-center">
    All Category
  </a>
  {% for big in big_categories %}
  <a class="list-group-item text-xs-center" data-toggle="collapse" href="#c-{{ big.pk }}" aria-expanded="false" aria-controls="c-{{ big.pk }}">
    {{ big.name }}
  </a>
  <div class="collapse" id="c-{{ big.pk }}">
     <a href="{% url 'torina_blog:category' big.name %}" class="list-group-item">
      {{ big.name }}
    </a>
    {% for small in big.smallcategory_set.all %}
    <a href="{% url 'torina_blog:category' big.name small.name %}" class="list-group-item">{{ small.name }}</a>
    {% endfor %}
  </div>
  {% endfor %}
</div>



<div class="list-group m-t-3">
  <a href="#" class="list-group-item active text-xs-center">
    Tag
  </a>
  <div class="list-group-item">
    {% for tag in tags  %}
     <a href="{% url 'torina_blog:tag' tag.name %}">{{ tag.name }}</a>,
    {% endfor %}
  </div>
</div>


サイドバーなので、以下の画像の部分ですね。


これはカテゴリの一覧です。
真ん中寄せには、text-xs-centerが便利です。xsとありますが、smにしたりサイズによって分けれます。わーお
<div class="list-group">
  <a href="#" class="list-group-item active text-xs-center">
    All Category
  </a>
  {% for big in big_categories %}
  <a class="list-group-item text-xs-center" data-toggle="collapse" href="#c-{{ big.pk }}" aria-expanded="false" aria-controls="c-{{ big.pk }}">
    {{ big.name }}
  </a>
  <div class="collapse" id="c-{{ big.pk }}">
     <a href="{% url 'torina_blog:category' big.name %}" class="list-group-item">
      {{ big.name }}
    </a>
    {% for small in big.smallcategory_set.all %}
    <a href="{% url 'torina_blog:category' big.name small.name %}" class="list-group-item">{{ small.name }}</a>
    {% endfor %}
  </div>
  {% endfor %}
</div>


上の内容は、例えば以下のようなHTMLになります。
data-toggle="collapse" とし、hrefに指定したIDを持つ要素が表示される、ということですね。
<div class="list-group">
  <a href="#" class="list-group-item active text-xs-center">
    All Category
  </a>
  
  <a class="list-group-item text-xs-center" data-toggle="collapse" href="#c-1" aria-expanded="false" aria-controls="c-1">
    プログラミング言語
  </a>
  <div class="collapse" id="c-1">
     <a href="/category/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/" class="list-group-item">
      プログラミング言語
    </a>
    
    <a href="/category/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/Python/" class="list-group-item">Python</a>
    
    <a href="/category/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/Ruby/" class="list-group-item">Ruby</a>
    
    <a href="/category/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/Perl/" class="list-group-item">Perl</a>
    
    <a href="/category/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/PHP/" class="list-group-item">PHP</a>
    
    <a href="/category/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/C/" class="list-group-item">C</a>
    
    <a href="/category/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/Java/" class="list-group-item">Java</a>
    
  </div>
  
  <a class="list-group-item text-xs-center" data-toggle="collapse" href="#c-2" aria-expanded="false" aria-controls="c-2">
    OS
  </a>
  <div class="collapse" id="c-2">
     <a href="/category/OS/" class="list-group-item">
      OS
    </a>
    
    <a href="/category/OS/Windows/" class="list-group-item">Windows</a>
    
    <a href="/category/OS/Mac/" class="list-group-item">Mac</a>
    
    <a href="/category/OS/Unix%E7%B3%BB/" class="list-group-item">Unix系</a>
    
    <a href="/category/OS/Linux%E7%B3%BB/" class="list-group-item">Linux系</a>
    
  </div>
  
</div>



templateには大カテゴリが渡されている(context_processeors)ので、以下のようにして大カテゴリに紐づく小カテゴリが取得できます。
今回のカテゴリに限らず、よく使う書き方です。
{% for big in big_categories %}
  {% for small in big.smallcategory_set.all %}


こっちはタグです。面倒くさくなり、見た目は妥協しました。
tagsもcontext_processorsで渡していましたね。
<div class="list-group m-t-3">
  <a href="#" class="list-group-item active text-xs-center">
    Tag
  </a>
  <div class="list-group-item">
    {% for tag in tags  %}
     <a href="{% url 'torina_blog:tag' tag.name %}">{{ tag.name }}</a>,
    {% endfor %}
  </div>
</div>


m-t-3ですが、これはmargin-topをしてくれます。数字は間隔の大きさで、m-t-0ならmargin-top: 0です。便利ですね。
p-t-1ならpadding-topになりますし、m-l-1ならmarin-left、詳しくは以下のページに載っています。
https://v4-alpha.getbootstrap.com/components/utilities/
現在は、mt-3 等のようにリネームされたようですね。


blog/torina_blog/templates/torina_blog/post_list.html
一覧画面です。
{% extends "torina_blog/base.html" %}
{% load static %}
{% block nav_home %}active{% endblock %}
{% block content %}
  {% for post in post_list %}
    <div class="row m-b-3">
      <div class="col-xs-12 col-sm-6 text-xs-center">
        {% if post.thumbnail %}
          <img class="img-thumbnail" src="{{ post.thumbnail.url }}">
        {% else %}
          <img class="img-thumbnail" src="{% static 'torina_blog/img/noimage.png' %}">
        {% endif %}
      </div>
	    <div class="col-xs-12 col-sm-6">
	      <h4>{{ post.title }}</h4>
	      <span class="tag tag-info">{{ post.category.parent }}</span>
	      <span class="tag tag-primary">{{ post.category }}</span>
	      {% for tag in post.tag.all %}
	        <span class="tag tag-success">{{ tag.name }}</span>
	      {% endfor %}
	      <p class="text-muted">{{ post.created_at }}</p>
        {% if user.is_authenticated %}
          <p>
            <a class="nav-link" href="{% url 'admin:torina_blog_post_change' post.pk %}">管理画面へ</a>
          </p>
        {% endif %}
	      <a class="btn btn-outline-primary btn-lg btn-block" href="{% url 'torina_blog:detail' post.pk %}">続きを読む</a>
	    </div>
    </div>
  {% endfor %}

  <div class="text-xs-center">
    {% include "torina_blog/page.html" %}
  </div>
{% endblock %}


m-b-3は、margin-bottomで下に余白をつけています。
img-thumbnailには以下の指定がされています。
max-width:100%、height:auto;あたりは、img要素をレスポンシブにしたい場合によく使う処理です。
width:100%だと、小さい画像は引き延ばされてきたなくなったりします。
これの親要素にtext-xs-centerをしているので、更に中央寄せです。割と汎用的に使えるのではないでしょうか。
.img-thumbnail {
    padding: .25rem;
    background-color: #fff;
    border: 1px solid #ddd;
    border-radius: .25rem;
    -webkit-transition: all .2s ease-in-out;
    -o-transition: all .2s ease-in-out;
    transition: all .2s ease-in-out;
    display: inline-block;
    max-width: 100%;
    height: auto;
}


以下は、ログイン済みならその記事の更新画面へのリンクをつけています。
        {% if user.is_authenticated %}
          <p>
            <a class="nav-link" href="{% url 'admin:torina_blog_post_change' post.pk %}">管理画面へ</a>
          </p>
        {% endif %}


admin管理画面への各種リンクは、以前に書きました。

Django、管理画面へのリンク
https://torina.top/main/255/

このtagはお洒落なので好きです。tag-pillをつけると丸まります。
<span class="tag tag-info">{{ post.category.parent }}</span>


ページング部分は他HTMLにしています。
  <div class="text-xs-center">
    {% include "torina_blog/page.html" %}
  </div>



blog/torina_blog/templates/torina_blog/page.html
これは他のGETパラメータがあっても正しく動作します。お勧めです。
{% load page %}
<nav aria-label="Page navigation">
  <ul class="pagination">

      {% if page_obj.has_previous %}
        <li class="page-item">
        <a class="page-link" href="?{% url_replace request page_obj.previous_page_number %}" aria-label="Previous">
          <span aria-hidden="true">&laquo;</span>
        </a>
      </li>
      {% endif %}
    
      {% for link_page in page_obj.paginator.page_range %}
        {% if link_page == page_obj.number %}
          <li class="page-item active">
            <a class="page-link" href="?{% url_replace request link_page %}">
              {{ link_page }}
            </a>
          </li>
        {% else %}
          <li class="page-item">
            <a class="page-link" href="?{% url_replace request link_page %}">
              {{ link_page }}
            </a>
          </li>
        {% endif %}
      {% endfor %}
    
      {% if page_obj.has_next %}
        <li class="page-item">
        <a class="page-link" href="?{% url_replace request page_obj.next_page_number %}" aria-label="Next">
          <span aria-hidden="true">&raquo;</span>
        </a>
      </li>
      {% endif %}
    
  </ul>
</nav>


そのかわり、少し手間があります。
app、今回ならばtorina_blogディレクトリ配下に「templatetags」ディレクトリを作成します。
その中に、page.pyを作りましょう。このhtmlの冒頭の、{% load page %}のpage部分はこのモジュール名です。
__init__.pyも、同じ階層につくっときましょう。空でいいです。

blog/torina_blog/templatetags/page.py
from django import template

register = template.Library()

@register.simple_tag
def url_replace(request, value):
    dict_ = request.GET.copy()
    dict_["page"] = value
    return dict_.urlencode()


http://stackoverflow.com/questions/2047622/how-to-paginate-django-with-other-get-variables
からパクりました。他のGETパラメータがあろうが、なかろうが、ちゃんと動くすごいやつです。

どこにディレクトリの作成をするんだ?と悩む場合は、前書いたのを見てもよいです。
Djangoのカスタムフィルタ
https://torina.top/main/240/



blog/torina_blog/templates/torina_blog/post_detail.html
記事の詳細画面です。
{% extends "torina_blog/base.html" %}
{% block content %}
<div class="card card-outline-primary">
  <div class="card-header bg-primary">
    <h1 class="card-title">{{ post.title }}</h1>
  </div>

  <div class="card-block">
    {% if post.is_html %}
      {{ post.text | safe}}
    {% else %}
      {{ post.text | urlize | linebreaks }}
    {% endif %}
    
    <hr>
    {% include "torina_blog/latest_post.html" %}
    
  </div>

  <div class="card-footer">
	  <span class="tag tag-info">{{ post.category.parent }}</span>
	  <span class="tag tag-primary">{{ post.category }}</span>
	  {% for tag in post.tag.all %}
	    <span class="tag tag-success">{{ tag.name }}</span>
	  {% endfor %}
	  <p class="text-muted">{{ post.created_at }}</p>
    {% if user.is_authenticated %}
      <p><a class="nav-link" href="{% url 'admin:torina_blog_post_change' post.pk %}">管理画面へ</a></p>
    {% endif %}
  </div>
</div>
{% endblock %}


cardというものを使用しています。
Bootstrap3のpanelやwell、thumbnailはcardに統合された感じです。
なかなか便利に使えます。
<div class="card card-outline-primary">
  <div class="card-header bg-primary">
    <h1 class="card-title">{{ post.title }}</h1>
  </div>

  <div class="card-block">
    ...
    ...
  </div>

  <div class="card-footer">
    ..
    ..
  </div>
</div>


modelにis_htmlというフィールドがありました。
is_htmlにチェック(True)をいれた場合、{{ post.text | safe}}でHTMLとして解釈されます。
チェックをしていないFalseならば、{{ post.text | urlize | linebreaks }}です。urlizeでurlを<a>タグに変換し、linebreaksで改行を<p>タグ、<br>タグに変換します。
<p>タグがうっとしいな、と思ったらlinebreaksbrで<br>だけにするのも良いでしょう。
    {% if post.is_html %}
      {{ post.text | safe}}
    {% else %}
      {{ post.text | urlize | linebreaks }}
    {% endif %}



最新5件の表示は、長いので別HTMLにしました。
{% include "torina_blog/latest_post.html" %}



models.pyに定義していたget_latest_postメソッドが使われています。
<div class="row">
  <!-- 大カテゴリの最新5件 -->
  <div class="col-xs-12 col-sm-6 m-t-1">
    「{{ post.category.parent.name }}」の最新5件<br>
    {% for latest in post.category.parent.get_latest_post %}
      <a href="{% url 'torina_blog:detail' latest.pk %}">{{ latest.title }}</a><br>
    {% endfor %}
  </div>

  <!-- 小カテゴリの最新5件 -->
  <div class="col-xs-12 col-sm-6 m-t-1">
    「{{ post.category.name }}」の最新5件<br>
    {% for latest in post.category.get_latest_post %}
      <a href="{% url 'torina_blog:detail' latest.pk %}">{{ latest.title }}</a><br>
    {% endfor %}
  </div>
  
  <!-- 各タグの最新5件 -->
  {% for tag in post.tag.all %}
    <div class="col-xs-12 col-sm-6 m-t-1">
      「{{ tag.name }}」の最新5件<br>
      {% for latest in tag.get_latest_post %}
        <a href="{% url 'torina_blog:detail' latest.pk %}">{{ latest.title }}</a><br>  
      {% endfor %}
    </div>
  {% endfor %}     
</div>


コメントとかパンくずリストとかもつけてくぞー