DEV Community

kaede
kaede

Posted on • Updated on

Django REST FRAMEWORK Tutorial 12 -- Users のエンドポイントと Snippets の Users を使ったデータを作成する

前回までの復習と今回やること

https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#tutorial-4-authentication-permissions

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 を追加

https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#adding-information-to-our-model

モデルファイルの 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)
Enter fullscreen mode Exit fullscreen mode

現状 created, title, code, lineos, launguage, style,
これらのカラムが定義されている。

ここに

owner = models.ForeignKey(
  'auth.User', 
  related_name='snippets', 
  on_delete=models.CASCADE
)
highlighted = models.TextField()
Enter fullscreen mode Exit fullscreen mode

owner と highlighted のカラムを追加。

owner に外部キー制約をつける。
auth.User テーブルの snippets カラム。
削除時には関連テーブルも削除されるようにする。
こちらが親で、このカラムのデータが削除されたときは
参照していた User テーブルのデータも削除されると仮定。

highlighted は普通の自由な文字列。


Snippet モデルに save を追加

https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#adding-information-to-our-model

Class Snippet の中に def save を作る。
まずは必要なライブラリを import する。

from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

user系、auth系のmigrationファイルが作成された


 サーバーを立てて POST してみる

実際にこれでサーバーを立てて実行してみるとデータがなくなっており

NOT NULL constraint failed: snippets_snippet.owner_id

owner_id がないので実行エラーになった。


User のシリアライザ、ビュー、URL を作成してエンドポイントを作る

https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#adding-endpoints-for-our-user-models

User シリアライザを作る

snippets/serializers.py

ここに UserSerializers を追加する。

現状は

class SnippetSerializer(serializers.ModelSerializer):
    class Meta:

    def create(self, validated_data):

    def update(self, instance, validated_data):
Enter fullscreen mode Exit fullscreen mode

SnippetSerializer の中に Meta, create, update, がある

この SnippetSerializer に並列して
UserSerializer を作成して
中に Meta を入れる。

詳細を見る

from django.contrib.auth.models import User
Enter fullscreen mode Exit fullscreen mode

Django 本体の auth モデルから User を import する。

from snippets.models import Snippet
Enter fullscreen mode Exit fullscreen mode

自分でモデルファイルから作成した Snippet と比べてみると違う。

class UserSerializer(serializers.ModelSerializer):
    snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

    class Meta:
        model = User
        fields = ['id', 'username', 'snippets']
Enter fullscreen mode Exit fullscreen mode

そして モデルシリアライザから 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):
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()),
]
Enter fullscreen mode Exit fullscreen mode

現状
snippets/ で views ファイルの SnippetList
snippets/{id} で views ファイルの SnppetList
これらがリンク(ルーティング)されている。

path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),
Enter fullscreen mode Exit fullscreen mode

これに users/ users/{id} の URL を追加する。
users/ に views ファイルの UserList
users/{id} に views ファイルの UserDetail

これらを新しく追加した。



Users API の動作確認

http://localhost:8002/users

ここにアクセスすることで

Image description

User List
GET /users/
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

[]
Enter fullscreen mode Exit fullscreen mode

このように User API を DRF の管理画面で読み込むことができた。

しかし、モデルを作り直して中身がないので Django admin の 管理ユーザーを作る。

dc run web \
python manage.py \
createsuperuser
Enter fullscreen mode Exit fullscreen mode

username: root
e-mail: kaede0902js@gmail.com
pass: 1234

で作成する

Image description

すると users の API エンドポイントにアクセスして
今作った superuser が読み取れた!


views/SnippetList に perform_create を作成

https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#associating-snippets-with-users

現状、コードスニペットの snippet のインスタンスを作っても
作成したユーザーとの関連性がないらしい。

なので view にカスタム create ? な perform_create のエンドポイントを作る

def perform_create(self, serializer):
    serializer.save(owner=self.request.user)
Enter fullscreen mode Exit fullscreen mode

これで 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']
Enter fullscreen mode Exit fullscreen mode

この Meta の定義しているところに

        owner = serializers.ReadOnlyField(source='owner.username')
Enter fullscreen mode Exit fullscreen mode

views から owner として渡された self.request.user
これの中の username をシリアライザで Read Only にして
owner 変数に入れる。

これで owner として username が渡せるようになった。


views/SnippetList, SnippetDetail にログインユーザーの RW 、非ログインユーザーの RO をつける。

https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#adding-required-permissions-to-views

from rest_framework import permissions
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

この SnippetList と SnippetDetail に

permission_classes = [permissions.IsAuthenticatedOrReadOnly]
Enter fullscreen mode Exit fullscreen mode

シリアライザのクラスと同じノリで
パーミッション(アクセス許可)のクラスも指定する。
認証されていなければ読み取り専用、
つまり認証されていれば読み取りも書き込みも出来る、
と言ったパーミッションに設定する。


パーミッションの動作確認

https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#adding-login-to-the-browsable-api

上記の認証を書いた後に API にアクセスすると

Image description

OPTIONS のボタンを押しても POST が選択できなくなっている。
成功だ。


urls で api-auth のパスに rest_framework.urls を追加してログイン機能を作る

rest_framework.urls を どこかの url path に結びつけるとログイン機能ができるらしい。

from django.urls import path, include
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

現状 urlpatterns 変数には
snippets/, snippets/{id}
users/, users/{id}
これらをルーティングして URL 引数を受け取り可能にしたコードが書かれている。

urlpatterns += [
    path('api-auth/', include('rest_framework.urls')),
]
Enter fullscreen mode Exit fullscreen mode

ここでさらに urlpatterns の配列に追加をして
api-auth で rest_framework.urls が動くようにセットする。

Image description

これを付け加えるまでは存在しなかったログインボタンが

Image description

付け加えることによって出現した!

Image description

クリックするとログイン画面に向かえる。

Image description

これだけで API 管理画面でのログイン機能が追加できた!

Image description

ログイン後は管理画面で GET, POST, PUT, DELETE ができた。


Postman でも username:password を付け加えると GET/POST ができることを確認

Postman で普通にデータを POST リクエストで送ると

{
    "detail": "Invalid username/password."
}
Enter fullscreen mode Exit fullscreen mode

ユーザー名とパスワードが正しくないと出た。

Image description

Postman の body タブの 2 つ左に Authorization のタブを見つけ
開いてみると、username, password を入れる欄があったので
root/1234 で入れて投稿してみると

POST に成功した。

IsAuthenticatedOrReadOnly と書いてあったので、ログインしてなくても Read, つまり GET はできるかと思っていたが

Image description

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
Enter fullscreen mode Exit fullscreen mode

コメントを見るとそのデータの 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
Enter fullscreen mode Exit fullscreen mode

Image description

既存の 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]
Enter fullscreen mode Exit fullscreen mode

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 が作れる。

なおテーブルとユーザーが結びついていないのでまだまだ未完成。

Discussion (0)