【Django】ブログサイトにカテゴリー・タグ・関連記事機能を構築する


投稿日 2019年12月19日 >> 更新日 2023年3月2日

今回はDjangoのブログサイトにカテゴリー(Category)・タグ(Tag)・関連記事(Related Article)機能を構築し、記事のジャンルを明確にしていきたいと思います。

ちなみにこちら「【Django】サイト内検索機能を組み込んで複数のキーワード入力に対応させる」ではサイト内の検索機能について紹介しているので合わせてご覧ください。

すでにベースとなるブログサイトがある状態から始めていくので、エラーなどに気を付けつつ関数(function)定義・クラス(class)定義それぞれの実装方法で進めて行きたいと思います。

実行環境&使用ライブラリ

実行環境
Windows Subsystem for Linux
Python 3.6.9
pip 9.0.1
使用ライブラリ ライセンス
Django==2.2.6 BSD

構築前のブログサイト概要

使用するブログサイトは、既に数枚の記事が保存されている状態から構築していきます。

「models.py」では「Blog」というモデルだけがあります。

# project/blog/models.py

from django.db import models

class Blog(models.Model):
    title = models.CharField('タイトル', max_length=50)
    text = models.TextField('テキスト')
    created_at = models.DateField('作成日', auto_now_add=True)
    updated_at = models.DateField('更新日', auto_now=True)

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = 'ブログ'
        verbose_name_plural = 'ブログ'

「views.py」は関数定義です。

# project/blog/views.py

from django.shortcuts import render, get_object_or_404
from .models import Blog


def index(request):
    blog = Blog.objects.order_by('-id')
    return render(request, 'blog/index.html', {'blog': blog })


def detail(request, blog_id):
    blog_text = get_object_or_404(Blog, id=blog_id)
    return render(request, 'blog/detail.html', {'blog_text': blog_text })

テンプレートファイルの「base.html」「index.html」「detail.html」は以下のようになっています。

継承元のHTMLファイル

<!-- project/blog/templates/blog/base.html -->

<!doctype html>
<html>
  <head>

    <title>Blog</title>

  </head>
<body>

     <h1>Blog</h1>
     {% block content %}
     {% endblock %}

</body>
</html>

一覧表示用のHTMLファイル

<!-- project/blog/templates/blog/index.html -->

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

{% block content %}

    <a href='{% url 'blog:index' %}'>
        <p>トップ</p>
    </a>
    <h1>一覧</h1><br>

    {% for blog in blog %}
        <ul>
            <li>
               {{ blog.created_at }}
               {{ blog.title }}
               <a href="{% url 'blog:detail' blog.pk %}">詳細</a>
           </li>
       </ul>
    {% endfor %}

{% endblock %}

詳細表示用のHTMLファイル

# project/blog/templates/blog/detail.html

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

{% block content %}

    <h1>{{ blog_text.title }}</h1>
    <p>{{ blog_text.updated_at }}</p>
    {{ blog_text.text | safe | linebreaksbr }}<hr />
    </br>

    <a href="{% url 'blog:index' %}">トップページに戻る</a>

{% endblock %}

一覧はこのようになります。

記事のカテゴリーを構築

まず始めに、カテゴリー機能を構築していきます。

記事をカテゴリー別で分けたいので、「Category」モデルを作成して「Blog」モデルからForeignKeyフィールドを使って紐づけます。

ここで注意が必要なのは、既にデータなどが保存されている状態でモデルにフィールドを追加する際は何らかのデフォルト値を設定する必要があります。

CharFieldやDateFieldなどは、nullとblankにTrueもしくわdefaultに値を設定していれば問題無くマイグレーションが行えますが、ForeignKeyを使う際は紐づけ先が既にデータがある状態でないといけません。

なので今回は、無難にもCategoryモデルを構築しデータを保存した後にBlogモデルと紐づけを図りたいと思います。

では「models.py」にてCategoryモデルを作成します。

# project/blog/models.py

from django.db import models


""" カテゴリーモデル """
class Category(models.Model):
    name = models.CharField('カテゴリー', max_length=50)

    def __str__(self):
        return self.name


class Blog(models.Model):
    title = models.CharField('タイトル', max_length=50)
    ........
    ........

「admin.py」にも追記します。

# project/blog/admin.py

from django.contrib import admin
from .models import Category, Blog


admin.site.register(Category)
admin.site.register(Blog)

ターミナルにてマイグレーションコマンドを打ちます。


$ python3 manage.py makemigrations

$ python3 manage.py migrate

$ python3 manage.py runserver

