Python Django チュートリアル SPA編(1)


勉強会用の資料です．
以前本家チュートリアルを辿った資料を作っていましたが，
しばらく放置している間にdjangoのバージョンが2になったのと，
シングルページアプリケーション化を目標にするため，一度仕切り直します．

フロントエンドはvue-cli，バックエンドはdjango(rest-frameworks)を利用して，
本家チュートリアルをSPA化していきます．

例のごとくgitに作業開始，途中のソースコードを置くので，途中から始めるかたはcloneしてください．
ソースコードはgithubに置いてます → https://github.com/usa-mimi/tutorial-spa

# 環境構築

仮想環境を作成してから作業することを推奨します．
仮想環境の作成方法は過去記事等を参考にしてください．
私はvirtualenv（virtualenvwrapper）を使用していますが，
python3.3からはvenvが標準でついているので好きなものを使用してください．

この記事を書いている時点でのpython, pip, Djangoのバージョンは以下の通りです．

```
python 3.6.4
pip 9.0.3
Django 2.0.3
```

> バージョンが多少前後しても動くと思いますが，python3.7でdjango1.11を動かそうとするとエラーになります．

## 仮想環境の作成

まず仮想環境を作成します．
virtualenvwrapperを使用している場合は作り方が異なりますのでこの節はスキップして大丈夫です．

仮想環境の名前は `tutorial-spa` にしました．
また，pythonは2系ではなく3系を使用したいので `-p python3` オプションでpython3を指定しています．

```sh
$ mkvirtualenv -p python3 tutorial-spa
Running virtualenv with interpreter /usr/local/bin/python3
Using base prefix '/usr/local/Cellar/python/3.6.4_3/Frameworks/Python.framework/Versions/3.6'
New python executable in /Users/shimomura/.virtualenvs/tutorial-spa/bin/python3.6
Also creating executable in /Users/shimomura/.virtualenvs/tutorial-spa/bin/python
Installing setuptools, pip, wheel...done.
virtualenvwrapper.user_scripts creating /Users/shimomura/.virtualenvs/tutorial-spa/bin/predeactivate
virtualenvwrapper.user_scripts creating /Users/shimomura/.virtualenvs/tutorial-spa/bin/postdeactivate
virtualenvwrapper.user_scripts creating /Users/shimomura/.virtualenvs/tutorial-spa/bin/preactivate
virtualenvwrapper.user_scripts creating /Users/shimomura/.virtualenvs/tutorial-spa/bin/postactivate
virtualenvwrapper.user_scripts creating /Users/shimomura/.virtualenvs/tutorial-spa/bin/get_env_details
```

## 作業ディレクトリの用意

tutorialアプリのためのディレクトリを作成し，
その中にdjango用のディレクトリとフロント(js)用のディレクトリを配置するようにします．

> チュートリアルのためディレクト階層を深くし，バックエンドとフロントエンドをまとめてますが，
> 実際に作成する場合はそれぞれ別リポジトリで管理するほうがいいです．
> 同じリポジトリにまとめるとフロントとバックエンドのバージョンの差異はなくなりますが，
> フロントとバックエンドの作業分担がしづらくなります．
> また，circleCIなどでpush時に自動テストを実行するCIアプリを利用している場合
> フロントの修正だけでバックエンドのテストが実行されてしまいます．

```sh
$ mkdir tutorial-spa
$ cd tutorial-spa
```

まずはディレクトリを作って移動するだけです．
ここにバックエンドとフロントエンドのプログラムを作っていきます．

### djangoプロジェクトの作成(バックエンドの設置)

ソース: → `f34594e`

仮想環境にworkonした上でまずはdjangoをinstallし，新規プロジェクトを作成します．

```shell:djangoのインストール
(tutorial-spa) $ pip install django
Collecting django
  Using cached Django-2.0.3-py3-none-any.whl
Collecting pytz (from django)
  Using cached pytz-2018.3-py2.py3-none-any.whl
Installing collected packages: pytz, django
Successfully installed django-2.0.3 pytz-2018.3
pip install django  3.39s user 2.91s system 93% cpu 6.737 total
```

