[Django REST Framework] View の使い方をまとめてみた
2018-04-30

Django REST Framework (以下 DRF といいます) をまとめてみた記事のビュー編です。 細かいところまではカバーしきれていませんが、大まかな使い方はわかると思います。

Note

ビューと言っても新しい概念ではなく、Django のビューと概ね同じなので身構える心配はありません。 DRF から提供されているビューなので、 DRF の設定と絡めて定義できる便利なやつです。

シリアライザの記事と同じくらいの長さです。頑張って読もうな。

目次

Installation

ひとまず、手元で試すためには インストールが必要です。

ライブラリのインストールは pip で。 djangorestframework 以外はお好みでどうぞ。 django-rest-framework ではないので注意してください。

$ pip install djangorestframework
$ pip install markdown
$ pip install django-filter

当然ながら Django もインストールされていることを想定しています。

$ pip install django

settings もいじります。 settings.py 等の設定ファイルの INSTALLED_APPSrest_framework を追加します。

INSTALLED_APPS = (
    # :
    # :
    'rest_framework',
)

View types

ビューにはいくつか種類があります。 最初にそれぞれの使い所を抑えておきましょう。

Warning

  • 当記事では試しやすさを考慮して全部同じファイル(urls.py)に書いていきますが 実案件でこんな書き方をすると上司にぶっ飛ばされるので注意してください。

  • ビューの動作確認には curl という コマンドを使います。

Function view

お気付きの通り、一番簡単な View は Response を返却する関数型のビューということになります。

最小のビューは第一仮引数に request を受取るように定義するだけです。 URLにパラメータを埋め込む場合は引数を増やしたりします。この辺は Django と同じですね。

from rest_framework.response import Response
from rest_framework.decorators import api_view

@api_view(['GET', 'POST'])
def hello_world(request):
    if request.method == 'POST':
        return Response({"message": "Got some data!", "data": request.data})
    return Response({"message": "Hello, world!"})

Note

DRF から 提供されているビュー全てに共通して言えることですが、 通常の Django は HttpResponse というクラスで返していたと思いますが、DRFでは Response というクラスで返却します。

書き方はよくあるDjangoのビューと同じですね。 違うのは api_view というデコレータの有無です。 これを使うことにより、 DRF の設定がビューに引き継がれるようになります。

また 引数 として HTTP メソッド を文字列として指定できます。 この辺は require_http_methods と同じですね。

ちなみに後述するクラス型のビューにはパーミッションやパーサなど複数の設定項目があるのですが、関数型のビューには同じようにデコレータを使って設定することになります。

本当に単純な機能は関数ベースで書いたほうが逆に可読性がよく保守しやすいかもしれません。 設定項目が増えてきたら見栄えが悪いので後述するクラスベースにしちゃったほうがいいです。

Class based view

クラスベースのビューは HTTP メソッドごとの インスタンスメソッドを定義できます。 まさにこれは Django の View の様な感じですね。 まぁ実際に継承してるんですが

また、他にも後述する様々な設定を追加できます。 これは関数だとデコレータで連ねて書く必要があり可読性が悪くなってしまうのでクラスベースビューのメリットであると言えそうです。

クラスベース ビュー は APIView クラスを継承して定義します。 対象のHTTPメソッドがそのまま インスタンスメソッド になります。 DRF 的には ハンドラメソッド と呼ばれるようで、 後述する アクションメソッド とは区別します。

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication, permissions
from django.contrib.auth.models import User

class UserNameAPI(APIView):
    authentication_classes = (authentication.TokenAuthentication,)
    permission_classes = (permissions.IsAdminUser,)

    def get(self, request, format=None):
        usernames = [user.username for user in User.objects.all()]
        return Response(usernames)

    def post(self, request):
        # 普通こんなことはしないが..
        users = [User(username=name) for name in request.POST.getlist('name')]
        User.objects.bulk_create(users)
        return Response({'succeeded': True})

特定のモデルに紐付かないような処理は クラスベースで記述するのがおすすめと言えるでしょう。 私は、クエリが複雑すぎて queryset じゃ処理しきれないとかで SQLAlchemy で処理した結果を返したい という場合などに APIView を使っています。

