1ファイルでdjango restframeworkの機能を試すせるようにする方法

(面倒くさいフレームワークの機能を1ファイルで試したい時があるということで [onefile](http://qiita.com/tags/onefile) というタグを作ったのですが。勝手に作っても良いものなのかどうか分かっていなかったりします。)

## はじめに

[django restframework](http://www.django-rest-framework.org/) はdjangoの上に構成されたframework。REST APIを作る際には便利ではあるのだけれど。提供されている機能の確認をするのが面倒。結局、内部のソースコードを読んで挙動を把握するのがドキュメントなどを読むよりも楽なことも多いのだけれど。機能を手軽に試せる環境を作っていても損はない。

以下の様なコードで django restframework の機能を試せるようになる。(丁寧に読まずに読み飛ばして良い)

```py
# このshortcut moduleを作ろうという話
from shortcut import App  # shorthand
from shortcut import do_get, do_post, do_delete

app = App()
app.setup(apps=[__name__], root_urlconf=__name__)


from rest_framework import routers, serializers, viewsets

# models
from django.contrib.auth.models import User


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'url', 'username', 'email', 'is_staff')


# viewsets
class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer


# Routers provide an easy way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
urlpatterns = app.setup_urlconf(router)

if __name__ == "__main__":
    app.create_table(User)
    parser = app.create_arg_parser()
    args = parser.parse_args()

    if args.run_server:
        app.run_server(port=8080)
    else:
        app.run_client(main_client)
```

## 使い方

使い方に関する注意や説明。

### ファイル構成

後に作る `shortcut.py` に依存するので以下の様な構成になっている必要がある。(もちろん真面目にパッケージを作っても良い)

```
.
├── shortcut.py
└── view-sample1.py  # 試したいファイル
```

`view-sample1.py` と `-` を付けるのはpythonのモジュール名としては不適切。(他からimportされようとした時にimportエラーになってほしいという気持ちで個人的には不適切なファイル名を付けているがあまり意味はない)

### importの順序

djangoの設計の悪さによりimportの順序を気にする必要がある。以下のコードの順序や位置は守る必要がある。

```py
from shortcut import App  # shorthand
from shortcut import do_get, do_post, do_delete

# restframeworkをimportする前に色々と設定が必要
app = App()
app.setup(apps=[__name__], root_urlconf=__name__)

# modelやrestframeworkのmoduleをimport
from django.contrib.auth.models import User
from rest_framework import routers, serializers, viewsets
```

django restframeworkのmoduleをimportした瞬間にdjangoの設定が必要などのエラーが出たりするので、それ以前に色々設定しておく必要がある。 `urls.py` を分けたり `django app` を分けたい場合には `app.setup()` の引数が変わる。この記事では1ファイルで試す方法の紹介なので `__name__` で固定で良い。


### model, serializer, routerを雑に定義

このあたりはdjango restframeworkの機能なので詳細は説明しない。ドキュメントなど参照のこと。

```py
from django.contrib.auth.models import User
# models
from rest_framework import routers, serializers, viewsets

# serializers
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'url', 'username', 'email', 'is_staff')


# viewsets
class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    
# Routers provide an easy way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
urlpatterns = app.setup_urlconf(router)
```

基本的にはmodelの出力表現やvaridationをserialierが提供し、serialiezerを利用した一連のviewをviewsetが受け持ち、viewsetをrouterに登録すると各種REST APIが利用できるようになる。

#### model定義時の注意点

そう言えば忘れていたけれど。1ファイルで試す場合には定義するモデルが所属する `django app` を決めるために `__meta__.app_label` を設定する必要がある。例えば以下の様にして定義すると良い。

```py
class Skill(models.Model):
    name = models.CharField(max_length=255, default="", null=False)
    user = models.ForeignKey(User, null=False, related_name="skills")

    class Meta:
        app_label = __name__
```

### serverとして利用もしくは実行例を表示

`--run-server` オプションをつけて立ち上げると実際にアプリとして動作するようになっている。 browsable apiも有効になっているのでブラウザでアクセスしてフォームに何らかの値を入力してGET/POST/PUT/DELETEなどしてみることができる。内部的には `django` の `runserver` を呼んでいるだけ。

```
$ python view-sample1.py --run-server
Performing system checks...

System check identified no issues (0 silenced).

You have unapplied migrations; your app may not work properly until they are applied.
Run 'python manage.py migrate' to apply them.

July 18, 2016 - 23:32:11
Django version 1.9.6, using settings None
Starting development server at http://127.0.0.1:8080/
Quit the server with CONTROL-C.
```

### モデルの利用(永続化)

利用されるモデル自体は sqliteの インメモリーDBに登録される設定になっているので毎回tableを生成する必要がある。以下で行なっている。

```py
app.create_table(User)
```

### 無引数で実行した場合には実行例の表示

無引数で実行した場合には `django.test.client.Client` による実行例の表示。上の例では `main_client()` が呼ばれる想定だが書いていない。例えば以下の様にして書く。

```py
def main_client(client):
    """call view via Client"""
    # success request
    msg = "listing (empty)"
    do_get(client, msg, "/users/")

    msg = "create user (name=foo)"
    do_post(client, msg, "/users/", {"username": "foo"})
    msg = "create user (name=bar)"
    do_post(client, msg, "/users/", {"username": "bar"})

    msg = "listing"
    do_get(client, msg, "/users/")

    msg = "show information for user(id=1)"
    do_get(client, msg, "/users/1/")

    msg = "delete user(id=1)"
    do_delete(client, msg, "/users/1")

    msg = "listing"
    do_get(client, msg, "/users/")
```

実行結果は以下の様になる。 (`python view-sample1.py`)

listing (empty)

```
request: GET /users/
status code: 200
response: []
```
create user (name=foo)

```
request: POST /users/
status code: 201
response: {
  "id": 1,
  "url": "http://testserver/users/1/",
  "username": "foo",
  "email": "",
  "is_staff": false
}
```

create user (name=bar)

```
request: POST /users/
status code: 201
response: {
  "id": 2,
  "url": "http://testserver/users/2/",
  "username": "bar",
  "email": "",
  "is_staff": false
}
```

listing

```
request: GET /users/
status code: 200
response: [
  {
    "id": 1,
    "url": "http://testserver/users/1/",
    "username": "foo",
    "email": "",
    "is_staff": false
  },
  {
    "id": 2,
    "url": "http://testserver/users/2/",
    "username": "bar",
    "email": "",
    "is_staff": false
  }
]
```

show information for user(id=1)

```
request: GET /users/1/
status code: 200
response: {
  "id": 1,
  "url": "http://testserver/users/1/",
  "username": "foo",
  "email": "",
  "is_staff": false
}
```
delete user(id=1)

```
request: DELETE /users/1/
status code: 204
```

listing

```
request: GET /users/
status code: 200
response: [
  {
    "id": 2,
    "url": "http://testserver/users/2/",
    "username": "bar",
    "email": "",
    "is_staff": false
  }
]
```
## shortcut.py

shortcut.pyは以下のようになっている。こちらの説明は面倒なのでしない。

```py
import os.path
import json
import copy
import importlib
import argparse
from django.db import connections
from django.test.client import Client


default_settings = dict(
    DEBUG=True,
    ALLOWED_HOSTS=['*'],
    INSTALLED_APPS=[
        "django.contrib.staticfiles",
        "django.contrib.contenttypes",
        "django.contrib.auth",
        "rest_framework",
    ],
    STATIC_URL='/static/',
    MIDDLEWARE_CLASSES=(
        'django.middleware.common.CommonMiddleware',
    ),
    REST_FRAMEWORK={
        "DEFAULT_PERMISSION_CLASS": [
            "rest_framework.permissions.AllowAny"
        ]
    },
    DATABASES={"default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": ":memory:"
    }},
    CACHES={
        'default': {
            'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        }
    },
    TEMPLATES=[
        {
            'BACKEND': 'django.template.backends.django.DjangoTemplates',
            'DIRS': [],
            'APP_DIRS': True
        },
    ]
)


def create_table(model, dbalias="default"):
    connection = connections[dbalias]
    with connection.schema_editor() as schema_editor:
        schema_editor.create_model(model)


def maybe_list(x):
    if isinstance(x, (list, tuple)):
        return x
    else:
        return [x]


class SettingsHandler(object):
    defaults = {
        "settings": default_settings,
        "STATIC_ROOT": None,
        "dbalias": "default"
    }

    def get_settings_options(self, root_urlconf):
        options = copy.copy(self.defaults["settings"])
        options.update(
            STATIC_ROOT=self.defaults["STATIC_ROOT"] or self.get_static_root(),
            ROOT_URLCONF=root_urlconf
        )
        return options

    def get_static_root(self):
        import rest_framework
        return os.path.abspath(os.path.join(rest_framework.__path__[0], 'static'))


class App(object):
    def __init__(self, settings_handler=SettingsHandler()):
        self.settings_handler = settings_handler

    def setup(self, apps, root_urlconf, extra_settings=None):
        import django
        from django.conf import settings
        apps = maybe_list(apps)
        options = self.settings_handler.get_settings_options(root_urlconf)
        options["INSTALLED_APPS"].extend(apps)
        if extra_settings:
            options.update(extra_settings)
        settings.configure(**options)
        django.setup()

    def setup_urlconf(self, router):
        # url
        from django.conf.urls import url, include
        from django.contrib.staticfiles.urls import staticfiles_urlpatterns

        urlpatterns = [
            url(r'^', include(router.urls))
        ]
        urlpatterns += staticfiles_urlpatterns()
        return urlpatterns

    def load_module(self, module_name):
        return importlib.import_module(module_name)

    def run(self, main_client):
        parser = self.create_arg_parser()
        args = parser.parse_args()

        if args.run_server:
            self.run_server(port=8080)
        else:
            self.run_client(main_client)

    def run_server(self, port=8000):
        from django.core.management.commands.runserver import Command
        return Command().execute(addrport=str(port))

    def run_client(self, callback):
        client = Client()
        return callback(client)

    def create_arg_parser(self):
        parser = argparse.ArgumentParser()
        parser.add_argument("--run-server", dest="run_server", action="store_true", default=False)
        return parser

    def create_table(self, *models):
        for model in models:
            create_table(model, dbalias=self.settings_handler.defaults["dbalias"])


def do_get(client, msg, path):
    print(msg)
    print("```")
    print("request: GET {}".format(path))
    response = client.get(path)
    print("status code: {response.status_code}".format(response=response))
    print("response: {content}".format(content=json.dumps(response.data, indent=2)))
    print("```")


def do_post(client, msg, path, data):
    print(msg)
    print("```")
    print("request: POST {}".format(path))
    response = client.post(path, data)
    print("status code: {response.status_code}".format(response=response))
    print("response: {content}".format(content=json.dumps(response.data, indent=2)))
    print("```")


def do_delete(client, msg, path):
    print(msg)
    print("```")
    print("request: DELETE {}".format(path))
    response = client.delete(path)
    print("status code: {response.status_code}".format(response=response))
    print("```")
```

## 追記(paginationの機能を試してみる)

例えば以下のような変更を加えるとpaginationの機能を試せる。

```diff
--- view-sample1.py	2016-07-18 23:39:33.000000000 +0900
+++ view-sample2.py	2016-07-19 00:02:14.000000000 +0900
@@ -16,10 +20,21 @@
         fields = ('id', 'url', 'username', 'email', 'is_staff')
 
 
+# pagination
+from rest_framework import pagination
+
+
+class MyPagination(pagination.PageNumberPagination):
+    page_size = 5
+    page_size_query_param = 'page_size'
+    max_page_size = 10000
+
+
 # viewsets
 class UserViewSet(viewsets.ModelViewSet):
     queryset = User.objects.all()
     serializer_class = UserSerializer
+    pagination_class = MyPagination
 
 
 # Routers provide an easy way of automatically determining the URL conf.
```

page_size=5での一覧表示。

```
request: GET /users/
status code: 200
response: {
  "count": 10,
  "next": "http://testserver/users/?page=2",
  "previous": null,
  "results": [
    {
      "id": 1,
      "url": "http://testserver/users/1/",
      "username": "foo0",
      "email": "",
      "is_staff": false
    },
    {
      "id": 2,
      "url": "http://testserver/users/2/",
      "username": "foo1",
      "email": "",
      "is_staff": false
    },
    {
      "id": 3,
      "url": "http://testserver/users/3/",
      "username": "foo2",
      "email": "",
      "is_staff": false
    },
    {
      "id": 4,
      "url": "http://testserver/users/4/",
      "username": "foo3",
      "email": "",
      "is_staff": false
    },
    {
      "id": 5,
      "url": "http://testserver/users/5/",
      "username": "foo4",
      "email": "",
      "is_staff": false
    }
  ]
}
```