【初心者】Djangoでいいね機能を実装してみた（非同期処理）

# はじめに
こんにちは。ryoです。Django初心者です。
Djangoでいいね機能を実装してみました。今回はAjaxを利用し、非同期処理を実現しています。

# そもそもAjaxって何？
Ajaxとは「Asynchronous JavaScript and XML」の略で、JavaScriptでサーバー側と非同期通信を行うための技術です。


Ajaxの非同期通信のおかげで画面遷移なく、サーバー側と通信することができるので、SNSのいいね機能などに幅広く利用されています。

Ajaxや非同期処理の詳細については以下の記事を確認いただければと思います。
[初心者目線でAjaxの説明](https://qiita.com/hisamura333/items/e3ea6ae549eb09b7efb9)

今回はFetchAPIを用いてAjaxを実装していきます。

# サンプルアプリの概要
サンプルアプリとして日記投稿アプリを作りました。まずはいいね機能を実装する前のサンプルコードになります。
（説明のために画面の見栄えなどは気にしてません。必要最低限です。）<br>

まずはmodel.pyです。モデルとして日記モデルといいねモデル（日記に対するいいね）を定義しています。

```python:diary/model.py
from django.db import models
from accounts.models import CustomUser

class Post(models.Model):
    """日記モデル"""
    user = models.ForeignKey(CustomUser, verbose_name='ユーザー', on_delete=models.PROTECT)
    title = models.CharField(verbose_name='タイトル', max_length=30)
    content = models.TextField(verbose_name='本文', blank=True, null=True)
    created_at = models.DateTimeField(verbose_name='作成日時', auto_now_add=True)

class Like(models.Model):
    """いいねモデル"""
    user = models.ForeignKey(CustomUser, verbose_name='ユーザー', on_delete=models.PROTECT)
    post = models.ForeignKey(Post, verbose_name='日記', on_delete=models.PROTECT)
    created_at = models.DateTimeField(verbose_name='更新日時', auto_now_add=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["user", "post"],
                name="like_unique"
            ),
        ]
```
<br>

次にurl.pyです。この後、いいね機能は日記詳細ページに実装するので日記詳細ページを表示する部分のコードのみを示しています。日記一覧ページから日記を選択するとその日記詳細ページに遷移するイメージです。
```python:diary/url.py
from django.urls import path
from . import views

app_name = 'diary'
urlpatterns = [
    path('detail/<int:pk>', views.DetailView.as_view(), name="detail")
]
```
<br>
次にview.pyです。url.py同様、日記詳細ページを表示する部分の処理のみを示しています。

```python:diary/view.py
from django.shortcuts import render
from django.views import generic
from .models import Post,Like

class DetailView(generic.DetailView):
    """日記の詳細ページの表示"""
    model = Post
    template_name = "detail.html"
```
<br>
次に日記詳細ページの画面です。

```html:diary/detail.html（日記詳細ページ）
{% extends 'base.html' %}

{% load static %}

{% block title %}Detail{% endblock %}

{% block header %}{% endblock %}

{% block contents %}
<div class="background left">
    <h1>{{ object.title }}</h1>
    <h6>{{ object.user.username}}</h6>
    <br>
    <p>{{ object.content }}</p>
    <br>
    <h6>{{ object.created_at }}</h6>
</div>
<hr>
{% endblock %}
{% block extrajs %}
{% endblock %}
```

# いいね機能の実装
ここから日記詳細ページにいいね機能を実装します。
いいねは1つの日記につき1人1いいねまでしか出来なく、またいいねしている状態でいいねボタンを押すと言い値が解除されるしようとします。
画面側のイメージとしては投稿日時の下にいいねボタンといいねの件数を表示します。
自分がいいねしていない状態はハートの塗りつぶしを無、いいねした後はハートの塗りつぶしを有として表現しています。

- いいねしていない状態
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2510920/fff885b8-5638-c638-041c-fb89732e0cd8.png)
- いいねしている状態
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2510920/f71bf7e1-fa81-aa57-c4f9-bc714faddaba.png)

サーバー側（View側）の処理としては以下の通りに処理します。
- 日記詳細ページを表示する際に該当日記の自分のいいね状態といいね件数をデータベースより取得し、その状態で日記詳細ページを表示します。
- いいねボタンが押下されると、Ajaxを用いてサーバと非同期通信をします。いいねしていない状態でいいねボタンが押下された場合は、Likeテーブルにデータを追加、いいねしている状態でいいねボタンが押下された場合は、Likeテーブルからデータを削除します。最後に最新のいいねの件数を取得し、画面にレスポンスを返します。

<br>
ここからはサンプルコードにいいね機能のコードを追加していきます。
コードの簡単な解説は最後に記載しています。

まずは日記詳細ページの画面です。