ちなみに ジェネリックビュークラスは この APIView を継承して定義されています。

リファレンス

http://www.django-rest-framework.org/api-guide/views/

Generic view

Django や Django REST framework のお作法に従うなら便利で、そして種類も多い View です。

このビューはモデルというか クエリセット にひも付くため簡略化した書き方ができるのが特徴です。 場合によっては処理を書かなくてよいことが多々あります。

と思ってリファレンスを見ると種類がめっちゃ多いことに気づきますが、 作成, 一覧, 取得, 削除, 更新 の用途によって使い分ければよいだけの話なので身構えることはありません。

Generic Viewクラスは 特定のレコードに 紐づくか, 紐付かないか で分類されています。

紐付かない
紐づく

手始めに ListAPIView を使ってみましょう。

from rest_framework import generics

class UserList(generics.ListCreateAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

これだけでユーザ一覧を返却するAPIビューの出来上がりです。 各ユーザレコードは UserSerializer に従ってシリアライズされます。

(シリアライザの説明は Serializer の 使い方 をまとめてみた を参照ください。

内部的な話をすると、ジェネリックビューは クエリセット と密接に結びついた GenericAPIView という基底クラスと、 クエリセット を使ってレスポンスを組み立てるための Mixin クラス を継承して定義されています。

Mixin は レスポンスを返却するためのインスタンスメソッドを ハンドラメソッドとは別の名前で提供しており、これを アクションメソッドと呼ぶそうです。

ジェネリックビューでは ハンドラメソッドから呼び出される形で実行されます。

以下のように対応しています。

HTTP

メソッド

アクションメソッド

説明

対応しているクラス

GET

retrieve(self, request, pk)

レコード(単体)の取得

  • RetrieveAPIView

  • RetrieveUpdateAPIView

  • RetrieveDestroyAPIView

  • RetrieveUpdateDestroyAPIView

GET

list(self, request)

レコード一覧の取得

  • ListAPIView

  • ListCreateAPIView

POST

create(self, request)

レコードの作成

  • CreateAPIView

  • ListCreateAPIView

PUT

update(self, request, pk)

レコードの更新

  • UpdateAPIView

  • RetrieveUpdateAPIView

  • RetrieveUpdateDestroyAPIView

PATCH

partial_update(self, request, pk)

レコードの部分更新

  • UpdateAPIView

  • RetrieveUpdateAPIView

  • RetrieveUpdateDestroyAPIView

DELETE

destroy(self, request, pk)

レコードの削除

  • DestroyAPIView

  • RetrieveUpdateDestroyAPIView

リファレンスには明記されていなかったんですが、 処理を変えたいときは多分アクションメソッドをオーバーライドすることになるんじゃないかな。 確証はないです。

リファレンス

http://www.django-rest-framework.org/api-guide/generic-views/

ViewSets

ビューセットは 取得, 一覧, 登録, 更新, 削除 をまとめて管理するビューです。 (取得と一覧だけできる ReadOnlyModelViewSet というのもあります)

当たり前の処理は 自分で書かなくても この ViewSet が肩代わりしてくれます。

from rest_framework import viewsets

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

一つ前に書いた UserList の名前と継承クラスを変えただけです。 これだけで、このビューは 取得, 一覧, 登録, 更新, 削除 ができるようになりました。

ViewSet は ハンドラメソッドでなく アクションメソッドで定義するべきと記述がありました。

ジェネリックビューとの一番の違いはおそらく、 アクション にあると思います。

action デコレータ を使ってインスタンスメソッドを定義することで、パスに機能を追加できます。

説明が難しいですが、百聞は一見にしかずということで以下のように ViewSet を定義することで

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

    @action(methods=['get'], detail=False)
    def fullnames(self, request):
        return Response([
            '{user.first_name} {user.last_name}'.format(user=user)
            for user in self.get_queryset()
        ])

    @action(methods=['get'], detail=True)
    def fullname(self, request, pk=None):
        user = self.get_object()
        return Response('{user.first_name} {user.last_name}'.format(user=user))

# Router については後述
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
urlpatterns = [url(r'^api/', include(router.urls))]

インスタンスメソッド名で URL にアクセスすることで 任意の 処理が実行できます。

$ curl -X GET 'http://127.0.0.1:8000/api/users/fullnames/'
["takayuki shimizukawa","takanory suzuki","teruhiko teruya"]
$ curl -X GET 'http://127.0.0.1:8000/api/users/1/fullname/'
"takayuki shimizukawa"
$ curl -X GET 'http://127.0.0.1:8000/api/users/2/fullname/'
"takanory suzuki"
$ curl -X GET 'http://127.0.0.1:8000/api/users/3/fullname/'
"teruhiko teruya"
  • methods は 登録対象の HTTP メソッド

  • detail は 詳細API or 一覧API

    • True の場合は pk 引数が必要

これめっちゃ便利ですね。これは検証してないけど Router で登録したときしか使えないのかな。

リファレンス

http://www.django-rest-framework.org/api-guide/viewsets/

個人的に考えた使い分けとしては

  • クエリセットを使わない API は クラスベースビュー(APIView) か 関数ビュー

  • モデルに対する処理をまとめて一つのクラスで定義したい場合は View set

  • それ以外は Generic view

みたいな感じかなぁ。

Note

ビューは ルーティングのヒモ付をしないとページとして認識されません。

urls.pyurlpatterns 変数に登録します。

from django.conf.urls import url, include

urlpatterns = [
  url(r'^hello/$', hello_world),
  url(r'^user/$', UserList.as_view()),
]

Django と同じように関数のビューはそのまま、クラスのビューは as_views() メソッドの結果を url 関数でマッピングすることで登録するできます。 ViewSet で定義したビューは 次に説明する Router を使って登録することもできます。

Router

urls.py に Router というものを使って生成された ルーティングルール を追加していきます。 やること自体は簡単なんですが、 残念なことにこれにもいくつかの種類があります。

ここでは DefaultRouter だけ説明します。 SimpleRouterDefaultRouter と大体同じものです。 SimpleRouter の機能に加え DefaultRouter は Router のルート画面にアクセスしたときに API のリンク一覧を見せてくれたり、少し便利です。

じゃあブラウザから直接APIを見たいときはどうするのかというと .json の拡張子をつけたり、クエリストリングとして ?format=json をつけてアクセスすれば良いです。

from rest_framework import routers

router = routers.DefaultRouter()
router.register(r'users', UserViewSet)

urlpatterns = [
    url(r'^api/', include(router.urls)),
]

これにより /api/ 配下にビューが登録されます。 The Browsable API (私の場合は localhost:8000/api/) にアクセスすると register で登録された ビューをたどることができるようになります。

drf-browsable-api.png

できましたね。

Warning

Router で登録できるのは ViewSet だけです。ジェネリックビューとかを登録しようとすると

AttributeError: type object 'UserListView' has no attribute 'get_extra_actions'

のようにエラーが発生します。 get_extra_actions メソッドは viewsets.py にしか定義されていません。

Router は 詳細API を users/{pk}/$ のように pk を自動的に付加してURL登録してくれるいい子です。

ここは 実際に触ってみるのが一番早いと思います。

リファレンス

Request

ビュー から参照できる request オブジェクトは 概ね Django と同じですが、少し機能追加されています。

request.GET

URLパラメータを解析したものを辞書形式で格納されている

request.POST
  • リクエストボディを解析したものが辞書形式で格納されている。

  • ここに入るのは Form データのみで JSON形式 のパラメータは後述する request.data に格納されるようです。

request.query_params
  • 挙動は request.GET と同じ

  • RESTFramework ではこちらの使用が推奨されている。

    • request.GET だと GET メソッドのときしか取れないように聞こえてしまうということっぽい

request.data
  • POST, PUT, PATCH メソッド のリクエストの場合にリクエストボディを解析したものが辞書形式で格納している。

  • これには request.FILES (添付ファイル) を 含む。

  • おそらく最も利用されるパラメータ属性

リファレンス

http://www.django-rest-framework.org/api-guide/requests/

次セクションからは ビュー に対する制限や追加機能を説明します。

Authentication

ビューの authentication_classes に指定された Authentication クラスを使って認証が行われます。 Authentication クラスは複数指定でき、最初に指定されたものから順に認証に利用され、成功した時点で認証処理は終了します。

今回は認証をチェックするために以下の View を定義します。

from rest_framework.authentication import SessionAuthentication, BasicAuthentication, TokenAuthentication

class CheckView(APIView):
    authentication_classes = (SessionAuthentication, BasicAuthentication, TokenAuthentication)
    def get(self, request, format=None):
        content = {'user': str(request.user), 'auth': str(request.auth)}
        return Response(content)

urlpatterns += [url(r'^check/', CheckView.as_view())]

適切な認証情報を持ってこのビューにアクセスするとユーザ情報と認証情報が返却されます。 まぁただのチェック用です。

認証が成功すると AnonymousUserusername フィールドの値になります。

BasicAuthentication

DRFが提供する最も単純な認証です。Basic認証と同じように ユーザとパスワード(をBASE64エンコードしたもの) を毎回送信することでユーザの認証をします。

たとえば、ユーザの認証情報が test1 / test だとすると、これらを BASE64 化した dGVzdDE6dGVzdA== を Authentication ヘッダに渡せば認証できます。

$ curl -X GET http://127.0.0.1:8000/check/ -H 'Authorization: Basic dGVzdDE6dGVzdA=='
{"user":"test1","auth":"None"}

復号が容易なためセキュリティ的にはあまり推奨できません。 ローカルでの動作確認など閉ざされた目的でのみ利用するようにしてください。

Note

サーバ側から WWW-Authenticate: Basic realm="api" のようなヘッダを返却することでブラウザから認証ダイアログが起動し 認証情報をブラウザに保存できます。

SessionAuthentication

SessionAuthentication は従来の Django と同じ認証方法、つまり Cookie による認証です。

簡易的にセッションを発行するためのビューを以下のように定義します。

class SessionView(APIView):
    def get(self, request):
        # 本来は認証しよう
        # user = authenticate(request.query_params['username'], request.query_params['password'])
        user = User.objects.get(username=request.GET['username'])
        r = login(request, user)
        return Response({'session': request.session.session_key})

urlpatterns += [url(r'^session/', SessionView.as_view())]

アクセスするとセッションIDが得られます。

$ curl -X GET http://127.0.0.1:8000/session/?username=test1
{"session":"a55jor0z61vzhk8n0t2z71vz2rgyleg1"}

発行された Session ID (Set-Cookieされたものと同じ) を Cookie ヘッダに渡すと

$ curl -X GET http://127.0.0.1:8000/check/ -H 'Cookie: sessionid=a55jor0z61vzhk8n0t2z71vz2rgyleg1;'
{"user":"test1","auth":"None"}

認証できましたね。

Warning

SessionAuthentication では 普通の Django と同じように PUT, PATCH, POST, DELETE メソッドで CSRFトークン が必要になります。

TokenAuthentication

TokenAuthentication は DRF が提供する認証で SessionAuthentication に少し似ていますが、Cookie は使いません。

利用するためには settings.py を編集し、

INSTALLED_APPS = (
    # :
    'rest_framework.authtoken',
)

トークンを管理するためのテーブル用のマイグレーションをする必要があります。

スキーマはこんな感じになってます。 user_id で一意制約がついてるので、ユーザに対して一つしか発行できません。

sqlite> .schema authtoken_token
CREATE TABLE IF NOT EXISTS "authtoken_token" (
  "key" varchar(40) NOT NULL PRIMARY KEY,
  "created" datetime NOT NULL,
  "user_id" integer NOT NULL UNIQUE REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED
);

例えば トークンを発行するためのビューを以下のように定義します。

class TokenView(APIView):
    def get(self, request):
        # 本来は認証しよう
        # user = authenticate(request.query_params['username'], request.query_params['password'])
        user = User.objects.get(username=request.GET['username'])
        token, _ = Token.objects.get_or_create(user=user)
        return Response({'token': str(token)})

urlpatterns += [url(r'^token/', TokenView.as_view())]

ユーザ名を指定してトークンを得て、

$ curl -X GET http://127.0.0.1:8000/token/?username=test2
{"token":"38a7db568458feb9f8d6008805bba3fca7e1ca00"}

Authorization ヘッダに トークンを指定すると

$ curl -X GET http://127.0.0.1:8000/check/ -H 'Authorization: Token 38a7db568458feb9f8d6008805bba3fca7e1ca00'
{"user":"test2","auth":"38a7db568458feb9f8d6008805bba3fca7e1ca00"}

認証できました。

Basic Authentication や Session Authentication では None だった auth に値が入っています。 トークン認証の場合は Token モデルのインスタンスが格納されるようです。何に使えるかはよくわからない。

コマンドラインからやる場合は、 drf_create_token を使います。すでに有効なトークンがある場合はそれが返却される模様です。

$ ./manage.py drf_create_token test1
Generated token d2f2eacba404528ff49dd6c998bd02fc0a9b3069 for user test1
# 再生成は -r をつける
$ ./manage.py drf_create_token -r test1
Generated token b87175b965cf7d886173fff45eb9cab790d2ae6d for user test1

Warning

ドキュメントを読む限りでは このトークンに 有効期間を与えるような設定値は提供していないようです。

DRF認証のサードパーティ製ライブラリがいくつかあるようなので、必要な方はそちらを検討してみてください。

関連ライブラリを増やすとDjangoバージョンアップ等の足かせになることがあるのでメンテナンスされているかどうかということも考慮に入れて選択することをオススメします。

トークン認証の特徴はドメインに紐付かないことにあります。

先に説明した2つの認証は認証情報がドメインにひも付き、自動的に送信されるという特性がありました。 これは便利な半面 CSRF という攻撃を受けるリスクを孕んでいます。トークンの場合はクライアント側で明示的に指定されない限り認証情報が送信されないため、CSRFの心配はありません。

また、Cookieと違いドメインの制約がないため外部ドメインに対するリクエストであっても認証情報を渡せます。

Note

CSRF (Cross Site Request Forgery) とは

攻撃者の誘導によって被害者の端末からリクエストが発行される際、 対象ドメインに紐づくユーザ認証情報(主にCookie)が勝手に送信されることで被害者に紐づく何らかの処理を意図せず行う攻撃のことです。 例えば殺害予告などが被害者のアカウントから行われたりします。

Django では CSRFトークン と呼ばれる推測困難な値を Cookie と POSTパラメータ などで送信し、サーバ側で一致確認することで CSRF チェックをしています。

フレームワークによっては 毎回 CSRFトークン を発行するものもあります。(ここでは認証トークンとCSRFトークンは異なる意味で使っています)

画面ごとに クラスを指定するのが面倒な場合、settings.py の REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] 対して Authentication クラスのモジュールパスを指定します。

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ),
}

