Django REST Framework + Vue で都道府県と連携した市区町村プルダウンメニューを作成

# 目標
選択された都道府県に属する市区町村を選択候補とするプルダウンメニューの作成

[Githubでコードをみる](https://github.com/timchanpon/django_rest_framework_pulldown_address_form/)

# 開発環境
- 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
```

### ２．プロジェクト作成

```bash
# プロジェクト用ディレクトリ作成
mkdir your_project_name
cd your_project_name

# プロジェクト作成
django-admin startproject config .

# 作成されるプロジェクトの中身
└── your_project_name
    │
    ├── config
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    │
    └── manage.py
```

### ３．言語＆タイムゾーン設定変更

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

LANGUAGE_CODE = 'ja'  # 英語から日本語に変更

TIME_ZONE = 'Asia/Tokyo'  # タイムゾーンを東京に変更

USE_I18N = True

USE_L10N = True

USE_TZ = True

...
```
今回は試験的開発のため、データベースはデフォルトのSQLiteのままでいきます。

# さっそく開発開始（バック処理）

### １．アプリを作成

```bash
python manage.py startapp addresses
```

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

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

    'addresses.apps.AddressesConfig',  # 作成したアプリを登録

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

...
```

### ２．モデルを作成

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


class Pref(models.Model):
    """都道府県モデル"""

    name = models.CharField(max_length=5)

    def __str__(self):
        return self.name


class City(models.Model):
    """市区町村モデル"""

    pref = models.ForeignKey(Pref, on_delete=models.PROTECT)
    name = models.CharField(max_length=15)

    def __str__(self):
        pref_name = self.pref.name
        city_name = self.name

        return pref_name + city_name

```
コマンドでDBに変更を反映

```bash
python manage.py makemigratios
python manage.py migrate
```
作成したモデルを管理サイトに登録

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

from .models import Pref, City


admin.site.register(Pref)
admin.site.register(City)

```

### ３．シリアライザを作成

このシリアライザファイルが、モデルオブジェクトをJson形式に変換してくれます。

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

from .models import City


class CitySerializer(serializers.ModelSerializer):
    class Meta:
        model = City
        fields = ('name', )  # Json形式に変換してほしいフィールドを追加

```

### ４．Viewを追加

```python:addresses/views.py
from django.views import generic
from rest_framework import generics

from .models import Pref, City
from .serializers import CitySerializer


class PrefListView(generic.ListView):  # 通常のListViewを指定
    model = Pref
    template_name = 'index.html'


class CityAPIView(generics.ListAPIView):  # rest-frameworkのListViewを指定
    serializer_class = CitySerializer

    # パラメータから受け取った都道府県オブジェクトに属する
    # 市区町村オブジェクトを返すよう設定
    def get_queryset(self):
        pref = self.kwargs['pk']
        queryset = City.objects.filter(pref=pref)

        return queryset

```

### ５．ルーティンを追加

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

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

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

```

Addressesアプリにもurls.pyを作成

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

from .views import PrefListView, CityAPIView


urlpatterns = [
    # トップページアクセスをPrefListViewに指定
    path('', PrefListView.as_view()),
    # 選択された都道府県オブジェクトのパラメータを受け取るAPIルートを追加
    path('city/<int:pk>/', CityAPIView.as_view()),
]

```

# 開発（フロント処理）
### １．都道府県プルダウンメニューを追加

```html:addresses/templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Index</title>
</head>
<body>

<select>
  <!-- for文で、都道府県オブジェクトをプルダウン表示 -->
  {% for pref in pref_list %}
    <option value="{{ pref.pk }}">{{ pref.name }}</option>
  {% endfor %}
</select>

</body>
</html>

```

### ２．都道府県プルダウンメニューをVueと連携

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

<div id="app">
  <!-- v-modelで、選択された都道府県オブジェクトのpk値をprefに代入 -->
  <select v-model="pref">
    {% for pref in pref_list %}
      <option value="{{ pref.pk }}">{{ pref.name }}</option>
    {% endfor %}
  </select>
</div>

<script>
  new Vue({
    el: '#app',
    data: {
      pref: null,
    },
  })
</script>

</body>
</html>

```

### ３．選択された都道府県に属する市区町村オブジェクトをAxiosで取得

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

<div id="app">
  <select v-model="pref">
    {% for pref in pref_list %}
      <option value="{{ pref.pk }}">{{ pref.name }}</option>
    {% endfor %}
  </select>
</div>

<script>
  new Vue({
    el: '#app',
    data: {
      pref: null,
      cityList: null,
    },
    watch: {
      pref: function() {
        // prefに値が代入された場合
        if (this.pref) {
          getUrl = 'city/' + this.pref + '/'
          // それに属する市区町村オブジェクトを取得しcityListに代入
          axios.get(getUrl).then(res => (this.cityList = res.data))
        }
      },
    },
  })
</script>

</body>
</html>

```

### ４．市区町村プルダウンメニューを追加

```html:addresses/templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Index</title>
  <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="pref">
    {% for pref in pref_list %}
      <option value="{{ pref.pk }}">{{ pref.name }}</option>
    {% endfor %}
  </select>
  <!-- 都道府県が選択された場合のみ選択可能になるよう設定 -->
  <select :disabled="!pref">
    <!-- v-forで取得した市区町村オブジェクトをプルダウン表示 -->
    <option v-for="city in cityList">[[ city.name ]]</option>
  </select>
</div>

<script>
  new Vue({
    el: '#app',
    data: {
      pref: null,
      cityList: null,
    },
    watch: {
      pref: function() {
        if (this.pref) {
          getUrl = 'city/' + this.pref + '/'
          axios.get(getUrl).then(res => (this.cityList = res.data))
        }
      },
    },
    // Vueデフォルトの "{{ }}" だと、Djangoのそれと被ってしまい
    // うまく表示されないため "[[ ]]" に変更
    delimiters: ['[[', ']]'],
  })
</script>

</body>
</html>

```

# 実際に動かしてみる
### １．管理者ユーザーを作成

```bash
python manage.py createsuperuser
ユーザー名:
メールアドレス:
Password:
Password (again):

Superuser created successfully.
```

### ２．管理サイトで都道府県＆市区町村オブジェクトを新規作成

まずサーバーを起動

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

無事起動したら [http://127.0.0.1:8000/admin](http://127.0.0.1:8000/admin/) から管理サイトへログインし、オブジェクトを新規作成する。

# 完成！

トップページ [http://127.0.0.1:8000](http://127.0.0.1:8000/) にアクセスすれば結果が確認できます。