外观
Django框架-为在线学习平台构建API
约 9566 字大约 32 分钟
2026-03-24
在上一节中,我们构建了一个学生注册和选课系统,实现了课程内容的创建和展示功能。
在本节中,我们将为在线学习平台创建一个 RESTful API。
API 是一种通用的编程接口,可以对接多种平台,比如网站、移动 App、插件等等。
我们可以创建一个 API,给在线学习平台的移动端使用,也可以向第三方提供 API 接口服务。
本章内容包括:
安装 Django REST framework
为模型创建序列化器
构建 RESTful API
实现序列化器方法字段
创建嵌套序列化器
实现 ViewSet 视图和路由
构建自定义 API 视图
处理 API 身份验证
向 API 视图添加权限
创建自定义权限
使用 Requests 库来调用 API
参考代码:educa3.zip
功能概述
下图展示了本节将要构建的视图和 API 端点:
要构建的 API 视图和端点示意图
在本节中,我们将创建两组不同的 API 视图:SubjectViewSet 包含学科的列表视图和详情视图,CourseViewSet 包含课程的列表视图和详情视图。我们还会在 CourseViewSet 中实现一个 enroll 操作,用于将学生注册到课程中。这个操作只对已登录的用户开放,需要 IsAuthenticated 权限。此外,我们还将在 CourseViewSet 中创建一个 contents 操作,用于访问课程内容。要访问课程内容,用户必须已登录并且注册了对应的课程。我们会实现一个自定义的 IsEnrolled 权限,确保只有已选课的学生才能访问课程内容。
1、构建 RESTful API
构建 API 时,有多种方式可以设计端点和操作,但建议遵循 REST 原则。
REST 架构源自表述性状态转移(Representational State Transfer)。RESTful API 是基于资源的:模型对象就是资源,HTTP 方法(如 GET、POST、PUT、DELETE)分别对应检索、创建、更新和删除操作。HTTP 响应状态码也有对应含义,比如 200 表示成功、404 表示资源不存在等。
RESTful API 中最常见的数据交换格式是 JSON 和 XML。我们将为项目构建一个基于 JSON 序列化的 RESTful API,提供以下功能:
检索学科列表
检索可用课程
获取课程内容
注册课程
可以用 Django 从零开始构建 API,只需创建自定义视图即可。此外,还有很多第三方库可以简化 API 的开发,其中最流行的就是 Django REST framework(DRF)。
DRF 提供了一整套工具来构建 RESTful API。下面是我们构建 API 时会用到的一些核心组件:
序列化器(Serializer):负责把数据转换成标准化的输出格式(序列化),也负责把传入的数据转换成程序可以处理的格式(反序列化)。
解析器(Parser)和渲染器(Renderer):渲染器负责把序列化后的数据格式化并写入 HTTP 响应;解析器负责解析传入请求中的数据,确保格式正确。
API 视图(View):实现具体的业务逻辑。
URL:定义 API 的访问端点。
身份验证和权限:定义 API 的认证方式,以及每个视图所需的访问权限。
我们先安装 DRF,然后逐步了解这些组件,构建第一个 API。
1.1 安装 Django REST framework
打开终端,使用以下命令安装框架:
python -m pip install djangorestframework==3.16编辑项目的 settings.py 文件,在 INSTALLED_APPS 中添加 rest_framework 以激活该应用:
INSTALLED_APPS = [
# ...
'rest_framework', # 添加Django REST framework插件
]然后,将以下代码添加到settings.py文件中:
# Django REST framework 配置
REST_FRAMEWORK = {
# 权限设置,允许认证用户进行所有操作,未认证用户只能读取数据
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
]
}你可以通过 REST_FRAMEWORK 这个配置项来设置 API 的各种选项。DRF 提供了很多可配置的默认行为。这里的 DEFAULT_PERMISSION_CLASSES 设置了默认的权限类为 DjangoModelPermissionsOrAnonReadOnly,它依赖 Django 自带的权限系统:已认证的用户可以创建、更新或删除对象,匿名用户只能进行只读访问。关于权限的更多内容,我们会在后面"为视图添加权限"部分详细讲解。 如需查看 DRF 所有可用配置项,可以访问https://www.django-rest-framework.org/api-guide/settings/。
1.2 定义序列化器
安装好 DRF 之后,我们需要定义数据的序列化方式。输出数据需要按特定格式序列化,输入数据则需要反序列化后才能处理。DRF 提供了以下几个类来构建序列化器:
Serializer:用于普通 Python 类实例的序列化ModelSerializer:用于模型实例的序列化HyperlinkedModelSerializer:和ModelSerializer类似,但使用超链接而不是主键来表示对象之间的关系。
下面来构建第一个序列化器。在 courses 应用目录中创建以下文件结构:
api/
__init__.py
serializers.py为了保持代码结构清晰,我们把所有 API 相关的功能都放在 api 目录下。编辑 api/serializers.py 文件,添加以下代码:
from rest_framework import serializers # 导入序列化器
from courses.models import Subject # 导入课程科目模型
# 课程科目序列化器
class SubjectSerializer(serializers.ModelSerializer):
class Meta:
model = Subject # 指定序列化的模型
fields = ['id', 'title', 'slug'] # 指定要序列化的字段这是 Subject 模型的序列化器。序列化器的定义方式和 Django 的 ModelForm 很像:在 Meta 类中指定对应的模型(model)和要包含的字段(fields)。如果不设置 fields 属性,则所有模型字段都会被包含进来。 我们来测试一下这个序列化器。打开终端,用以下命令启动 Django shell:
python manage.py shell运行以下代码:
# 导入课程科目和课程科目序列化器
>>> from courses.models import Subject
>>> from courses.api.serializers import SubjectSerializer
# 获取id为最后的科目信息对象
>>> subject = Subject.objects.latest('id')
# 创建一个实例Subject Serializer
>>> serializer = SubjectSerializer(subject)
>>> serializer.data
{'id': 4, 'title': '编程', 'slug': 'programming'}在这个例子中,我们获取一个 Subject 对象,创建一个 SubjectSerializer 实例,然后访问序列化后的数据。可以看到,模型数据被转换成了 Python 原生数据类型。
🏭REST_FRAMEWORK.SERIALIZER_BAY
STANDBY
models.Subject Object
int id: 4
str title: 'Programming'
str slug: 'programming'
datetime created: <django.utils...>
QuerySet courses: <django.db...>
class SubjectSerializer(serializers.ModelSerializer):
class Meta:
model = Subject
fields = ['id', 'title', 'slug']
fields = ['id', 'title', 'slug']
📦 application/json
{ "id": 4, "title": "Programming", "slug": "programming" }
||| ||||| | |||| ||
等待数据录入。上方为包含冗余内建属性的粗糙 Python 模型实例。
Awaiting data. Above is the raw Python Model instance with redundant internal attributes.
1.3 了解解析器和渲染器
序列化后的数据需要以特定格式渲染,然后通过 HTTP 响应返回给客户端。同样,收到 HTTP 请求时,需要先解析传入的数据并反序列化,才能进行后续处理。DRF 内置了渲染器和解析器来完成这些工作。
我们来看看如何解析接收到的数据。在 Python shell 中执行以下代码:
>>> from io import BytesIO
>>> from rest_framework.parsers import JSONParser
# 用中文报:SyntaxError: bytes can only contain ASCII literal characters
>>> data = b'{"id":4,"title":"Programming","slug":"programming"}'
>>> JSONParser().parse(BytesIO(data))
{'id': 4, 'title': 'Programming', 'slug': 'programming'}给定一个 JSON 字符串,你可以用 DRF 提供的 JSONParser 类把它解析成 Python 对象。 DRF 还提供了多种 Renderer 类,用于格式化 API 响应。框架会通过内容协商(Content Negotiation)来决定使用哪个渲染器——它会检查请求的 Accept 头来确定客户端期望的内容类型。此外,渲染器也可以通过 URL 的格式后缀来决定。比如,访问 http://127.0.0.1:8000/api/data.json 就会触发 JSONRenderer 返回 JSON 格式的响应。
回到 shell 中,执行以下代码来渲染上一个例子中的 serializer 对象:
>>> from rest_framework.renderers import JSONRenderer
>>> JSONRenderer().render(serializer.data)将看到以下输出:
b'{"id":4,"title":"\xe7\xbc\x96\xe7\xa8\x8b","slug":"programming"}'JSONRenderer 可以把序列化后的数据渲染成 JSON 格式。默认情况下,DRF 使用两种渲染器:JSONRenderer 和 BrowsableAPIRenderer。后者提供了一个 Web 界面,方便你在浏览器中浏览和测试 API。你可以在 REST_FRAMEWORK 配置中通过 DEFAULT_RENDERER_CLASSES 选项来修改默认的渲染器。 更多关于渲染器和解析器的信息,可以分别查看https://www.django-rest-framework.org/api-guide/renderers/和https://www.django-rest-framework.org/api-guide/parsers/。
🛸REST_FRAMEWORK.AIRLOCK_BAY
AIRLOCK.LOCKED
EXTERNAL SPACE (HTTP)
JSON Byte Streams
JSON Byte Streams
Incoming Request Body
b'{"id":4,"title":"Programming"}'
Outgoing Response
b'{"id":4,"title":"\xe7\xbc...}'
JSONParser
.parse(BytesIO(data))
JSONRenderer
.render(serializer.data)
INTERNAL STATION (Python)
Native Dictionaries
Native Dictionaries
dict (Deserialized)
{'id': 4, 'title': 'Programming'}
serializer.data
{'id': 4, 'title': 'Programming'}
系统待机。左侧为浩瀚太空(HTTP原生字节流),右侧为星港内部(Python 原生字典)。
System standby. Left is deep space (HTTP byte streams), right is internal port (Python dictionaries).
接下来,我们来看如何构建 API 视图,并在视图中使用序列化器。
1.4 构建列表和详情视图
DRF 自带一套通用视图(Generic View)和混入类(Mixin),用于快速构建 API 视图。通用视图和混入类我们在前面的课程中已经接触过了。
这些基础视图和混入类提供了检索、创建、更新、删除模型对象的通用功能。你可以在https://www.django-rest-framework.org/api-guide/generic-views/查看 DRF 提供的所有通用混入类和视图。
下面我们来创建 Subject 的列表视图和详情视图。在 courses/api/ 目录中新建 views.py 文件,添加以下代码:
from rest_framework import generics # 导入通用视图类
# 导入课程科目序列化器
from courses.api.serializers import SubjectSerializer
from courses.models import Subject # 导入课程科目模型
# 课程科目列表视图
class SubjectListView(generics.ListAPIView):
queryset = Subject.objects.all() # 获取所有课程科目
serializer_class = SubjectSerializer # 使用课程科目序列化器
# 课程科目详情视图
class SubjectDetailView(generics.RetrieveAPIView):
queryset = Subject.objects.all() # 获取所有课程科目
serializer_class = SubjectSerializer # 使用课程科目序列化器这里用到了 DRF 的 ListAPIView 和 RetrieveAPIView 两个通用视图。它们都有以下属性:
queryset:用于检索对象的基础查询集serializer_class:用于序列化对象的序列化器类
接下来给视图配置 URL。在 courses/api/ 目录下新建 urls.py 文件,内容如下:
from django.urls import path # 导入路径函数
from . import views # 导入当前目录下的视图模块
app_name = 'courses' # 设置应用命名空间
# 定义URL模式列表
urlpatterns = [
path(
'subjects/', # 课程科目列表路径
views.SubjectListView.as_view(), # 关联课程科目列表视图
name='subject_list' # 命名空间
),
path(
'subjects/<pk>/', # 课程科目详情路径
views.SubjectDetailView.as_view(), # 关联课程科目详情视图
name='subject_detail' # 命名空间
),
]在 SubjectDetailView 的 URL 中,我们用 pk 参数来指定要检索的对象主键(也就是 Subject 模型的 id 字段)。接下来编辑项目主 URL 文件 educa/urls.py,把 API 的 URL 配置引入进来:
urlpatterns = [
# ...
# 课程列表API
path('api/', include('courses.api.urls', namespace='api')),
]我们用 api 作为命名空间来组织 API 的 URL。到这里,初始的 API 端点就准备好了,可以开始使用了。
2、使用 API
通过上面的步骤,我们已经创建了第一个可以通过 URL 访问的 API 端点。现在来测试一下。先确保开发服务器正在运行:
python manage.py runserver我们用 curl 来调用 API。curl 是一个命令行工具,可以和服务器进行数据传输。如果你用的是 Linux、macOS 或 Windows 10/11,系统一般都自带了 curl。也可以从https://curl.se/download.html下载。 打开终端,用 curl 请求 http://127.0.0.1:8000/api/subjects/:
curl http://127.0.0.1:8000/api/subjects/收到类似如下的回复:
[
{
"id": 1,
"title": "数学",
"slug": "mathematics"
},
{
"id": 3,
"title": "物理学",
"slug": "physics"
},
{
"id": 4,
"title": "编程",
"slug": "programming"
},
{
"id": 2,
"title": "音乐",
"slug": "music"
}
]如果想要更易读、缩进更清晰的 JSON 输出,可以用 json_pp 工具:
curl http://127.0.0.1:8000/api/subjects/ | json_ppHTTP 响应包含了一个 JSON 格式的 Subject 对象列表。 除了 curl,你也可以用浏览器扩展或其他工具来发送自定义 HTTP 请求,比如 Postman,可以从https://www.getpostman.com/获取。
在浏览器中打开 http://127.0.0.1:8000/api/subjects/,你会看到 DRF 的可浏览 API 界面,如下所示:
REST 框架可浏览 API 中的主题列表页面
这个 HTML 界面是由 BrowsableAPIRenderer 渲染器提供的。它会显示结果标题和内容,还可以直接在页面上发送请求。你也可以在 URL 中加上 Subject 对象的 ID 来访问详情视图。
在浏览器中打开 http://127.0.0.1:8000/api/subjects/1/,你会看到一个 JSON 格式的 Subject 对象详情。
REST 框架可浏览 API 中的主题详情页面
3、扩展序列化器
我们已经学会了如何序列化模型对象。不过在实际开发中,经常需要在响应中包含额外的关联数据或计算字段。下面来看看扩展序列化器的几种方式。
3.1 向序列化器添加额外字段
我们来修改学科视图,让它返回每个学科下可选课程的数量。这里用 Django 的聚合函数来统计每个学科关联的课程数。
编辑 courses 应用的 api/views.py 文件,修改如下:
from rest_framework import generics # 导入通用视图类
# 导入课程科目序列化器
from courses.api.serializers import SubjectSerializer
from courses.models import Subject # 导入课程科目模型
from django.db.models import Count # 导入计数函数
# 课程科目列表视图
class SubjectListView(generics.ListAPIView):
queryset = Subject.objects.annotate(total_courses=Count('courses')) # 获取所有课程科目
serializer_class = SubjectSerializer # 使用课程科目序列化器
# 课程科目详情视图
class SubjectDetailView(generics.RetrieveAPIView):
queryset = Subject.objects.annotate(total_courses=Count('courses')) # 获取所有课程科目
serializer_class = SubjectSerializer # 使用课程科目序列化器现在 SubjectListView 和 SubjectDetailView 的 QuerySet 都用了 Count 聚合函数,给每个学科标注了关联课程的数量。 编辑 courses 应用的 api/serializers.py 文件,修改如下:
from rest_framework import serializers
from courses.models import Subject
class SubjectSerializer(serializers.ModelSerializer):
total_courses = serializers.IntegerField()
class Meta:
model = Subject
fields = ['id', 'title', 'slug', 'total_courses']我们在 SubjectSerializer 中新增了 total_courses 字段,类型是 IntegerField(整数字段)。这个字段的值会自动从被序列化对象的 total_courses 属性中获取——而这个属性就是我们在 QuerySet 中通过 annotate() 添加上去的。 在浏览器中打开 http://127.0.0.1:8000/api/subjects/1/,现在序列化后的 JSON 对象中就包含了 total_courses 属性:
科目详情页面,包括 total_courses 属性
我们已经成功地把 total_courses 属性添加到了学科列表和详情视图中。接下来看看如何通过自定义序列化方法来添加更多属性。
3.2 实现序列化器方法字段
DRF 提供了 SerializerMethodField,它是一种只读字段,字段的值通过调用序列化器类中的某个方法来获取。当你需要在序列化结果中包含一些自定义格式的数据,或者需要做一些不属于模型本身的计算时,这个字段就特别有用。
我们来创建一个方法,用于序列化某个学科下最热门的 3 门课程(按选课人数排名)。编辑 courses 应用的 api/serializers.py 文件,修改如下:
from django.db.models import Count # 导入计数函数
from rest_framework import serializers # 导入序列化器
from courses.models import Subject # 导入课程科目模型
# 课程科目序列化器
class SubjectSerializer(serializers.ModelSerializer):
total_courses = serializers.IntegerField() # 课程总数
popular_courses = serializers.SerializerMethodField() # 热门课程
def get_popular_courses(self, obj): # 获取热门课程
courses = obj.courses.annotate( # 课程数量
total_students=Count('students') # 统计学生数
).order_by('total_students')[:3] # 按学生数排序,取前三个
return [
# 返回课程标题和学生数
f'{c.title} ({c.total_students})' for c in courses
]
class Meta:
model = Subject # 指定序列化的模型
# 指定要序列化的字段
fields = ['id', 'title', 'slug', 'total_courses', 'popular_courses']在这段代码中,我们给 SubjectSerializer 新增了一个 popular_courses 方法字段。这个字段的值来自 get_popular_courses() 方法。你也可以通过 SerializerMethodField 的 method_name 参数来指定要调用的方法名,如果不指定,默认就是 get_<field_name>。 在浏览器中打开 http://127.0.0.1:8000/api/subjects/4/,现在序列化后的 JSON 对象中就包含了 popular_courses 属性:
主题详情页面,包括 popular_courses 属性
你已经成功使用了 SerializerMethodField。需要注意的是,在 SubjectListView 返回列表时,每个结果都会额外执行一条 SQL 查询。接下来我们来学习如何通过分页功能来控制 SubjectListView 返回的结果数量。
4、为视图添加分页
DRF 内置了分页功能,可以控制 API 响应中返回的对象数量。当网站内容越来越多时,学科和课程的数量也会不断增长,分页在处理大数据量时尤其重要,能提升性能和用户体验。
让我们给 SubjectListView 加上分页功能。首先定义一个分页类。
在 courses/api/ 目录下新建 pagination.py 文件,添加以下代码:
from rest_framework.pagination import PageNumberPagination # 导入分页类
# 标准分页类
class StandardPagination(PageNumberPagination):
page_size = 10 # 每页默认显示10条
page_size_query_param = 'page_size' # 允许客户端设置每页显示条数
max_page_size = 50 # 最大显示50条这个类继承自 PageNumberPagination,提供基于页码的分页支持。我们设置了以下属性:
page_size:默认每页返回的条目数(请求中未指定时使用)。page_size_query_params:用于指定每页大小的查询参数名。max_page_size:允许的最大每页条目数。
现在编辑 courses 应用的 api/views.py 文件,添加分页配置:
from django.db.models import Count
from rest_framework import generics
from courses.models import Subject
from courses.api.pagination import StandardPagination
from courses.api.serializers import SubjectSerializer
class SubjectListView(generics.ListAPIView):
queryset = Subject.objects.annotate(total_courses=Count('courses'))
serializer_class = SubjectSerializer
pagination_class = StandardPagination
# ...现在 SubjectListView 返回的对象就会自动分页了。在浏览器中打开 http://127.0.0.1:8000/api/subjects/,你会发现返回的 JSON 结构变了,多了分页信息:
{
"count": 4,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"title": "Mathematics",
...
},
...
]
}返回的 JSON 数据现在包含以下字段:
count:结果总数。next:下一页的 URL,如果没有下一页则为null。previous:上一页的 URL,如果没有上一页则为null。results:当前页返回的序列化对象列表。
在浏览器中打开 http://127.0.0.1:8000/api/subjects/?page_size=2&page=1,这会按每页 2 条来分页,检索第一页的结果,如图所示:
图 15.6:主题列表分页结果的第一页,页面大小为 2
我们已经实现了基于页码的分页。DRF 还支持基于 limit/offset 和基于游标(cursor)的分页方式。更多信息可以参考https://www.django-rest-framework.org/api-guide/pagination/。
🚛REST_FRAMEWORK.StandardPagination
PAGE 1 / 5
DATABASE.QUERYSET
Total Items: 50
Total Items: 50
1
2
3
4
5
6
7
8
9
10
HTTP Response JSON
{
"count": 50,
"next": "http://127.0.0.1:8000/api/subjects/?page=2",
"previous": null,
"results": [ Array (10) ]
}
"count": 50,
"next": "http://127.0.0.1:8000/api/subjects/?page=2",
"previous": null,
"results": [ Array (10) ]
}
page_size = 10
当前正在处理第 1 页。通过 "count" 得知总量,凭借 "next" / "previous" 链接平滑翻页,避免单次返回过多数据导致崩溃。
Currently serving Page 1. "count" reveals total size. "next" and "previous" URLs allow smooth traversal without loading massive datasets at once.
学科视图的 API 端点已经创建完毕,接下来我们为课程也添加 API 端点。
6、构建课程序列化器
我们来为 Course 模型创建序列化器。编辑 courses 应用的 api/serializers.py 文件,添加以下代码:
# ...
from courses.models import Course, Subject # 导入课程科目模型和课程模型
# 课程序列化器def get_popular_courses(self, obj):
class CourseSerializer(serializers.ModelSerializer):
class Meta:
model = Course # 模型
fields = [ # 要序列化的字段
'id',
'subject',
'title',
'slug',
'overview',
'created',
'owner',
'modules'
]来看看 Course 对象序列化后是什么样子。打开终端,启动 Django shell:
python manage.py shell运行以下代码:
>>> from rest_framework.renderers import JSONRenderer
>>> from courses.models import Course
>>> from courses.api.serializers import CourseSerializer
>>> course = Course.objects.latest('id')
>>> serializer = CourseSerializer(course)
>>> JSONRenderer().render(serializer.data)你会得到一个包含 CourseSerializer 中指定字段的 JSON 对象。可以看到,关联的 modules 字段被序列化成了一个主键列表:
"modules": [6, 7, 9, 10]这些是关联 Module 对象的 ID。接下来我们学习几种不同的方式来序列化关联对象。
序列化关系
DRF 提供了多种关系字段类型,用于表示模型之间的关系(ForeignKey、ManyToManyField、OneToOneField 以及通用关系等)。
我们先用 StringRelatedField 来改变 Module 关联对象的序列化方式。StringRelatedField 会调用关联对象的 __str__() 方法来表示该对象。
编辑 courses 应用的 api/serializers.py 文件,修改如下:
# ...
class CourseSerializer(serializers.ModelSerializer):
modules=serializers.StringRelatedField(many=True, read_only=True)
class Meta:
# ...这里我们定义了 modules 字段来序列化关联的 Module 对象。many=True 表示这是一个多对象关系,read_only=True 表示这个字段是只读的,不会出现在创建或更新对象的输入中。 打开 shell,再次创建一个 CourseSerializer 实例,用 JSONRenderer 渲染它的 data 属性。这次,模块列表会用 __str__() 方法的返回值来序列化:
"modules": ["0. Installing Django", "1. Configuring Django"]需要注意的是,DRF 不会自动优化 QuerySet。序列化课程列表时,每个课程都会生成一条额外的 SQL 查询来获取关联的 Module 对象。你可以在 QuerySet 中使用 prefetch_related() 来减少额外查询,比如 Course.objects.prefetch_related('modules')。我们会在后面"创建 ViewSet 和路由器"一节中详细介绍。 更多关于序列化器关系的信息可以参考https://www.django-rest-framework.org/api-guide/relations/。
接下来,我们用嵌套序列化器来定义关联对象的序列化方式。
7、创建嵌套序列化器
如果想要包含每个模块的更多信息,就需要对 Module 对象进行序列化并嵌套到课程中。编辑 courses/api/serializers.py 文件,修改如下:
from django.db.models import Count # 导入计数函数
from rest_framework import serializers # 导入序列化器
# 导入课程科目、模型和课程模型
from courses.models import Course, Module, Subject
# 模型序列化器
class ModuleSerializer(serializers.ModelSerializer):
class Meta:
model = Module # 模型
fields = ['order', 'title', 'description'] # 字段
# 课程序列化器
class CourseSerializer(serializers.ModelSerializer):
modules = ModuleSerializer(many=True, read_only=True) # 模块
class Meta:
# ...我们新建了一个 ModuleSerializer 来定义 Module 的序列化方式,然后在 CourseSerializer 中把 modules 字段改成了嵌套的 ModuleSerializer。many=True 表示序列化多个对象,read_only=True 表示只读。 打开 shell,再次创建 CourseSerializer 实例并用 JSONRenderer 渲染。这次模块会用嵌套的 ModuleSerializer 来序列化,结果如下:
"modules": [
{
"order": 0,
"title": "Introduction to overview",
"description": "A brief overview about the Web Framework."
},
{
"order": 1,
"title": "Configuring Django",
"description": "How to install Django."
},
...
]🛸REST_FRAMEWORK.NestedSerializer
DEPTH: LEVEL 1 (COURSE)
Course: Introduction to Django
CourseWithContentsSerializer.data
{
"id": 1,
"title": "Introduction to Django",
"modules": []
}
"id": 1,
"title": "Introduction to Django",
"modules": [
... (Module Serializers Pending)
}
初始状态:由于设定了 Serializer,我们只看到了 Course 母舰外壳属性和其关联的 modules 引用。
Initial state: Course spaceship exterior. Only primary attributes and related module references are visible.
8、创建视图集和路由器
ViewSet(视图集)可以让你定义 API 的交互逻辑,并通过 Router 对象自动生成 URL。使用视图集可以避免在多个视图中重复编写逻辑。视图集包含以下标准操作:
创建操作:
create()检索操作:
list()和retrieve()更新操作:
update()和partial_update()删除操作:
destroy()
下面为 Course 模型创建一个视图集。编辑 api/views.py 文件,添加以下代码:
from django.db.models import Count # 导入计数函数
from rest_framework import generics # 导入通用视图类
from rest_framework import viewsets # # 导入视图集
from courses.api.pagination import StandardPagination # 导入标准分页
# 导入课程和科目序列化器、
from courses.api.serializers import CourseSerializer, SubjectSerializer
from courses.models import Course, Subject # 导入课程和科目模型
# ... ....
# 课程列表视图
class CourseViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Course.objects.prefetch_related('modules') # 获取所有课程
serializer_class = CourseSerializer # 使用课程序列化器
pagination_class = StandardPagination # 使用标准分页CourseViewSet 继承自 ReadOnlyModelViewSet,它提供了只读操作 list() 和 retrieve(),分别用于列出对象和获取单个对象。我们指定了查询集(用 prefetch_related('modules') 来高效加载关联的 Module 对象,避免序列化嵌套模块时产生额外的 SQL 查询),以及序列化器和分页类。 编辑 api/urls.py 文件,为视图集创建路由器:
from django.urls import include, path # 导入路径函数
from rest_framework import routers # 导入路由模块
from . import views # 导入当前目录下的视图模块
app_name = 'courses' # 设置应用命名空间
router = routers.DefaultRouter() # 创建默认路由
router.register('courses', views.CourseViewSet) # 注册课程视图
# 定义URL模式列表
urlpatterns = [
# ... ... ,
path('', include(router.urls)), # 引入默认路由
]我们创建了一个 DefaultRouter 对象,并用 courses 前缀注册了 CourseViewSet。路由器会自动为视图集生成对应的 URL。 在浏览器中打开 http://127.0.0.1:8000/api/,你会看到路由器在根 URL 中列出了 courses 视图集:
图 :REST 框架可浏览 API 的 API 根页面
访问 http://127.0.0.1:8000/api/courses/ 可以查看课程列表:
图:REST 框架可浏览 API 中的课程列表页面
接下来,我们把之前的 SubjectListView 和 SubjectDetailView 合并成一个 ViewSet。编辑 api/views.py 文件,删除或注释掉 SubjectListView 和 SubjectDetailView 两个类,然后添加以下代码:
# ...
# 课程科目列表视图
class SubjectViewSet(viewsets.ReadOnlyModelViewSet):
# 课程科目
queryset = Subject.objects.annotate(total_courses=Count('courses'))
serializer_class = SubjectSerializer # 序列化器
pagination_class = StandardPagination # 分页编辑 api/urls.py 文件,删除或注释掉以下 URL 配置(因为视图集会自动生成这些 URL,不再需要手动定义了):
# path(
# 'subjects/', # 课程科目列表路径
# views.SubjectListView.as_view(), # 关联课程科目列表视图
# name='subject_list' # 命名空间
# ),
# path(
# 'subjects/<pk>/', # 课程科目详情路径
# views.SubjectDetailView.as_view(), # 关联课程科目详情视图
# name='subject_detail' # 命名空间
# ),在同一文件中,把 SubjectViewSet 也注册到路由器中:
from django.urls import include, path
from rest_framework import routers
from . import views
app_name = 'courses'
router = routers.DefaultRouter()
router.register('courses', views.CourseViewSet)
router.register('subjects', views.SubjectViewSet) # 注册课程科目视图
urlpatterns = [
path('', include(router.urls)),
]在浏览器中打开 http://127.0.0.1:8000/api/,你会看到路由器现在同时列出了 courses 和 subjects 两个视图集的 URL:
图 :REST 框架可浏览 API 的 API 根页面
📡REST_FRAMEWORK.DefaultRouter
AWAITING TRAFFIC...
HTTP REQUEST FREQUENCIES
GET/api/subjects/
POST/api/subjects/
GET/api/subjects/1/
DELETE/api/subjects/1/
router.register(
'subjects',
SubjectViewSet
)
'subjects',
SubjectViewSet
)
class SubjectViewSet(viewsets.ReadOnlyModelViewSet):
BAY 01
def list():
# Returns QuerySet
# Returns QuerySet
BAY 02
def create():
# Creates Instance
# Creates Instance
BAY 03
def retrieve():
# Returns Instance
# Returns Instance
BAY 04
def destroy():
# Deletes Instance
# Deletes Instance
请点击上方空域中的任意 HTTP 请求飞船。DefaultRouter 将替代冗长的人工 urls.py 书写,展示智能化动态路由。
Click any HTTP request ship in the upper airspace. DefaultRouter replaces manual urls.py wiring with intelligent dynamic routing.
通用 API 视图和视图集非常适合基于模型和序列化器快速构建 REST API。不过有时候你还需要实现一些带有自定义逻辑的视图。下面来学习如何创建自定义 API 视图。
9、构建自定义 API 视图
DRF 提供了一个 APIView 类,它在 Django 的 View 类基础上增加了 API 相关的功能。APIView 和 Django 原生 View 的区别在于:它使用 DRF 自定义的 Request 和 Response 对象,能自动处理 APIException 异常并返回合适的 HTTP 响应,还内置了身份验证和授权机制来管理视图的访问权限。
我们来创建一个供用户选课的视图。编辑 courses 应用的 api/views.py 文件,添加以下代码:
from django.db.models import Count
from django.shortcuts import get_object_or_404 # 导入对象获取函数
from rest_framework import generics
from rest_framework import viewsets
from rest_framework.response import Response # 导入响应类
from rest_framework.views import APIView # 导入视图类
from courses.api.pagination import StandardPagination
from courses.api.serializers import CourseSerializer, SubjectSerializer
from courses.models import Course, Subject
# ...
# 课程注册视图
class CourseEnrollView(APIView):
# 注册课程
def post(self, request, pk, format=None):
course = get_object_or_404(Course, pk=pk) # 获取课程
course.students.add(request.user) # 添加课程
return Response({'enrolled': True}) # 返回结果CourseEnrollView 视图处理用户选课的逻辑。代码说明如下:
创建了一个继承自
APIView的自定义视图。定义了
post()方法来处理POST请求,这个视图不接受其他 HTTP 方法。通过 URL 中的
pk参数获取课程对象,如果课程不存在则返回404错误。将当前用户添加到
Course对象的students多对多关系中,并返回成功响应。
编辑 api/urls.py 文件,添加 CourseEnrollView 的 URL 配置:
path( # 课程内容排序视图
'courses/<pk>/enroll/', # 课程内容排序路径
views.CourseEnrollView.as_view(), # 课程内容排序视图
name='course_enroll' # 命名空间
),现在理论上可以通过 POST 请求把当前用户注册到课程中了。但是我们还需要识别用户身份,并阻止未登录的用户访问这个视图。接下来了解 API 身份验证和权限的工作原理。
10、处理身份验证
DRF 提供了多种身份验证类,用于识别发出请求的用户。如果验证成功,框架会把认证通过的 User 对象赋值给 request.user;如果没有通过认证,request.user 就是 Django 的 AnonymousUser 实例。
DRF 内置以下身份验证方式:
BasicAuthentication:HTTP 基本认证。用户名和密码通过Authorization请求头以 Base64 编码发送。更多信息可以查看https://en.wikipedia.org/wiki/Basic_access_authentication。TokenAuthentication:基于令牌(Token)的认证。使用Token模型来存储用户令牌,用户在请求时将令牌放在Authorization请求头中。SessionAuthentication:基于 Django Session 的认证,适合在网站前端通过 AJAX 请求调用 API。RemoteUserAuthentication:把认证委托给 Web 服务器处理,通过REMOTE_USER环境变量来识别用户。
你也可以继承 DRF 的 BaseAuthentication 类并重写 authenticate() 方法来自定义身份验证后端。
实施基本身份验证
你可以为每个视图单独设置身份验证方式,也可以通过 DEFAULT_AUTHENTICATION_CLASSES 配置项进行全局设置。
注意:身份验证只是识别"谁在发请求",并不会决定"能不能访问"。要限制视图的访问权限,还需要配合权限(Permission)来使用。
更多身份验证相关的信息可以查看https://www.django-rest-framework.org/api-guide/authentication/。
下面给 CourseEnrollView 添加 BasicAuthentication 认证。编辑 courses 应用的 api/views.py 文件,给 CourseEnrollView 加上 authentication_classes 属性:
# ...
from rest_framework.authentication import BasicAuthentication
class CourseEnrollView(APIView):
authentication_classes = [BasicAuthentication]
# ...这样,用户就需要通过 Authorization 请求头中的凭据来进行身份验证了。
11、为视图添加权限
DRF 内置了一套权限系统,用于限制视图的访问。常用的内置权限类包括:
AllowAny:无限制访问,无论用户是否已登录。IsAuthenticated:只允许已登录的用户访问。IsAuthenticatedOrReadOnly:已登录的用户拥有完整访问权限,匿名用户只能执行读取操作(如GET、HEAD、OPTIONS)。DjangoModelPermissions:基于django.contrib.auth的模型权限。视图需要定义queryset属性,只有经过认证并被分配了相应模型权限的用户才能访问。DjangoObjectPermissions:基于对象级别的权限。
如果用户被拒绝访问,通常会收到以下 HTTP 错误码:
HTTP 401:未认证HTTP 403:没有权限
更多权限相关的信息可以查看https://www.django-rest-framework.org/api-guide/permissions/。
编辑 courses 应用的 api/views.py 文件,给 CourseEnrollView 添加 permission_classes 属性:
# ...
from rest_framework.authentication import BasicAuthentication
from rest_framework.permissions import IsAuthenticated
class CourseEnrollView(APIView):
authentication_classes = [BasicAuthentication]
permission_classes = [IsAuthenticated]
# ...添加了 IsAuthenticated 权限后,匿名用户就无法访问这个视图了。现在你可以向这个 API 端点发送 POST 请求了。 确保开发服务器正在运行,然后打开终端运行以下命令:
curl -i -X POST http://127.0.0.1:8000/api/courses/1/enroll/你会收到以下响应:
HTTP/1.1 401 Unauthorized
...
{"detail": "Authentication credentials were not provided."}因为没有提供身份凭据,所以收到了 401 状态码。下面用一个已有用户进行基本认证。运行以下命令,把 student:password 替换成真实用户的用户名和密码:
curl -i -X POST -u student:password http://127.0.0.1:8000/api/courses/1/enroll/你会收到以下响应:
HTTP/1.1 200 OK
...
{"enrolled": true}你可以在管理后台查看该用户是否已成功注册到课程中。
12、向视图集添加其他操作
你可以给 ViewSet 添加额外的自定义操作。下面把 CourseEnrollView 改造成 CourseViewSet 的一个自定义操作。编辑 api/views.py 文件,修改 CourseViewSet 类如下:
# ...
from rest_framework.decorators import action
class CourseViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Course.objects.prefetch_related('modules')
serializer_class = CourseSerializer
@action(
detail=True,
methods=['post'],
authentication_classes=[BasicAuthentication],
permission_classes=[IsAuthenticated]
)
def enroll(self, request, *args, **kwargs):
course = self.get_object()
course.students.add(request.user)
return Response({'enrolled': True})代码说明如下:
使用
action装饰器并设置detail=True,表示这是针对单个对象的操作。装饰器允许你为操作指定自定义属性:这里限定只允许
POST方法,并设置了身份验证和权限类。使用
self.get_object()获取Course对象。将当前用户添加到
students多对多关系中,并返回成功响应。
编辑 api/urls.py 文件,删除或注释掉以下 URL 配置(因为不再需要了):
path(
'courses/<pk>/enroll/',
views.CourseEnrollView.as_view(),
name='course_enroll'
),然后编辑 api/views.py 文件,删除或注释掉 CourseEnrollView 类。 现在课程注册的 URL 会由路由器自动生成。由于 URL 是根据操作名称 enroll 动态生成的,所以访问路径保持不变。
学生注册课程后,需要能访问课程内容。接下来我们来实现这个功能,确保只有已选课的学生才能查看课程内容。
13、创建自定义权限
我们希望学生能访问自己所选课程的内容,但只有已选课的学生才有权限查看。实现这个需求最好的办法就是创建一个自定义权限类。DRF 提供了 BasePermission 基类,你可以重写以下两个方法:
has_permission():视图级别的权限检查has_object_permission():对象级别的权限检查
这两个方法返回 True 表示允许访问,返回 False 表示拒绝。
在 courses/api/ 目录下新建 permissions.py 文件,添加以下代码:
from rest_framework.permissions import BasePermission
class IsEnrolled(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.students.filter(id=request.user.id).exists()我们继承了 BasePermission 并重写了 has_object_permission() 方法,检查当前用户是否在该 Course 的 students 关系中。接下来就可以使用这个 IsEnrolled 权限了。
🛑REST_FRAMEWORK.BasePermission
GATE LOCKED. AWAITING REQUEST.
HTTP REQUEST
request.user.id = 7
Student
Student
def has_object_permission():
return obj.students.filter(
id=?
).exists()
return obj.students.filter(
id=?
).exists()
DATABASE (obj)
Course #1
Enrolled Students
Enrolled Students
ID: 3 (Alice)
ID: 7 (Student)
ID: 42 (Charlie)
请在左下方选择一个测试用户发起资源请求。自定义的 IsEnrolled 权限闸门将进行拦截比对。
Select a test user below to request resources. The custom IsEnrolled permission gate will intercept and verify.
14、课程内容序列化
接下来对课程内容进行序列化。Content 模型使用了通用外键(Generic ForeignKey),可以关联不同类型的内容模型。在上一节中,我们给所有内容模型添加了一个通用的 render() 方法,这里就可以用它来输出渲染后的内容。
编辑 courses 应用的 api/serializers.py 文件,添加以下代码:
from courses.models import Content, Course, Module, Subject
class ItemRelatedField(serializers.RelatedField):
def to_representation(self, value):
return value.render()
class ContentSerializer(serializers.ModelSerializer):
item = ItemRelatedField(read_only=True)
class Meta:
model = Content
fields = ['order', 'item']这里我们继承了 DRF 的 RelatedField 并重写了 to_representation() 方法来自定义字段的序列化方式。然后定义了 ContentSerializer,把自定义字段用在了通用外键 item 上。 接下来,我们需要一个包含内容的 Module 序列化器和一个扩展版的 Course 序列化器。编辑 api/serializers.py 文件,添加以下代码:
class ModuleWithContentsSerializer(serializers.ModelSerializer):
contents = ContentSerializer(many=True)
class Meta:
model = Module
fields = ['order', 'title', 'description', 'contents']
class CourseWithContentsSerializer(serializers.ModelSerializer):
modules = ModuleWithContentsSerializer(many=True)
class Meta:
model = Course
fields = [
'id',
'subject',
'title',
'slug',
'overview',
'created',
'owner',
'modules'
]下面创建一个视图来获取课程内容。编辑 api/views.py 文件,在 CourseViewSet 类中添加以下方法:
# ...
from courses.api.permissions import IsEnrolled
from courses.api.serializers import CourseWithContentsSerializer
class CourseViewSet(viewsets.ReadOnlyModelViewSet):
# ...
@action(
detail=True,
methods=['get'],
serializer_class=CourseWithContentsSerializer,
authentication_classes=[BasicAuthentication],
permission_classes=[IsAuthenticated, IsEnrolled]
)
def contents(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)代码说明如下:
使用
action装饰器并设置detail=True,表示这是针对单个对象的操作。限定只允许
GET方法。使用
CourseWithContentsSerializer序列化器,它会包含渲染后的课程内容。同时使用
IsAuthenticated和自定义的IsEnrolled权限,确保只有已选课的用户才能访问课程内容。复用现有的
retrieve()方法来返回Course对象。
在浏览器中打开 http://127.0.0.1:8000/api/courses/1/contents/,如果用正确的凭据访问,你会看到课程的每个模块都包含了渲染后的 HTML 内容:
{
"order": 0,
"title": "Introduction to Django",
"description": "Brief introduction to the Django Web Framework.",
"contents": [
{
"order": 0,
"item": "<p>Meet Django. Django is a high-level
Python Web framework
...</p>"
},
{
"order": 1,
"item": "\n<iframe width=\"480\" height=\"360\"
src=\"http://www.youtube.com/embed/bgV39DlmZ2U?
wmode=opaque\"
frameborder=\"0\" allowfullscreen></iframe>\n"
}
]
}到这里,我们已经构建了一个简单但完整的 API,允许其他服务以编程方式访问课程应用。DRF 还提供了 ModelViewSet 类来处理对象的创建和编辑。我们已经介绍了 DRF 的核心功能,更多详细信息可以查看官方文档https://www.django-rest-framework.org/。
15、使用 RESTful API
API 已经实现好了,接下来就可以从其他应用中以编程方式调用它。你可以在前端用 JavaScript 的 Fetch API 与后端 API 交互,也可以从 Python 或其他编程语言编写的应用中调用。
下面我们创建一个简单的 Python 脚本,通过 RESTful API 获取所有可用课程,然后将学生批量注册到所有课程中。你将学会如何使用 HTTP 基本认证来调用 API,以及如何发送 GET 和 POST 请求。
我们会用到 Python 的 Requests 库。Requests 库封装了处理 HTTP 请求的各种细节,提供了非常简洁的接口。文档地址:https://requests.readthedocs.io/en/master/。
打开终端,使用以下命令安装 Requests 库:
python -m pip install requests==2.31.0在 educa 项目目录旁边创建一个新目录 api_examples,在其中新建 enroll_all.py 文件。目录结构如下:
api_examples/
enroll_all.py
educa/
...编辑 enroll_all.py 文件,添加以下代码:
import requests
base_url = 'http://127.0.0.1:8000/api/'
url = f'{base_url}courses/'
available_courses = []
while url is not None:
print(f'Loading courses from {url}')
r = requests.get(url)
response = r.json()
url = response['next']
courses = response['results']
available_courses += [course['title'] for course in courses]
print(f'Available courses: {", ".join(available_courses)}')这段代码的逻辑如下:
导入 Requests 库,定义 API 的基础 URL 和课程列表端点的 URL。
定义一个空列表
available_courses来存放课程名称。用
while循环遍历所有分页结果。通过
requests.get()向http://127.0.0.1:8000/api/courses/发送GET请求获取数据。这个端点是公开的,不需要认证。用响应对象的
json()方法解析返回的 JSON 数据。把
next字段的值存到url变量中,用于获取下一页数据。把每门课程的
title添加到available_courses列表中。当
url为None时,说明已经到了最后一页,循环结束。打印所有可选课程的列表。
先启动开发服务器:
python manage.py runserver然后在另一个终端中,进入 api_examples/ 目录运行:
python enroll_all.py你会看到类似如下的输出:
Available courses: Introduction to Django, Python for beginners, Algebra basics这是我们第一次以编程方式调用 API。 接下来编辑 enroll_all.py 文件,添加选课功能:
import requests
username = ''
password = ''
base_url = 'http://127.0.0.1:8000/api/'
url = f'{base_url}courses/'
available_courses = []
while url is not None:
print(f'Loading courses from {url}')
r = requests.get(url)
response = r.json()
url = response['next']
courses = response['results']
available_courses += [course['title'] for course in courses]
print(f'Available courses: {", ".join(available_courses)}')
for course in courses:
course_id = course['id']
course_title = course['title']
r = requests.post(
f'{base_url}courses/{course_id}/enroll/',
auth=(username, password)
)
if r.status_code == 200:
# successful request
print(f'Successfully enrolled in {course_title}')把 username 和 password 替换成实际用户的凭据(你也可以用环境变量来加载凭据,避免明文写在代码里)。 新增的代码做了以下事情:
设置要选课的学生的用户名和密码。
遍历从 API 获取到的课程列表。
把每门课程的
id和title分别存到变量中。用
requests.post()向http://127.0.0.1:8000/api/courses/[id]/enroll/发送POST请求。这个端点对应CourseEnrollView视图,需要认证。Requests 库原生支持 HTTP 基本认证,通过auth参数传入用户名和密码的元组即可。如果响应状态码为
200 OK,就打印选课成功的消息。
更多关于 Requests 库认证方式的信息,可以查看https://requests.readthedocs.io/en/master/user/authentication/。
在 api_examples/ 目录下运行:
python enroll_all.py你会看到类似这样的输出:
Available courses: Introduction to Django, Python for beginners, Algebra basics
Successfully enrolled in Introduction to Django
Successfully enrolled in Python for beginners
Successfully enrolled in Algebra basics太棒了!你已经成功通过 API 把用户注册到了所有可用课程中。每门课程都会显示 Successfully enrolled 的消息。可以看到,从其他应用调用这个 API 是非常简单的。