CustomAuthentication

自分で Authentication クラスを作る場合は Authentication 関連クラスを継承して、認証処理を authenticate メソッドに記述します。

今回は TokenAuthentication クラスを継承して トークン認証に有効期限を設けてみます。

class ExpirationTokenAuthentication(TokenAuthentication):
    delta = timedelta(seconds=10)

    def authenticate(self, request):
        user, auth = super().authenticate(request)
        if timezone.now() - auth.created > self.delta:
            auth.delete()
            raise exceptions.AuthenticationFailed('有効期限切れだよ')

        # 毎回有効期限を更新する場合
        # auth.create = timezone.now()
        # auth.save()
        return user, auth

10秒で有効期限が切れるなんてネットバンクも真っ青なセキュリティですね。

$ curl token/?username=test1
{"token":"8fd40352d28a5c8bcd691c5d517acf4a5e574ea1"}
# 5秒後くらい
$ curl -X GET http://127.0.0.1:8000/check/ -H 'Authorization: Token 8fd40352d28a5c8bcd691c5d517acf4a5e574ea1'
{"user":"test1","auth":"8fd40352d28a5c8bcd691c5d517acf4a5e574ea1"}
# 11秒後くらい
$ curl -X GET http://127.0.0.1:8000/check/ -H 'Authorization: Token 8fd40352d28a5c8bcd691c5d517acf4a5e574ea1'
{"detail":"有効期限切れだよ"}
リファレンス