好きなカテゴリーを保存します。

保存ができましたら、Blogモデルにカテゴリーと紐づくフィールドを作成します。

# project/blog/models.py

from django.db import models


class Category(models.Model):
    name = models.CharField('カテゴリー', max_length=50)

    def __str__(self):
        return self.name


""" Blogモデル """
class Blog(models.Model):
    title = models.CharField('タイトル', max_length=50)
    text = models.TextField('テキスト')

    """ 追加 """
    category = models.ForeignKey(
                    Category, verbose_name='カテゴリー',
                    on_delete=models.PROTECT
               )
    created_at = models.DateField('作成日', auto_now_add=True)
    updated_at = models.DateField('更新日', auto_now=True)

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = 'ブログ'
        verbose_name_plural = 'ブログ'

先ほどフィールドを追加する際はデフォルト値などを設定しなければならないと言いましたが、マイグレーションする際にも設定することができます。

ただし間違った値などを指定しないように気を付ける必要があります。

ではターミナルを開きマイグレーションコマンドを打ちます。


$ python3 manage.py makemigrations
You are trying to add a non-nullable field 'category' to blog without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 1)今すぐ1回限りのデフォルトを指定します(この列にnull値を持つ既存のすべての行に設定されます)
 2) Quit, and let me add a default in models.py
 2)終了し、models.pyにデフォルトを追加します
Select an option: 1
オプションを選択してください:1

上記のように1回限りデフォルト値を指定します。

すると以下のように打ち込めるので、Categoryモデルに保存されているデータのID(ここでは1のPythonカテゴリーを指定)を入力します。


$ python3 manage.py makemigrations
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> 1

Categoryモデルにしっかり一致されれば、問題無く移行されるのでモデルを反映させることができます。


$ python3 manage.py migrate

$ python3 manage.py runserver

ではadminサイトの追加・編集ページを開いてみましょう。

しっかりと紐づけられています。

記事のカテゴリーをそれぞれ変更したら、表示させるための処理を行います。

blogディレクトリ内に「context.py」ファイルを新規作成し、Category要素をテンプレートへ渡すための処理を定義します。

# project/blog/context.py

from .models import Category


def related(request):
    context = {
        'category_list': Category.objects.all(),
    }
    return context

後にタグ機能を実装する際も「context.py」ファイルに追記します。

Djangoのテンプレートで表示させるには、「context.py」ファイルを設定ファイルでインポートさせる必要があります。

projectディレクトリ内の「settings.py」を開き追記します。

# project/project/settings.py

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',
                'blog.context.related',    # 追記
            ],
        },
    },
]

これでカテゴリーリストを表示させる準備ができました。

次はカテゴリー一覧を表示させる処理を行います。

関数(function)定義

blogディレクトリ内の「urls.py」を編集します。

#project/blog/urls.py

from django.urls import path
from . import views


app_name = 'blog'

urlpatterns = [
        path('', views.index, name='index'),
        path('category/<str:category>/', views.category, name='category'),  # 追記
        path('detail/<int:blog_id>/', views.detail, name='detail'),
]

「views.py」にてカテゴリー一覧表示用の関数を定義します。

# project/blog/views.py

from django.shortcuts import render, get_object_or_404
from .models import Category, Blog  # 追記


def index(request):
    .......


""" カテゴリー一覧 """
def category(request, category):
    category = Category.objects.get(name=category)
    blog = Blog.objects.filter(category=category)
    return render(request, 'blog/index.html',
                   {'category': category, 'blog': blog })


def detail(request, blog_id):
    ......

クラス(class)定義

クラス定義に置き換える場合です。

「urls.py」は以下のようになります。

# project/blog/urls.py

from django.urls import path
from . import views


app_name = 'blog'

urlpatterns = [
        path('', views.IndexView.as_view(), name='index'),
        path('category/<str:category>/', views.CategoryView.as_view(),
                                       name='category'),
        # path('', views.index, name='index'),
        # path('category/<str:category>/', views.category, name='category'),
        path('detail/<int:blog_id>/', views.detail, name='detail'),
]

クラス定義での「views.py」は以下のようになります。

# project/blog/views.py

from django.shortcuts import render, get_object_or_404
from .models import Category, Blog  # 追記
from django.views import generic  # 追記


class IndexView(generic.ListView):
    model = Blog
    template_name = 'blog/index.html'

    def get_queryset(self):
        queryset = Blog.objects.order_by('-id')
        return queryset


