DjangoアプリにFacebookやTwitterなどでおなじみのいいね機能を実装する方法について解説します。いいね機能自体は汎用ビューのCreateViewなどで簡単に実装できますが、こういった機能はJavaScriptのAjaxを使ってページ遷移を伴わない非同期通信で処理するのが一般的です。
※この記事では弊サイトの Django入門編 を終えている前提で進めます。また、機能の実装先もDjango入門編で制作した掲示板アプリをベースに進めていきます。
目次
完成イメージ
このように投稿詳細ページにいいねボタンを表示し、ユーザーがクリックすると
このようにハートマークの中が塗られたように見え、いいねしたユーザーの数が表示されるいいね機能を実装します。
いいねボタンは1ユーザーにつき1回までしか押せず、もう一度押した場合いいねを解除します。
今回はログイン前提の機能として実装するため、投稿詳細ページにアクセス制御をかけ、ログインしていない状態でアクセスするとログインページにリダイレクトされるようにします。
テーブル設計
いいね機能に必要な値は「誰が」「どの投稿に」いいねをしたのかの2つです。
Likesテーブルを用意し、いいねをしたユーザーをDjangoが予め用意してくれているUserモデルと紐づけ、いいねされた投稿をArticleテーブルの投稿idと紐づけます。
モデル
上記のテーブル設計をモデルに落とし込みます。
from django.db import models from django.urls import reverse class Article(models.Model): author = models.ForeignKey( 'auth.User', on_delete=models.CASCADE, ) content = models.TextField() created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.content def get_absolute_url(self): return reverse('bbs:detail', kwargs={'pk': self.pk}) class Like(models.Model): user_id = models.ForeignKey( 'auth.User', on_delete=models.CASCADE, ) target = models.ForeignKey( Article, on_delete=models.CASCADE )
モデルの作成が完了したらマイグレーションでデータベースに反映します。
python manage.py makemigrations bbs python manage.py migrate
これでテーブルの用意ができました。
ルーティング
bbs/urls.pyにいいね機能のURLを追記します。
from django.urls import path from . import views app_name = 'bbs' urlpatterns = [ path('', views.IndexView.as_view(), name='index'), path('<int:pk>/', views.DetailView.as_view(), name='detail'), path('create/', views.CreateView.as_view(), name='create'), path('<int:pk>/update/', views.UpdateView.as_view(), name='update'), path('<int:pk>/delete/', views.DeleteView.as_view(), name='delete'), path('like/', views.like, name='like'), # 追加 ]
ビュー
from django.urls import reverse_lazy from django.views import generic from .models import Article, Like # Likeを追加 from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import PermissionDenied from django.http import JsonResponse # 追加 from django.shortcuts import get_object_or_404 # 追加 # 中略 class DetailView(LoginRequiredMixin, generic.DetailView): # アクセス制御を追加 model = Article def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # 投稿に対するいいねの数 like_count = self.object.like_set.count() context['like_count'] = like_count if self.object.like_set.filter(user_id=self.request.user).exists(): context['is_user_liked'] = True else: context['is_user_liked'] = False return context # 中略 def like(request): article_pk = request.POST.get('article_pk') context = { 'user_id': f'{ request.user }', } article = get_object_or_404(Article, pk=article_pk) like = Like.objects.filter(target=article, user_id=request.user) if like.exists(): like.delete() context['method'] = 'delete' else: like.create(target=article, user_id=request.user) context['method'] = 'create' context['like_count'] = article.like_set.count() return JsonResponse(context)
投稿詳細ページのビュー(DetailView)にアクセス制御を追加しログインユーザーのみアクセスできるようにします。
そしてcontextの中身にその投稿がいいねされている数と、ログイン中のユーザーが既にいいねしているかどうか、という2つの情報を持たせます。
likeというビュー関数を作成します。これはいいねボタンがクリックした時に呼び出される関数です。
その中でその投稿の詳細ページを呼び出し、いいねテーブルの中から「ログイン中のユーザーかつその投稿」のものを探します。
該当するデータがあればmethodにdeleteを代入、なければmethodにcreateを格納。
like_countにその投稿に紐づいているいいねレコードの数を格納します。
テンプレート
まずは共通テンプレートです。
ハートマークはFont Awesomeを、CSSフレームワークはMDBを利用するので、base.htmlにこれらを読み込むよう記述します。
# 前略 <title>Djangoを使ってみよう!</title> <!-- Font Awesome --> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" rel="stylesheet" /> <!-- MDB --> <link href="https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/3.10.1/mdb.min.css" rel="stylesheet" /> # 中略 <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/4.0.0/mdb.min.js"></script> {% block extrajs %} {% endblock %} </body> </html>
また、非同期通信にAjaxを利用するのでその読み込みと、いいねボタンが押された時に動作するJavaScriptを入れるためのextrajsブロックも用意しました。
最後に詳細ページのテンプレートにいいねボタンの表示と押された時の処理を追記します。
{% block extrajs %} <script type="text/javascript"> // いいねボタンが押された時 document.getElementById('ajax-like').addEventListener('click', e => { e.preventDefault(); const url = '{% url "bbs:like" %}'; fetch(url, { method: 'POST', body: `article_pk={{ article.pk }}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 'X-CSRFToken': '{{ csrf_token }}', }, }).then(response => { return response.json(); }).then(response => { // いいね数を書き換える const counter = document.getElementById('like-count') counter.textContent = response.like_count const icon = document.getElementById('like-icon') // いいねした時はハートを塗る if (response.method == 'create') { icon.classList.remove('far') icon.classList.add('fas') icon.id = 'like-icon' } else { icon.classList.remove('fas') icon.classList.add('far') icon.id = 'like-icon' } }).catch(error => { console.log(error); }); }); </script> {% endblock %} {% block content %} <h1>{{ article.id }}の投稿詳細ページ</h1> <div class="container"> <p>{{ article.author }}:{{ article.created_at }}</p> <p>{{ article.content }}</p> <div class="card-header"> {% if is_user_liked %} <button type="button" id="ajax-like" style="border:none;background:none"> <!-- すでにいいねしている時はfasクラス --> <i class="fas fa-heart text-danger" id="like-icon"></i> </button> {% else %} <button type="button" id="ajax-like" style="border:none;background:none"> <!-- いいねしていないときはfarクラス --> <i class="far fa-heart text-danger" id="like-icon"></i> </button> {% endif %} <!-- いいねの数 --> <span id="like-count">{{ like_count }}</span> <span>いいね</span> </div> <!-- ユーザー情報がその投稿のものと一致する場合 --> {% if request.user.id == object.author_id %} <p><a href='{% url "bbs:update" article.pk %}'>編集</a></p> <p><a href='{% url "bbs:delete" article.pk %}'>削除</a></p> {% endif %} </div> <p><a href='{% url "bbs:index" %}'>一覧ページへ戻る</a></p> {% endblock %}
extrajsブロックではいいねボタンが押された時と外された時で、いいねアイコン部分のHTMLに付けるクラス(farとfas)を変えます。
あとはcontentブロックに、ユーザーがそのページにアクセスした時にも、既にいいねしている場合はfasクラスが付いたソースを、いいねしていない時はfarクラスが付いたソースを表示するように記述します。
動作確認
アプリにログインして投稿詳細ページにアクセスするといいねボタンが表示されています。
ボタンをクリックすると
アイコン部分に付けているクラス名が変わり、適用されるCSSが変わったことでハートマークが塗られたように見せることができています。
いいねされた数も増えました。
もう一度いいねボタンをクリックすると
またクラス名が変わり塗られていないハートが表示されます。
いいね数も0に戻っていますね。
別のユーザーを作って同じ記事にいいねしてみると、ちゃんといいねしているユーザーの数がカウントされているのが確認できます。