http://www.django-rest-framework.org/api-guide/authentication/

Permission

Permission は ユーザのもつ何らかの値によって View に対するアクセスを制限する機能です。 Authentication と似ていますが、ユーザの認証より後に実施されます。認証されたユーザの値を用いて処理を行うことが多いです。

一番良く使うのはログインの有無を確認する IsAuthenticated でしょう。 Django の login_required デコレータみたいなもんですね。

ちょうどいいので先程の CheckView を編集して設置してみましょう。

class CheckView(APIView):
    permission_classes = (IsAuthenticated,)
    authentication_classes = (SessionAuthentication, BasicAuthentication, TokenAuthentication)
    def get(self, request, format=None):
        print(dir(request.auth))
        content = {'user': str(request.user), 'auth': str(request.auth)}
        return Response(content)
$ curl -X GET http://127.0.0.1:8000/check/
{"detail":"Authentication credentials were not provided."}

他には管理者ユーザのみに許可する IsAdminUser なんてのもあります。 詳しくは Permissionドキュメント を参照してください。

カスタマイズするには BasePermission を継承し has_permissionhas_object_permission のいずれかを上書きします。

以下はグループに対する権限を確認するパーミッションです。

class GroupMemberPermission(permissions.BasePermission):
    def has_permission(self, request, view):
        # ユーザが何のグループにも属していない場合は False になる
        return request.user.groups.count()

    def has_object_permission(self, request, view, obj):
        # 対象グループに属していない場合は False になる
        return  request.user.groups.filter(id=obj.id).exists()
  • has_permission メソッドは 設定されたビューすべてで呼び出されます。権限がある場合は 真と評価されるオブジェクトを返します。

  • has_object_permission は設定されたビュー、かつ、特定のオブジェクトに対して権限があるかどうかを評価します。こちらも 権限の有無により真偽値を返します。

    • こちらは check_object_permissions メソッドが実行された場合だけ呼び出されることに注意してください。

    • 通常、上記のメソッドは get_object メソッドから自動的に呼び出されますが、 get_object をオーバーライドする場合は 明示的に呼び出す必要があります。

