Django REST FRAMEWORK Tutorial 12 -- Users のエンドポイントと Snippets の Users を使ったデータを作成する
Tags: django, djangorestframework
Published: 2022-05-14 08:33:38


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

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 クラスを変更。

```py
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, 
これらのカラムが定義されている。

ここに 

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

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

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

```py
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

```py
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 を打ってみる

```php
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 のイニシャルファイルが無事に作成された。

```php
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 を作成してエンドポイントを作る

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

## User シリアライザを作る

snippets/serializers.py

ここに　UserSerializers を追加する。

現状は

```py
class SnippetSerializer(serializers.ModelSerializer):
    class Meta:

    def create(self, validated_data):

    def update(self, instance, validated_data):
```

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

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

詳細を見る

```py
from django.contrib.auth.models import User
```

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

```py
from snippets.models import Snippet
```

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

```py
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 を作成する。

現状は

```py
class SnippetList(generics.ListCreateAPIView):

class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
```

List で generics の List, Create, 
Detail で generics の Retrieve, Update, Destroy,

これらで API を提供している。
ここに UserList と UserDetail を作る。

```py
from django.contrib.auth.models import User
from snippets.serializers import SnippetSerializer, UserSerializer
```

Django 組み込みの認証モデルから User を import
snippets/serializer から UserSerializer を追加で import

```py
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 (ルーティング) を作る

```py
urlpatterns = [
    path('snippets/', views.SnippetList.as_view()),
    path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
]
```

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

```py
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 の動作確認

http://localhost:8002/users

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gunxsqxvr0uiqpac1xzs.png)

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

[]
``` 

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

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

```php
dc run web \
python manage.py \
createsuperuser
```

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

で作成する

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4lt8hydh4hp8218nm0rj.png)

すると 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 のエンドポイントを作る

```py
def perform_create(self, serializer):
    serializer.save(owner=self.request.user)
```

これで create を呼んだ時にこれが呼ばれる
そして
models ファイルの Snippet クラスに先ほど追加で作成した
owner のカラムが request から現在の user を取れるようになると解釈した。

---

## serializers/SnippetSerializer/Meta に owner を追加

シリアライザでも読み取れるようにする。

```py
class SnippetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Snippet
        fields = ['id', 'title', 'code', 'linenos', 'language', 'style']
```

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

```py
        owner = serializers.ReadOnlyField(source='owner.username')
```

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

```py
from rest_framework import permissions
```

views で認証のための permission を DRF から import

```py
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 に

```py
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
```

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

---

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

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

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dd4b16owe9tvh5ktq9bk.png)

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

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

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

```py
from django.urls import path, include
```

path についで urls も import

```py
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 引数を受け取り可能にしたコードが書かれている。

```py
urlpatterns += [
    path('api-auth/', include('rest_framework.urls')),
]
```

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ejessb1q7jgfias8ax9q.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qplic8xp1bwoh8qrcw01.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9q6sofupojvggy774pfg.png)

クリックするとログイン画面に向かえる。
 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lljywmbx1t2qle0bd48w.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0t2zru0obliyogmvr1jn.png)

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

---

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

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

```JSON
{
    "detail": "Invalid username/password."
}
```

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s2d8anzjc7gpi62dl3p4.png)
 

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

POST に成功した。

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eiosjjr3b8q3vvmvpcrf.png)

GET も username/password なしだと失敗した。

また DELETE も使えない
 
毎回 username:password の情報をつけて送らなければいけないため
まだ未完成だと予測する。

---

## snippets.py を作り permission で SAFE_METHODS が使えることを明記する

```py
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 を試す

```php
dc run web \
python manage.py \
createsuperuser

user1
user1@gmail.com
1234
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jkva8xcsypv6tyuonyah.png)
 
既存の root とは別に user1 を作ったが、同じ DB にアクセスできてしまう

POST しても、owner のデータが入らない...

perform_create は print を挟むと動いているのを確認した。

---

## views で permissions の IsOwnerOrReadOnly を読み込んで使用

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

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