React+Djangoでブログを作る2〈REST Framework編〉

今回は、REST Frameworkの実装をします。

Djangoでバックエンドを作る

前回の続きから作ります。

Django REST Frameworkのインストール

以下のコマンドでインストールします。

# バージョン確認
python -V
Python 3.7.4

# Django REST Frameworkのインストール pip install djangorestframework

REST Frameworkの登録

Django REST Frameworkもアプリなので、blogress/settings.pyに追記して登録します。
ついでに、REST Framework用の権限設定も追加してください。

INSTALLED_APPS = [
    # ローカルアプリ
    'users.apps.UsersConfig',
    'posts.apps.PostsConfig',

    # サードパーティアプリ
    'rest_framework', # 追記

    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

# 追記
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]
}

ルーティングの設定

次にREST APIにアクセスするためのURLを指定するため、blogress/urls.pyにてルーティングの設定を行います。

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('posts.urls')),
]

これでlocalhost:8000/api/にアクセスすると、postsアプリのルーティングが参照されるようになりました。

APIバージョンをURLに書き込むらしいのですが、私はヘッダにバージョンを付与する方針で行きます。

アプリレベルのルーティングの設定

先ほどはプロジェクトレベルのルーティング設定なので、今度はアプリレベルのルーティングを行います。

新たにposts/urls.pyを作成して以下を記述してください。

from django.urls import path

from .views import PostList, PostDetail

urlpatterns = [
    path('<str:pk>/', PostDetail.as_view()),
    path('', PostList.as_view()),
]

シリアライザの設定

次に、データを送信可能な状態に整形してくれるシリアライザの設定を行います。
このシリアライザによって、REST APIでクライアントから要求されたデータがJSON形式に変換されます。

新たにposts/serializers.pyを作成して以下を記述してください。

from rest_framework import serializers
from .models import Post


class PostSerializer(serializers.ModelSerializer):

    class Meta:
        model = Post
        fields = ('id', 'author', 'title', 'slug', 'thumbnail', 'body', 'created_at',)

ビューの設定

次に、データのビューを設定します。

from rest_framework import generics

from .models import Post
from .serializers import PostSerializer


# ListCreateAPIView -> read-write endpoint
class PostList(generics.ListCreateAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer


# RetrieveUpdateDestoryAPIView -> ALlows read, update, delete
class PostDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

一見どちらも同じ内容のクラスに見えますが、継承しているスーパークラスが違っていて、継承するクラスによってどういったビューになるかが異なるようです。

APIの確認

ここまで設定したら、実際にREST APIにアクセスしてみましょう。

$ python manage.py runserver
# http://localhost:8000/api/ にアクセス

プロジェクトレベルのルーティングでhttp://localhost:8000/api/postsアプリを参照するようになっているので、posts/urls.pyに設定したpath('', PostList.as_view())というアプリレベルのルーティングで記事の一覧が表示されます。

適当な記事のidをコピーしてapi/の後ろに貼り付けると、アプリレベルのルーティングで指定した通り、記事の詳細情報が開かれます。

ちなみに、IDに指定しているUUIDはハイフンを含みますが、ハイフンが無くても同じ情報が参照出来ます。

# 以下の2つは同じ情報を参照する
http://localhost:8000/api/c1e373ee-b513-4410-9c0a-b5608e2f2f06/
http://localhost:8000/api/c1e373eeb51344109c0ab5608e2f2f06/

ログインの実装

Django REST Frameworkを用いるとログインが簡単に実装出来ます。

プロジェクトレベルのルーティングblogress/urls.pyを次のように変更してください。

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('posts.urls')),
    path('api-auth/', include('rest_framework.urls')), # 追記
]

このように追記する事で、画面右上に先ほどは無かった「Log in」のリンクが表示されるようになったはずです。

一般ユーザーの追加

これまで、ユーザーは前回作成した管理者しか居ませんでしたが、権限の設定と確認を行うため一般ユーザーを追加します。

Usersの「+ Add」をクリックしてください。

