Django REST Framework + Vue で選択されたタグに関連する投稿を非同期的に取得し表示する

## きっかけ
ページ遷移せずに、必要なデータだけを取得し表示する方がよりスマートだと思ったため。
[Githubでコードを見る](https://github.com/timchanpon/django_rest_framework_posts_filter_tag/)

## 開発環境
- Python 3.7.3
- Django 3.0.5
- Django REST Framework 3.11.0
- Vue.js 2.6.11 (CDN)
- Axios 0.19.2 (CDN)

※Djangoは、Class-Basedで開発していきます。

## まずは準備から
***１．適当な場所に作った仮想環境に入る。***（環境毎にコマンドが変わるので割愛）
***２．必要なライブラリのインストール***

```bash
pip install django
pip install djangorestframework
```

***３．Djangoプロジェクトの作成***

```bash
# 適当な場所にプロジェクト用ディレクトリを作成
mkdir your_project_name
# 作成したディレクトリに移動し以下のコマンドを打つ
django-admin startproject config .

# 作成されるプロジェクトの中身
└── your_project_name
    │
    ├── config
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    │
    └── manage.py
```
下記のように打つことで、設定ディレクトリの名前をプロジェクト名と別にすることができます。
```django-admin startproject config .```

例）普通に```django-admin startproject your_project_name```と打った場合

```bash
# 作成されるプロジェクトの中身
└── your_project_name  # ここと
    │
    ├── your_project_name  # ここが一緒の名前になるのでややこしい
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    │
    └── manage.py
```

***４．Tagsアプリを作成***

```bash
# manage.py があるディレクトリまで移動
# 移動したら以下のコマンドを打つ
python manage.py startapp tags

# プロジェクトの中身
└── your_project_name
    │
    ├── config
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    │
    ├── tags  # 新しく作成したTagsアプリ
    │   ├── migrations
    │   │   └── __init__.py
    │   ├── __init__.py
    │   ├── admin.py
    │   ├── apps.py
    │   ├── models.py
    │   ├── tests.py
    │   └── views.py
    │
    └── manage.py
```

***５．Tagsアプリを設定ファイルに登録***

```python:config/settings.py
...

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'tags.apps.TagsConfig',  # ここにTagsアプリを登録

    'rest_framework',  # 最初にインストールしたdjango-rest-frameworkも一緒に登録
]

...
```

## いざ開発開始（普通のDjangoアプリ開発と変わりません）

***１．Tagモデルを作成***

```python:tags/models.py
from django.db import models


class Tag(models.Model):
    """タグモデル"""

    name = models.CharField(max_length=10)  # 名前フィールドを追加（最大文字数１０文字）

    def __str__(self):  # 管理画面上で各オブジェクトの識別をし易くするための設定
        return self.name  # オブジェクト自身の名前フィールドの値を返すように設定

```
次にコマンドでマイグレーションファイルを作成し、MigrateでDBに反映

```bash
python manage.py makemigrations
python manage.py migrate
```
最後に、作成したTagモデルを管理サイトに登録

```python:tags/admin.py
from django.contrib import admin

from .models import Tag


admin.site.register(Tag)

```

***２．Viewを追加***

```python:tags/views.py
from django.views import generic

from .models import Tag


class IndexView(generic.ListView):  # 一覧表示に特化したViewを作成
    model = Tag
    template_name = 'index.html'

```

***３．Templateの作成***

```bash
cd tags  # Tagアプリのディレクトリに移動
mkdir templates

# プロジェクトの中身
└── your_project_name
    │
    ├── config
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    │
    ├── tags
    │   ├── migrations
    │   │   └── __init__.py
    │   ├── templates  # 新しくTemplate用のディレクトリを作成
    │   ├── __init__.py
    │   ├── admin.py
    │   ├── apps.py
    │   ├── models.py
    │   ├── tests.py
    │   └── views.py
    │
    └── manage.py
```

```html:tags/templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Index</title>
</head>
<body>
  {% for tag in tag_list %}
    <p>{{ tag.name }}</p>
  {% endfor %}
</body>
</html>
```

とりあえず、Tagオブジェクトの名前をFor分で一覧表示するようにしました。
早速、管理画面へログインしてタグを追加してみましょう。

***４．管理画面からタグを追加する***
管理画面へログインするために、管理者アカウントを作ります。

```bash
python manage.py createsuperuser
ユーザー名：
パスワード：
```
※公式ドキュメントでも、最初にカスタムユーザーモデルを作成するよう強く推奨されていますが、今回は試験的開発なので割愛します。

管理者アカウントを作成したら、コマンドを打ってサーバーを立ち上げます。

```bash
python manage.py runserver
```

サーバーが無事立ち上がったら、[http://127.0.0.1:8000/admin](http://127.0.0.1:8000/admin/) から管理画面へアクセスし、タグを追加してみましょう。

***５．ルーティンの追加***

```python:config/urls.py
from django.contrib import admin
from django.urls import path, include  # includeを追加でインポート


urlpatterns = [
    path('admin/', admin.site.urls),  # 管理サイトへのルーティン

    path('', include('tags.urls')),  # トップページアクセスでTagsアプリに飛ぶように設定
]

```

次に、Tagsアプリ内にもurls.pyファイルを新規作成


```python:tags/urls.py
from django.urls import path

from . import views  # 同ディレクトリからViews.pyをインポート


app_name = 'tags'  # アプリ識別用の名前を設定（オプション）
urlpatterns = [
    path('', views.IndexView.as_view(), name="index"),  # 先ほど追加したIndexViewを指定
]

```

これで、[http://127.0.0.1:8000](http://127.0.0.1:8000/) に接続すれば、先ほど管理サイトで追加したタグが一覧表示されているはずです。

## いよいよ REST Framework を用いての開発

***１．Postsアプリの作成***

```bash
python manage.py startapp posts

# プロジェクトの中身
└── your_project_name
    │
    ├── config
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    │
    ├── posts  # 新しく作成したPostsアプリ
    │   ├── migrations
    │   │   └── __init__.py
    │   ├── __init__.py
    │   ├── admin.py
    │   ├── apps.py
    │   ├── models.py
    │   ├── tests.py
    │   └── views.py
    │
    ├── tags
    │   ├── migrations
    │   │   └── __init__.py
    │   ├── templates
    │   │   └── index.html
    │   ├── __init__.py
    │   ├── admin.py
    │   ├── apps.py
    │   ├── models.py
    │   ├── tests.py
    │   ├── urls.py
    │   └── views.py
    │
    └── manage.py
```

新規作成したPostsアプリを設定ファイルに登録

```python:config/settings.py
...

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'tags.apps.TagsConfig',
    'posts.apps.PostsConfig'  # ここにPostsアプリを登録

    'rest_framework',
]

...
```

***２．Postモデルを作成***

```python:posts/models.py
from django.db import models

from tags.models import Tag  # Tagモデルをインポート


class Post(models.Model):
    """投稿モデル"""

    body = models.TextField(max_length=150)
    tag = models.ForeignKey(Tag, on_delete=models.PROTECT)  # Tagモデルとのリレーション設定

    created_at = models.DateTimeField(auto_now_add=True)  # 作成日時を自動追加するよう設定
    updated_at = models.DateTimeField(auto_now=True)  # 更新されたら日時を更新するよう設定

```

コマンド打つ

```bash
python manage.py makemigrations
python manage.py migrate
```

管理サイトに登録

```python:posts/admin.py
from django.contrib import admin

from .models import Post


admin.site.register(Post)

```

***３．Serializersを追加***

ここから、通常のDjangoアプリ開発とは少し変わってきます。

このSerializersというファイルは、DjangoのモデルオブジェクトをJson形式に変換してくれるものです。

最初から用意されていないので、Postsアプリ内にserializes.pyを新規作成しましょう。

```python:posts/serializers.py
from rest_framework import serializers

from .models import Post


class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ('body', )  # Json形式に変換させたいフィールドを追加

```

***４．Viewを追加***

```python:posts/views.py
from rest_framework import generics

from .models import Post
from .serializers import PostSerializer  # 先ほど作成したシリアライザをインポート


class PostAPIView(generics.ListAPIView):
    serializer_class = PostSerializer  # シリアライザを指定
    queryset = Post.objects.all()  # Postモデルの全オブジェクトをVue.jsに渡すよう設定

```

***５．ルーティンの追加***

```python:config/urls.py
from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    path('admin/', admin.site.urls),

    path('', include('tags.urls')),
    path('api/', include('posts.urls')),  # api/ アクセス時にPostsアプリに飛ぶように設定
]

```

次に、Postsアプリ内にもurls.pyファイルを新規作成

```python:posts/urls.py
from django.urls import path

from . import views


urlpatterns = [
    path('', views.PostAPIView.as_view()),  # 先ほど作成したPostAPIViewを指定
]

```

この時点で、[http://127.0.0.1:8000/api](http://127.0.0.1:8000/api/) に接続すると、Django REST Framework に最初から用意されているテンプレートが、APIから受け取ったJson形式に変換されたPostモデルオブジェクトを一覧表示してくれます。

## いざ、題名に書いてある機能を実装しましょう！

***１．ルーティンの編集***

```python:posts/urls
from django.urls import path

from . import views


urlpatterns = [
    path('<int:tag>/', views.PostAPIView.as_view()),  # TagモデルのIDをパラメータで受け取るように変更
]

```

***２．Viewの編集***

```python:posts/views.py
from rest_framework import generics

from .models import Post
from .serializers import PostSerializer


class PostAPIView(generics.ListAPIView):
    serializer_class = PostSerializer

    # queryset = Post.objects.all() を以下に変更
    def get_queryset(self):
        tag = self.kwargs['tag']  # 渡されたパラメータの値をtagに代入
        if tag == 0:  # tagで条件分岐
            # order_by('-created_at') で最新の投稿順に配列に格納されるように設定
            queryset = Post.objects.all().order_by('-created_at')
        else:
            queryset = Post.objects.filter(tag=tag).order_by('-created_at')

        return queryset

```

***３．Templateの編集***

```html:tags/templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Index</title>
  <!-- CDNでVue.jsとAxiosをインポート -->
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>

<div id="app">
  <!-- タグをセレクトタグで表示 -->
  <select v-model="tag">
    <option value="0">全てのポスト</option>
    {% for tag in tag_list %}
      <option value="{{ tag.id }}">{{ tag.name }}に関するポスト</option>
    {% endfor %}
  </select>
  <!-- 選択したタグに関連する投稿を表示 -->
  <div v-if="posts">
    <p v-for="post in posts">[[ post.body ]]</p>
  </div>
</div>

<script>
  new Vue({
    el: '#app',
    data: {
      tag: null,
      posts: null,
    },
    watch: {
      // 選択されたタグのIDをパラメータに追加し、APIにリクエスト
      tag: function() {
        if (this.tag) {
          getUrl = 'api/' + this.tag + '/'
          // 返ってきたデータをthis.postsに代入
          axios.get(getUrl).then(res => (this.posts = res.data))
        }
      },
    },
    // Vueデフォルトの "{{ }}" ではDjangoのそれと被ってしまい
    // うまく識別されないので "[[ ]]" に変更
    delimiters: ['[[', ']]'],
  })
</script>

</body>
</html>

```

## 完成！