```shell:tutorialプロジェクト作成
(tutorial-spa) $ django-admin startproject tutorial
```

ここまででこういうファイル構成になっているはずです．

```sh
tutorial-spa/
└── tutorial/
    ├── manage.py
    └── tutorial/
        ├── __init__.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py
```

# pollsアプリケーションの追加

本家: https://docs.djangoproject.com/ja/2.0/intro/tutorial01/

pollsアプリを追加します．
本家ではviews.pyを編集してますが，API化する場合は通常のレスポンスは必要ないの記述は不要です．

## 追加
ソース: `f34594e` → `ac92ba1`

`startapp` でpollsアプリを追加して， `settings.py` のINSTALL_APPSに追加します．

- プロジェクトのルートディレクトリに移動

manage.pyがある，プロジェクトの(tutorial)ルートディレクトリに移動します．
manage.pyを利用して様々なコマンド処理(アプリの新規作成など)をすることができます.

```sh
(tutorial-spa) $ python manage.py startapp polls
```

```python:tutorial/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'polls',
]
```

## polls/models.py の編集

ソース: `ac92ba1` → `f3c16d6`
本家： https://docs.djangoproject.com/ja/2.0/intro/tutorial02/

`polls/models.py` にpollsで使用するモデルを記述します．
> この辺は手抜きで本家からのコピペです．

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


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)
```

## DBへの適用

ソース: `f3c16d6` → `f6cc667`

`makemigrations` コマンドと`migrate` コマンドを実行してDBを作成します．
実行すると `db.sqlite3` ファイルが作成されます．

> `makemigrations` はmodels.py（と存在する場合は前回のmigrateファイル)から，
> DBを作成（もしくは変更／削除）するためのmigrateファイルを作成するコマンドです．
> models.pyを変更した場合は `makemigrations` でmigrateファイルを作成し，
> `migrate` コマンドでDBへ変更を適用する，という流れになります．

初めて実行する場合はdjangoが使用するテーブルも作成されます．

```sh
(tutorial-spa)$ python manage.py makemigrations polls
Migrations for 'polls':
  polls/migrations/0001_initial.py
    - Create model Choice
    - Create model Question
    - Add field question to choice
```
```sh
(tutorial-spa) $ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying polls.0001_initial... OK
  Applying sessions.0001_initial... OK
```

## 管理サイトに追加

ソース: `f6cc667` → `7945f45`

管理サイトからDBの中身を確認できるように， `polls/admin.py` を編集して
`Question` モデルと `Choice` モデルを追加します．

```python:polls/admin.py
from .models import Question, Choice


admin.site.register(Question)
admin.site.register(Choice)
```

## 確認
ソース: `7945f45` （このセクションでの変更なし）

管理画面から見えることを確認します．

- 管理画面に入れるようにsuperuserを作成します．

```shell:superuserを作成
(tutorial-spa) $ python manage.py createsuperuser
Username (leave blank to use 'shimomura'): admin
Email address:
Password:
Password (again):
Superuser created successfully.
```

- 開発サーバを立ち上げ 

```shell:開発サーバを立ち上げ
(tutorial-spa) $ python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
March 31, 2018 - 07:26:23
Django version 2.0.3, using settings 'tutorial.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
```

この状態でブラウザを開き，以下のURLを叩くと管理ページへのログイン画面が表示されるので，
そこで先ほど作成した username, passwordを入力します．

http://localhost:8000/admin/

![Kobito.Hgwi7C.png](https://qiita-image-store.s3.amazonaws.com/0/58048/f339422b-25a0-3d75-2cf6-8f434fb61305.png "Kobito.Hgwi7C.png")

ログイン後，pollsの下に2つのモデルがあることを確認します．

![Kobito.i27V27.png](https://qiita-image-store.s3.amazonaws.com/0/58048/9a3ef701-5e6f-f72c-ce1d-3d4dc87fa6d8.png "Kobito.i27V27.png")

# API化
DjangoRESTframeworkを使用してAPI化を行います．

http://www.django-rest-framework.org/

## ライブラリのインストール／設定
ソース: `7945f45` → `2d3388c`

pipで `djangorestframework` をインストールします．

```sh
(tutorial-spa) $ pip install djangorestframework
```

```python:tutorial/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',  # <-------- 追加
    'polls',
]
```

ついでなのでこのタイミングでtimezoneと言語を日本語に変更します．

```python:tutorial/settings.py
# LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'ja'