グループの一覧、詳細APIページに対して先程のパーミッションを設定してみます。

class GroupListView(generics.ListAPIView):
    queryset = Group.objects.filter()
    permission_classes = (GroupMemberPermission, )
    authentication_classes = (CustomTokenAuthentication, )
    def list(self, request):
        groups = self.get_queryset().values('id', 'name')
        return Response({'groups': groups})

class GroupRetrieveView(generics.RetrieveAPIView):
    queryset = Group.objects.filter()
    permission_classes = (GroupMemberPermission, )
    authentication_classes = (CustomTokenAuthentication, )
    def retrieve(self, request, *args, **kwargs):
        group = self.get_object()
        return Response({'group': {'id': group.id}})

urlpatterns += [
    url(r'^group/$', GroupListView.as_view()),
    url(r'^group/(?P<pk>.+)/$', GroupRetrieveView.as_view()),
]

動作確認してみます。 group1, group2 を作り、 group1 には test1 というユーザが所属しているというシナリオでデータを作ります。 group2 には誰も属していません。

>>> from django.contrib.auth.models import User, Group
>>> g1, _ = Group.objects.get_or_create(id=1, defaults={'name': 'group1'})
>>> g2, _ = Group.objects.get_or_create(id=2, defaults={'name': 'group2'})
>>> u1 = User.objects.get(id=1)
>>> u2 = User.objects.get(id=2)
>>> g1.user_set.add(u1)
>>> g1.user_set.all()
<QuerySet [<User: test1>]>