""" カテゴリー一覧 """
class CategoryView(generic.ListView):
    model = Blog
    template_name = 'blog/index.html'

    def get_queryset(self):
        category = Category.objects.get(name=self.kwargs['category'])
        queryset = Blog.objects.order_by('-id').filter(category=category)
        return queryset

    """ アクセスされた値を取得し辞書に格納 """
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['category_key'] = self.kwargs['category']
        return context


def detail(request, blog_id):
    ......

あとは関数定義・クラス定義に沿ったオブジェクトをテンプレートファイルに記述していくだけです。

テンプレートファイル(カテゴリー)

関数定義の場合は以下のように「index.html」を編集します。

<!-- project/blog/templates/blog/index.html -->

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

{% block content %}

    <a href='{% url 'blog:index' %}'><p>トップ</p></a>
    <div>
        <h4>カテゴリー</h4>
        <!-- context.pyのオブジェクトを取得しています。(category_list)-->
        {% for category in category_list %}
            <a href='{% url 'blog:category' category %}'>
                <p style='float: left; margin-right: 5px;>
                    {{ category.name }}
                </p>
            </a>
        {% endfor %}
    </div>
    <br>

    <!-- 上記のfloatスタイルの影響を受けないようにしている。(clear)-->
    <h1 style='clear: both;'>一覧</h1>
    <br>

    {% if category %}
        <h3>カテゴリー:{{ category }}</h3>
    {% endif %}

    {% for blog in blog %}
        <ul>
            <li>
                {{ blog.created_at }}
                {{ blog.title }}
                <a href="{% url 'blog:detail' blog.pk %}">詳細</a>
            </li>
        </ul>
    {% endfor %}

{% endblock %}

クラス定義では以下の3か所を関数定義から書き換えるだけです。

<!-- project/blog/templates/blog/index.html -->

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

{% block content %}

    ........
    ........
    <!-- contextの辞書から取得 -->
    {% if category_key %}
        <h3>カテゴリー:{{ category_key }}</h3>
    {% endif %}

    <!-- blog→blog_listに変更 -->
    {% for blog in blog_list %}
        .......
        .......
    {% endfor %}

{% endblock %}

カテゴリーのテンプレートタグ内ではCSSのStyle属性として「float」を設定しpタグを並列に並べています。

「一覧」のh1タグ内Style属性に「clear」を渡しているのは、カテゴリーテンプレートタグで指定した「float」の影響を受けないようにする記述です。

設定ファイルを弄ったので、一度システムを停止し起動させたのち表示してみましょう。

問題無くページ遷移できれば成功です。

タグ・関連記事機能の構築

タグ機能はカテゴリーと少し違って、細かく分類することができます。

カテゴリー機能は「ForeignKey」フィールドを使って1対多の関係を持てるのに対して、タグ機能は多対多の関係を築くことができます。

それを適用させるのが「ManyToMany」フィールドです。

「Tag」モデルを作成し「Blog」モデルと紐づけしますが、「Blog」モデル内にもう一つモデル内の記事を参照する関連記事用のフィールドも作成していきます。

では「models.py」を編集します。

※先ほどForeignKeyフィールドを追加した時と違ってManyToManyフィールドを追加する際は、初期値を設定しなくてもマイグレーションを実行できます。

# project/blog/models.py

from django.db import models


class Category(models.Model):
    .......
    .......


""" Tagモデル"""
class Tag(models.Model):
    name = models.CharField('タグ', max_length=50)

    def __str__(self):
        return self.name


class Blog(models.Model):
    title = models.CharField('タイトル', max_length=50)
    text = models.TextField('テキスト')
    category = models.ForeignKey(
                    Category, verbose_name='カテゴリー',
                    on_delete=models.PROTECT
               )

    """ Tagモデルと紐づけ """
    tag = models.ManyToManyField(Tag, verbose_name='タグ')

    """ Blogモデル内の要素と紐づけ """
    relation = models.ManyToManyField('self', verbose_name='関連', blank=True, null=True)
    created_at = models.DateField('作成日', auto_now_add=True)
    updated_at = models.DateField('更新日', auto_now=True)

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = 'ブログ'
        verbose_name_plural = 'ブログ'

「relation」のフィールド引数ではblankとnullにTrueを設定していますが、これは関連される記事が無かった場合に記事を指定せずに保存することができます。

「admin.py」でTagモデルを追加します。

# project/blog/admin.py

from django.contrib import admin
from .models import Category, Tag, Blog  # 追記


admin.site.register(Category)
admin.site.register(Tag)   # 追記
admin.site.register(Blog)

ターミナルを開きマイグレーションコマンドを打ちます。


$ python3 manage.py makemigrations

