はじめに
注意 : ここは作業メモ、大した情報は無いですょ。あと、 Androidは今回も出てきません。
Androidのアプリを書いてみようかと思うのですが、それなりのことをやろうとするとどうしてもサーバー側もなんとかしないと便利なものが作れない気がしてます。
ということで、この記事では(私が飽きたり挫折したりしなければ)、簡単なTODOアプリを作ることを目標として、
- サーバー側を Django REST Framework で適当なものを書く
- せっかくなので Vue.js でwebからのインターフェースもつくってみる
- サーバーからRESTで持ってきてAndroid AppからもRecycler Viewをつかって表示したり、TODOの新規作成・変更できるようにする
ということをやっていきたいと思ってます。
今後この記事の続きが作成されるかは謎ですが(ぉ)、一応手元ではRecycler Viewで一覧表示するところくらいまでは確認済みであったりはします。
なお、この記事で作業しているリポジトリをGithubで公開しています。 > rarewin/todoapp-study
間違いの指摘や、もっと良い実装方法等について、紳士的な助言お待ちしております。
今回のパートでは、Androidアプリ……を作る前に、Androidアプリからjsonでデータを取得できるように、
前回 作成したVue+Djangoなwebアプリをherokuにデプロイしたいかと思います。
その際、自由にアクセスされては困るので、APIキーを使って認証できるようにします。
過去の記事
pip+venvからpipenvに乗り換える
前回の環境では pip と venv を使ったのですが、どうやら heroku では、それらをまとめたような便利ツール pipenv がサポートされてるようです。
SoftwareDesignの2018年2月号でも「今後周流のパッケージングツールになると期待」と紹介されていたので、あまり深く考えずに、さっそく乗り換え作業から行きます。
ところで、私の環境は Debian/sid なんですが、どうやらDebianのPythonまわりには pip freeze
で不要な行を出す問題があるようです。 > 参考サイト
これのせいでpipenvのインストール時に問題が発生したので、まずはrequirements.txtから以下の行を削除しておきます。
pkg-resources==0.0.0
つづけて、以下のような感じでコマンドを叩きます。なお、pipenvは予め pip install pipenv
等でインストール済みとします。
% pipenv --python 3.6
Creating a virtualenv for this project…
Using /usr/bin/python3.6m to create virtualenv…
⠋Running virtualenv with interpreter /usr/bin/python3.6m
Using base prefix '/usr'
New python executable in /home/rare/.local/share/virtualenvs/todoapp.server-QYHaGtXw/bin/python3.6m
Also creating executable in /home/rare/.local/share/virtualenvs/todoapp.server-QYHaGtXw/bin/python
Installing setuptools, pip, wheel...done.
Virtualenv location: /home/rare/.local/share/virtualenvs/todoapp.server-QYHaGtXw
Requirements.txt found, instead of Pipfile! Converting…
Warning: Your Pipfile now contains pinned versions, if your requirements.txt did.
We recommend updating your Pipfile to specify the "*" version, instead.
つづけて、勧められている通りupdateをかけときます。
% pipenv install
Pipfile.lock not found, creating…
Locking [dev-packages] dependencies…
Locking [packages] dependencies…
Updated Pipfile.lock (a69925)!
Installing dependencies from Pipfile.lock (a69925)…
🐍 ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 13/13 — 00:00:06
To activate this project's virtualenv, run the following:
$ pipenv shell
このあと、メッセージにある通り pipenv shell
とすると、venvで source .venv/bin/activate
をやったような状態になります。
デフォルトだと、環境の作成にpipのユーザのディレクトリが使われる点と、別途シェルが立ち上がってくるあたりが違いでしょうか。
インストールされたディレクトリは pipenv --venv
とすると表示されるようです。
さて、pipからpipenvに乗り換えたので、前回用意したシェルスクリプトは以下の変更を入れました。
diff --git a/todoapp.server/deploy.sh b/todoapp.server/deploy.sh
index 42a3e0f..828098a 100644
--- a/todoapp.server/deploy.sh
+++ b/todoapp.server/deploy.sh
@@ -7,7 +7,7 @@ python format_index_html.py
echo 'Done...'
echo 'Install python modules'
-pip install -r requirements.txt
+pipenv install
echo 'Done...'
echo 'Collect static'
あと、これまでどうもありがとうrequirements.txtさん。
% git rm -f requirements.txt
RESTアクセス用のAPIキーを作成する
さて、誰からでもRESTでアクセスされては困るので、APIトークンでのアクセス制御をしたいと思います。
といっても、既にある資産を使わせていただくだけですが。
今回は、チュートリアルの環境に組み込まれている Django REST frameworkにある機能をそのまま使わせてもらいます。
マニュアルは こちら に。
まずは、settings.pyの INSTALLED_APPS に以下の行を追加します。 (snip snipはチョキチョキの意味なので打ち込まないようお願いします。)
INSTALLED_APPS = [
... snip snip ...
'rest_framework.authtoken',
]
その後、 pipenv shell
してる状態でおもむろに ./manage.py migrate
を実行しましょう。
% ./manage.py migrate
Operations to perform:
Apply all migrations: admin, app, auth, authtoken, contenttypes, sessions
Running migrations:
Applying authtoken.0001_initial... OK
Applying authtoken.0002_auto_20160226_1747... OK
では、次にトークンを発行したいと思います。
方法は色々あるみたいなんですが、最近のバージョンでは manage.py
にコマンド drf_create_token
が実装されているようなので、そちらを使うのが簡単なようです。
おっと、実行する前にちゃんとユーザを作っておきましょう(一敗)。
% ./manage.py createsuperuser
Username (leave blank to use 'xxxx'):
Email address: xxxx@hogehoge.org
Password:
Password (again):
Superuser created successfully.
% ./manage.py drf_create_token <username>
Generated token (ここにトークンの文字列が表示される) for user (引数で入れたusernameが表示)
さて、これだけだとトークンを作っただけでアクセスし放題になってしまうので、以下のようにsettings.pyに追記してトークンでのアクセス制御を有効にします。
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAdminUser',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
'PAGE_SIZE': 10
}
有効になると、以下のようになります。
% curl -X GET -H 'Authorization: Token (トークン文字列)' 'http://localhost:8000/todos/?format=json'
[{"text":"hoge","deadline":null,"priority":null,"done":false,"memo":"afdahoge"}, ....
}
% curl -X GET -H 'Authorization: Token hoge' 'http://localhost:8000/todos/?format=json'
{"detail":"Invalid token."}%
Vueのコードでセッション認証を使うようにする
さて、アクセス制限するようにしたのはよいのですが、今度はVueのフロントエンドからDjango上のデータにアクセスできなくなってしまいました。
ここでVueのコードにトークンを埋めたら、結局アクセスし放題と変わりないので、Djangoのセッション管理機能をそのまま使います。
てっとり早く、差分は以下になります。
ちょっと正確なことは追ってないのですが、 MIDDLEWARE_CLASSES
という定義は古いようで、
最近のDjangoだと MIDDLEWARE
としないと色々と動かないようです?
あと、 DEFAULT_AUTHENTICATION_CLASSES
に rest_framework.authentication.SessionAuthentication
を追加してるのがミソかと思います。
diff --git a/todoapp.server/vuedj/settings.py b/todoapp.server/vuedj/settings.py
index 3c9973a..93d8363 100644
--- a/todoapp.server/vuedj/settings.py
+++ b/todoapp.server/vuedj/settings.py
@@ -45,13 +45,12 @@ INSTALLED_APPS = [
'app',
]
-MIDDLEWARE_CLASSES = [
+MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
@@ -148,5 +147,6 @@ REST_FRAMEWORK = {
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
+ 'rest_framework.authentication.SessionAuthentication',
],
}
diff --git a/todoapp.server/vuedj/urls.py b/todoapp.server/vuedj/urls.py
index e1b2384..cc07fec 100644
--- a/todoapp.server/vuedj/urls.py
+++ b/todoapp.server/vuedj/urls.py
@@ -13,16 +13,21 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
-from django.conf.urls import url, include
+from django.conf.urls import (
+ url,
+ include
+)
+from django.contrib import admin
+from django.urls import path
from rest_framework import routers
-
from app import views
router = routers.SimpleRouter()
router.register(r'todos', views.TodoViewSet)
urlpatterns = [
+ path('admin/', admin.site.urls),
url(r'^$', views.index, name='home'),
- url(r'^', include(router.urls))
+ url(r'^', include(router.urls)),
]
なんにせよ、上記の変更で http://localhost:8000/admin/ にアクセスするとログイン画面が表示されるようになり、
トークン作る際に作成した管理ユーザでログインすると、無事にVueで書いたフロントエンドからデータをさわれるようになり…… ませんでした。
どうやら、一覧表示のGETはうまくいくのですが、POSTのときだけ失敗しています。
ブラウザのデバッガを見てると、以下のようなエラーが出ていました。
{…}
body: {…}
detail: "CSRF Failed: CSRF token missing or incorrect."
__proto__: Object { … }
bodyText: "{\"detail\":\"CSRF Failed: CSRF token missing or incorrect.\"}"
headers: {…}
map: {…}
StackOverflowのこの記事を見てみると、セッション認証を使う場合はCSRFトークンを付けろとありました。
StackOverflowからのリンクの張られているDjangoのページでも AjaxでPOSTリクエストをする場合は、
CSRFトークンをクッキーから取得して渡せとありました。
JavaScript Cookie libraryを使えば楽勝だとも書いてあるので、今回はそちらに従います。
ということで、ひとまず npm コマンドでライブラリを入れます。
で、POSTのリクエストのヘッダに X-CSRFToken
を付けて……とやっていたら、なんと、vue-resourceは引退しているらしい ということがわかりました。
なにそれこわい……。この世界は色々とはやい……。
ということで、ついでに、代わりの axios に乗り換えておきます。
そしてさようなら、vue-resourceくん……。
% npm install --save js-cookie axious
% npm uninstall --save vue-resource
ここまでやって、ようやくPOSTリクエストとDELETEリクエストのヘッダに X-CSRFToken
をつけることにします。
store.js
と api.js
を以下のようにしました。
import Vue from 'vue'
import Vuex from 'vuex'
import api from './api.js'
Vue.use(Vuex)
const apiRoot = 'http://localhost:8000'
const store = new Vuex.Store({
state: {
todos: []
},
mutations: {
'GET_TODOS': function (state, response) {
state.todos = response.data
},
'ADD_TODO': function (state, response) {
state.todos.push(response.data)
},
'CLEAR_TODOS': function (state) {
const todos = state.todos
todos.splice(0, todos.length)
},
'API_FAIL': function (sate, error) {
console.error(error)
}
},
actions: {
getTodos (store) {
return api.get(apiRoot + '/todos/')
.then((response) => store.commit('GET_TODOS', response))
.catch((error) => store.commit('API_FAIL', error))
},
addTodo (store, todo) {
return api.post(apiRoot + '/todos/', todo)
.then((response) => store.commit('ADD_TODO', response))
.catch((error) => store.commit('API_FAIL', error))
},
clearTodos (store) {
return api.delete(apiRoot + '/todos/clear_todos/')
.then((response) => store.commit('CLEAR_TODOS'))
.catch((error) => store.commit('API_FAIL', error))
}
}
})
export default store
import Axios from 'axios'
import Cookies from 'js-cookie'
const csrfToken = Cookies.get('csrftoken')
const requestHeader = {
headers: {
'X-CSRFToken': csrfToken
}
}
const requestHeaderWithNullData = {
headers: {
'X-CSRFToken': csrfToken
},
data: {}
}
export default {
get (url, request) {
return Axios.get(url, request)
.then((response) => Promise.resolve(response))
.catch((error) => Promise.reject(error))
},
post (url, request) {
return Axios.post(url, request, requestHeader)
.then((response) => Promise.resolve(response))
.catch((error) => Promise.reject(error))
},
patch (url, request) {
return Axios.patch(url, request, requestHeader)
.then((response) => Promise.resolve(response))
.catch((error) => Promise.reject(error))
},
delete (url) {
return Axios.delete(url, requestHeaderWithNullData)
.then((response) => Promise.resolve(response))
.catch((error) => Promise.reject(error))
}
}
注意点として2点ありました。
-
vue-resource
ではbody
というメンバだったのが、axios
ではdata
になっている - DELETEする場合、第二引数にあたえる辞書に
data
がないとおかしなことになるそうです…… > 参考
とくに後者は罠でした……。
herokuデプロイ用のDjangoの設定をする
というわけで、ようやくherokuにデプロイするところまで辿りつきました。
Djangoのプロジェクトをherokuにデプロイするてっとり早い方法の手順としては、
- django-heroku を使う
- Procfileをつくる
な感じです。
ということで、まずは django-heroku を入れます。これはデータベースや、シークレットキーや、アクセス可能なホストやらの設定をheroku的にいい感じに書き換えてくれるものです。
% pipenv install django-heroku
インストールできたら、以下の差分を投入します。これで設定の一つ目はほぼ完了。らくちん。
diff --git a/todoapp.server/vuedj/settings.py b/todoapp.server/vuedj/settings.py
index 93d8363..b16c5ea 100644
--- a/todoapp.server/vuedj/settings.py
+++ b/todoapp.server/vuedj/settings.py
@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/1.9/ref/settings/
"""
import os
+import django_heroku
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -150,3 +151,6 @@ REST_FRAMEWORK = {
'rest_framework.authentication.SessionAuthentication',
],
}
+
+# Configure Django App for Heroku.
+django_heroku.settings(locals())
つづいて、Procfileの方ですが、herokuでDjangoを動かすさいには Gunicorn というWSGIのモジュールを使うのが一般的みたいです。
ということで、まずは入れましょう。
% pipenv install gunicorn
そして、heroku用のProcfileは以下のようになりました。
web: gunicorn vuedj.wsgi --log-file -
さて、基本は上記の2点で充分かと思いますが、前回作成した環境では追加でいくつか変更なり対応が必要でした。
箇条書きにすると以下の点。
- PythonとNode.jsを両方とも入れて、Node.jsの環境から先に構築する
- Node.jsインストール後にnodeコマンドを走らせる
- 変数
apiRoot
を変更する - サブディレクトリをherokuにpushする
ということで、まずはherokuのappだけ作成し……
% heroku login
Enter your Heroku credentials:
Email: xxxx@xxxxx.xxx
Password: **********
Two-factor code: ******
Logged in as xxxx@xxxxx.xxx
% heroku create
Creating app... done, ⬢ xxxxx-xxxxx-00000
https://xxxxx-xxxxx-00000.herokuapp.com/ | https://git.heroku.com/xxxxx-xxxxx-00000.git
% heroku git:remote -a xxxxx-xxxxx-00000
set git remote heroku to https://git.heroku.com/xxxxx-xxxxx-00000.git
順を追って見ていきます。
PythonとNode.jsを両方とも入れて、Node.jsの環境から先に構築する
通常、HerokuさんはPipfileやらpackage.jsonがあれば良きに計らってくるのですが、今回の環境では PythonとNode.js の二つが必要になります。
さらに言うと、先にNode.jsの環境を構築してから、PythonないしDjango側を構築してほしいのです。
ということで、 Herokuのマニュアル に従って以下のようにNode.js→Pythonと設定できるようにしてあげました。
% heroku buildpacks:set heroku/python
Buildpack set. Next release on xxxxx-xxxxx-00000 will use heroku/python.
Run git push heroku master to create a new release using this buildpack.
% heroku buildpacks:add --index 1 heroku/nodejs
Buildpack added. Next release on xxxxx-xxxxx-00000 will use:
1. heroku/nodejs
2. heroku/python
Run git push heroku master to create a new release using these buildpacks.
Node.jsインストール後にnodeコマンドを走らせる
前回準備した環境では、 deploy.sh
の中でdjangoの起動前に npm run dev
を実行してました。
package.json
の中を見ると、実施には node build/build.js
を実行しているようです。
今回の場合、 package.json
に以下の行を加えることにより、herokuでのNode.js環境構築後にスクリプトを走らせるようにしました。
diff --git a/todoapp.server/package.json b/todoapp.server/package.json
index 8d8bea2..931ddd0 100644
--- a/todoapp.server/package.json
+++ b/todoapp.server/package.json
@@ -12,7 +12,7 @@
"test": "npm run unit && npm run e2e",
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
"build": "node build/build.js",
+ "heroku-postbuild": "node build/build.js"
},
"dependencies": {
"axios": "^0.17.1",
また、 npm run dev
ではdevDependencies
で指定されているパッケージも必要だったので、 NPM_CONFIG_PRODUCTION
の値をfalseにする必要もありました。
% heroku config:set NPM_CONFIG_PRODUCTION=false
Setting NPM_CONFIG_PRODUCTION and restarting ⬢ xxxxx-xxxxx-00000... done, v3
NPM_CONFIG_PRODUCTION: false
変数 apiRoot
を変更する。
実は、地味に一番困りました。
前回の環境では、Vue.jsからDjangoへのアクセスのために用意した store.js
の中で、 apiRoot
という変数を http://localhost:8000/
としてました。
が、これだと当然 heroku 側に向いてくれません。
世の中には process.env.PORT
だとか process.env.HEROKU_APP_NAME
とかを使って云々という情報も転がってましたが、今回JavaScript側が動くのがクライアント側なのでどうしようもありません。
デプロイ時にその辺りを拾って、設定ファイルを生成するしかないかなぁ、と考えてたりしましたが、結局のところ以下で大丈夫でした。なんかの拍子に駄目になりそうな気もするけど。
diff --git a/todoapp.server/src/store/store.js b/todoapp.server/src/store/store.js
index ad5e7c6..4c2e0b4 100644
--- a/todoapp.server/src/store/store.js
+++ b/todoapp.server/src/store/store.js
@@ -4,8 +4,8 @@ import api from './api.js'
Vue.use(Vuex)
-const apiRoot = 'http://localhost:8000'
+const apiRoot = '.'
+
const store = new Vuex.Store({
state: {
todos: []
なお、 HEROKU_APP_NAME
を使いたい場合、 heroku labs:enable runtime-dyno-metadata
してあげないと駄目なので ご注意を(一敗)。
サブディレクトリをherokuにpushする
そして、順番は前後しますがherokuにpushしようとした段階で、今回はAndroidアプリと共存したようなgitリポジトリを作成してしまった事に気付きました。
サブディレクトリだけherokuにデプロイとかできないのかなぁ、とか考えてたら、 みんな大好きStack Overflow にて回答がありました。
git-subtreeをつかえばいけるようです。
あと、subtreeにはforceオプションがないのですが、 こういったむずかしいこと をすれば同等の事ができるようです。
% git subtree push --prefix todoapp.server heroku master
git push using: heroku master
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 314 bytes | 314.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
... snip ...
remote: -----> Launching...
remote: Released v7
remote: https://xxxxx-xxxxx-00000.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/xxxxx-xxxxx-00000.git
cc2cec0..2f6aa7f 2f6aa7f8e28661c7e3c1ee003785c18fc7f4304a -> master
その後、migrateコマンドを走らせ、superuserをつくり、REST framework用のtokenもつくり……。
% heroku run python manage.py migrate
Running python manage.py migrate on ⬢ xxxxx-xxxxx-00000... up, run.9898 (Free)
Operations to perform:
Apply all migrations: admin, app, auth, authtoken, contenttypes, 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 app.0001_initial... OK
Applying app.0002_auto_20180112_1451... 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 authtoken.0001_initial... OK
Applying authtoken.0002_auto_20160226_1747... OK
Applying sessions.0001_initial... OK
% heroku run python manage.py createsuperuser
Running python manage.py createsuperuser on ⬢ xxxxx-xxxxx-00000... up, run.6738 (Free)
Username (leave blank to use 'u38481'): xxxx
Email address: xxxx@xxxxx.xxx
Password:
Password (again):
Superuser created successfully.
% heroku run python manage.py drf_create_token xxxx
Running python manage.py drf_create_token xxxx on ⬢ xxxxx-xxxxx-00000... up, run.8523 (Free)
Generated token xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx for user xxxx
ようやっとAndroidアプリから良い感じにアクセスできる環境ができました!!
ひとまずここまでのまとめ
ということで、今回の記事はここまで。
大分ながくなりましたが、前回のwebアプリをAndroidアプリから使うために、
- jsonにtoken認証でアクセスできるようにした
- Herokuにデプロイした
といったところまでいきました。
さて、いよいよAndroidアプリの方の実装にいきたいと思います。ツカレタ……。
参考にさせていただいたもの
- Python Development Workflow for Humans
- pip freeze includes "pkg-resources==0.0.0"
- TokenAuthentication
- Django Rest Framework Token Authentication @ Stack Overflow
- CSRF Failed: CSRF token missing or incorrect
- Cross Site Request Forgery protection
- vue-resource の引退について
- axios
- Can't set headers for DELETE method
- django-heroku
- Gunicorn
- Using Multiple Buildpacks for an App
- How can I deploy/push only a subdirectory of my git repo to Heroku?
- How do I reset a Heroku git repository to its initial state?