こんにちは。KOUKIです。
前回は、Djangoのマネジメントコマンドを作成しました。
今回からDjango REST frameworkを使って、API開発をしていきます。
最初に作成するAPIは、UserAPIです。
このAPIは、ユーザーの作成や認証をしてくれます。
以前作成したaccountアプリケーションにそのまま実装していきます。
<目次>
記事まとめ
事前準備
まずは、settings.pyに次の設定を追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# settings.py INSTALLED_APPS = [ 'account', 'rest_framework', # new 'rest_framework.authtoken', # new 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] |
次にテストコード追加用のフォルダとファイルを作成してください。
1 |
touch app/account/tests/test_users_api.py |
UserAPI – CREATE – の実装
最初にテストコードを書きます。
先ほど追加したtest_users.api.pyに次のテストコードを書いてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# app/account/tests/test_users_api.py from django.test import TestCase from django.contrib.auth import get_user_model from django.urls import reverse from rest_framework.test import APIClient from rest_framework import status CREATE_USER_URL = reverse('account:create') def create_user(**params): return get_user_model().objects.create_user(**params) class PublicUserAPITest(TestCase): def setUp(self): self.client = APIClient() # 1 def test_create_valid_user_success(self): """Check if user can create with valid payload""" payload = { 'email': 'test@selfnote.work', 'password': 'selfnote', 'name': 'SELF NOTE USER', } res = self.client.post(CREATE_USER_URL, payload) self.assertEqual(res.status_code, status.HTTP_201_CREATED) user = get_user_model().objects.get(**res.data) self.assertNotIn('password', res.data) # 2 def test_user_exists(self): """Check if creating user is already exists fails""" payload = {'email': 'test@selfnotedev.work', 'password': 'selfnote'} create_user(**payload) res = self.client.post(CREATE_USER_URL, payload) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) # 3 def test_password_too_short(self): """Check if the password must be more than 5 chaaracters""" payload = {'email': 'test@selfnote.work', 'password': 'pw'} res = self.client.post(CREATE_USER_URL, payload) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) user_exists = get_user_model().objects.filter( email=payload['email']).exists() self.assertFalse(user_exists) |
「CREATE_USER_URL = reverse(‘account:create’)」は、これから作成するUserAPIのリクエスト先です。
テストについては、次の意味があります。
- 1 -> ユーザー作成テスト
- 2 -> 同一ユーザーが作成されないかのテスト
- 3 -> パスワードの文字数テスト(最低5文字以上)
それでは、テストを実行してみましょう。
1 2 3 4 |
make test app=account E django.urls.exceptions.NoReverseMatch: 'account' is not a registered namespace |
このエラーは、「CREATE_USER_URL = reverse(‘account:create’)」で出ていますね。
accountアプリケーション配下に「urls.py」を作成してください。
1 |
touch app/account/urls.py |
ここにUserAPIへのリクエスト先を記載します。
1 2 3 4 5 6 7 8 9 10 11 12 |
# app/account/urls.py from django.urls import path from account import views app_name = 'account' urlpatterns = [ path('create/', views.CreateUserView.as_view(), name='create'), ] |
テストを実行します。
1 2 3 |
make test app=account E django.urls.exceptions.NoReverseMatch: 'account' is not a registered namespace |
まだ同じエラーがでます。実は、urls.pyには2種類あって、appプロジェクト配下のurls.pyにもパスを指定する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# app/app/urls.py from django.contrib import admin from django.urls import include, path # new urlpatterns = [ path('admin/', admin.site.urls), path('api/account/', include('account.urls')),# new ] |
テストを実行します。
1 2 3 4 |
make test app=account E AttributeError: module 'account.views' has no attribute 'CreateUserView' |
エラーが変わりましたね。いい感じです。
次は、accountのviews.pyにCreateUserViewを追加しましょう。
1 2 3 4 5 6 7 |
# app/account/views.py from rest_framework import generics class CreateUserView(generics.CreateAPIView): pass |
テストを実行します。
1 2 3 4 |
make test app=account AssertionError: 'CreateUserView' should either include a `serializer_class` attribute, or override the `get_serializer_class()` method. |
これは、Serializerクラスが定義されていないことに対するエラーですね。
Django REST frameworkでは、データ変換時の処理にSerializerクラスを使用します。
ざっくり言うと、Django のモデルデータを JSON形式に変換して処理できるようにします。
accountアプリケーション配下に次のファイルを追加してください。
1 |
touch app/account/serializers.py |
このファイルの中にUserSerializerクラスを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# app/account/serializers.py from django.contrib.auth import get_user_model from rest_framework import serializers class UserSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() fields = ('email', 'password', 'name') extra_kwargs = {'password': {'write_only': True, 'min_length': 5}} def create(self, validated_data): return get_user_model().objects.create_user(**validated_data) |
accountモデルのフィールド「’email’, ‘password’, ‘name’」をシリアライズします。
その際にオプションとして、書き込みOKと最小文字数を5に設定しています。
create関数の”validated_data”には、シリアライズ後の有効データが入ります。
views.pyにこのシリアライズクラスを追加しましょう。
1 2 3 4 5 6 7 8 |
# app/account/views.py from rest_framework import generics from account.serializers import UserSerializer class CreateUserView(generics.CreateAPIView): serializer_class = UserSerializer |
テストを実行します。
1 2 3 |
make test app=account ========= 12 passed in 2.58s ========== |
UserAPI – AUTHENTICATION – の実装
続いて、ユーザー認証を実装します。
まずは、テストコードを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# app/account/tests/test_users_api.py from django.test import TestCase from django.contrib.auth import get_user_model from django.urls import reverse from rest_framework.test import APIClient from rest_framework import status CREATE_USER_URL = reverse('account:create') # new TOKEN_URL = reverse('account:token') def create_user(**params): return get_user_model().objects.create_user(**params) class PublicUserAPITest(TestCase): def setUp(self): self.client = APIClient() # ... # 1 def test_create_token_for_user(self): """Check if user has a token after creating user""" payload = {'email': 'test@selfnotedev.work', 'password': 'selfnote'} create_user(**payload) res = self.client.post(TOKEN_URL, payload) self.assertIn('token', res.data) self.assertEqual(res.status_code, status.HTTP_200_OK) # 2 def test_create_token_invalid_credentials(self): """Check if the token is not created by passing invalid credentials""" create_user(email='test@selfnote.work', password='selfnote') payload = {'email': 'test@selfnote.work', 'password': 'wrongpassword'} res = self.client.post(TOKEN_URL, payload) self.assertNotIn('token', res.data) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) # 3 def test_create_token_no_user(self): """Check if token is not created if user doen't exiist""" payload = {'email': 'test@selfnote.work', 'password': 'wrongpassword'} res = self.client.post(TOKEN_URL, payload) self.assertNotIn('token', res.data) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) # 4 def test_create_token_missin_field(self): """Check if email and password are required""" res = self.client.post(TOKEN_URL, {'email': 'one', 'password': ''}) self.assertNotIn('token', res.data) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) |
URLパスとして「TOKEN_URL = reverse(‘account:token’)」を追加しました。
それぞれのテストの意味については、以下に記載します。
- 1 -> ユーザー作成後Tokenが取得できること
- 2 -> 認証情報に誤りがあった場合、Tokenが取得できないこと
- 3 -> ユーザーが作成されていない状態の場合、400エラーが返却されること
- 4 -> email, passwordに誤りがある場合、400エラーが返却されること
テストを実行します。
1 2 3 4 |
make test E django.urls.exceptions.NoReverseMatch: Reverse for 'token' not found. 'token' is not a valid view function or pattern name. |
「TOKEN_URL = reverse(‘account:token’)」でエラーが発生していますね。
accountのurls.pyに以下のパスを追加してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 |
# app/account/urls.py from django.urls import path from account import views app_name = 'account' urlpatterns = [ path('create/', views.CreateUserView.as_view(), name='create'), path('token/', views.CreateTokenView.as_view(), name='token'), # new ] |
テストを実行します。
1 2 3 4 |
make test app=account E AttributeError: module 'account.views' has no attribute 'CreateTokenView' |
views.pyに’CreateTokenView’を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# app/account/views.py from rest_framework import generics from rest_framework.authtoken.views import ObtainAuthToken from rest_framework.settings import api_settings from account.serializers import UserSerializer class CreateUserView(generics.CreateAPIView): serializer_class = UserSerializer class CreateTokenView(ObtainAuthToken): pass |
テストを実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
make test app=account =================================== FAILURES =================================== _________________ PublicUserAPITest.test_create_token_for_user _________________ self = <account.tests.test_users_api.PublicUserAPITest testMethod=test_create_token_for_user> def test_create_token_for_user(self): """Check if user has a token after creating user""" payload = {'email': 'test@selfnotedev.work', 'password': 'selfnote'} create_user(**payload) res = self.client.post(TOKEN_URL, payload) > self.assertIn('toekn', res.data) E AssertionError: 'toekn' not found in {'username': [ErrorDetail(string='This field is required.', code='required')]} |
“token”が生成されていないようですね。
認証用のシリアライザを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# app/account/serializers.py # new from django.contrib.auth import get_user_model, authenticate from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers class UserSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() fields = ('email', 'password', 'name') extra_kwargs = {'password': {'write_only': True, 'min_length': 5}} def create(self, validated_data): return get_user_model().objects.create_user(**validated_data) # new class AuthTokenSerializer(serializers.Serializer): email = serializers.CharField() password = serializers.CharField( style={'input_type': 'password'}, trim_whitespace=False) def validate(self, attrs): email = attrs.get('email') password = attrs.get('password') user = authenticate( request=self.context.get('request'), username=email, password=password) if not user: msg = _('Unable to authenticate with provide credentials') raise serializers.ValidationError(msg, code='authentication') attrs['user'] = user return attrs |
続いて、views.pyにAuthTokenSerializerを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# app/account/views.py from rest_framework import generics from rest_framework.authtoken.views import ObtainAuthToken from rest_framework.settings import api_settings from account.serializers import UserSerializer, AuthTokenSerializer class CreateUserView(generics.CreateAPIView): serializer_class = UserSerializer class CreateTokenView(ObtainAuthToken): serializer_class = AuthTokenSerializer # https://www.django-rest-framework.org/api-guide/renderers/ renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES |
テストを実行します。
1 2 3 4 |
make test app=account ========= 16 passed in 3.21s ======== |
テストがパスしましたね。
UserAPI – Endpoint作成 –
ユーザー情報をアップデートする関数を作成します。
最初にテストコードを書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# test_users.api.py from django.test import TestCase from django.contrib.auth import get_user_model from django.urls import reverse from rest_framework.test import APIClient from rest_framework import status CREATE_USER_URL = reverse('account:create') TOKEN_URL = reverse('account:token') ME_URL = reverse('account:me') # new def create_user(**params): return get_user_model().objects.create_user(**params) class PublicUserAPITest(TestCase): ... # 1 def test_retrieve_user_unauthorized(self): """Check if the authentication is required for users""" res = self.client.get(ME_URL) self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) class PrivateUserApiTests(TestCase): def setUp(self): self.user = create_user( email='test@selfnote.work', password='selfnote', name='SELF nOTE') self.client = APIClient() self.client.force_authenticate(user=self.user) # 2 def test_retrieve_profile_success(self): """Check if retrieving profile for logged in userd""" res = self.client.get(ME_URL) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertEqual(res.data, { 'name': self.user.name, 'email': self.user.email}) # 3 def test_post_me_not_allowed(self): """Check if POST is not allowd on the me url""" res = self.client.post(ME_URL, {}) self.assertEqual(res.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) # 4 def test_update_user_profile(self): """Check if updating the user profile for authenticated user""" payload = {'name': 'new name', 'password': 'newpassword123'} res = self.client.patch(ME_URL, payload) self.user.refresh_from_db() self.assertEqual(self.user.name, payload['name']) self.assertEqual(res.status_code, status.HTTP_200_OK) |
「ME_URL = reverse(‘account:me’)」を新たに追加しました。
まだ、urls.pyに登録していないパスなので、これでエラーがでますね。
そして、「 self.client.force_authenticate(user=self.user)」では、ユーザーを強制的にログイン済みにできます。
テストについては、次の通りです。
- # 1 -> 未ログイン時のレスポンスが401かチェック
- # 2 -> ログイン時のレスポンスが200かチェック
- # 3 -> POSTが許可されていないかチェック(GETを使用する)
- # 4 -> User Profileの更新チェック
テストを実行します。
1 2 3 |
make test E django.urls.exceptions.NoReverseMatch: Reverse for 'me' not found. 'me' is not a valid view function or pattern name. |
予想通りのエラーですね。
account配下のurls.pyにパスを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# app/account/urls.py from django.urls import path from account import views app_name = 'account' urlpatterns = [ path('create/', views.CreateUserView.as_view(), name='create'), path('token/', views.CreateTokenView.as_view(), name='token'), path('me/', views.ManageUserView.as_view(), name='me'), # new ] |
テストを実行します。
1 2 3 4 |
make test E AttributeError: module 'account.views' has no attribute 'ManageUserView' |
views.pyにManageUserView関数を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# app/account/views.py from rest_framework import generics, authentication, permissions # new from rest_framework.authtoken.views import ObtainAuthToken from rest_framework.settings import api_settings from account.serializers import UserSerializer, AuthTokenSerializer class CreateUserView(generics.CreateAPIView): serializer_class = UserSerializer class CreateTokenView(ObtainAuthToken): serializer_class = AuthTokenSerializer # https://www.django-rest-framework.org/api-guide/renderers/ renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES # new class ManageUserView(generics.RetrieveUpdateAPIView): serializer_class = UserSerializer authentication_classes = (authentication.TokenAuthentication,) permission_classes = (permissions.IsAuthenticated,) def get_object(self): return self.request.user |
Update用のRetrieveUpdateAPIViewを継承したクラスを作成しました。
続いてserializers.pyにupdate関数を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# app/account/serializers.py from django.contrib.auth import get_user_model, authenticate from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers class UserSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() fields = ('email', 'password', 'name') extra_kwargs = {'password': {'write_only': True, 'min_length': 5}} def create(self, validated_data): return get_user_model().objects.create_user(**validated_data) # new def update(self, instance, validated_data): password = validated_data.pop('password', None) user = super().update(instance, validated_data) if password: user.set_password(password) user.save() return user |
テストを実行します。
1 2 3 4 |
make test ============== 20 passed in 3.41s ============== |
テストがパスしましたね。
長くなってきましたので、ここで一旦終わりにしましょう。
次回
次回は、Tag APIの実装していきます。
関連記事
基礎編はこちらです。
Djangoおすすめ書籍
Djangoを学ぶなら以下の書籍がオススメです。
緑 -> 赤 -> 紫の順でやればOKです。読みやすい英語で書かれているので、英語力もついでに上がるかもしれません^^
コメントを残す
コメントを投稿するにはログインしてください。