$ python3 manage.py migrate

$ python3 manage.py runserver

問題無く進んだら、adminサイトを開きタグを幾つか保存します。

そして追加・編集ページではこのように表示されます。

複数の項目を選択する場合は、キーボード左下の「Ctrl」(Windowsの場合)を押したまま選択を行います。

設定が完了しましたら、表示させる処理を行っていきます。

カテゴリー機能を実装した際に作成した「context.py」を開き、Tag要素をテンプレートへ渡す処理を追記します。

# project/blog/context.py

from .models import Category, Tag  # 追記


def related(request):
    context = {
        'category_list': Category.objects.all(),
        'tag_list': Tag.objects.all(),   # 追記
    }
    return context

そしてタグ一覧を表示するためにblogディレクトリ内の「urls.py」から一連の処理を加えます。

関数(function)定義

# project/blog/urls.py

from django.urls import path
from . import views


app_name = 'blog'

urlpatterns = [
        path('', views.index, name='index'),
        path('category/<str:category>/', views.category, name='category'),
        path('tag/<str:tag>/', views.tag, name='tag'),   # 追記
        path('detail/<int:blog_id>/', views.detail, name='detail'),
]

「views.py」を編集します。

# project/blog/views.py

from django.shortcuts import render, get_object_or_404
from .models import Category, Tag, Blog  # 追記


def index(request):
    .......


def category(request, category):
    .......


""" タグ一覧 """
def tag(request, tag):
    tag = Tag.objects.get(name=tag)
    blog = Blog.objects.filter(tag=tag)
    return render(request, 'blog/index.html', {'tag': tag, 'blog': blog })


def detail(request, blog_id):
    .......

一覧表示用の関数である「index」「category」「tag」には「blog」という同じ変数をrender関数に渡しています。

これは表示させる際の都合上HTMLアフィルに同じ要素を扱えるようにしています。

クラス(class)定義

クラス定義では以下のように「urls.py」を編集します。

# project/blog/urls.py

from django.urls import path
from . import views


app_name = 'blog'

urlpatterns = [
        path('', views.IndexView.as_view(), name='index'),
        path('category/<str:category>/', views.CategoryView.as_view(),
                                       name='category'),
        path('tag/<str:tag>/', views.TagView.as_view(),
                                       name='tag'),
        # path('', views.index, name='index'),
        # path('category/<str:category>/', views.category, name='category'),
        # path('tag/<str:tag>/', views.tag, name='tag'),
        path('detail/<int:blog_id>/', views.detail, name='detail'),
]

クラス定義では以下のように「views.py」を編集します。

カテゴリーで記述したような処理と殆ど変わらないです。

# project/blog/views.py

from django.shortcuts import render, get_object_or_404
from .models import Category, Tag, Blog  # 追記
from django.views import generic   # 追記


class IndexView(generic.ListView):
    ......


class CategoryView(generic.ListView):
    .......


""" タグ一覧 """
class TagView(generic.ListView):
    model = Blog
    template_name = 'blog/index.html'

    def get_queryset(self):
        tag = Tag.objects.get(name=self.kwargs['tag'])
        queryset = Blog.objects.order_by('-id').filter(tag=tag)
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['tag_key'] = self.kwargs['tag']
        return context


def detail(request, blog_id):
    .......

次はテンプレートファイルの設定です。

テンプレートファイル(タグ・関連記事)

関数定義の場合は以下のように「index.html」を編集します。

<!--project/blog/templates/blog/index.html -->

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

{% block content %}

    <a href='{% url 'blog:index' %}'><p>トップ</p></a>
    <div>
        <h4>カテゴリー</h4>
        {% for category in category_list %}
            <a href='{% url 'blog:category' category %}'>
                <p style='float: left; margin-right: 5px;>
                    {{ category.name }}
                </p>
            </a>
        {% endfor %}
    </div>
    <br>

    <!-- タグ用 -->
    <div style='clear: both;'>
        <h4>タグ</h4>
        {% for tag in tag_list %}
            <a href='{% url 'blog:tag' tag %}'>
                <p style='float: left; margin-right: 5px;>
                    {{ tag.name }}
                </p>
            </a>
        {% endfor %}
    </div>

    <h1 style='clear: both;'>一覧</h1>
    <br>

    <!-- タグ用も追記 -->
    {% if category %}
        <h3>カテゴリー:{{ category }}</h3>
    {% elif tag %}
        <h3>タグ: {{ tag }}</h3>
    {% endif %}

    {% for blog in blog %}
        <ul>
            <li>
                {{ blog.created_at }}
                {{ blog.title }}
                <a href="{% url 'blog:detail' blog.pk %}">詳細</a>
            </li>
        </ul>
    {% endfor %}

