- Python 3.11.4 / Django 4.2.4
目次
チュートリアル④:Authentication & Premissions
APIには現在、コードスニペットを編集または削除できるユーザーに関する制限がない。より高度な動作を持つことで、以下の条件を確実に満たしたい:
- コードスニペットには常に作成者が関連付けられていること
- 認証されたユーザーのみがスニペットを作成できること
- スニペットの作成者のみがそれを更新または削除できること
- 認証されていないリクエストは読み取り専用のフルアクセスを持つべきであること
- Adding information to our model Add the following two fields to the
Snippet
model inmodels.py
.owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE) highlighted = models.TextField()
- ownerフィールド
- models.ForeignKey(‘auth.User’, related_name=’snippets’, on_delete=models.CASCADE) :外部キーとしてDjangoの組み込み**
User
モデルを参照している。これにより、各スニペットには所有者(作成者)が関連付けられ、この所有者はUser
**モデルのインスタンスとして存在する。 - related_name=’snippets :この属性は、Userモデルのインスタンスから逆参照でスニペットにアクセスするための関連名を設定する。これにより、特定のユーザーが所有するすべてのスニペットにアクセスできる。
- on_delete=models.CASCADE :これは、関連するユーザーが削除された場合、そのユーザーに関連付けられたすべてのスニペットも自動的に削除されることを意味する。
- models.ForeignKey(‘auth.User’, related_name=’snippets’, on_delete=models.CASCADE) :外部キーとしてDjangoの組み込み**
- highlightedフィールド
- models.TextField() :これは、大量のテキストデータ(この場合は、ハイライトされたHTMLコード)を保存するためのフィールドを定義している。
from pygments.lexers import get_lexer_by_name from pygments.formatters.html import HtmlFormatter from pygments import highlight
And now we can add a.save()
method to our model class: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) # pygmentsライブラリのget_lexer_by_name関数を使って、self.language(スニペットのプログラム言語)に対応するレクサーを取得。 linenos = 'table' if self.linenos else False # self.linenosの真偽値に基づいて、行番号をどのように表示するかを決定。Trueであればlinenosに'table'を設定。 options = {'title': self.title} if self.title else {} # スニペットにタイトルが設定されている場合、そのタイトルをオプションとして追加する処理。タイトルがなければ空の辞書をoptionsに設定する。 formatter = HtmlFormatter(style=self.style, linenos=linenos, full=True, **options) # pygmentsのHtmlFormatterを使ってHTML形式でのフォーマッターを作成。スニペットのスタイル、行番号の表示方法、その他のオプションをこのフォーマッターに渡している。 self.highlighted = highlight(self.code, lexer, formatter) # pygmentsのhighlight関数を使用して、スニペットのコードをハイライトしている。そして、そのハイライトされたHTML表現をモデルのhighlightedフィールドに格納している。 super().save(*args, **kwargs) # 親クラス(Djangoモデルのデフォルトのsaveメソッド)のsaveメソッドを呼び出して、モデルインスタンスを実際にデータベースに保存。
When that’s all done we’ll need to update our database tables. Normally we’d create a database migration in order to do that, but for the purposes of this tutorial, let’s just delete the database and start again.del db.sqlite3 # データベースファイルdb.sqlight3を削除 rmdir /s /q snippets\\migrations # snippetsアプリケーションのmigrationsディレクトリを削除(このディレクトリには以前のデータベースの変更履歴が含まれているため) python manage.py makemigrations snippets # snippetsアプリケーションにタイ足手新しいデータベースの変更履歴を作成 python manage.py migrate # 新しい変更履歴をもとにデータベースのテーブルを更新または作成
You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with thecreatesuperuser
command.python manage.py createsuperuser
- ownerフィールド
- Adding endpoints for our User models Now that we’ve got some users to work with, we’d better add representations of those users to our API. Creating a new serializer is easy. In
serializers.py
add:from django.contrib.auth.models import User class UserSerializer(serializers.ModelSerializer): snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all()) class Meta: model = User fields = ['id', 'username', 'snippets']
Because'snippets'
is a reverse relationship on the User model, it will not be included by default when using theModelSerializer
class, so we needed to add an explicit field for it. We’ll also add a couple of views toviews.py
. We’d like to just use read-only views for the user representations, so we’ll use theListAPIView
andRetrieveAPIView
generic class-based views.from django.contrib.auth.models import User class UserList(generics.ListAPIView): queryset = User.objects.all() serializer_class = UserSerializer class UserDetail(generics.RetrieveAPIView): queryset = User.objects.all() serializer_class = UserSerializer
Make sure to also import theUserSerializer
classfrom snippets.serializers import UserSerializer
Finally we need to add those views into the API, by referencing them from the URL conf. Add the following to the patterns insnippets/urls.py
.path('users/', views.UserList.as_view()), path('users/<int:pk>/', views.UserDetail.as_view()),
- Associating Snippets with Users 現在コードスニペットを作成すると、そのスニペットを作成したユーザーとスニペットインスタンスとを関連付ける方法がない。ユーザーはシリアライズされた表現の一部として送信されるわけではなく、代わりに受信したリクエストのプロパティとして存在する。 これに対処する方法は、スニペットのビューにある**
.perform_create()
**メソッドをオーバーライドすること。これにより、インスタンスの保存の方法を変更したり、受信したリクエストやリクエストされたURLに暗黙的に含まれる情報を取り扱ったりすることができる。 On theSnippetList
view class, add the following method:class SnippetList(generics.ListCreateAPIView): queryset = Snippet.objects.all() serializer_class = SnippetSerializer # ここから下を追加 def perform_create(self, serializer): serializer.save(owner=self.request.user)
シリアライザのcreate()メソッドは、リクエストからの検証済みデータとともに、追加の’owner’フィールドも渡されるようになる。 - Updating our serializer Now that snippets are associated with the user that created them, let’s update our
SnippetSerializer
to reflect that. Add the following field to the serializer definition inserializers.py
:class SnippetSerializer(serializers.ModelSerializer): owner = serializers.ReadOnlyField(source='owner.username') # この行を追加 class Meta: ...
Note: Make sure you also add'owner',
to the list of fields in the innerMeta
class. source引数は、どのデータをフィールドに表示するかを決める役割がある。例えば、ユーザーの名前を表示したい場合、その名前の情報がある場所を指定することができる。ドット記法(例:owner.username
)で、情報の場所を詳しく指定することもできる。これは、Djangoのテンプレートで使う方法に似ている。 私たちが追加したフィールドは、ReadOnlyField
という特別なもので、これは読み取り専用のフィールドで、データを表示するだけの役割がある。つまり、データを変更したり更新したりすることはできない。他のフィールドのように、具体的な型(文字列や真偽値など)が指定されていないのが特徴。しかし、同じようなことをCharField
という別の方法で実現することも可能。 - Adding required permissions to views Now that code snippets are associated with users, we want to make sure that only authenticated users are able to create, update and delete code snippets. REST framework includes a number of permission classes that we can use to restrict who can access a given view. In this case the one we’re looking for is
IsAuthenticatedOrReadOnly
, which will ensure that authenticated requests get read-write access, and unauthenticated requests get read-only access. First add the following import in the views modulefrom rest_framework import permissions
Then, add the following property to both theSnippetList
andSnippetDetail
view classes.permission_classes = [permissions.IsAuthenticatedOrReadOnly]
- Adding login to the Browsable API If you open a browser and navigate to the browsable API at the moment, you’ll find that you’re no longer able to create new code snippets. In order to do so we’d need to be able to login as a user. We can add a login view for use with the browsable API, by editing the URLconf in our project-level
urls.py
file (not snippets\urls.py). Add the following import at the top of the file:from django.urls import path, include
And, at the end of the file, add a pattern to include the login and logout views for the browsable API.urlpatterns += [ path('api-auth/', include('rest_framework.urls')), ]
The'api-auth/'
part of pattern can actually be whatever URL you want to use. Now if you open up the browser again and refresh the page you’ll see a ‘Login’ link in the top right of the page. If you log in as one of the users you created earlier, you’ll be able to create code snippets again. Once you’ve created a few code snippets, navigate to the ‘/users/’ endpoint, and notice that the representation includes a list of the snippet ids that are associated with each user, in each user’s ‘snippets’ field. - Object level permissions Really we’d like all code snippets to be visible to anyone, but also make sure that only the user that created a code snippet is able to update or delete it. To do that we’re going to need to create a custom permission. In the snippets app, create a new file,
permissions.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
Now we can add that custom permission to our snippet instance endpoint, by editing thepermission_classes
property on theSnippetDetail
view class:permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
Make sure to also import theIsOwnerOrReadOnly
class.from snippets.permissions import IsOwnerOrReadOnly
Now, if you open a browser again, you find that the ‘DELETE’ and ‘PUT’ actions only appear on a snippet instance endpoint if you’re logged in as the same user that created the code snippet. - Authentication with the API APIには現在一連の権限が設定されているため、スニペットを編集したい場合はリクエストを認証する必要がある。認証クラスはまだ設定していないので、現在はデフォルトのものが適用されており、それは
SessionAuthentication
とBasicAuthentication
です。 ウェブブラウザを通じてAPIとやり取りする場合、ログインすることができ、その後のブラウザセッションがリクエストに必要な認証を提供する。 APIとプログラム的にやり取りする場合は、各リクエストで認証の資格情報を明示的に提供する必要がある。 認証せずにスニペットを作成しようとすると、エラーが発生する。 サーバーを起動してから以下のコマンドを実行してみる。http POST <http://127.0.0.1:8000/snippets/> code="print(123)" { "detail": "Authentication credentials were not provided." }
We can make a successful request by including the username and password of one of the users we created earlier.
チュートリアル⑤:Relationships & Hyperlinked APIs
現在、API内の関係性はプライマリーキーを使用して表現されている。このチュートリアルでは、関係性にハイパーリンクを使用することで、APIの結束力と発見性を向上させる。
- Creating an endpoint for the root of our API Right now we have endpoints for ‘snippets’ and ‘users’, but we don’t have a single entry point to our API. To create one, we’ll use a regular function-based view and the
@api_view
decorator we introduced earlier. In yoursnippets/views.py
add:from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.reverse import reverse @api_view(['GET']) def api_root(request, format=None): return Response({ 'users': reverse('user-list', request=request, format=format), 'snippets': reverse('snippet-list', request=request, format=format) })
Two things should be noticed here. First, we’re using REST framework’sreverse
function in order to return fully-qualified URLs; second, URL patterns are identified by convenience names that we will declare later on in oursnippets/urls.py
. - Creating an endpoint for the highlighted snippets The other obvious thing that’s still missing from our pastebin API is the code highlighting endpoints. Unlike all our other API endpoints, we don’t want to use JSON, but instead just present an HTML representation. There are two styles of HTML renderer provided by REST framework, one for dealing with HTML rendered using templates, the other for dealing with pre-rendered HTML. The second renderer is the one we’d like to use for this endpoint. コードハイライトのビューを作成する際に考慮するもう一つのことは、使用できる既存の具体的なジェネリックビューがないという点。ここでは、オブジェクトのインスタンスを返すのではなく、オブジェクトインスタンスのプロパティを返す。 具体的なジェネリックビューを使用する代わりに、インスタンスを表現するための基底クラスを使用し、
.get()
メソッドを作成する。snippets/views.py
に以下の内容を追加:from rest_framework import renderers # renderersモジュールは、データを特定のフォーマット(この場合はhtml)に変換するためのクラスを含んでいる。 class SnippetHighlight(generics.GenericAPIView): queryset = Snippet.objects.all() renderer_classes = [renderers.StaticHTMLRenderer] # ビューで使用するレンダラークラスを指定。データを静的なhtmlとしてレンダリングするクラス。 def get(self, request, *args, **kwargs): snippet = self.get_object() return Response(snippet.highlighted)
As usual we need to add the new views that we’ve created in to our URLconf. We’ll add a url pattern for our new API root insnippets/urls.py
:path('', views.api_root),
And then add a url pattern for the snippet highlights:path('snippets/<int:pk>/highlight/', views.SnippetHighlight.as_view()),
- Hyperlinking our API エンティティ間の関係を扱うことは、Web APIの設計において、難しい。関係を表現するためのいくつかの異なる方法が考えられる:
- Using primary keys.
- Using hyperlinking between entities.
- Using a unique identifying slug field on the related entity.
- Using the default string representation of the related entity.
- Nesting the related entity inside the parent representation.
- Some other custom representation.
HyperlinkedModelSerializer
instead of the existingModelSerializer
. TheHyperlinkedModelSerializer
has the following differences fromModelSerializer
:- It does not include the id field by default.
- It includes a url field, using
HyperlinkedIdentityField
. - Relationships use
HyperlinkedRelatedField
, instead ofPrimaryKeyRelatedField
.
snippets/serializers.py
add:class SnippetSerializer(serializers.HyperlinkedModelSerializer): owner = serializers.ReadOnlyField(source='owner.username') highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html') class Meta: model = Snippet fields = ['url', 'id', 'highlight', 'owner', 'title', 'code', 'linenos', 'language', 'style'] class UserSerializer(serializers.HyperlinkedModelSerializer): snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail', read_only=True) class Meta: model = User fields = ['url', 'id', 'username', 'snippets']
新たにhighlightフィールドも追加。このフィールドは、urlフィールドと同じタイプだが、snippet-detailのURLパターンの代わりにsnippet-highlightのURLパターンを指している。.json
のようなフォーマットサフィックス付きのURLを含めているため、highlightフィールドが返すハイパーリンクのフォーマットサフィックスとして.html
サフィックスを使用するよう指示する必要がある。 - Making sure our URL patterns are named If we’re going to have a hyperlinked API, we need to make sure we name our URL patterns. Let’s take a look at which URL patterns we need to name.
- The root of our API refers to
'user-list'
and'snippet-list'
. - Our snippet serializer includes a field that refers to
'snippet-highlight'
. - Our user serializer includes a field that refers to
'snippet-detail'
. - Our snippet and user serializers include
'url'
fields that by default will refer to'{model_name}-detail'
, which in this case will be'snippet-detail'
and'user-detail'
.
snippets/urls.py
file should look like this:from django.urls import path from rest_framework.urlpatterns import format_suffix_patterns from snippets import views # API endpoints urlpatterns = format_suffix_patterns([ path('', views.api_root), path('snippets/', views.SnippetList.as_view(), name='snippet-list'), path('snippets/<int:pk>/', views.SnippetDetail.as_view(), name='snippet-detail'), path('snippets/<int:pk>/highlight/', views.SnippetHighlight.as_view(), name='snippet-highlight'), path('users/', views.UserList.as_view(), name='user-list'), path('users/<int:pk>/', views.UserDetail.as_view(), name='user-detail') ])
- The root of our API refers to
- Adding pagination ユーザーやコードスニペットのリストビューは、多くのインスタンスを返す可能性があるので、結果をページネーションで表示し、APIクライアントが各ページを個別に閲覧できるようにしたい。
tutorial/settings.py
ファイルを少し修正することで、デフォルトのリストスタイルをページネーションを使用するスタイルに変更することができる。以下の設定を追加:REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 10 }
Note that settings in REST framework are all namespaced into a single dictionary setting, namedREST_FRAMEWORK
, which helps keep them well separated from your other project settings. We could also customize the pagination style if we needed to, but in this case we’ll just stick with the default. - Browsing the API If we open a browser and navigate to the browsable API, you’ll find that you can now work your way around the API simply by following links. You’ll also be able to see the ‘highlight’ links on the snippet instances, that will take you to the highlighted code HTML representations.
コメント