準備が完了したので、それぞれのユーザに対してリクエストを飛ばしてみます。

test1

# トークン取得
$ curl -X GET http://127.0.0.1:8000/token/?username=test1
{"token":"67a1fb9588b9ec9794e02d5addc8743243fecb1a"}
# 一覧API
$ curl -X GET http://127.0.0.1:8000/group/ -H 'Authorization: Token 67a1fb9588b9ec9794e02d5addc8743243fecb1a'
{"groups":[{"id":1,"name":"group1"},{"id":2,"name":"group2"}]}
# group1 の詳細API
$ curl -X GET http://127.0.0.1:8000/group/1/ -H 'Authorization: Token 67a1fb9588b9ec9794e02d5addc8743243fecb1a'
{"group":{"id":1}}
# group2 の詳細API
$ curl -X GET http://127.0.0.1:8000/group/2/ -H 'Authorization: Token 67a1fb9588b9ec9794e02d5addc8743243fecb1a'
{"detail":"You do not have permission to perform this action."}

test2

# トークン取得
$ curl -X GET http://127.0.0.1:8000/token/?username=test2
{"token":"38a7db568458feb9f8d6008805bba3fca7e1ca00"}
# 一覧API
$ curl -X GET http://127.0.0.1:8000/group/ -H 'Authorization: Token 38a7db568458feb9f8d6008805bba3fca7e1ca00'
{"detail":"You do not have permission to perform this action."}
# group1 の詳細API
$ curl -X GET http://127.0.0.1:8000/group/1/ -H 'Authorization: Token 38a7db568458feb9f8d6008805bba3fca7e1ca00'
{"detail":"You do not have permission to perform this action."}
# group2 の詳細API
$ curl -X GET http://127.0.0.1:8000/group/2/ -H 'Authorization: Token 38a7db568458feb9f8d6008805bba3fca7e1ca00'
{"detail":"You do not have permission to perform this action."}

