DjangoアプリにAjaxで非同期通信のいいね機能を実装する方法

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に戻っていますね。

別のユーザーを作って同じ記事にいいねしてみると、ちゃんといいねしているユーザーの数がカウントされているのが確認できます。

このエントリーをはてなブックマークに追加

コメントを残す

頂いたコメントは一読した後表示させて頂いております。
反映まで数日かかる場合もございますがご了承下さい。