#Introduction
تعریف : RESTful API
واسطی میان اپلیکیشن و برنامه Application Program Interface
است که با استفاده از ریکوست های HTTP
میتواند داده ای ایجاد کند POST
یا ویرایش کند PUT
یا حذف کند DELETE
یا ان را بخواند GET
ابزار پیشنهادی ما : Django REST framework
:یک ابزار قدرتمند و انعطاف پذیر برای ساختن API های وب
#Requirements
Django (1.11, 2.0, 2.1, 2.2, 3.0)
#Installation
با استفاده از pip
:
pip install djangorestframework
git clone https://github.com/encode/django-rest-framework
سپس برای تکمیل فرایند به settings.py رفته و 'rest_framework'
را به INSTALLED_APPS اضافه کنید.
INSTALLED_APPS = [
...
'rest_framework',
]
اکنون میخواهیم به بررسی عناصر مهم بپردازیم:
#Requests
کلاس Request
در Rest Framework از کلاس HttpRequest های استاندارد extend میکند.
موارد بسیار کاربردی:
request.data
state={
profile:{
...
city:'Tehran'
}
}
.
.
.
fetch(`http://127.0.0.1:8000/api/profiles/${this.props.username}/`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Token ${this.props.token}`
},
body: JSON.stringify(this.state.profile)
}).then(response => response.json())
.catch(error => console.log(error))
request.user
این عبارت یک instance ای را از django.contrib.auth.models.User
برمیگرداند.
که در مواقع مورد نیاز میتوان از ان برای فهمیدن اینکه چه کسی ریکوستی زده استفاده کرد.
request.method
که مشخص میکند method ریکوست ..put | post |delete |etc است
در قسمت های بعد مثال هایی از این کاربرد ها را خواهیم دید.
#Responses
from rest_framework.response import Response
Response(data, status=None, template_name=None, headers=None, content_type=None)
برای ساختن ریسپانس از این کلاس استفاده میشود که باید به صورت بالا آن را ایمپورت کرد.
چگونه ریسپانس بسازیم ؟
Response(data, status=None, template_name=None, headers=None, content_type=None)
example:
return Response(ProfileSerializer(profile).data, status.HTTP_200_OK)
همانطور که در خط اول مشخص است برای ساختن instance ای از کلاس Response میتوان یک سری ورودی ها به ان داد . از جمله دیتا و status و هدر ها و content-type و ... که از میان این ارگومان ها ما پرکاربرد ترین آن ها یعنی دیتا و status را بررسی میکنیم.
دیتا یا میتواند به صورت string باشد یا میتواند به صورت دیتای سریالایز شده باشد . یعنی همانطور که در مثال بالا دیده میشود instance ای از مدل x را باید به عنوان ورودی Xserializer داد در ادامه بیشتر با serializer ها آشنا خواهیم شد.
و اما status ها :
لازم است بدانید که status ها انواع مختلفی دارند ه به تناسب موضوع میتوان از آن ها استفاده کرد. برای مثال اگر کاربر درخواستی برای user ای که در سیستم وجود ندارد زده بود میتوان از status.HTTP_404_NOT_FOUND استفاده کرد . یا مثلا اگر درخواست برای ایجاد یک ابجکتی بود از status.HTTP_201_CREATED میتوان استفاده کرد . در زیر تنوعی از پرکاربرد ترین status ها را مشاهده میکنید که بر حسب نیازتان میتوانید از مناسب ترین آن ها استفاده کنید
status.HTTP_200_OK
status.HTTP_400_BAD_REQUEST
status.HTTP_502_BAD_GATEWAY
status.HTTP_401_UNAUTHORIZED
status.HTTP_403_FORBIDDEN
...
#Serializer
سوال : serializer چیست ؟
سریالایزر ها به داده های پیچیده مانند queryset ها و instance های مدل ها اجازه میدهند که به سادگی به json یا xml تبدیل بشوند.
همچنین serializer ها امکان deserialize کردن را نیز فراهم میکنند. یعنی میتوان به کمک ان ها میتوان دیتای parse شده را به داده های پیچیده تر ذکر شده در خط قبل تبدیل کرد.
در یک کلام serializer ها به ما اجازه ی کنترل کردن حرفه ای خروجی ریسپانس ها را میدهند.
معمولا به ازای هر modelclass در models.py یک serializer نیز تعریف میشود.
در زیر نمونه ای ازمدل و serializer آن دیده میشود:
class Channel(models.Model):
channelId = models.CharField(max_length=16, blank=False, null=False, unique=True, primary_key=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE, blank=False, related_name='owningChannels')
followers = models.ManyToManyField(User, blank=True, related_name='followings')
description = models.TextField(default='Description')
def __str__(self):
return self.channelId
from rest_framework import serializers
class ChannelSerializer(serializers.ModelSerializer):
owner = UserSerializer(many=False)
followers = UserSerializer(many=True)
class Meta:
model = Channel
fields = ('channelId', 'owner', 'description', 'followers')
حال به تحلیل آن میپردازیم:
مدل Channel دارای ۴ فیلد است که هر کدام ویژگی خاص دارند که جلوتر به شرح آن ها میپردازیم.
همانطور که مشاهده میکنید برای ساختن serializer ابتدا باید ان را از rest_framework
ایمپورت کرد.
دقت کنید که باید در subclass Meta فیلد هایی را که میخواهیم بعد از سریالایز کردن داده در ان موجود باشند را در یک تاپل مشخص کنیم. یعنی چه؟
همانطور که در بالا گفتیم به هنگام ریسپانس دادن میتوان از دیتای سریالایز شده استفاده کرد . و شما میتوانید انتخاب کنید کدام فیلد ها رد ریسپانس حضور داشته باشند مثلا در مثال بالا اگر 'owner' را در fields قرار نمیدادیم در ریسپانس خروجی اثری از ان دیده نمیشد . این بسیار مفید و کاربردی است زیرا در بعضی جاها ما نیاز داریم بعضی از اطلاعات کاربران را در ریسپانس هایمان نفرستیم.
دقت کنید که در subclass meta باید مشخص کنید که این سریالایزر مربوط به کدام مدل است.
اخرین نکته ی باقی مانده مربوط به دو خط قبل از تعریف subclass meta است .
در بخش بعدی انواع فیلد ها را توضیح خواهیم داد و در انجا فلسفه ی قرار دادن آن UserSerializer ها را بیان خواهیم کرد.
#Serializer fields
هر فیلد در serializer نه تنها مسوول validate داده است بلکه داده را به یک فرمت ثابتی نیز normalize میکند. همچنین انچه که بدیهی ست این است که این فیلد ها برای تبدیل کردن primitive value ها به dataType های داخلی استفاده میشوند.
هر فیلد تعدادی آرگومان را به عنوان ورودی می گیرد. در زیر به شرح برخی از این آرگومان ها میپردازیم:
required: آیا این فیلد باید حتما مقدار بگیرد یا نه
default: در صورت اینکه مقداری به صورت مشخص برای این فیلد تعیین نشود مقدار دیفالت را داراست
allow_null: میتواند نال باشد یا نه
write_only: سطح دسترسی را تعیین میکند
max_length: حداکثر میتواند چند کاراکتر داشته باشد
related_name: در قسمت بعد بیشتر توضیح داده میشود
on_delete: به هنگام پاک کردن هر چیزی مربوط به این فیلد از بقیه مدل ها نیز باید حزف شود
...
فیلد ها نیز انواع گوناگونی دارند که به اختصار چندی از آن ها را در ادامه بیان میکنیم:
Numeric fields:
IntegerField
String fields:
CharField
EmailField
Date and time fields:
DateField
File upload fields:
ImageField
FileField
Choice selection fields:
ChoiceField
...
..
#Serializer relations
میدانیم که مدل ها سه نوع رابطه میتوانند با هم داشته باشند که برای یادآوری از هر کدام مثالی ذکر میکنیم:
class BookId(models.Model):
id = models.CharField(max_length=10, blank=True)
class Book(models.Model):
title = models.CharField(blank=False, unique=True, max_length=36)
description = models.TextField(max_length=256, blank=True)
no = models.OneToOneField(BookId, null=True, blank=True, on_delete=models.CASCADE)
class Hero(models.Model):
name = models.CharField(max_length=30)
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='hero')
class Publisher(models.Model):
name = models.CharField(max_length=36)
books = models.ManyToManyField(Book, related_name='publisher')
در مدل های بالا بین publisher و book یه رابطه ی manyToMany هست به این معنی که ممکنه یه کتاب چند ناشر و یه ناشر چن کتاب داشته باشه .
همچنین بین Hero و book یه رابطه ی oneToMany هست یعنی ممکنه به ازای یه کتاب چن تا قهرمان داشته باشیم ولی یه قهرمان در چنتا کناب نیس.
و همچنین یه رابطه ی oneToOne داریم بین Book و BookId که نشون میده هر کتاب دارای یه ایدی منحصر به فرد هست.
تا اینجا دیدیم که روابط مختلفی میتونه بن مدل هامون باشه .
در یه رابطه ی manyToMany مثلا در مثال بالا بین ناشر و کتاب مشکلی ایجاد میشه که برای حل اون راه حلی ارایه میدیم:
وقتی میخوایم کتاب های یه instance از ناشر رو پیدا کنیم به طور دیفالت یه آرایه از آیدی کتاب ها در serializer گذاشته میشه ولی اگه بخوایم خود ابجکت کتاب رو قرار بده از خط زیر استفاده میکنیم همون کاری که در قسمت اول serializer ها اون بالا انجام دادیم:
book = BookSerializer(many=True)
book = BookSerializer(many=False) // if it was oneToOneField
و اینطوری به سادگی میتونیم بهشون دسترسی داشته باشیم.
مطلب آخر در این قسمت تحلیلی در باره ی related_name هست .
class Publisher(models.Model):
name = models.CharField(max_length=36)
books = models.ManyToManyField(Book, related_name='publishers')
در داده ی سریالایز شده از instance ای از کلاس Publisher میتوان با .books به لیست کتاب های ان دست یافت ولی چطور میتوان از ان کتاب به لیست ناشر های آن رسید؟
از طریق اسمی که در related_name برای ان انتخاب کردیم یعنی :
book.publishers
#Function views
معرفی Function View : در واقع Function View یک تابع است که یک HttpRequest را در ورودی میگیرد (به همراه تعدادی ورودی دلخواه دیگر) و یک HttpResponse را در خروجی می دهد .
کد زیر زمان کنونی را در قالب HTML خروجی می دهد.
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
html = "<html><body>It is now %s.</body></html>" % now
return HttpResponse(html)
ریسپانس می تواند HTML یا یک تصویر یا یک متن json یا ... باشد.
در این نوع View برای محدود کردن نوع HTTP methods باید از decorator زیر استفاده کنیم.
from django.views.decorators.http import require_http_methods
@require_http_methods(["GET", "POST"])
def my_view(request):
# I can assume now that only GET or POST requests make it this far
# ...
pass
#Class-based Views
در اینجا دیگر نیازی نیست برای Http methods مختلف از if استفاده کنیم و می توانیم برای هر کدام یک تابع جداگانه داشته باشیم.
from django.http import HttpResponse
def my_view(request):
if request.method == 'GET':
# <view logic>
return HttpResponse('result')
from django.http import HttpResponse
from django.views import View
class MyView(View):
def get(self, request):
# <view logic>
return HttpResponse('result')
دو کد بالا باهم معادل اند.
نحوه استفاده از این دو کمی باهم متفاوت است که در زیر این تفاوت را می بینید.
# urls.py
from django.urls import path
from myapp.views import MyView
urlpatterns = [
path('1/', ClassBasedView.as_view()),
path('2/', FunctionView),
]
استفاده از class based امکانات داشتن کلاس مانند داشتن method ها و attribute ها را به ما می دهد.
از اینجا به بعد با امکانات REST Framework آشنا خواهیم شد.
کلاس APIView یک subclass از View خود جنگو است.
تفاوت ها:
- وجود دارد را ورودی میگیر RestFrameWork را که در Request یک HttpRequest به جای
- را Resopnse یک HttpResponse به جای
- خاص دارد که کلی امکانات اماده به ما میدهند attribute یک سری
یک نمونه:
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication, permissions
from django.contrib.auth.models import User
class ListUsers(APIView):
"""
View to list all users in the system.
* Requires token authentication.
* Only admin users are able to access this view.
"""
authentication_classes = [authentication.TokenAuthentication]
permission_classes = [permissions.IsAdminUser]
def get(self, request, format=None):
"""
Return a list of all users.
"""
usernames = [user.username for user in User.objects.all()]
return Response(usernames)
]
در زیر کلیه policy attributes اشاره شده را می بینید.
- renderer_classes
- parser_classes
- authentication_classes
- throttle_classes
- permission_classes
- content_negotiation_class
#Authentication
وظیفه این بخش این است که تشخیص دهد request دریافتی را چه کسی فرستاده است.
این عمل در اولین مراحل پردازش request دریافتی انجام می شود و پس از این مرحله request.user حاوی نتیجه این بخش(user ای که این request را فرستاده است) می باشد.
در صورتی که نتوانیم تشخیص دهیم این request را چه کسی فرستاده است request.user مقدار UNAUTHENTICATED_USER را خواهد گرفت.
نحوه set کردن Authentication class در APIView:
class ExampleView(APIView):
authentication_classes = [SessionAuthentication, BasicAuthentication]
def get(self, request, format=None):
content = {
'user': unicode(request.user), # `django.contrib.auth.User` instance.
}
return Response(content)
می توان با تغیراتی در settings.py یک دیفالت برای Authentication class ها معرفی کرد.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
]
}
شما برای Authentication class می توانید از مقادیر آماده زیر استفاده کنید و یا در صورت لزوم یک Authentication class برای پروژه تان طراحی کنید.
- TokenAuthentication
- SessionAuthentication
- RemoteUserAuthentication
- Custom authentication
برای آشنایی کمی درباره مورد اول توضیح خواهیم داد.
در این روش از token-based HTTP Authentication استفاده می شود.برای استفاده از این روش در ابتدا باید'rest_framework.authtoken' را به INSTALLED_APPS اضافه کنید. سپس باید migration انجام دهید.
از این پس برای register کردن یک user جدید باید token هم برایش بسازید.
from rest_framework.authtoken.models import Token
token = Token.objects.create(user=...)
حال باید یک صفحه لاگین داشته باشیم که توکن کاربر هارا به آن ها بدهد:
from rest_framework.authtoken import views
urlpatterns += [
url(r'^api-token-auth/', views.obtain_auth_token)
]
حال اگر username و password کاربر را به url گفته شده POST کنید در جواب json ای حاوی توکن شخصیتان را دریافت می کنید.
{ 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' }
در سمت کلاینت باید توکن دریافتی هنگام لاگین را در header مربوط به Authorization قرار دهد.
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
#Permissions
این قسمت از نتایج قسمت قبل استفاده می کند و تشخیص می دهند کهuser فرستنده request آیا به درخواست مورد نظرش دسترسی دارد یا خیر
نحوه set کردن Permission class در APIView:
class ExampleView(APIView):
permission_classes = [IsAuthenticated,...]
در صورتی جواب داده می شود که همه permission class ها برقرار باشند.
لیستی از permission class های آماده را در زیر می بینید.
- AllowAny
- IsAuthenticated
- IsAdminUser
- IsAuthenticatedOrReadOnly
- DjangoModelPermissions
- DjangoModelPermissionsOrAnonReadOnly
- DjangoObjectPermissions
در صورتی که هیچ کدام از موارد بالا برای کار شما کافی نباشند باید کلاس BasePermission را override کرده و دو متود زیر را implement کنید
class CustomerAccessPermission(permissions.BasePermission):
message = 'Adding customers not allowed.'
def has_permission(self, request, view):
...
def has_object_permission(self, request, view, obj):
...
اگر user مورد نظر شناسایی شده باشد(مثلا توکن داشته باشد) دسترسی به همه موارد دارد و در غیر این صورت به هیچ چیز دسترسی ندارد
اگر user مورد نظر شناسایی شده باشد(مثلا توکن داشته باشد) دسترسی به همه موارد دارد و در غیر این صورت فقط دسترسی های از نوع خواندن دارد.
شما می توانید مثلا از APIView به شیوه زیر نیز استفاده کنید.
@renderer_classes(...)
@parser_classes(...)
@authentication_classes(...)
@throttle_classes(...)
@permission_classes(...)
@api_view(['GET', 'POST'])
def hello_world(request):
if request.method == 'POST':
return Response({"message": "Got some data!", "data": request.data})
return Response({"message": "Hello, world!"})
]