期待通りですね。

リファレンス

http://www.django-rest-framework.org/api-guide/permissions/

Filtering

APIを作成するときに検索機能は欠かせません。

単純なフィルタリングであれば get_queryset メソッド から フィルタリングしたクエリセットを返せばいいのですが、 検索とかを自力で実装するのは結構だるいものです。

そこで Generic Filtering を使いますが、 django-filter というライブラリが必要です。

最初に インストールはお好みでと言いましたが、フィルタリングをしたい方はインストールしてください。

設定は簡単で settings に対して以下を追記するだけです

  • INSTALL_APPSdjango_filters を追加

  • DEFAULT_FILTER_BACKENDS('django_filters.rest_framework.DjangoFilterBackend',) を追加

INSTALL_APPS = (
    # :
    # :
    'django_filters',
)

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',)
}

フィルタは filter_backends 属性に 定義することで 利用可能になります。

ここでは django-filter に関連する 3つのバックエンドを紹介します。

DjangoFilterBackend

DjangoFilterBackend は フィールドに対応する完全一致フィルタのようです。

対象のフィールドを filter_fields に複数指定します。

SearchFilter

View の search_fields 属性 にフィールド名を指定するだけで利用できます。 一致ルールはデフォルトで部分一致ですがプリフィックスを指定することで変更できます。

文字

説明

^

前方一致

=

完全一致

@

全文検索(MySQLのみサポート)

$

正規表現一致

デフォルトでは search パラメータが使われます。

変更する場合は settings の SEARCH_PARAM を変更します。 簡単ですね。

REST_FRAMEWORK = {
    'SEARCH_PARAM': 'q',
}
OrderingFilter

さて、実は フィルタリングと言っていますが、ソート機能も含まれます。

ordering パラメータ にフィールド名を指定することで そのフィールドでソートできます。 フィールド名だけを指定した場合は 昇順 となり、 先頭に - を加えることで 降順 になります。

この機能を有効化するためには ordering_fields 属性に ソートを許可するフィールド名を タプルで 指定します。 全フィールド許可の場合は __all__文字列で 指定します。

デフォルトのソートを指定するには ordering 属性にフィールドをタプルで指定します。

実際に 以下のいずれかのルールにマッチし、IDかユーザ名でソートするようなビューを定義してみます。

  • username に部分一致する

  • email に前方一致する

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

class UserListView(generics.ListAPIView):
    #pagination_class = MyPagination
    serializer_class = UserSerializer
    queryset = User.objects.filter()
    filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter)
    filter_fields = ('username',)
    search_fields = ('username', '^email')
    ordering_fields = ('id', 'username')
    ordering = ('id', 'username',)