# TIME_ZONE = 'UTC'
TIME_ZONE = 'Asia/Tokyo'
```

> ![Kobito.PCyTk2.png](https://qiita-image-store.s3.amazonaws.com/0/58048/ad7589be-93c6-e194-5dc0-6ec41e1c401b.png "Kobito.PCyTk2.png")

これで管理サイトが日本語化されます．

## APIレスポンス(serializerクラス)の定義
ソース: `2d3388c` → `d802bad`

APIの戻り値を設定するためにserializerを定義していきます．
serializerはdjangoの`Form`のようなものですが，表示のための`widget`を持っていません．
その代わり，データを階層的に表現できます．
また，新規作成時の処理，更新時の処理をそれぞれ独立メソッドで定義します．
> ModelFormの場合はsaveメソッド内でinstanceがあるかどうかを自分で記述する必要があります

serializerクラスの定義は慣例的に `serializers.py` ファイルを作り，その中に記述していきます．

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

from .models import Question, Choice


class QuestionSerializer(serializers.ModelSerializer):
    class Meta:
        model = Question
        fields = (
            'id',
            'question_text',
            'pub_date',
        )


class ChoiceSerializer(serializers.ModelSerializer):
    class Meta:
        model = Choice
        fields = (
            'id',
            'question',
            'choice_text',
            'votes',
        )
```

> `fields`は羅列せずに，`fields = '__all__'` と書くこともできます．
> `__all__`と書くとModelの全フィールドが対象となります（ModelFormの__all__と同じです）

## Viewクラスの定義
ソース: `d802bad` → `9aff5a9`

次にAPIのためのViewクラスを定義します．
DjangoRESTFrameworkではAPIに特化してViewクラスを提供しています．
RESTではあるリソースとURIを紐付け，さらに **HTTPメソッド** を **CRUD操作** に紐付ける，ということがよく行われます．
> **CRUD**は，Create, Read, Update, Deleteの略です．

厳密には異なる部分があり，よく議論の対象となっていますが，大抵の場合は
POST→Create，GET→Read，PUT(PATCH)→Update，DELETE→Deleteと割り当てることができます．
そのため，DRFではそれぞれのメソッドを定義した`ViewSet`を提供しています．
今回はその派生クラスである`ReadOnlyModelViewSet`を使ってみます．

なお，ソースコードは通常のviewと区別するため，`api_views.py`というファイルを作成し，そこに記述していきます．

```python:polls/api_views.py
from rest_framework.viewsets import ReadOnlyModelViewSet

from .models import Question
from .serializers import QuestionSerializer


class QuestionViewSet(ReadOnlyModelViewSet):
    queryset = Question.objects.all()  # ここが対象となるレコードの指定．今回は全部
    serializer_class = QuestionSerializer  # 戻り値を定義したSerializer
```

## url設定
ソース: `9aff5a9` → `aac490f`

通常のdjango同様，作成したviewとurlを紐付けます．
今回は`ViewSet`を使用したため，通常のdjangoと若干書き方がことなります．
ViewSetではメソッドをHTTPメソッドと紐付けるために，`as_view()`の呼び出し時に引数に辞書を与え，
`ViewSet.as_view({'get': 'list', 'post': 'create'})` のように書きます．
この操作もほぼ決まりきったルールがあるため，DRFではRouterクラスを提供しています．
RouterはViewSetのメソッドを自動的にURLに展開してくれます．

このソースコードも，通常のurls.pyと区別するため， `api_urls.py`に書くことにします．