{% endblock %}

関数定義用からクラス定義用に変更した場合は以下のようになります。

<!-- project/blog/templates/blog/index.html -->

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

{% block content %}
    ........
    ........

    <!-- タグ用 -->
    <div style='clear: both;'>
        <h4>タグ</h4>
        {% for tag in tag_list %}
            <a href='{% url 'blog:tag' tag %}'>
                <p style='float: left; margin-right: 5px;>
                    {{ tag.name }}
                </p>
            </a>
        {% endfor %}
    </div>

    <h1 style='clear: both;'>一覧</h1>
    <br>

    <!-- クラス定義にした場合 -->
    <!-- タグ用も追記 -->
    {% if category_key %}
        <h3>カテゴリー:{{ category_key }}</h3>
    {% elif tag_key %}
        <h3>タグ: {{ tag_key }}</h3>
    {% endif %}

    {% for blog in blog_list %}
        .......
        .......
    {% endfor %}

{% endblock %}

関連記事の表示は、Blogモデルでフィールドを追加した「relation」を呼び出すだけです。

詳細表示の「detail.html」を編集します。

<!-- project/blog/templates/blog/detail.html -->

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

{% block content %}

    <h1>{{ blog_text.title }}</h1>
    <p>{{ blog_text.updated_at }}</p>
    {{ blog_text.text | safe | linebreaksbr }}<hr />
    </br>

    <a href="{% url 'blog:index' %}">トップページに戻る</a>

    <!-- 関連記事の呼び出し -->
    <h1>関連項目</h1>
    {% if blog_text.relation.all %}
        {% for blog in blog_text.relation.all %}
            <a href='{% url 'blog:detail' blog.pk %}'>
                <li>{{ blog }}</li>
            </a>
        {% endfor %}
    {% else %}
        <h2>ありません<h2>
    {% endif %}

{% endblock %}

関連記事の部分では少し複雑そうに見えますが、条件分岐の{% if %}タグは無くても処理できます。

単に関連する記事が存在しない場合に「ありません」という表示をさせたかったので、{% if %}を使いました。

そして要素の取り出しはイテレーションを使い「blog_text.relation.all」として順番に取り出しています。

一覧ではこのように表示されます。

タグをクリックすると下図のように絞り込まれているのが分かると思います。

記事の詳細ページを開くと関連記事が表示されています。

関連記事が存在しない場合は{% if %}の条件分岐Falseによって「ありません」と表示されます。

カテゴリー・タグリンクのスタイルをオシャレにする

最後にカテゴリーとタグのリンクを押したくなるようなオシャレボタンに変更していきたいと思います。

「index.html」にてpタグの要素をbuttonに変更しStyle属性に色や形を追加していきます。

<!-- project/blog/templates/blog/index.html -->

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

{% block content %}

    <a href='{% url 'blog:index' %}'><p>トップ</p></a>

    <div>
        <h4>カテゴリー</h4>
        {% for category in category_list %}
            <a href='{% url 'blog:category' category %}'>
                <!-- ボタンとスタイルの変更 -->
                <button style='float: left; 
                                          margin-right: 5px; 
                                          background: lime; 
                                          border-radius: 5px;'>
                    {{ category.name }}
                </button>
            </a>
        {% endfor %}
    </div>
    <br>

    <div style='clear: both;'>
        <h4>タグ</h4>
        {% for tag in tag_list %}
            a href='{% url 'blog:tag' tag %}'>
            <!-- ボタンとスタイルの変更 -->
                <button style='float: left;
                                          margin-right: 5px;
                                          background: deepskyblue;
                                          border-radius: 5px;'>
                    {{ tag.name }}
                </button>
            </a>
        {% endfor %}
    </div>

    <h1 style='clear: both;'>一覧</h1>
    <br>

    ........
    ........

{% endblock %}

「button」のStyle属性内にある「background」ではカラーを指定、「border-radius」では丸みを帯びた形に設定しています。

ブラウザによって「border-radius」のスタイルがくずれてしまう恐れがあるので、SafariとChromeでは「-webkit-border-radius」、Firefoxでは「-moz-border-radius」とすることで対応するとのことです。

では表示してみます。

万能でちょっぴりオシャレなブログサイトに生まれ変わりました。

まだまだ追加すべき機能はあるのでどんどん試しながらオリジナルアプリを開発していきましょう!

それでは良い開発ライフを!

最後までご覧いただきありがとうございました。