```html:diary/detail.html（日記詳細ページ）
{% extends 'base.html' %}

{% load static %}

{% block title %}Detail{% endblock %}

{% block header %}{% endblock %}

{% block contents %}

<div class="background left">
    <h1>{{ object.title }}</h1>
    <h6>{{ object.user.username}}</h6>
    <br>
    <p>{{ object.content }}</p>
    <br>
    <h6>{{ object.created_at }}</h6>
    {% if is_liked %}
        <button type="button" id="like-post" style="border:none;background:none">
            <i class="fas fa-heart text-danger" id="icon">
                <span id="like_count">{{ like_count }}件のいいね</span>
            </i>
        </button>
    {% else %}
        <button type="button" id="like-post" style="border:none;background:none">
            <i class="far fa-heart text-danger" id="icon">
                <span id="like_count">{{ like_count }}件のいいね</span>
            </i>
        </button>
    {% endif %}
</div>
<hr>
{% endblock %}
{% block extrajs %}
<!-- 投稿に対するお気に入り（Ajax）-->
    <script type="text/javascript">
        document.getElementById('like-post').addEventListener('click', e => {
            e.preventDefault();
            const url = '{% url "diary:like_post" %}';
            fetch(url, {
                method: 'POST',
                body: 'post_pk={{object.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('icon')
            if (response.method == 'create'){
                icon.classList.remove('far')
                icon.classList.add('fas')
                icon.id = 'icon'
            } else {
                icon.classList.remove('fas')
                icon.classList.add('far')
                icon.id = 'icon'
            }
            }).catch(error => {
          console.log(error);
        });
      });
    </script>
{% endblock %}
```
次にurl.pyです。
```python:diary/url.py
from django.urls import path
from . import views

app_name = 'diary'
urlpatterns = [
    path('detail/<int:pk>', views.DetailView.as_view(), name="detail"),
    path('like-post', views.like_post, name="like_post")
]
```
次にview.pyです。

```python:diary/view.py
from django.shortcuts import render
from django.views import generic
from .models import Post,Like
from django.shortcuts import get_object_or_404
from django.http import JsonResponse

class DetailView(generic.DetailView):
    """日記の詳細ページの表示"""
    model = Post
    template_name = "detail.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        post = get_object_or_404(Post, pk=self.object.pk)
        like = Like.objects.filter(post=post, user=self.request.user)
        if like.exists():
            context['is_liked'] = True
        else:
            context['is_liked'] = False
        # 該当の投稿のいいねの数を取得
        context['like_count'] = Like.objects.filter(post=post).count()
        return context

def like_post(request):
    """日記のいいね処理"""
    post_pk = request.POST.get('post_pk')#POSTメソッドのbodyに格納されているpost_pkを取得
    post = get_object_or_404(Post, pk=post_pk)
    like= Like.objects.filter(post=post, user=request.user)
    context = {}
    if like.exists():
        like.delete()
        context['method'] = 'delete'
    else:
        like.create(post=post, user=request.user)
        context['method'] = 'create'
    #該当の投稿のいいねの数を取得
    context['like_count'] = Like.objects.filter(post=post).count()
    return JsonResponse(context)
```
- 日記詳細ページの初期表示
まず、日記詳細ページの初期表示として、viewのDetailViewクラスで該当の日記の自分のいいね状態（is_liked）といいね件数(like_count)をデータベースより取得し、contextに格納して画面に渡しています。
日記詳細ページの画面側としてはいいね状態をもとにアイコン表示を変えています。（アイコンはFont Awesomeを使用しています。）
[Font Awesome](https://fontawesome.com/)

- いいねボタンを押下したときの画面側の処理（Ajax）
いいねボタンが押下された場合には、{% block extrajs %}内部の処理が実行されます。処理としてはサンプルアプリのURL："diary:like_post"に対して、fetchAPIを用いてpostでリクエストを送信しています。bodyには該当の日記のpkを格納しています。
[JavaScriptのFetch API について](https://qiita.com/sotasato/items/31be24d6776f3232c0c0)

- いいねボタンを押下したときのサーバー側の処理
画面からpostされたリクエストはviewのlike_postメソッドで処理されます。データベースより該当の日記のいいね状態を取得します。いいねしていない状態でリクエストを受理した場合は、Likeテーブルにデータを追加、いいねしている状態でリクエストを受理した場合は、Likeテーブルからデータを削除します。最後に最新のいいねの件数を取得し、画面にレスポンスを返します。

- サーバーからのレスポンスを受け取った後の画面側の処理
いいねの件数とアイコンの表示状態をjavascriptで書き換えています。

# 動かしてみる
あとは動かしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2510920/fff885b8-5638-c638-041c-fb89732e0cd8.png)
↓いいねボタンクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2510920/f71bf7e1-fa81-aa57-c4f9-bc714faddaba.png)
↓もう一回いいねボタンクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2510920/fff885b8-5638-c638-041c-fb89732e0cd8.png)

無限ポチポチはなかなかストレス発散になりますね。

# 終わりに
今回はAjax(fetchAPI)を用いて、Djangoでいいね機能を実装してました。
今回、fetchAPIを初めて使ってみましたが非同期処理が簡単にかけてびっくりでした。
まだまだ初心者の私なので、もしより良い実装方法があればぜひ教えていただけると嬉しいです。

# 参考
[Djangoでイイね機能を実装する方法について](https://qlitre-weblog.com/django-iine-ajax-create/)