適当なメールアドレスとパスワードを記入して、画面右下のSAVEをクリックしてください。

SAVEを押した後トップページから一覧に戻るとユーザーが作成されているのが分かると思います。STAFF STATUSが管理権限の有無を示しています。

権限の設定

現在は全てのAPIに対して登録に関わらず全てのユーザーが記事の投稿・削除・閲覧・更新を出来るようになっています。

ブログシステムとしては未登録ユーザーには閲覧のみ許可をしたいので、権限の設定を行いましょう。

ビューレベルの権限の設定

まずはビューレベルでの権限を設定します。
posts/views.pyを次のように変更します。

from rest_framework import generics, permissions # 追記

from .models import Post
from .serializers import PostSerializer


# ListCreateAPIView -> read-write endpoint
class PostList(generics.ListCreateAPIView):
    permission_classes = (permissions.IsAuthenticated,) # 追記
    queryset = Post.objects.all()
    serializer_class = PostSerializer


# RetrieveUpdateDestoryAPIView -> ALlows read, update, delete
class PostDetail(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = (permissions.IsAdminUser,) # 追記
    queryset = Post.objects.all()
    serializer_class = PostSerializer

IsAuthenticatedは登録済みユーザーのみアクセス可能、IsAdminUserは管理者権限のあるユーザーのみアクセス可能となっています。

このように変更すると、ログインしていない状態では記事の一覧も記事の詳細も見る事が出来なくなります。

一般ユーザーでログインすると、記事の一覧は見る事が出来ます。今は詳細表示が複数並んだ形になっていて一覧表示では無いですが。

しかし、記事の詳細を見ようとすると権限で弾かれます。

同じページを管理者で見ると通常通り表示されるのが分かります。

プロジェクトレベルの権限の設定

ビューレベルでの権限の設定は、ビューが増える度に権限設定を行わなければならず、もし忘れてしまった場合に大変な事になる可能性が有ります。

Djangoではプロジェクトレベルでの権限の設定を行えるので、安全のために一旦プロジェクトレベルで管理者以外への全ての操作を禁じて、そこから各操作に対して必要な権限を設定していくのが安全かと思います。

プロジェクトレベルでの権限の設定はblogress/settings.pyで行います。

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAdminUser', # AllowAnyから変更した
    ]
}

確認のために一時的にビューレベルの権限を削除します。

from rest_framework import generics, permissions

from .models import Post
from .serializers import PostSerializer


# ListCreateAPIView -> read-write endpoint
class PostList(generics.ListCreateAPIView):
    # permission_classes = (permissions.IsAuthenticated,) # 一時的にコメントアウト
    queryset = Post.objects.all()
    serializer_class = PostSerializer


# RetrieveUpdateDestoryAPIView -> ALlows read, update, delete
class PostDetail(generics.RetrieveUpdateDestroyAPIView):
    # permission_classes = (permissions.IsAdminUser,) # 一時的にコメントアウト
    queryset = Post.objects.all()
    serializer_class = PostSerializer


プロジェクトレベルの権限の設定によって、今は管理者のみ全ての操作が可能なので、一般ユーザーでログインした場合でも見る事が出来ません。


しかし、先ほどのコメントアウトを元に戻すと一般ユーザーでも見れるようになります。

このように、プロジェクトレベルの権限の設定は、ビューレベルの権限の設定でオーバーライドが可能なので、フェイルセーフのためにプロジェクトレベルの権限を設定するのは有効です。

カスタムパーミッション

Django REST FrameworkのBasePermissionクラスのhas_object_permissionをオーバーライドする事で、独自の権限の設定を行う事が出来るようです。
has_object_permissionはユーザーのリクエストに対して何等かの判定を行った結果、許可する場合はTrueを返し、拒否する場合はFalseを返す関数です。

posts/permissions.pyに以下のように記述してください。

from rest_framework import permissions


class IsAuthorOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        # 著者がリクエストユーザーか、閲覧リクエストの場合のみ許可
        return obj.author == request.user or request.method in permissions.SAFE_METHODS

これに応じて、posts/views.pyも変更します。

