前回までの復習と今回やること
DRF チュートリアル
前回は基礎的なリストと詳細の CRUD を Mixin, Generics を使って簡単に書く方法をやった
今回は認証と許可、ログイン機能の章に進む
外部キーとは
先に親子間のテーブルで出てくる外部キーについてまとめる。
https://products.sint.co.jp/siob/blog/what-is-foreign-key
この記事が分かりやすかった。
雇用者テーブルが ID 、名前、部門ID、支店ID、
とあり、部門テーブルと支店テーブルと結びついている。
この雇用者テーブルの部門ID、支店IDが
外部キーで結ばれていると定義されるらしい。
なので部門IDが 3 までしかないとしたら
雇用者テーブルに新規カラムでを作るときに部門 ID を 4 で作成すると
失敗する。
また、雇用者テーブルで部門ID 3 を使っているときに
部門テーブルのID 3 を削除すると失敗する。使われているので。
これが外部キー制約だと解釈する。
なお、今の雇用者テーブルと部門テーブルだと、
部門ID として使われている方の部門テーブルが親テーブルになる。
また ON DELETE CASCADE という設定があり、
これを設置していると、部門ID 3 が使われていても削除可能になり
そのときに部門ID 3 を使っていてた雇用者テーブルのカラムは削除される。
*args, **kwargs とは
https://aiacademy.jp/media/?p=1496
可変長引数というらしい。
*args
で取った場合は順番通りに引数を受け取れる。
JS のスプレッド演算子 ...args
と同じだと解釈。
**kwargs
で取った場合は順番なしで辞書型でデータを受け取る。
sample(arg1=1, arg2=2)
これで渡して
{'arg1' :1, 'arg2': 2}
これが渡される。
JS に同じものはないと推測する。
model ファイルの Snippet クラスに User から使われる owner と highlighted を追加
モデルファイルの Snippet テーブルを生成させる Snippet クラスを変更。
class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default='')
code = models.TextField()
linenos = models.BooleanField(default=False)
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
現状 created, title, code, lineos, launguage, style,
これらのカラムが定義されている。
ここに
owner = models.ForeignKey(
'auth.User',
related_name='snippets',
on_delete=models.CASCADE
)
highlighted = models.TextField()
owner と highlighted のカラムを追加。
owner に外部キー制約をつける。
auth.User テーブルの snippets カラム。
削除時には関連テーブルも削除されるようにする。
こちらが親で、このカラムのデータが削除されたときは
参照していた User テーブルのデータも削除されると仮定。
highlighted は普通の自由な文字列。
Snippet モデルに save を追加
Class Snippet の中に def save を作る。
まずは必要なライブラリを import する。
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight
pygments から get_lexer_by_name, HtmlFormatter, highlight,
これらの HTML のハイライト用のライブラリを import
def save(self, *args, **kwargs):
"""
Use the `pygments` library to create a highlighted HTML
representation of the code snippet.
"""
lexer = get_lexer_by_name(self.language)
linenos = 'table' if self.linenos else False
options = {'title': self.title} if self.title else {}
formatter = HtmlFormatter(style=self.style, linenos=linenos,
full=True, **options)
self.highlighted = highlight(self.code, lexer, formatter)
super().save(*args, **kwargs)
Class Snippet の中にこれを書く。
詳しく見ていくと
get_lexer_by_name を使って今の内部のデータの launguage に基づいて
解析機を取得。 lexer という変数に入れる。
今の内部データの lineo が True なら 'table' の文字列
なければ False を lineos 変数に入れる。
今の内部データに title があれば
それを {'title': titleData} という状態で
なければ {} を options に入れる。
今の内部データの
style (厳しさ?)と lineos, options を 辞書型で取って
内部データとは別に full に True をセットして
HTMLFormatter にかけて、formatter 変数に入れる。
今の内部データの highlighted に
内部データの code と先ほどこの関数で作った lexer, formatter,
これらを highlight() にかけて
最後に super で save をかけている。
migration/migrate して DB の変更を適用
このチュートリアルでは
Sqlite ファイル、migrations/ ディレクトリ、
これらを削除して
makemigration, migrate, を実行して
DB テーブルを再度構築する流れになっている。
こちらでは Docker と Postgres を使っているが、同じようにファイルを削除して
migrations,migrate を打ってみる
dc run web \
python manage.py \
makemigrations snippets
Starting rest5_db_1 ... done
Creating rest5_web_run ... done
Migrations for 'snippets':
snippets/migrations/0001_initial.py
- Create model Snippet
0001 のイニシャルファイルが無事に作成された。
dc run web \
python manage.py \
migrate
Creating rest5_web_run ... done
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, snippets
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 admin.0003_logentry_add_action_flag_choices... 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 auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
Applying snippets.0001_initial... OK
user系、auth系のmigrationファイルが作成された
サーバーを立てて POST してみる
実際にこれでサーバーを立てて実行してみるとデータがなくなっており
NOT NULL constraint failed: snippets_snippet.owner_id
owner_id がないので実行エラーになった。
User のシリアライザ、ビュー、URL を作成してエンドポイントを作る
User シリアライザを作る
snippets/serializers.py
ここに UserSerializers を追加する。
現状は
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
def create(self, validated_data):
def update(self, instance, validated_data):
SnippetSerializer の中に Meta, create, update, がある
この SnippetSerializer に並列して
UserSerializer を作成して
中に Meta を入れる。
詳細を見る
from django.contrib.auth.models import User
Django 本体の auth モデルから User を import する。
from snippets.models import Snippet
自分でモデルファイルから作成した Snippet と比べてみると違う。
class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
class Meta:
model = User
fields = ['id', 'username', 'snippets']
そして モデルシリアライザから User シリアライザを作成
Snippet のオブジェクト全てから PrimaryKeyRelatedField で snippets
を取ってくる。
これは外部で親テーブルの値を取ってくるのに必要だと予測する。
中身の詳細の Meta で先ほど Django 本体から import した User をモデルにセット。
フィールドは id, username, snippets とする。
User ビューを作る
snippets/views.py
に UserList と UserDetail を作成する。
現状は
class SnippetList(generics.ListCreateAPIView):
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
List で generics の List, Create,
Detail で generics の Retrieve, Update, Destroy,
これらで API を提供している。
ここに UserList と UserDetail を作る。
from django.contrib.auth.models import User
from snippets.serializers import SnippetSerializer, UserSerializer
Django 組み込みの認証モデルから User を import
snippets/serializer から UserSerializer を追加で import
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
List で List(全て並べる), Detail で Retrieve(取ってくる)
これらのみの API を作成する。
create, update, delete, は作らない。
createsuperuser によって user は作成される。
User URL (ルーティング) を作る
urlpatterns = [
path('snippets/', views.SnippetList.as_view()),
path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
]
現状
snippets/ で views ファイルの SnippetList
snippets/{id} で views ファイルの SnppetList
これらがリンク(ルーティング)されている。
path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),
これに users/ users/{id} の URL を追加する。
users/ に views ファイルの UserList
users/{id} に views ファイルの UserDetail
これらを新しく追加した。
Users API の動作確認
ここにアクセスすることで
User List
GET /users/
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
[]
このように User API を DRF の管理画面で読み込むことができた。
しかし、モデルを作り直して中身がないので Django admin の 管理ユーザーを作る。
dc run web \
python manage.py \
createsuperuser
username: root
e-mail: kaede0902js@gmail.com
pass: 1234
で作成する
すると users の API エンドポイントにアクセスして
今作った superuser が読み取れた!
views/SnippetList に perform_create を作成
現状、コードスニペットの snippet のインスタンスを作っても
作成したユーザーとの関連性がないらしい。
なので view にカスタム create ? な perform_create のエンドポイントを作る
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
これで create を呼んだ時にこれが呼ばれる
そして
models ファイルの Snippet クラスに先ほど追加で作成した
owner のカラムが request から現在の user を取れるようになると解釈した。
serializers/SnippetSerializer/Meta に owner を追加
シリアライザでも読み取れるようにする。
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
model = Snippet
fields = ['id', 'title', 'code', 'linenos', 'language', 'style']
この Meta の定義しているところに
owner = serializers.ReadOnlyField(source='owner.username')
views から owner として渡された self.request.user
これの中の username をシリアライザで Read Only にして
owner 変数に入れる。
これで owner として username が渡せるようになった。
views/SnippetList, SnippetDetail にログインユーザーの RW 、非ログインユーザーの RO をつける。
from rest_framework import permissions
views で認証のための permission を DRF から import
class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
この SnippetList と SnippetDetail に
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
シリアライザのクラスと同じノリで
パーミッション(アクセス許可)のクラスも指定する。
認証されていなければ読み取り専用、
つまり認証されていれば読み取りも書き込みも出来る、
と言ったパーミッションに設定する。
パーミッションの動作確認
上記の認証を書いた後に API にアクセスすると
OPTIONS のボタンを押しても POST が選択できなくなっている。
成功だ。
urls で api-auth のパスに rest_framework.urls を追加してログイン機能を作る
rest_framework.urls を どこかの url path に結びつけるとログイン機能ができるらしい。
from django.urls import path, include
path についで urls も import
urlpatterns = [
path('snippets/', views.SnippetList.as_view()),
path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),
]
urlpatterns = format_suffix_patterns(urlpatterns)
現状 urlpatterns 変数には
snippets/, snippets/{id}
users/, users/{id}
これらをルーティングして URL 引数を受け取り可能にしたコードが書かれている。
urlpatterns += [
path('api-auth/', include('rest_framework.urls')),
]
ここでさらに urlpatterns の配列に追加をして
api-auth で rest_framework.urls が動くようにセットする。
これを付け加えるまでは存在しなかったログインボタンが
付け加えることによって出現した!
クリックするとログイン画面に向かえる。
これだけで API 管理画面でのログイン機能が追加できた!
ログイン後は管理画面で GET, POST, PUT, DELETE ができた。
Postman でも username:password を付け加えると GET/POST ができることを確認
Postman で普通にデータを POST リクエストで送ると
{
"detail": "Invalid username/password."
}
ユーザー名とパスワードが正しくないと出た。
Postman の body タブの 2 つ左に Authorization のタブを見つけ
開いてみると、username, password を入れる欄があったので
root/1234 で入れて投稿してみると
POST に成功した。
IsAuthenticatedOrReadOnly と書いてあったので、ログインしてなくても Read, つまり GET はできるかと思っていたが
GET も username/password なしだと失敗した。
また DELETE も使えない
毎回 username:password の情報をつけて送らなければいけないため
まだ未完成だと予測する。
snippets.py を作り permission で SAFE_METHODS が使えることを明記する
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions are only allowed to the owner of the snippet.
return obj.owner == request.user
コメントを見るとそのデータの owner だけがアクセスできるようになる?
permissions のライブラリを DRF 本体から import して
BasePermission を使って IsOwnerOrReadOnly クラスを作り
その中に has_object_permission 関数を作り
普通の HTTP リクエストだったら True を返し
obj の owner にリクエストのオーナーを入れるようにする
この permisson ファイルの IsOwnerOrReadOnly クラスは
views で使用する
別のユーザーを作って GET/POST を試す
dc run web \
python manage.py \
createsuperuser
user1
user1@gmail.com
1234
既存の root とは別に user1 を作ったが、同じ DB にアクセスできてしまう
POST しても、owner のデータが入らない...
perform_create は print を挟むと動いているのを確認した。
views で permissions の IsOwnerOrReadOnly を読み込んで使用
from snippets.permissions import IsOwnerOrReadOnly
class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
views の SnippetList や SnippetDetail で呼ばれている。
Postman で DELETE を再度試す
200 はでるが実際にはデータは消えない。
ブラウザでしかできない...?
POST はできるのに...
まとめ
snippets/model.py/snippet クラスに
外部キーとしてユーザー情報をとる owner を追加
フォーマットされたコードを保存する save クラスを追加
snippets/serializers.py/UserSerializer クラスを作成して
Meta で id, user, snippets を持つようにする
snippets/views.py/UserList,UserDetail クラスを作成して
List と Retrieve を作成
snippets/urls.py の urlpatterns で users/, users/{id},
これらのルーティングを作成
DB ファイルを消して migration/migrate する
これで
localhost:8002/users
localhost:8002/user/{id}
これらの URL が機能するようになる
ユーザーは createsuperuser で作る。
snippets/views.py/SnippetList に perform_create を作成して
owner をシリアライズ
snippets/serializers.py/SnippetSerializer/Meta/ に owner を ReadOnlyField で追加
snippets/views.py/SnippetList, SnippetDetail に IsAuthenticatedOrReadOnly をつけてログイン時のみ GET/POST できるようにする
snippets/urls.py で auth-api のパスに rest_framework.urls をつける
これで API 管理画面でもログインしないと使えないようにできた。
snippets/snippets.py を作り permission で SAFE_METHODS が使えることを明記したが、これは効果がなかった
別のユーザーを createsuperuser で作成したが、作成したユーザーのみデータを見れるなんてことはなかった。どうして作るように書いてあったか謎。
しかし、
views にセキュリティをつけてログインしないと見れなくして
urls で rest_framework.urls でログインを作って
serializers, views, urls,
これらで User のテーブルの操作を作って
DB ファイルを消して migration/migrate すると
ログイン必須な API が作れる。
なおテーブルとユーザーが結びついていないのでまだまだ未完成。
Top comments (0)