# フィルタ条件なし(全県
$ curl -X GET 'http://127.0.0.1:8000/user/'
[{"id":1,"username":"test1"},{"id":2,"username":"test2"},{"id":3,"username":"test3"}]
# username が test1 と完全一致するレコード
$ curl -X GET 'http://127.0.0.1:8000/user/?username=test1'
[{"id":1,"username":"test1"}]
# 完全一致でないと抽出できない
$ curl -X GET 'http://127.0.0.1:8000/user/?username=1'
[]
# username か email(前方) に 3 を含む user
$ curl -X GET 'http://127.0.0.1:8000/user/?q=3'
[{"id":3,"username":"test3"}]
# id フィールドの昇順
$ curl -X GET 'http://127.0.0.1:8000/user/?ordering=id'
[{"id":1,"username":"test1"},{"id":2,"username":"test2"},{"id":3,"username":"test3"}]
# id フィールドの降順
$ curl -X GET 'http://127.0.0.1:8000/user/?ordering=-id'
[{"id":3,"username":"test3"},{"id":2,"username":"test2"},{"id":1,"username":"test1"}]

通常は search パラメータで絞り込まれますが、 今回は settings で q に変えているので q パラメータで絞込文字列を指定します。

リファレンス

http://www.django-rest-framework.org/api-guide/filtering/

Error

'RenameAttributes' object is not iterable が発生した場合 DEFAULT_FILTER_BACKENDS がタプルで指定されているかどうかを確認してください。

'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',) の最後のカンマが抜けてたりすると発生します。

Parser

リクエストパラメータの解析方法を指定します。

例えば、 JSON だけを許可する場合は settings のキーに DEFAULT_PARSER_CLASSESJSONParser のモジュールパスを指定します。

REST_FRAMEWORK = {
    'DEFAULT_PARSER_CLASSES': (
        'rest_framework.parsers.JSONParser',
    )
}

ビューによって変更したい場合は parser_classes 属性にタプルで指定します。

from rest_framework.parsers import JSONParser, FormParser

class EchoView(APIView):
    parser_classes = (JSONParser, FormParser)

    def post(self, request):
        return Response(request.data)

urlpatterns += [url(r'^echo/$', EchoView.as_view())]

リクエストを Content-Type にあった 形式で投げてみます

# form-data 形式のパラメータ
$ curl -X POST 'http://127.0.0.1:8000/echo/' -d 'a=1&b=2'
{"a":"1","b":"2"}
# json 形式のパラメータ
$ curl -X POST 'http://127.0.0.1:8000/echo/' -H 'Content-Type: application/json' -d '{"a":1,"b":2}'
{"a":1,"b":2}
# content-type とデータの形式が合わないとエラー
$ curl -X POST 'http://127.0.0.1:8000/echo/' -H 'Content-Type: application/json' -d 'a=1&b=2'
{"detail":"JSON parse error - Expecting value: line 1 column 1 (char 0)"}

ちなみに parser_classes を JSONParser だけにすると ..

$ curl -X POST 'http://127.0.0.1:8000/echo/' -d 'a=1&b=2'
{"detail":"Unsupported media type \"application/x-www-form-urlencoded\" in request."}

DRFに用意されているのは以下で、 Content-Type に指定された MIME-Type によって どれを使うか決定します。

パーサ

対象のMIMEタイプ

説明

JSONParser

application/json

JSON を解析するためのパーサ

FormParser

application/x-www-form-urlencoded

URLエンコード形式のパラメータを解析するためのパーサ。 例) a=1&b=2

MultiPartParser

multipart/form-data

マルチパート形式の パラメータを解析するためのパーサ。 ファイルアップロードとかに使われる

FileUploadParser

*/*

上記のどれにも該当しないデータの受取に使われるパーサ。受け取ったデータは request.data['file'] に入る。

ドキュメントの記述は見つけられませんでしたが、指定しない場合でも JSONParser, FormParser はデフォルトで使えるみたいです。

リファレンス

http://www.django-rest-framework.org/api-guide/parsers/

お疲れ様でした。よいDRFライフを。