from rest_framework import generics, permissions

from .models import Post
from .permissions import IsAuthorOrReadOnly # 追加
from .serializers import PostSerializer


# ListCreateAPIView -> read-write endpoint
class PostList(generics.ListCreateAPIView):
    permission_classes = (permissions.IsAuthenticated,)
    queryset = Post.objects.all()
    serializer_class = PostSerializer


# RetrieveUpdateDestoryAPIView -> ALlows read, update, delete
class PostDetail(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = (IsAuthorOrReadOnly,) # IsAuthorOrReadOnlyに変更
    queryset = Post.objects.all()
    serializer_class = PostSerializer

未登録ユーザーは閲覧のみ許可されています。

登録済みの一般ユーザーも編集する事は出来ません。

著者のみ編集が可能です。

ちなみに、上記のカスタムパーミッションは「リクエストユーザーが著者、またはリクエストが閲覧の場合」を許可しているので、仮にこの一般ユーザーに管理権限を持たせても編集する事は許されません。

カスタムパーミッションを作る際は、管理者のみ絶対に操作出来るようにしておいた方が後々助かるかも知れないです。

トークン認証の実装

Django REST Frameworkにはトークンで認証する機能も予め用意されているため、blogress/settings.pyを次のように編集し、アプリを登録してから有効化します。

INSTALLED_APPS = [
    # ローカルアプリ
    'users.apps.UsersConfig',
    'posts.apps.PostsConfig',

    # サードパーティアプリ
    'rest_framework',
    'rest_framework.authtoken', # 追記

    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAdminUser',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [ # 追記
        # HTTPヘッダにセッションIDを付与してAPIに渡す
        'rest_framework.authentication.BasicAuthentication',
        # ブラウザにログイン・ログアウトの機能を提供する
        'rest_framework.authentication.SessionAuthentication',
        # トークン認証の機能を提供する
        'rest_framework.authentication.TokenAuthentication',
    ],
}

REST Frameworkの認証に関して合計3つの設定を行ったように見えますが、上2つの項目はデフォルトで設定されている物なので、実際に有効化した機能はトークン認証1つです。

INSTALLED_APPSに変更を加えたので、マイグレーションを実行します。

# 今回はマイグレーションファイルは既に用意されているのでmigrateだけ
$ python manage.py migrate

AUTH TOKENという項目が増えていると思います。

RESTでログイン・ログアウト・パスワードリセットを提供

これを実装するために、追加のパッケージを導入する。

# Django-Rest-Authのインストール
$ pip install django-rest-auth

例の如く、blogress/settings.pyにアプリを登録します。カスタムユーザーモデルを使用しているので、それについての設定も行います。

INSTALLED_APPS = [
    # ローカルアプリ
    'users.apps.UsersConfig',
    'posts.apps.PostsConfig',

    # サードパーティアプリ
    'rest_framework',
    'rest_framework.authtoken',
    'rest_auth', # 追記

    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

# 追記
REST_AUTH_SERIALIZERS = {
    'USER_DETAILS_SERIALIZER': 'users.serializers.UserSerializer'
}

そして、上記機能を行うためにblogress/urls.pyでプロジェクトレベルのルーティングを設定しましょう。

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('posts.urls')),
    path('api-auth/', include('rest_framework.urls')),
    path('api/rest-auth/', include('rest_auth.urls')), # 追加
]

これで、以下のURLでそれぞれの機能が提供されるようになったようです。

# ログイン
http://localhost:8000/api/rest-auth/login/
# ログアウト
http://localhost:8000/api/rest-auth/logout/
# パスワードリセット
http://localhost:8000/api/rest-auth/reset/
# パスワードリセットの確認
http://localhost:8000/api/rest-auth/reset/confirm/

恐らく、先ほどまでのログイン・ログアウトはAPIのサイトというかDjango自体のページでの処理で、APIを扱うような形では利用出来なかったためにこのようなパッケージを導入するようになっていると思います。

RESTでサインアップを提供

サインアップとは所謂登録処理です。これも別のパッケージが必要なようです。