```python:polls/api_urls.py
from rest_framework.routers import DefaultRouter

from . import api_views


question_router = DefaultRouter()
question_router.register(r'', api_views.QuestionViewSet)
```

さらにRootのurls.pyを編集し，`polls/api_urls.py` で定義したrouterを紐付けます．

```python:tutorial/urls.py
from django.contrib import admin
from django.urls import path, include  # includeを追加

from polls.api_urls import question_router  # 定義したquestion_routerをimport


api_urlpatterns = [  # apiのURL一覧 (まだquestionだけ)
    path('questions/', include(question_router.urls)),  # 慣例として複数形にする
]

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/1.0/', include(api_urlpatterns)),  # api/1.0/としてapi一覧を登録
]
```

apiはurlの一部に`v1`や`1.0`などのようにバージョン番号を付けておくと後々の改修時に役立ちます．
api用のURLは一箇所でまとめて定義したほうが，フロントエンド開発時見直す時に見やすいです．
今回はしてませんが，`tutorial/api_urls.py` として別に書き出してもいいと思います．

## 確認

admin画面 ( http://localhost:8000/admin/polls/question/ ) から適当なテストデータを登録し，
apiの戻り値を確認してみましょう．

DRFではブラウザからアクセスするとapiの戻り値をキレイに整形して表示してくれます．
`http://localhost:8000/api/1.0/questions/` にアクセスすると以下のように表示してくれるはずです．

![Kobito.kOXyb1.png](https://qiita-image-store.s3.amazonaws.com/0/58048/339dbbac-01e0-d85e-1e1e-fbcd06d719c9.png "Kobito.kOXyb1.png")

また，`ReadOnlyModelViewSet` ではリストの他に，詳細表示用のメソッド (retrieve) が定義されており， 
`DefaultRouter` によって `/pk/` として自動でURLが登録されます．

`http://localhost:8000/api/1.0/questions/1/` にアクセスすると以下のように表示してくれるはずです．

![Kobito.JMy5so.png](https://qiita-image-store.s3.amazonaws.com/0/58048/b9e9d46d-dfd8-f47b-257e-953bdce78365.png "Kobito.JMy5so.png")

# 関連ライブラリの吐き出し

ソース: `aac490f` → `8b39332`
djangoやdjangorestframeworkなど，pipでインストールしたライブラリを書き出しておきましょう．
pip7.1からconstraintsが使えるようになったので，本チュートリアルではそれを活用していきます．

インストール済みのライブラリはシェルで `pip freeze` と打つことで確認できます．
従来のやり方ではこれを `requirments.txt`　に書き出してましたが，今回は `constraints.txt` として書き出します．

```sh
$ pip freeze > constraints.txt
```

また，手動で `requirements.txt` を編集し， `pip install` コマンドで指定したライブラリを書きます．

```text:requirements.txt
Django
djangorestframework
```

ライブラリを復元するときは
`$ pip install -r requirements.txt -c constraints.txt`
とすればOKです．

>`$ pip freeze > requiremnts.txt` で書き出し，
>`$ pip install -r requirements.txt` で復元しても結果としては同じですが，
>`pip install` コマンドではライブラリが必要としている関連ライブラリも自動でインストールされます．
>そのため，自分が必要としていないライブラリまでrequirements.txtに含まれてしまいます．
>
>先ほど`pip freeze`で吐き出した`constraints.txt` の中身を確認すると以下のようになっているはずです．
>
>```text:constraints.txt
Django==2.0.3
djangorestframework==3.7.7
pytz==2018.3
```
>
>pytzはインストールした覚えがないと思いますが，こいつはDjangoの関連ライブラリとして自動でインストールされます．
>このように，自分が本当に必要でインストールしたものと，結果としてインストールされたライブラリ群を分けて管理することで，
>後で必要のないライブラリを削除したり，アップデートを行うことが容易になります．

---

次はフロント側を作っていきます．

→ [チュートリアル2へ](https://qiita.com/maisuto/items/2bf1b072f189a65d6f6e)

→ [チュートリアルまとめ](http://qiita.com/maisuto/items/17653f344d1f64afa019)