「Djangoで始めるブログ作成講座」の第五回です。
前回はDjangoでページング機能を実装しました。
「Djangoで始めるブログ作成講座」の第四回です。 前回はブログにコメント投稿機能を実装し、管理画面のカスタマイズも同時に行いましたね。 [sitecard subtitle=関連記事 url=https://freemas.st[…]
今回はブログによくあるカテゴリー機能を実装していきましょう。
この記事を読んで取り組むことで、Djangoの以下の基本的な使い方を習得することができます。
- DjangoのModelでのURL作成関数の作り方
- レシーバー関数について
- 初期データ登録の仕方
- Djangoにおけるマイグレーション順序の注意点
- クラスベースビューの継承の仕方
コメント投稿機能の時と同様に、今回もDjangoの多くのことが学べる回となっています。
便利機能が盛りだくさんですので、楽しく作りながら学んでいきましょう!
Djangoで始めるブログ作成講座⑤:カテゴリー機能を導入しよう!
カテゴリーのModelを作成する
新たにmodels.pyに追記します。このモデルは現在あるモデルの中で一番上(from文の直下)に記載してください。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | -- 省略 -- class Category(models.Model):     name = models.CharField(max_length=255)     slug = models.SlugField(unique=True, max_length=255, null=True, blank=True)     description = models.TextField(null=True, blank=True)     created_at = models.DateTimeField(auto_now_add=True)     updated_at = models.DateTimeField(auto_now=True)     def __str__(self):         return self.name     #カテゴリーでの記事一覧を作成するためのURL生成     def get_absolute_url(self):         if self.slug:             return reverse('post_list_by_category', args=[self.slug])         else:             return reverse('post_list_by_category', args=['undefined']) | 
- name:カテゴリーの名前
- slug:カテゴリー名に紐づくURL
- description:カテゴリーの説明
- created_at:カテゴリー作成日時
- updated_at:カテゴリー更新日時
urls.pyの記述からの逆引きでURLを表示する関数を定義しています。
指定された場合には、指定されたカテゴリーの記事一覧を表示するURLを、指定されなかった場合にはundefined、つまり未分類のカテゴリー一覧を表示するURLを返します。
モデルを修正したのでマイグレーションをしておきましょう。
マイグレーション時に、未分類カテゴリーを作成するようにする
ブログのカテゴリーの場合、どんな投稿でも最初は未分類カテゴリーから初めて、執筆途中か完了時にカテゴリーを変更するものとします。
先ほどのmodels.pyのCategoryクラスのすぐ下に、以下の記載を加えてください。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | from django.db import models from django.urls import reverse from django.db.models.signals import post_migrate from django.dispatch import receiver from django.apps import apps from django.contrib.contenttypes.models import ContentType class Category(models.Model):     -- 省略 -- #マイグレーション時にだけ起動する関数を定義 @receiver(post_migrate) def create_default_categories(sender, **kwargs):     #djangoblogアプリマイグレーション時にだけ起動する     if sender.name == apps.get_app_config('djangoblog').name:         #未分類カテゴリーがなければ作成、あれば取得する         category, created = Category.objects.get_or_create(             name='未分類',             slug='undefined',             defaults={                 'description': 'Default category created by migration',             }         )         # キャッシュをクリア         ContentType.objects.clear_cache() | 
Djangoにはデータベースの保存や削除、モデルの作成、マイグレーションの実行など、さまざまなイベントに対して関数が自動的に呼び出されるレシーバー関数と呼ばれる仕組みがあります。
上記のように「@receiver」と記載し、後ろの()にpost_migrateを指定することで、データベースのマイグレーション時のみに動くようにしています。
他のアプリと競合しないように、djagnoblogのアプリに対してマイグレーションが行われた時のみ起動するようにしています。
get_or_create関数は引数で指定されたデータが存在すればそれを取得し、ない場合は新たに作成します。
今回は未分類というカテゴリーを自動で作成するようにしています。
これで初回のみ未分類カテゴリーが作成され、二回目以降は未分類カテゴリーは自動作成されないという構図が出来上がります。
ContentTypeはデータを運ぶ箱のようなものです。
マイグレーションによって構造が変更される場合が多く、キャッシュをクリアすることが推奨されています。
これにより、次回の登録時にデータベースのデータと衝突する可能性が減ります。
この状態でマイグレーションを行うと、データベースに未分類のカテゴリーが登録されます。
投稿モデルにカテゴリーモデルを紐づける
次はカテゴリーを投稿に紐づけていきましょう。models.pyに追記します。
| 1 2 3 | class Post(models.Model):     -- 省略 --     categories = models.ForeignKey(Category, on_delete=models.SET_DEFAULT, related_name='posts', default=1) | 
外部結合でCategoryモデルと紐づき、削除時の動きはデフォルトに戻すとしています。
defaultには1、つまり先ほど作成した未分類のカテゴリーのIDが指定されています。
これでカテゴリーが削除されても未分類カテゴリーに戻ります。
さて、もう一度マイグレーションをしてみましょう。
defaultの引数に1を入力したので、全ての投稿のカテゴリーが未分類に設定されます。
今回はModelの更新を3回に分けて行いましたが、これは順序を間違えると正しく動作しないためです。
例えばPostモデルから先に実装してもCategoryモデルがなければエラーになります。
さらに言えば、Categoryモデル、Postモデルの順で実行してもデータが入っていないので、defaultの1のデータがないと怒られることになります。
このようにModelの実行順番はどのように組み立てていくのかを論理的に考えて実行する必要があることを覚えておくと良いでしょう。
カテゴリーを管理する機能を管理画面に追加する
Modelが実装できたので、実際にカテゴリーをデータベースに追加してみましょう。
admin.pyに以下のように記載して、管理画面にCategoryの項目を出現させます。
| 1 2 3 4 5 6 7 8 9 10 11 | from django.contrib import admin from .models import Post, Comment,Category -- 省略 -- class CategoryAdmin(admin.ModelAdmin):     list_display = ('name', 'slug', 'created_at', 'updated_at') admin.site.register(Post, PostAdmin) admin.site.register(Comment, CommentAdmin) admin.site.register(Category, CategoryAdmin) | 
今回は3つのカテゴリーを追加しました。最初に追加した未分類と合わせて4つのカテゴリーがあるはずです。
この3つのカテゴリーを、作成した記事に適当に反映させておきましょう。
Modelを更新してマイグレーションした段階で、投稿のデータ編集ページに以下のようなプルダウンが設定されているはずです。
Categoriesと書かれている部分をお好みで変更しておいてください。
カテゴリー一覧画面用のViewを作成する
続いてViewsの作成ですが、今回はカテゴリーの一覧ページを作成することとします。
その際、Views.pyのコードは以下のようになります。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | from django.shortcuts import redirect, get_object_or_404 from .models import Post, Category from django.views.generic import ListView, DetailView from .forms import CommentForm class PostListView(ListView):     model = Post     template_name = 'post_list.html'     context_object_name = 'posts'     ordering = '-id'     paginate_by = 5 -- 省略 -- #PostListViewを継承してカテゴリー一覧を作成 class PostListByCategoryView(PostListView):     template_name = 'post_by_category.html'     #カテゴリーで絞り込み     def get_queryset(self):         queryset = super().get_queryset()         category = get_object_or_404(Category, slug=self.kwargs['slug'])         return queryset.filter(categories=category)     #コンテキストにカテゴリー名を追加     def get_context_data(self, **kwargs):         context = super().get_context_data(**kwargs)         context.update({'categories': self.get_queryset().first().categories})         return context | 
新たにカテゴリー一覧ページを作りますが、このページは投稿一覧ページと非常に似通っています。
指定したカテゴリーで絞り込むだけで、他の表示内容は同じですね。
こういった場合に便利なのが継承と呼ばれる機能です。
Djangoのみならず多くのプログラミング言語が備えている機能で、クラスの内容をそのまま丸ごとコピーできてしまいます。

今回の例を図にしてみると上記のようになります。
そしてこの継承の凄いところは、コーディングをするのが「+α」の部分だけということです。
PostListByCategoryView直下で指定しているのは「template_name」のみで、他はありません。
この場合、他の項目に関しては継承元であるPostListViewから以下の内容を引き継いでいます。
- model
- context_object_name
- ordering
- paginate_by
つまりPostモデルを指定する必要もなく、指定していないのに投稿は降順(最近の投稿から順)に並べられていて、かつ5つの投稿で区切られているということです。
これだけ多くの内容をコーディングすることなく継承のみで実現できてしまいます。
しかもやり方は簡単で、Viewの後ろの()に継承したいクラスを入れるだけです。
かなり楽に実装出来ましたね。
とはいえこれでは投稿一覧が表示されるだけです。
このViewでは投稿一覧との差、つまりカテゴリーでの絞り込みを指定する必要があります。
それを行っているのがget_querysetです。
この関数の中でslug、つまりカテゴリのURLと一致するものを絞り込んでいます。
この関数はURLから取得できるので、後ほどurls.pyにカテゴリー名を指定するルーティングを設定すれば良いことになります。

上記の図のようなステップを踏むことで、カテゴリーでの投稿の絞り込みができます。
絞り込みの際には「get_object_or_404」関数が便利です。これを用いることで、データがない場合にエラーページに飛ばすことができます。
カテゴリ―一覧ページではカテゴリー名を表示するために、コンテキストも追加する必要があります。
取得したカテゴリー名をコンテキストに入れて、テンプレートに渡しましょう。
カテゴリー一覧画面のTemplateを作成する
Viewsを作成したので、それに対するTemplateも作成していきましょう。
新たにpost_by_category.htmlというファイルを作成して、以下のように記載します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | {% extends 'blog_base.html' %} {% block content %}     <h1>Category: {{ categories.name }}</h1>     {% for post in posts %}         <div class="card mt-4 mb-4">             {% if post.thumbnail %}                 <img class="card-img-top" src="{{ post.thumbnail.url }}" alt="{{ post.title }}">             {% endif %}             <div class="card-body">                 <h2 class="card-title">{{ post.title }}</h2>                 <p class="card-text">{{ post.content|truncatechars:80 | safe }}</p>                 <a href="{% url 'post_detail' pk=post.id %}" class="btn btn-primary">Read More →</a>             </div>             <div class="card-footer text-muted">                 Posted on {{ post.created_at }}             </div>         </div>     {% endfor %} {% endblock %} | 
このようにあっさりとページを作成できるのもDjangoの良いところですね。
またページング機能についてはblog_base.htmlを継承しているので、HTMLを記載しなくても表示されます。
同時に投稿一覧ページにもカテゴリー名を表示するようにしましょう。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | -- 省略 --         <div class="card-body">             <h2 class="card-title">{{ post.title }}</h2>             <p class="card-text">{{ post.content|truncatechars:80 | safe }}</p>             <a href="{% url 'post_detail' pk=post.id %}" class="btn btn-primary">Read More →</a>         </div>         <div class="card-footer text-muted">             Posted on {{ post.created_at }}             <a href="{{ post.categories.get_absolute_url }}">{{ post.categories.name }}</a> <!-- 追加 -->         </div>         </div> -- 省略 -- | 
これはカテゴリに紐づくURLを作成してくれる関数でした。
カテゴリ―一覧へアクセスするリンクが作成されていることが分かります。
仕上げとしてurlsを作成して、動きを確認
最後にurls.pyを実装して終了です。
アプリのurls.pyに以下のように追記しましょう。
| 1 2 3 4 5 6 7 8 | from django.urls import path from .views import PostListView,PostDetailView, PostListByCategoryView #,PostListByMonthView urlpatterns = [     path('', PostListView.as_view(), name='post_list'),     path('post/<int:pk>/', PostDetailView.as_view(), name='post_detail'),     path('category/<slug:slug>/', PostListByCategoryView.as_view(), name='post_list_by_category'), #追加 ] | 
開発環境を立ち上げて投稿一覧ページにアクセスしましょう。
記事の下に、カテゴリーが表示されているはずです。
カテゴリ―名をクリックしてみると、カテゴリーの一覧ページに飛ぶことができます。
カテゴリ―での絞り込みもきちんと機能していることが分かります。
おわりに
今回も内容の濃い記事となりましたね。ですがその分学びも多く、ためになっていれば幸いです。
ここまでのソースをGithubに掲載しておくので、もしも見返したい場合は確認してみてください。
次の記事では、今回やった内容と同じように、年月別の投稿一覧を表示する機能を実装していきましょう。
「Djangoで始めるブログ作成講座」の第六回です。 前回はDjangoで作成したブログにカテゴリー機能を追加しました。 [sitecard subtitle=関連記事 url=https://freemas.stepupkarao[…]
 
					         
                    