# Django-AllAuthのインストール
$ pip install django-allauth

blogress/settings.pyでアプリの登録を行います。

INSTALLED_APPS = [
    # ローカルアプリ
    'users.apps.UsersConfig',
    'posts.apps.PostsConfig',

    # サードパーティアプリ
    'rest_framework',
    'rest_framework.authtoken',
    'allauth', # 追記
    'allauth.account', # 追記
    'allauth.socialaccount', # 追記
    'rest_auth',
    'rest_auth.registration', # 追記

    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites', # 追記
]

# メールで認証確認をする時に使うバックエンドの指定
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# sitesフレームワークの1つでいくつかのDjangoプロジェクトから複数のWebサイトをホストするためのID
SITE_ID = 1

EMAIL_BACKENDはともかく、SITE_IDは何なのか良く分かりませんでした。とりあえず使わなくても大丈夫だそうなのでこのまま行きます。

blogress/urls.pyを編集して、プロジェクトレベルのルーティングの設定を行います。

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('posts.urls')),
    path('api-auth/', include('rest_framework.urls')),
    path('api/rest-auth/', include('rest_auth.urls')),
    path('api/rest-auth/registration/', include('rest_auth.registration.urls')), # 追加
]

そしてマイグレーションを実行します。

$ python manage.py migrate

これで、http://localhost:8000/api/rest-auth/registration/から登録が出来るようになりました。SMTPサーバーの設定をすればメール通知も可能のようです。

ここから登録すると、トークン認証を有効化している場合に同時にトークンが発行されるようです。

ユーザーのビュー

postsアプリでposts/serializers.pyposts/views.pyを作成してビューを作ったように、ユーザーのビューも作成します。

users/serializers.pyを次のように記述します。

from rest_framework import serializers
from .models import User


class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('id', 'username',)

そして、users/views.pyを次のように記述します。

from rest_framework import generics, permissions

from .models import User
from .serializers import UserSerializer


class UserList(generics.ListCreateAPIView):
    permission_classes = (permissions.IsAuthenticated,)
    queryset = User.objects.all()
    serializer_class = UserSerializer


class UserDetail(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = (permissions.IsAuthenticated,)
    queryset = User.objects.all()
    serializer_class = UserSerializer

このビューにアクセス出来るようにルーティングを設定しましょう。

まずはプロジェクトレベルのルーティングblogress/urls.pyから設定します。

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('posts.urls')),
    path('api/users/', include('users.urls')),
    path('api-auth/', include('rest_framework.urls')),
    path('api/rest-auth/', include('rest_auth.urls')),
    path('api/rest-auth/registration/', include('rest_auth.registration.urls')),
]

そして、アプリレベルのルーティングusers/urls.pyを設定します。

from django.urls import path

from .views import UserList, UserDetail


urlpatterns = [
    path('<str:pk>/', UserDetail.as_view()),
    path('', UserList.as_view()),
]

全て設定して、http://localhost:8000/api/users/にアクセスするとユーザー一覧が見られます。

管理者で見ていますが、権限は一般ユーザー以上にしています。

記事と同じくIDを末尾に付けるとそのユーザーの詳細表示が出来ます。

ViewSetsでViewを統合

現在は、UserPostに対してそれぞれListDetailが存在しますが、これをViewSetsを使って統合します。

posts/views.pyは統合を行うと以下のような形になります。

from rest_framework import generics, permissions, viewsets

from .models import Post
from .permissions import IsAuthorOrReadOnly # 追加
from .serializers import PostSerializer


class PostViewSet(viewsets.ModelViewSet):
    permission_classes = (IsAuthorOrReadOnly,)
    queryset = Post.objects.all()
    serializer_class = PostSerializer

同じようにusers/views.pyも統合します。

from rest_framework import generics, permissions, viewsets

from .models import User
from .serializers import UserSerializer


class UserViewSet(viewsets.ModelViewSet):
    permission_classes = (permissions.IsAuthenticated,)
    queryset = User.objects.all()
    serializer_class = UserSerializer

このままではエラーが出るので、次にルーティングを編集します。

SimpleRouterでURLを統合

ビューの次はURLの統合です。

リスト表示と詳細表示の2つがあったposts/urls.pyをSimpleRouterを用いて統合します。

from django.urls import path
from rest_framework.routers import SimpleRouter

from .views import PostViewSet

router = SimpleRouter()
router.register('', PostViewSet, basename='posts')

urlpatterns = router.urls

同じくusers/urls.pyも統合します。

from django.urls import path
from rest_framework.routers import SimpleRouter

from .views import UserViewSet

router = SimpleRouter()
router.register('', UserViewSet, basename='users')

urlpatterns = router.urls

これまで見て来たリストや詳細表示が変わらず見られる事が確認出来ると思います。

OpenAPIを使ってドキュメント表示

OpenAPIを使うとDjango REST FrameworkのAPIスキーマを自動生成出来るようです。

# OpenAPIRendererに必要なPyYAMLをインストール
$ pip install pyyaml

blogress/urls.pyを編集して、プロジェクトレベルのルーティングにスキーマへのルーティングを追記します。

from django.contrib import admin
from django.urls import include, path
from rest_framework.schemas import get_schema_view # 追記
from django.views.generic import TemplateView # 追記

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/posts/', include('posts.urls')),
    path('api/users/', include('users.urls')),
    path('api-auth/', include('rest_framework.urls')),
    path('api/rest-auth/', include('rest_auth.urls')),
    path('api/rest-auth/registration/', include('rest_auth.registration.urls')),
    path('schema/', get_schema_view( # スキーマ表示の追加
        title="Blogress",
        description="API for all things …"
    ), name='openapi-schema'),
    path('docs/', TemplateView.as_view( # ドキュメント表示の追加
        template_name='swagger-ui.html',
        extra_context={'schema_url':'openapi-schema'}
    ), name='swagger-ui'),
]

加えて、ドキュメント表示では表示用のテンプレートファイルが必要になるので、まずはテンプレートディレクトリをblogress/settings.pyで設定しましょう。

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates'], # リスト内に追記
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

そしてこれに対応するようにtemplates/ディレクトリを作成してください。

スキーマのドキュメント表示用のテンプレートはいくつか用意されているようですが、今回はswagger-ui.htmlを利用します。

なので、templates/swagger-ui.htmlを作成し、以下の内容を記述してください。

<!DOCTYPE html>
<html>
  <head>
    <title>Blogress API References</title>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="//unpkg.com/swagger-ui-dist@3/swagger-ui.css" />
  </head>
  <body>
    <div id="swagger-ui"></div>
    <script src="//unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
    <script>
    const ui = SwaggerUIBundle({
        url: "{% url schema_url %}",
        dom_id: '#swagger-ui',
        presets: [
          SwaggerUIBundle.presets.apis,
          SwaggerUIBundle.SwaggerUIStandalonePreset
        ],
        layout: "BaseLayout"
      })
    </script>
  </body>
</html>

新しくディレクトリを手動作成するのはこのシリーズでは初めてなので、確認も兼ねてディレクトリツリーを図示します。

blogress/ (プロジェクトルート)
|-- blogress/
|-- posts/
|-- templates/ (テンプレートディレクトリ)
   |-- swagger-ui.html (テンプレートファイル)
|-- users/
|-- db.sqlite3
|-- manage.py

これで、http://localhost:8000/schema/http://localhost:8000/docs/に繋ぐとそれぞれの表示形式でAPIスキーマを見る事が出来ます。

こちらが生のスキーマです。

そしてこちらがswagger-ui.htmlで装飾表示されたスキーマです。

まとめ

Reactと連携させるためにはもう少し作業が必要ですが、バックエンド側のRESTful APIの基礎的な実装が終わりました。

ほとんどDjangoの機能で実現出来て正直驚きました。

ただ、ほぼ自動で行ってくれるので内部の理解が難しい部分もあるため、使いこなすには時間が掛かりそうです。

参考

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です