Django学习2

Django学习2

Setup

我们为了保留第一部分代码,因此新开一个文件夹,叫storefront2, 并在里面创建虚拟环境。

创建好以后,VSCode可能不会把该虚拟环境当做默认虚拟环境,因此我们需要设置一下:

command+p 并在搜索栏敲> 呼出命令面板. 选择 python: select Interpreter选项,点击输入解释器路径,并输入当前文件夹下的虚拟环境的路径(通过pipenv shell可查看),就设置完成了,如下图所示

Building. RESTful APIs with Django REST Framework

What are RESTful APIs

之前我们看到的数据只有Admin才有权限查看。现在我们要写一些接口,能让我们以非Admin的角色来获取数据库中的信息。这些接口就是API。打一个不恰当的比方,API就好像是电视遥控板中的一个按键。

RESTful API则是对API提出了一下几点要求

  • Fast
  • Scalable
  • Reliable
  • Easy to understand
  • Easy to change

首先,我们要了解一些基本概念:

Resources

RESTful API 中的 Resources 概念就好像是我们在application中创建的对象一样。浏览器能通过 URL(Uniform Resource Locator) 访问到 Resources,URL就好像是这些资源的地址。

比如说,我想访问 products 数据,那么,我们的URL可以是这样的:localhost:9000/products

或者,products中的具体数据,比如localhost:9000/products/1

再者,Resources中也可能有Resources,比如说一个特定的产品有很多评测,那么url可以是这样的:

1
2
localhost:9000/products/1/reviews
localhost:9000/products/1/reviews/1

Resource Representations

当client用URL访问Resources的时候,后端就需要向客户回传这些Resources

一般,Resources 传给前端的格式是:HTML, XML, JSON。这些我们都比较熟悉了。

HTTP Methods

HTTP主要有几种请求方法:

  • GET: 用来获取数据
  • POST: 创建数据,比如/products
  • PUT: 更新数据,如果要修改所有的对象,就要使用PUT, /products
  • PATCH: 更新一部分数据, 比如只修改第一个对象,那么就要使用PATCH,/products/1
  • DELETE:删除数据, 比如/products/1

Installing Django REST Framework

我们需要用这行代码下载Django REST Framework, 这样写起API就方便很多,

pipenv install djangorestframework

下载完成后,我们要在APP中注册:

1
2
3
4
INSTALLED_APPS = [
'rest_framework',
#...
]

Creating API Views

现在我们来创建 127.0.0.1:9000/store/products 对应的API

首先要明白,Django有自己的HttpRequest和HttpResponse模块,同时,Django Rest Framework也有自己的Request和Response模块。在这个项目中我们主要使用后两者,因为更方便,实现的功能也更强

其次,我们要把Django框架理解成层级访问,最高层就是我们创建的storefront,然后是storefront下面的各个注册的app,最后是app中的各种Object(Resources)。我们通过URL访问的时候,也是要”层级访问“

127.0.0.1:9000/ 访问的就是最顶层 storefront ,如果我们要访问storefront中的store,需要在storefront里面的urls.py注册store/ ,如下所示:

1
2
3
4
5
urlpatterns = [
path('admin/', admin.site.urls),
path('playground/', include('playground.urls')),
path('store/', include('store.urls')),
]

再者,我们如果要访问store中的products对象,我们就需要在store里面的urls.py 中注册products/.

1
2
3
4
5
6
7
from django.urls import path
from . import views

# URLConf
urlpatterns = [
path('products/', views.product_list)
]

最后,我们在view中编写api,来处理client通过URL这个“地址”发送来的请求:在这里,我使用了rest_framework中的api_view 这个修饰器,当api被@api_view修饰后,里面的request就会自动替换成rest_framework 中更简介、强大的request对象。

然后,我们用 rest_framework 中的 Response 来替换 django.http 中的HttpResponse,这样,返回的就是一个 Brosable API对象,非常简洁美观,一目了然:

1
2
3
4
5
6
7
8
9
from django.shortcuts import render
from django.http import HttpResponse
from rest_framework.decorators import api_view
from rest_framework.response import Response
# Create your views here.

@api_view()
def product_list(request):
return Response('OK')

如果使用Django的HttpResponse,那么返回的仅仅是一个”OK”,但如果用的是rest_framework 的Response,就会显示我们的Http method, URL,以及Response的报文。同时,也可以以切换为json格式,查看浏览器真正会显示的画面,也就是”OK”

product details

接下来,我们再来创建一个API:product detail,用来访问每个Product特定的信息

首先我们编写 view: 功能很简单,从client发来的URL中找到参数id,然后返回这个id的值

1
2
3
@api_view()
def product_detail(request,id):
return Response(id)

然后,我们在urls.py中注册路由:

1
2
3
4
urlpatterns = [
#...
path('products/<int:id>/', views.product_detail)
]

注意,因为我们访问的是每个product的编号,因此只能是整数类型,为了防止出现类似于/store/products/1/的非法请求出现,我们在注册路由的时候,要给参数id添加一个限制条件:<int:id>

Creating Serializers

我们要将Object转换为浏览器可以处理的JSON格式的信息,首先我们需要 Serializer(序列化工具) 将这些对象信息 转换成字典,然后使用 REST Framework中的JSONRenderer模块将字典转换成JSON。

现在,我们就来学习如何创建一个Serializer,它的样子和models很像,只不过model负责的是和数据库对接,而serializer负责的是和client对接。然而,数据库中的某些信息我们是不希望用户去了解的,因此在serializer中一般不会提供产品的所有信息

首先我们要在store中创建serializers.py ,需要用到rest_framework 中的 serializers 模块.在下面这个网站中,有serializers提供的数据类型:

https://www.django-rest-framework.org/api-guide/fields/

这里,我们只想用户开放了三个字段信息:产品id,产品名称以及产品单价。因此可以这么写:

1
2
3
4
5
6
7
from rest_framework import serializers


class ProductSerializers(serializers.Serializer):
id = serializers.IntegerField()
title = serializers.CharField(max_length=255)
unit_price = serializers.DecimalField(max_digits=6, decimal_places=2)

Serializing Objects

在这个view中,我们要根据url中的id,从数据库中取出对象;然后,把这个对象交给ProductSerializers 去序列化,最后,把序列化后的数据传给Response,它会将其转换为JSON格式的文档回传给client

1
2
3
4
5
6
7
8
from .models import Product
from .serializers import ProductSerializers

@api_view()
def product_detail(request, id):
product = Product.objects.get(pk=id)
serializers = ProductSerializers(product)
return Response(serializers.data)

我们看到unit_price 是字符串类型的,我们需要设置一下将其变为数字类型:在setting.py中加入如下设置:

1
2
3
REST_FRAMEWORK = {
'COERCE_DECIMAL_TO_STRING': False
}

现在,参数如果正确的话,是能从数据库中获取信息的,但是,如果参数是0呢?Django会报错,而不是返回一个显示错误信息的JSON文档。对此,我们有两种解决方法:

  • rest_framework中的 status 模块
1
2
3
4
5
6
7
8
9
10
11
from rest_framework import status


@api_view()
def product_detail(request, id):
try:
product = Product.objects.get(pk=id)
serializers = ProductSerializers(product)
return Response(serializers.data)
except Product.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
  • Django.shortcut 中的 get_object_or_404 方法
1
2
3
4
5
6
@api_view()
def product_detail(request, id):

product = get_object_or_404(Product, pk=id)
serializer = ProductSerializer(product)
return Response(serializer.data)

然后,我们再注册一个store/products/ 路由,显示所有products信息。因此,我们可以用get_list_or_404,它是用来获取数据库中所有对象的方法,注意,当queryset中有很多元素的时候,需要设置 many=True,这样,serializer就会遍历queryset来执行操作:

1
2
3
4
5
@api_view()
def product_list(request):
queryset = get_list_or_404(Product)
serializer = ProductSerializer(queryset, many=True)
return Response(serializer.data)

Creating Custom Serializer Fields

首先,我们要明白API Model是不等于Data Model的,后者是应用实现的一个部分,而前者是要给外界展示的一个部分。因此,展示的部分可以在model本身的基础上做一个计算,比如说,我们可以再Serializer里给product增加一个税后的价钱:

我们这里创建了一个计算税后价格的函数calculate_tax, 即把原价乘以1.1后返回(这里,由于1.1是浮点数,需要用Decimal(1.1)包裹一下)。然后, 再用SerializerMethodField 客制化,将method_name参数设定为我们自己写的函数的名称:

1
2
3
4
5
6
7
8
9
10
class ProductSerializer(serializers.Serializer):
id = serializers.IntegerField()
title = serializers.CharField(max_length=255)
price = serializers.DecimalField(
max_digits=6, decimal_places=2, source='unit_price')
price_with_tax = serializers.SerializerMethodField(
method_name='calculate_tax')

def calculate_tax(self, product: models.Product):
return product.unit_price * Decimal(1.1)

注意了,这里我将unit_price改成了price,但是model中并没有这个字段,因此直接查询会报错。我们需要设定source参数为原来的字段

Serializing Relationships

使用serializers内置方法

如果我想在Serilizer中显示一对一关系、一对多关系。比如,我想在ProductSerializer中显示产品属于的集合类型,我可以使用 PrimaryKeyRelatedField这个方法。

1
2
3
4
5
6
7
8
from store.models import Product, Collection

class ProductSerializer(serializers.Serializer):
# ...
collection = serializers.PrimaryKeyRelatedField(
queryset=Collection.objects.all()
)
# ...

有时候,系统会发生报错,这是因为检查机制出了点问题,可以通过重启解决

但是,显示数字显然是不够的,我们想要显示collection的名称信息,因此我们可以用StringRelatedField方法:

1
2
class ProductSerializer(serializers.Serializer):
collection = serializers.StringRelatedField()

但这样虽然能显示collection字符串,但是对每个product都会额外多出一个关于Collection的查询,导致性能异常低下。为了解决这个bug,我们需要在view中修改一下:将原来个get_list_or_404改为Product.objects.select_related('collection').all()

1
2
3
4
@api_view()
def product_list(request):
queryset = Product.objects.select_related('collection').all()
#...

使用嵌套的Serializer对象:

除了上面这种方法,我们还可以使用嵌套对象的方法,能展示的数据更多:

1
2
3
4
5
6
7
class CollectionSerializer(serializers.Serializer):
id = serializers.IntegerField()
title = serializers.CharField(max_length=255)

class ProductSerializer(serializers.Serializer):
# ...
collection = CollectionSerializer()

Model Serializers

我们发现,现在创建的Serializers和Models是非常相似的。为了节省代码,增加代码的可维护性,我们可以使用ModelSerializer这个类:

使用了这个类之后,我们需要在里面创建Meta class,在 Meta中,我们要确定model的类型,并确定要放到ModelSerializer的字段, 即设置fields数组

如果我们想覆盖的话,我们可以自己写要呈现的内容。比如说collection,如果我不采用嵌套对象的方法,ModelSerializer会默认使用外键的值。

此外还可以自己加入model中没有的值,比如price_with_tax,这是我们自己写的一个字段,也可以加到fields中去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CollectionSerializer(serializers.ModelSerializer):
class Meta:
model = Collection
fields = ['id', 'title' ]


class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'title', 'unit_price', 'price_with_tax', 'collection']
price_with_tax = serializers.SerializerMethodField(
method_name='calculate_tax')
collection = CollectionSerializer()

def calculate_tax(self, product: Product):
return product.unit_price * Decimal(1.1)

Deserializing Objects

有了从server到client的对对象的序列化,那么就有对应从client到server的反序列化。比如说我想创建一个新的product,就要求sever反序列化得到client传入的关于新产品的信息,将它们变成一个对象。

我们知道,GET 方法是用来请求数据的,POST方法是用来更新数据的,我们现在在product_list 中实现一下两种方式的不同逻辑。

首先,在@api_view中要列出这个api能处理的HTTP METHOD的类型.这里是['GET','POST'].

然后,我们要设计if-else 来处理不同的请求。这里,如果是GET,就返回product列表,如果是POST我们要验证数据,并将合法的数据插入数据库,这里我们先不做验证,并直接返回OK作为测试

1
2
3
4
5
6
7
8
9
10
11
@api_view(['GET', 'POST'])
def product_list(request):
if request.method == 'GET':
queryset = Product.objects.select_related('collection').all()
serializer = ProductSerializer(
queryset, many=True, context={'request': request})
return Response(serializer.data)
elif request.method == 'POST':
serializer = ProductSerializer(data=request.data)
# serializer.validated_data
return Response('OK')

最后,我们把 http://127.0.0.1:9000/store/products/ 拉到最后,然后随便输入一个JSON格式的文件,提交后我们就会看到 RESPONSE报文,其HTTP method为POST。

Data Validation

现在我们来学习数据验证相关的问题。

if-else block

这个逻辑是这样的,首先,serializer需要调用is_valid()函数,如果通过,那么对就可以对validated_data进行保存等操作;如果不通过,我们这里就返回serializer.errors ,并附上状态码400

1
2
3
4
5
6
7
elif request.method == 'POST':
serializer = ProductSerializer(data=request.data)
if serializer.is_valid():
serializer.validated_data
return Response('OK')
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

测试结果如下图所示:

raise_exception=True

我们可以用一种更简单的方法来取代if-else逻辑:

1
2
3
4
5
elif request.method == 'POST':
serializer = ProductSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.validated_data
return Response("OK")

效果是一样的

现在,如果输入正确的信息,我们想将其在终端打印出来:(注意,这里要考虑好collection的类型,如果是对象的话,collection的值应该是一个字典。这里我使用的是默认的 PrimaryKeyRelatedField,因此只输入了一个INT值

1
2
3
4
5
{
"title":"a",
"unit_price":1,
"collection":1
}

我们发现,在终端打印出来的serializer.validated_data 是一个OrderedDict

1
2
OrderedDict([('title', 'a'), ('unit_price', Decimal('1.00')), ('collection', <Collection: collection1>)])
[08/Dec/2021 09:58:32] "POST /store/products/ HTTP/1.1" 200 16875

rest_framework 只提供给我们数据类型、是否为空这类简单的数据验证,如果我们要设计更加复杂的验证方法,需要在serializer.py中自己实现函数如:

1
2
3
4
def validate(self, data):  
if data['password'] != data['confirm_password']:
return serializers.ValidationError('passwords do not match')
return data

Saving Objects

我们可以用 ModelSerializer中的save方法将浏览器发送过来的数据存入数据库

由于我们在创建的时候,inventory字段设置了最小值为1的验证器,因此我们需要把这些必填的值也加入到serializer中去:

1
2
3
4
5
6
7
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'title', 'description', 'slug', 'inventory',
'unit_price', 'price_with_tax', 'collection']
price_with_tax = serializers.SerializerMethodField(
method_name='calculate_tax')

我们创建的产品如下:

1
2
3
4
5
6
7
{
"title":"a",
"slug":"a",
"unit_price":1,
"collection":1,
"inventory":1
}

提交后,Django就会保存了,非常方便

Updating Objects

想要更新数据库中的信息,需要使用 PUT 或者PATCH 方法, 且需要对一个特定的product进行修改。因此,我们把这个逻辑封装在product_detail 这个api当中。

首先,我们还是要确定这个API负责的HTTP Method类型,这里,为了方便我们就只用PUT类型来更新数据

然后做一个方法判断,在PUT方法中,由于我们要对一个特定的数据进行修改,因此我们往ProductSerializer中传入的,不只是client递交过来的更新信息,还要这个目标对象的一个实例。因此,我们在if-else block之前就把product提取出来,然后传给ProductSerializer,返回一个更新后的product

接着就是对product进行数据验证,返回一个保存后的信息并保存

1
2
3
4
5
6
7
8
9
10
11
@api_view(['GET', 'PUT'])
def product_detail(request, id):
product = get_object_or_404(Product, pk=id)
if request.method == 'GET':
serializer = ProductSerializer(product)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = ProductSerializer(product, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data,status=status.HTTP_201_CREATED)
  • 递交前:

  • 递交后

我们发现,id=2的产品已经更新了。title前面多了个’+’

Deleting Objects

如果我们在product_detail中添加DELETE Method: @api_view(['GET', 'PUT','DELETE'])

这时候,回到client,就会发现这里多出来一个DELETE按钮。这就是Brosable API带来的好处——方便简洁

如果我们只写删除的逻辑,是会报错的,因为product是orderItem的外键,是保护关系。也就是说,如果不删除orderitem,就不能删除它引用的product。

因此我们要这么处理逻辑,如果引用该产品的orderitem数量(用product.orderitem_set表示)大于0,我们就不能删除,并返回状态码 405,代表该方法对此情况不适用。如果等于0,那么放心删除,返回状态码204

1
2
3
4
5
6
elif request.method == 'DELETE':
if product.orderitem_set.count() > 0:
return Response({'error': 'Product cannot be deleted because it is associated with orderitem'},
status=status.HTTP_405_METHOD_NOT_ALLOWED)
product.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

这样,如果起了冲突的话,就会收到这样的返回报文:

如果该product没有被引用过,就可以安全删除:

注意点:

在代码中,反向引用计算该产品的orderitem 可以用product.orderitem_set 来表示,但是这种表示不容易记住。我们可以通过修改Orderitem模型,在product字段中添加related_name属性来提高代码的可读性:

1
2
3
4
class OrderItem(models.Model):
#...
product = models.ForeignKey(
Product, on_delete=models.PROTECT, related_name='orderitems')
1
2
3
elif request.method == 'DELETE':
if product.orderitems.count() > 0:
#...

Exercise Building the Collections API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@api_view(['GET', 'POST'])
def collection_list(request):
if request.method == 'GET':
queryset = Collection.objects.all()
serializer = CollectionSerializer(
queryset, many=True, context={'request': request})
return Response(serializer.data)
elif request.method == 'POST':
serializer = CollectionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)

@api_view(['GET', 'PUT', 'DELETE'])
def collection_detail(request, id):
collection = get_object_or_404(Product, pk=id)
if request.method == 'GET':
serializer = CollectionSerializer(collection)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = CollectionSerializer(collection, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
elif request.method == 'DELETE':
if collection.products.count() > 0:#注意修改Product Model
return Response({'error': 'Collection cannot be deleted because it is associated with products'},
status=status.HTTP_405_METHOD_NOT_ALLOWED)
collection.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
1
2
3
4
5
6
urlpatterns = [
path('products/', views.product_list),
path('products/<int:id>/', views.product_detail),
path('collections/', views.collection_list),
path('collections/<int:id>/', views.collection_detail),
]

Advanced API Concepts

在这一章我们将学习更多技术,帮助我们更快的编写API

Class-based Views

之前我们是单独创建View,即一个函数为单位进行对模型的操作。但是,如果是要处理很多HTTP Method的话,就会用到很多if-else blocks 这会让代码看起来非常杂乱。因此我们现在来学习 class-basesd Views

首先,要引入APIView, 然后,在这个类里面,我们可以定义各种方法的逻辑,每个方法写一个单独的函数。比如def get,def post. 函数内部的逻辑和之前if-else代码块内的逻辑是一致的。这样一来,我们就可以规避掉if-else的这种判断逻辑,让代码看起来更加整洁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from rest_framework.views import APIView

class ProductList(APIView):
def get(self, request):
queryset = Product.objects.select_related('collection').all()
serializer = ProductSerializer(
queryset, many=True, context={'request': request})
return Response(serializer.data)

def post(self, request):
serializer = ProductSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)

同理,我们也可以重写ProductDetail,但是注意了,因为要获取特定的product,所以需要在函数中再加一个参数:id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ProductDetail(APIView):
def get(self, request, id):
product = get_object_or_404(Product, pk=id)

serializer = ProductSerializer(product)
return Response(serializer.data)

def put(self, request, id):
product = get_object_or_404(Product, pk=id)
serializer = ProductSerializer(product, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)

def delete(self, request, id):
product = get_object_or_404(Product, pk=id)
if product.orderitems.count() > 0:
return Response({'error': 'Product cannot be deleted because it is associated with orderitem'},
status=status.HTTP_405_METHOD_NOT_ALLOWED)
product.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

写完class 在url中还需要作相应修改:

1
2
3
4
5
6
# URLConf
urlpatterns = [
path('products/', views.ProductList.as_view()),
path('products/<int:id>/', views.ProductDetail.as_view()),
#...
]

Mixins

我们发现,对于Collection和Product,它们的view方法基本上是一模一样的。比如 product_list 和 collection_list的功能基本一样,就是从数据库中获取所有对象并返回;又比如 product_detail 和 collection_detail,都是访问特定的对象的具体信息。那么如此相同的逻辑,是否有一种方法可以将它们封装起来,我们就可以减少很多不必要的代码了。由此,我们来学习 Mixins(混合类)的概念。所谓混合类,顾名思义就是将很多操作都封装在一个类里面

首先,我们从 mixins的源码开始看起:混合类都放在rest_framework.mixins

1
from rest_framework.mixins import ListModelMixin,CreateModelMixin

比如说,这个ListModelMixin,其源码如下所示,我们看到这个类中只有一个list函数。函数的逻辑和我们自己写的逻辑非常相似:首先,从数据库中创建一个queryset, 然后进行分页等操作,再送到Serializer中去序列化,并返回序列化后的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ListModelMixin:
"""
List a queryset.
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())

page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)

serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

又比如说 CreateModelMixin: 里面的create函数逻辑和我们def put的逻辑一样。
首先把数据传入serializer创建序列化对象,然后进行数据验证,再进行保存操作,如果成功,最后返回HTTP 201

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CreateModelMixin:
"""
Create a model instance.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

def perform_create(self, serializer):
serializer.save()

def get_success_headers(self, data):
try:
return {'Location': str(data[api_settings.URL_FIELD_NAME])}
except (TypeError, KeyError):
return {}

此外,还有几种 mixin:

mixins 作用 对应http的请求方法
Mixins.ListModelMixin 定义list方法,返回一个queryset列表 GET
Mixins.CreateModelMixin 定义create方法,创建一个实例 POST
Mixins.RetrieveModelMixin 定义retrieve方法,返回一个具体的实例 GET
Mixins.UpdateModelMixin 定义update方法,对某个实例进行更新 PUT/PATCH
Mixins.DestroyModelMixin 定义delete方法,删除某个实例 DELETE

官网上我们能找到更多信息

Generic Views

在了解了mixins之后,我们需要再上一层,去了解Generic Views,也就是封装了Mixins的视图类。放在rest_framework.generics

1
from rest_framework.generics import ListCreateAPIView

比如说,我们来看ListCreateAPIView 这个类。它里面封装了ListModelMixinCreateModelMixin 这两个混合类,由此,我们可以用它来获取一个model中的所有对象信息,还可以创建新的对象。

此外,在这个类里面有 两个句柄函数(handler method):get和post,get函数调用了从ListModelMixin继承下来的list方法;而post函数调用了从CreateModelMixin继承下来的create方法

1
2
3
4
5
6
7
8
9
10
11
12
class ListCreateAPIView(mixins.ListModelMixin,
mixins.CreateModelMixin,
GenericAPIView):
"""
Concrete view for listing a queryset or creating a model instance.
"""

def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)

def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)

此外,在 官网 上还有更多的Generic Views,比如ListAPIView,就仅仅提供get方法,而且是只读的;RetrieveUpdateDestroyAPIView 就提供 get,put,patch和delete四种方法。

那么,对于我们刚刚创建的 Class-based View ,我们就可以让它继承ListCreateAPIView类:由于里面已经封装了get和post函数。于是,我们只需要调用get_querysetget_serializer_class这两个方法来获取queryset和serializer这两个对象就可以了。此外,为了让serializer获取到request的报文(这样就可以用其 去序列化、创建新对象),我们需要使用get_serializer_context 方法,传入一个字典{'request': self.request}

1
2
3
4
5
6
7
8
9
class ProductList(ListCreateAPIView):
def get_queryset(self):
return Product.objects.select_related('collection').all()

def get_serializer_class(self, *args, **kwargs):
return ProductSerializer

def get_serializer_context(self):
return {'request': self.request}

我们甚至还能让这个类变得更简单。直接设置querysetserializer_class这两个类

1
2
3
4
5
6
class ProductList(ListCreateAPIView):
queryset = Product.objects.select_related('collection').all()
serializer_class = ProductSerializer

def get_serializer_context(self):
return {'request': self.request}

使用Generic View,不仅在保持原功能的情况下简化自己的代码,还会自己创建一张表单,方便我们创建新的product,我们再也不用苦逼的手写JSON了

同样的,我们可以为Collection 创建 Generic Views. 如下所示:

这里,我们新加了一个字段products_count,用来统计有多少products属于该collection

1
2
3
4
class CollectionList(ListCreateAPIView):
queryset = Collection.objects.annotate(
products_count=Count('products')).all()
serializer_class = CollectionSerializer

但是,当创建新的collection的时候,我希望products_count是一个只读的值,否则就乱了套了。因此,我们在CollectionSerializer里面,需要把products_count`设置为已读

1
2
3
4
5
6
class CollectionSerializer(serializers.ModelSerializer):
class Meta:
model = Collection
fields = ['id', 'title', 'products_count']

products_count = serializers.IntegerField(read_only=True)

结果如下所示,我们可以看到collection中的products_count字段,但是要新建collection的话我们只能填写一个title字段:

Customizing Generic Views

事实上我们可以客制化Generic Views。

对于ProductDetail这个View,我们需要用到 PUT、GET、DELETE这三个函数。其中,PUT和GET都是可以交给封装好的函数去执行的。但对于DELETE方法,我们写了自己的逻辑进去,就是要先做一个判断。

因此,我们不能用Generic View提供给我们的DELETE方法,需要自己重写DELETE,方法也很简单,就保留def delete即可。

修改完以后还没结束,运行会报错:

为了解决这个bug,我们可以修改url,把参数从id改为pk: path('products/<int:pk>/', views.ProductDetail.as_view());或者在 ProductDetail 里面设置 lookup_field属性为id:lookup_field=id,这里我们选择前者

如果选择前者,那么在delete方法中,需要把参数id也改成pk,否则会导致参数不识别。

1
2
3
4
5
6
7
8
9
10
11
class ProductDetail(RetrieveUpdateDestroyAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer

def delete(self, request, pk):
product = get_object_or_404(Product, pk=pk)
if product.orderitems.count() > 0:
return Response({'error': 'Product cannot be deleted because it is associated with orderitem'},
status=status.HTTP_405_METHOD_NOT_ALLOWED)
product.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

现在,我们把CollectionDetail也修改一下:

1
2
3
4
5
6
7
8
9
10
11
12
class CollectionDetail(RetrieveUpdateDestroyAPIView):

queryset = Collection.objects.all()
serializer_class = CollectionSerializer
# 记得修改url.py
def delete(self, request, pk):
collection = get_object_or_404(Collection, pk=pk)
if collection.products.count() > 0:
return Response({'error': 'Collection cannot be deleted because it is associated with products'},
status=status.HTTP_405_METHOD_NOT_ALLOWED)
collection.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

ViewSets

我们来关注Product和Collection,这两个model都有两个Generic View——一个List用来取得所有信息,一个Detail来获得单独信息。在每个View里面,都需要定义queryset和serializer_class这两个属性,比较重复。因此,我们可以引入ViewSets(视图集),顾名思义,他将很多view放在一个集合类里面,这个类提供了很多内置Minixs。这样,只需要确定一套queryset和serializer_class就能完成多个view的任务了。

我们导入

然后来看看ModelViewSet的源码:from rest_framework.viewsets import ModelViewSet

1
2
3
4
5
6
7
8
9
10
11
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
"""
pass

我们看到,ModelViewSet里面继承了很多Mixins,提供了很多http 方法,是一个集大成者

由此,我们可以让ProductViewSetCollectionViewSet 继承 ModelViewSet,如下所示。这样就把两个View集合为一个ViewSet了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ProductViewSet(ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer

def get_serializer_context(self):
return {'request': self.request}

def delete(self, request, pk):
product = get_object_or_404(Product, pk=pk)
if product.orderitems.count() > 0:
return Response({'error': 'Product cannot be deleted because it is associated with orderitem'},
status=status.HTTP_405_METHOD_NOT_ALLOWED)
product.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

# 如果希望ViewSet中的信息是只读的,我们可以使用ReadOnlyModelViewSet类
class CollectionViewSet(ModelViewSet):
queryset = Collection.objects.annotate(
products_count=Count('products')).all()
serializer_class = CollectionSerializer

def delete(self, request, pk):
collection = get_object_or_404(Collection, pk=pk)
if collection.products.count() > 0:
return Response({'error': 'Collection cannot be deleted because it is associated with products'},
status=status.HTTP_405_METHOD_NOT_ALLOWED)
collection.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

我们还要注意,对于ProductList,CollectionList这些View,是不能提供DELETE函数的,否则会出现删库跑路的情况。我们想要的是,给每一个特定的对象(ProductDetail)提供删除服务。

因此,我们不应该重写delete方法(这是对Products全适用的删除函数),而要重写destroy方法,它是用来删除一个单独的实例的。

在修改过程中,我们需要修改如下逻辑,因为在destroy方法中,已经有获取实例的逻辑了,我们不需要再访问一次数据库去获取product,因此,我们可以反向思考:查找OrderItem中的实例是否与该product相关联。

1
2
if product.orderitems.count() > 0:
return Response({'error': 'Product cannot be deleted because it is associated with orderitem'},

最终修改后如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
class ProductViewSet(ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer

def get_serializer_context(self):
return {'request': self.request}

def destroy(self, request, *args, **kwargs):
if OrderItem.objects.filter(product_id=kwargs['pk']).count() > 0:
return Response({'error': 'Product cannot be deleted because it is associated with orderitem'},
status=status.HTTP_405_METHOD_NOT_ALLOWED)
return super().destroy(request, *args, **kwargs)

修改完成后,程序是无法运行的,因为url并没有修改。那么,怎么让url定位到我们编写的ViewSet呢?我们接下来学习Routers路由的写法

Routers

当我们使用ViewSet来集成View,就不用显式使用urlpatterns来注册url了。

SimpleRouter

首先,我们导入DRF中的 routers 中的SimpleRouter类

然后,我们创建一个SimpleRouter实例,并将两个ViewSet注册进去

打印一下注册后router.urls支持的格式:发现router已经帮我们自动注册了四条 URLPattern

1
2
3
4
[ <URLPattern '^products/$' [name='product-list']>,
<URLPattern '^products/(?P<pk>[^/.]+)/$' [name='product-detail']>,
<URLPattern '^collections/$' [name='collection-list']>,
<URLPattern '^collections/(?P<pk>[^/.]+)/$' [name='collection-detail']>]

最后,我们让 urlpatterns 等于 router.urls 也就是上面个数组

1
2
3
4
5
6
7
8
9
from rest_framework.routers import SimpleRouter,DefaultRouter

from pprint import pprint

router = SimpleRouter()
router.register('products', views.ProductViewSet)
router.register('collections', views.CollectionViewSet)

urlpatterns = router.urls

DefaultRouter

如果我们使用的是 DefaultRouter 类,那么相比SimpleRouter会多出两个功能:

首先,如果我们访问 http://127.0.0.1:9000/store/ 就会出现一个根目录,也就会展示出这个app下开放的api,如下图所示

其次,如果我们访问http://127.0.0.1:9000/store/products.json,就会展示出所有products的JSON格式的文档信息:

Building the Reviews API

现在,我们要对product创建一个Review内容,即用户反馈。

首先,创建Review model

1
2
3
4
5
6
class Review(models.Model):
product = models.ForeignKey(
Product, on_delete=models.CASCADE, related_name='reviews')
name = models.CharField(max_length=255)
description = models.TextField()
date = models.DateField(auto_now_add=True)

然后,创建相应的Serializer:

1
2
3
4
class ReviewSerializer(serializers.ModelSerializer):
class Meta:
model = Review
fields = ['id', 'date', 'name', 'description', 'product']

接着,创建ViewSet

1
2
3
class ReviewViewSet(ModelViewSet):
queryset = Review.objects.all()
serializer_class = ReviewSerializer

最后,注册路由,但是我们发现,Product和Review是一对多的关系,因此我们需要在检索单个product的url http://localhost:9000/store/products/1/ 后面,再加上对Review的检索。这就是我们接下来要学习的嵌套路由

Nested Routers

首先,我们需要下载实现嵌套路由的包: pipenv install drf-nested-routers

具体教程在https://github.com/alanjds/drf-nested-routers 可以找到:

我们要实现的功能如下,前两行是通过domain pk找到单独的domain

第三行是找到关于此domain的所有nameservers

第四行是通过nameserver pk来找到关于此domain的特定的nameserver

1
2
3
4
/domain/ <- Domains list
/domain/{pk}/ <- One domain, from {pk}
/domain/{domain_pk}/nameservers/ <- Nameservers of domain from {domain_pk}
/domain/{domain_pk}/nameservers/{pk} <- Specific nameserver from {pk}, of domain from {domain_pk}

实现逻辑如下,首先,创建一个SimpleRouter实例,并注册domain,这都是前置操作

1
2
3
4
5
6
7
# urls.py
from rest_framework_nested import routers
from views import DomainViewSet, NameserverViewSet
(...)

router = routers.SimpleRouter()
router.register('domains', DomainViewSet)

接下来,我们要使用到上面导入的routers模块中的NestedSimpleRouter类,我们要传入三个参数:

  • 第一个参数是parent router,也就是之前注册的关于domain的路由;
  • 第二个参数是子路由的前缀,也就是http://localhost:9000/store/products/1/reviews/1/中的products
  • 第三个参数是lookup查询参数,这个嵌套路由要先查询domain

也就是说,这三个参数我们就可以理解为 父路由、”父类名+s”、loopup=”父类名”

然后,我们在这个NestedSimpleRouter中注册nameservers,和NameserverViewSet相对应。
basename 参数是一种名字模式,它是可选的,设置了这个参数以后,可以为我们提供命名View的选项。这里我们选择domain-nameservers, 那么就会自动生成两个name:domain-nameservers-list,domain-nameservers-detail

最后,我们创建两个urlpattern

1
2
3
4
5
6
7
8
domains_router = routers.NestedSimpleRouter(router,  'domains', lookup='domain')
domains_router.register('nameservers', NameserverViewSet, basename='domain-nameservers')
# 'basename' is optional. Needed only if the same viewset is registered more than once
# Official DRF docs on this option: http://www.django-rest-framework.org/api-guide/routers/
urlpatterns = [
path('', include(router.urls)),
path('', include(domains_router.urls)),
]

在我们这个项目中,可以这么写:

1
2
3
4
5
6
7
8
9
10
router = routers.DefaultRouter()
router.register('products', views.ProductViewSet)
router.register('collections', views.CollectionViewSet)

products_router = routers.NestedDefaultRouter(
router, 'products', lookup='products')
products_router.register('reviews', views.ReviewViewSet,
basename='product-reviews')
# 我们可以不用显式注册url,直接+就可以
urlpatterns = router.urls + products_router.urls

这里,我们设置的basename='product-reviews', 能提供的url模板如下

1
2
3
4
5
6
7
[<URLPattern '^products/(?P<products_pk>[^/.]+)/reviews/$' [name='product-reviews-list']>,
<URLPattern '^products/(?P<products_pk>[^/.]+)/reviews\.(?P<format>[a-z0-9]+)/?$' [name='product-reviews-list']>,
<URLPattern '^products/(?P<products_pk>[^/.]+)/reviews/(?P<pk>[^/.]+)/$' [name='product-reviews-detail']>,
<URLPattern '^products/(?P<products_pk>[^/.]+)/reviews/(?P<pk>[^/.]+)\.(?P<format>[a-z0-9]+)/?$' [name='product-reviews-detail']>,
# 下面两个是上层路由,不用管
<URLPattern '^$' [name='api-root']>,
<URLPattern '^\.(?P<format>[a-z0-9]+)/?$' [name='api-root']>]

结果如下,我们可以在http://localhost:9000/store/products/<id:pk>/reviews/中看到reviews信息,并且能创建新的review

但这个程序存在bug,因为url中,是先检索product,再在这个product里面创建review,所以我们在创建的时候根本不需要填写product字段,这个字段的值应该有url中的参数来决定。但是,如果在serializer里面直接把product给去掉,是不可以的,因为product_id 这个字段名是必须的。对此,我们可以这样修改:

我们可以在 View中,利用get_serializer_context函数将需要的参数(这里是product_pk) 传递给serializer:

1
2
3
4
5
6
class ReviewViewSet(ModelViewSet):
queryset = Review.objects.all()
serializer_class = ReviewSerializer

def get_serializer_context(self):
return {'product_id': self.kwargs['products_id']}

然后,在ReviewSerializer中,我们要重写create函数。我们从view那里传过来的context中获得了product_id,并获得了表单提交上来的validated_data,那么现在就集齐了创建一个新Review实例的所有信息。

我们可以调用objects.create方法,将这些信息传入,如下所示

1
2
3
4
5
6
7
8
class ReviewSerializer(serializers.ModelSerializer):
class Meta:
model = Review
fields = ['id', 'date', 'name', 'description']

def create(self,validated_data):
product_id = self.context['product_id']
Review.objects.create(product_id=product_id,**validated_data)

但是,还是有一个bug,就是现在不管是哪个product,在products/<product_id>/reviews/界面都可以获取所有的review信息:比如上面这个gif,我在访问product/3/的时候能看到对product1的评论。
因此,我们要对其做一个筛选,只选择该products的Review信息

Filtering

接下来,我们就来学习如何根据url中的参数来筛选信息:

比如说,我想筛选collection4中所有的product,可以这样来写http://localhost:9000/store/products/?collection_id=4

现在我们来实现这个功能。我们不能直接在queryset中调用.filter(),因为这时候参数没办法获取。因此我们要调用get_queryset来返回客制化的 queryset

首先,我们要确认默认queryset仍然是Product.objects.all(),因为当没有/?collection_id=<id>这个参数的时候,仍然需要返回所有的product。

然后,我们要从url的参数中获取collection_id的值。url中的参数都在quert_params中,也就是self.request.query_params.get('collection_id')。注意了这里我们需要用get函数,而不能直接写query_params['collection_id'] 后者是默认参数一定存在的,如果不存在就会报错;而前者当参数不存在的时候,collection_id的值就为空(None)

如果collection_id非空,那么我们就在默认的queryset上调用filter 筛选出目标对象,最后返回queryset

1
2
3
4
5
6
7
8
9
10
11
#views.py
class ProductViewSet(ModelViewSet):
serializer_class = ProductSerializer
def get_queryset(self):
queryset = Product.objects.all()
collection_id = self.request.query_params.get('collection_id')
if collection_id is not None:
queryset = queryset.filter(collection_id=collection_id)

return queryset
#...

但仅仅修改View是不够的,系统会报错。原因是在ProductViewSet中我们删去了queryset属性,变成了get_queryset函数,而router是根据这个属性默认生成basename的。

1
AssertionError: `basename` argument not specified, and could not automatically determine the name from the viewset, as it does not have a `.queryset` attribute.

因此,我们对url.py也要做一定的修改,即给ProductViewSet注册的路由组一个basename参数(可以使products也可以是product)

1
2
3
4
#urls.py
router = routers.DefaultRouter()
router.register('products', views.ProductViewSet, basename='products')
#...

效果如下图所示:

Generic Filtering

上面我们只筛选了一个collection字段,那么如果我们想筛选另外一个字段,是不是得改很多代码呢?这也太hard code了,因此我们现在来学习 Generic Filter

我们需要用到Django Filter 来帮助我们完成这个功能:pipenv install django-filter; 下载完后,记得在setting中注册:

1
2
3
4
INSTALLED_APPS = [
'django_filters',
#...
]

然后,我们在view中使用这个包,我们就不需要再调用get_queryset函数了,直接使用queryset = Product.objects.all()即可。

接着,我们引入filter_backends = [DjangoFilterBackend] ,并设置可供筛选的参数。这里除了collection_id还有unit_price可以选

1
2
3
4
5
6
7
from django_filters.rest_framework import DjangoFilterBackend

class ProductViewSet(ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['collection_id', 'unit_price']

结果如下所示:

但这样筛选是不符合常理的,对于unit_price的筛选应该是一个区间而不是一个特定的值。因此,我们需要对其进行改进。在 django-filter文档中我们可以获得答案

我们新建一个filter.py的文件,里面用来写自定义的筛选器:

  • 首先我们导入 FilterSet类,然后新建一个ProductFilter来继承这个类,
  • 在这个类中,我们需要确定一些元数据: 确定该筛选器应用于哪个model,以及可供筛选的字段
    • collection_id 这个字段不用改,因此我们就写['exact']
    • unit_price 这个字段需要填写区间范围,因此可以这么写:['gt', 'lt'] ,代表价格高于某个值或者低于某个值
1
2
3
4
5
6
7
8
9
10
11
from django_filters.rest_framework import FilterSet

from .models import Product

class ProductFilter(FilterSet):
class Meta:
model = Product
fields = {
'collection_id': ['exact'],
'unit_price': ['gt', 'lt']
}

结果如下,非常方便,调试起来很快:

Searching

那么,对于title,description这样的字符串字段,我们就没有办法做一个筛选了,需要做查找。方法也很简单:

首先,我们可以使用rest_framework.filters中的SearchFilter模块

然后,在filter_backends中添加SearchFilter

最后,确定search_field属性即可,除了model本身的字段之外,还可以查找外键model的字段。查找不区分大小写,支持模糊查找,中间用逗号隔开即可,简直太方便了

1
2
3
4
5
6
7
from rest_framework.filters import SearchFilter
class ProductViewSet(ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_class = ProductFilter
search_fields = ['title', 'description', 'collection__title']

Sorting

现在,我们还可以用filters中的OrderingFilter类帮助我们对 model进行筛选:

首先,我们导入OrderingFilter

然后,把OrderingFilter 加到 filter_backends中去

最后,确定能排序的字段,即ordering_fields

1
2
3
4
5
6
7
8
9
from rest_framework.filters import SearchFilter,OrderingFilter

class ProductViewSet(ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = ProductFilter
search_fields = ['title', 'description', 'collection__title']
ordering_fields = ['unit_price', 'last_update']

结果如下所示。我们发现,当对unit_price升序排列的时候,url为/products/?ordering=unit_price 而降序排列的时候,url为/products/?ordering=-unit_price

而且,对于serializer没有提供的字段 last_update,也可以进行排序

Pagination

方法1

现在我们来讲分页,首先,引入pagination模块

然后设置pagination_class属性为PageNumberPagination

1
2
3
4
5
from rest_framework.pagination import PageNumberPagination
class ProductViewSet(ModelViewSet):
#...
pagination_class = PageNumberPagination
#...

接着,我们在setting中确认每一页的数量:

1
2
3
4
REST_FRAMEWORK = {
'COERCE_DECIMAL_TO_STRING': False,
'PAGE_SIZE': 10,
}

如下图所示,现在已经可以实现分页了,每一页中还会告诉你总数、下一页的url和前一页的url,分页结果放在 results 数组当中:

如果我们想设置全部使用 Django Restful Framework的model都采用分页的方式呈现,可以这样设置:

1
2
3
4
5
REST_FRAMEWORK = {
'COERCE_DECIMAL_TO_STRING': False,
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
}

这样一来,我甚至不需要在view中规定pagination_class = PageNumberPagination也可以进行分页

方法2

但是如果我们不想对所有model进行分页,但是在settings中规定了PAGE_SIZE属性的话,系统会给一个warning,意思是如果规定了PAGE_SIZE,你最好规定一下DEFAULT_PAGE_CLASS

为了规避这个warning,我们可以自己实现:创建一个 pagination.py 文件:

1
2
3
4
# pagination.py 
from rest_framework.pagination import PageNumberPagination
class DefaultPagination(PageNumberPagination):
page_size = 10

然后,在view中,将原本的PageNumberPagination改为DefaultPagination 即可

1
2
3
4
class ProductViewSet(ModelViewSet):
#...
pagination_class = DefaultPagination
#...

Designing and Implementing a Shopping Cart API

Designing the API

现在我们来设计一个购物车的API,主要包含以下几个功能:创建购物车、访问购物车的内容、删除购物车、往购物车中添加物品,往购物车更新物品的数量,往购物车删除物品

  • Create a Cart 对于路人,不一定要登录,也可以创建一个购物车
METHOD url request Response
POST /carts/ {} cart
  • Getting a Cart
METHOD url request Response
GET /carts/:id {} cart
  • Deleting a Cart
METHOD url request Response
DELETE /carts/:id {} {}
  • Adding an Item
METHOD url request Response
POST /carts/:id/items {prodId,qty} item
  • Updating an Item 这里,我们要更新的话,只需要修改购物车内物品的数量,因此,我们使用PATCH方法
METHOD url request Response
PATCH /carts/:id/items/:id {qty} {qty}
  • Deleting an item
METHOD url request Response
DELETE /carts/:id/items/:id {} {}

我们创建两个类,一个来负责购物车的API,称为CartViewSet,一个来负责购物车中物品的API,称为CartItemViewSet

Revisiting the Data Model

首先,我们要知道,购物车是一个对用户比较较敏感的信息,不能轻易地被其他人获取到这个信息,因此我们需要使用到GUID,即Globally Unique Identifier。是一种由算法生成的二进制长度为128位的数字标识符

首先我们引入uuid4,然后重写Cart Model的 id为UUIDField,默认值为uuid的引用。注意,这里不能使用default = uuid64() 因为这样会导致在创建migration的时候就生成了一个UUID的值 ,进而导致所有的cart都会使用同一个UUID。

对于CartItem,我们也需要进行修改,首先,可以给ForeignKey添加一个related_name属性
其次,我们需要给数据库增加一个限制:我们不允许在同一个cart中,有多个products。如果用户往购物车中添加相同的product,只改变 quantity的值。要在数据库里添加这个数据,我们可以设置元数据中的unique_together属性

这个元数据是非常重要的一个!它等同于数据库的联合约束!

举个例子,假设有一张用户表,保存有用户的姓名、出生日期、性别和籍贯等等信息。要求是所有的用户唯一不重复,可现在有好几个叫“张伟”的,如何区别它们呢?(不要和我说主键唯一,这里讨论的不是这个问题)

我们可以设置不能有两个用户在同一个地方同一时刻出生并且都叫“张伟”,使用这种联合约束,保证数据库能不能重复添加用户(也不要和我谈小概率问题)。在模型中用unique_together,也就是联合唯一!

1
unique_together = [['name', 'birth_day', 'address']]

在这里,我们只要设置两个值即可:unique_together = [['cart', 'product']]

1
2
3
4
5
6
7
8
9
10
11
12
13
class Cart(models.Model):
id = models.UUIDField(primary_key=True, default=uuid4)
created_at = models.DateTimeField(auto_now_add=True)


class CartItem(models.Model):
cart = models.ForeignKey(
Cart, on_delete=models.CASCADE, related_name='items') # cartitem_set
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveSmallIntegerField()

class Meta:
unique_together = [['cart', 'product']]

migrate后,两个数据表如上图所示,我们看到了cart id从原来的bigint变成了char(32); 并且cart_id和product_id之间加上了一个约束

Creating a Cart

创建一个API的流程如下:

Serializer

1
2
3
4
class CartSerializer(serializers.ModelSerializer):
class Meta:
model = Cart
fields = ['id']

View

在这里我们并不希望CartViewSet去继承ModelViewSet这个类,因为对于cart来说,我只希望创建一个购物车,获得一个购物车,删除一个购物车。 我并不想用GET方法请求/carts/来获得所有的购物车信息。否则,其他人的购物车我们也看得到。

基于此,我们要客制化 ViewSets。在这一part,我先使用CreateModelMixin:

1
2
3
class CartViewSet(CreateModelMixin, GenericViewSet):
queryset = Cart.objects.all()
serializer_class = CartSerializer

Router

注册Router

1
router.register('collections', views.CollectionViewSet)

结果如下:

我们发现,id仍然是可以由client设置的,这和我们将其设为默认自动填充的UUID不符,由此,我们可以在serializer中将id设为只读:

1
2
3
4
5
6
class CartSerializer(serializers.ModelSerializer):
id = serializers.UUIDField(read_only=True)

class Meta:
model = Cart
fields = ['id']

这样就可以直接创建了,我们看到在这个/carts/下,是没有办法通过GET来获取cart信息的,但是可以通过POST创建一个cart:

Getting a Cart

在这个购物车里,我希望列出购物车的id,购物车中物品信息,以及整个购物车的商品总价值。首先,为了能获取到特定一个购物车的信息,我们需要使用RetrieveModelMixin

CartItem

为此,我们需要创建一个CartItemSerializer 来显示商品的详细信息,在这个Serializer中,我想展示的信息有:这个Item的id,这个商品的信息,购买数量,以及这件商品的总价值

但我们又不想展示这个商品的所有信息,因此我们还需要创建一个SimpleProductSerializer专门为cartitem展示信息。我们想展示在购物车中的商品信息时:商品id,商品名称,商品单价

然后,我们要计算这件cartitem的总金额,计算公式是:这件商品的数量乘以这件商品的价格。需要用到serializers.SerializerMethodField()这个方法。比如字段名是total_price,那么就需要创建一个get_total_price来计算这个字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SimpleProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'title', 'unit_price']


class CartItemSerializer(serializers.ModelSerializer):
product = SimpleProductSerializer()
total_price = serializers.SerializerMethodField()

def get_total_price(self, cart_item: CartItem):
return CartItem.quantity*CartItem.product.unit_price

class Meta:
model = CartItem
fields = ['id', 'product', 'quantity', 'total_price']

最后,我们要计算购物车的总金额,同样需要用到serializers.SerializerMethodField()方法。不过因为在这里items是个数组,因此我们需要用一些技巧来计算整个金额:

下面这行代码就是说,对于每个购物车里面的商品,我都计算出商品的数量和商品的价格,然后用sum()函数对其求和

1
sum([item.quantity*item.product.unit_price for item in cart.items.all()])

整体效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
class CartSerializer(serializers.ModelSerializer):
id = serializers.UUIDField(read_only=True)
# 我们希望items的信息在这里仅仅是呈现,之后会学怎么往里面添加商品
items = CartItemSerializer(many=True,read_only=True)
total_price = serializers.SerializerMethodField()

def get_total_price(self, cart):
return sum([item.quantity*item.product.unit_price for item in cart.items.all()])

class Meta:
model = Cart
fields = ['id', 'items','total_price']

最后,为了避免找到cart,再去一个一个找cartitem,增加数据库负担,我们可以用prefetch_related方法在查找Cart的时候就将里面的item都检索出来。注意了,prefetch_related是找反向关系的(父找子),select_related方法是查找正向关系的(子找父),不要用错!

1
2
3
class CartViewSet(CreateModelMixin, RetrieveModelMixin, GenericViewSet):
queryset = Cart.objects.prefetch_related('items').all()
serializer_class = CartSerializer

Deleting a Cart

要删除很简单,我们加一个RetrieveModelMixin就可以了

1
2
3
4
5
6
class CartViewSet(CreateModelMixin,
RetrieveModelMixin,
DestroyModelMixin,
GenericViewSet):
queryset = Cart.objects.prefetch_related('items').all()
serializer_class = CartSerializer

Getting Cart Items

现在,我们要把之前学的嵌套路由用上了,我们希望在输入/carts/<cart_id>/items 的时候,可以显示这个购物车里面所有的商品信息; 在输入/carts/<cart_id>/items/<item_id> 的时候可以显示购物车中某商品特定的信息

首先,我们要创建一个CartItemViewSet,因为对Cart Item,我们可以有获取list,获取detail,增加和删除的功能,因此,这里我们直接让其继承自ModelViewSet即可。注意,由于我们要用嵌套循环,父类cart的id是从request来的,因此我们要重写get_query函数,筛选出id为cart_pk的所有商品。

此外,由于我们想获取该item的商品信息,我们需要用selected_related将商品一并找出。否则会增加很多重复查询。

1
2
3
4
5
6
7
class CartItemViewSet(ModelViewSet):
serializer_class = CartItemSerializer

def get_queryset(self):
return CartItem.objects\
.filter(cart_id=self.kwargs['cart_pk'])\
.select_related('product')

然后,我们注册嵌套路由,模式和之前的 Product-Review是一样的

1
2
3
4
carts_router = routers.NestedDefaultRouter(router, 'carts', lookup='cart')
carts_router.register('items', views.CartItemViewSet, basename='cart-items')

urlpatterns = router.urls + products_router.urls+carts_router.urls

结果如下图所示:

Adding a Cart Item

现在,我们要实现给购物车添加商品的功能,现在的表单,我们不是不能添加,但是超级麻烦,如下所示:

事实上,我们往一个购物车里添加信息,只需要两个字段就行了:product_id 和 quantity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CartItemViewSet(ModelViewSet):

def get_serializer_class(self):
if self.request.method == 'POST':
return AddCartItemSerializer
return CartItemSerializer

def get_serializer_context(self):
return {'cart_id': self.kwargs['cart_pk']}

def get_queryset(self):
return CartItem.objects\
.filter(cart_id=self.kwargs['cart_pk'])\
.select_related('product')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class AddCartItemSerializer(serializers.ModelSerializer):
product_id = serializers.IntegerField()

def validate_product_id(self, value):
if not Product.objects.filter(pk=value).exists():
raise serializers.ValidationError('No Product with the given ID')
return value

def save(self, **kwargs):
cart_id = self.context['cart_id']
product_id = self.validated_data['product_id']
quantity = self.validated_data['quantity']
try:
cart_item = CartItem.objects.get(
cart_id=cart_id, product_id=product_id)
cart_item.quantity += quantity
cart_item.save()
except CartItem.DoesNotExist:
CartItem.objects.create(cart_id=cart_id, **self.validated_data)
return self.instance

class Meta:
model = CartItem
fields = ['id', 'product_id', 'quantity']

效果如下,如果product_id相同,那么数量就会在原来的基础上增加,并不会创建一个新的item

此外,我对quantity在model中加上了一个validator:

1
2
quantity = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1)])

这样,如果新增的product的数量为0,会报错:

Updating a Cart Item

现在,我们想更新 Cart Item的数量,即quantity:

我们首先定位到一个特定cart中的特定item,发现这个item里面字段太多了,我们只想让顾客修改quantity字段即可:

方法:

1
2
3
4
5
#serializer.py
class UpdateCartItemSerializer(serializers.ModelSerializer):
class Meta:
model = CartItem
fields = ['quantity']
1
2
3
4
5
6
7
8
9
#view.py
class CartItemViewSet(ModelViewSet):
http_method_names = ['get','post','patch','delete']
def get_serializer_class(self):
if self.request.method == 'POST':
return AddCartItemSerializer
if self.request.method == 'PATCH':
return UpdateCartItemSerializer
return CartItemSerializer

效果如下,我利用PATCH方法,成功地完成了修改quantity的值

Deleting a Cart Item

由于在上面我们已经确定了,CartItemViewSet中可行的请求方法:

1
http_method_names = ['get', 'post', 'patch', 'delete']

我们可以直接删除特定的item,效果如下:

Django Authentication System

Django Authentication System

在这一章我们来学习Django的内置认证系统。并将学习如何客制化User model,让其为我们的 项目服务

在INSTALLED_APPS中默认就有django.contrib.auth 应用,在这个app中有很多模型:User,Group,Permission等

1
2
3
4
5
INSTALLED_APPS = [
#...
'django.contrib.auth',
#...
]

在数据库中的auth_user表格里,我们也可以看到Django项目中的用户信息。里面有很多字段:密码、上次登录时间,是否是超级管理员,名字,邮箱,注册时间等。之后我们将介绍怎么客制化这张表格。

此外,我们还要了解Middleware, 下面是在这个项目中默认的中间件。在Django中,每当我们收到一个client发来的request的时候,request在经过view的时候,同时也在按照顺序将request一一经过下列中间件。每个中间件都可以往request中添加额外的信息或者直接返回一个Response(可能是发生错误了)。在这些中间件中我们看到有一个是用来处理用户信息的: django.contrib.auth.middleware.AuthenticationMiddleware, 这个中间件的功能,是用来读取request中用户的信息、并设置其属性的。

1
2
3
4
5
6
7
8
9
10
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

Customizing the User Model

现在我们来介绍两种客制化User Model的方法:

Extend User

第一种方法,是我新创建一个类(不妨叫做AppUser),让其继承自 User类. 使用这种方法,我们新加入的列就会显示在auth_user这张表格中。因此,如果要存储与身份验证(Authentication)相关的属性,建议使用这种方法

Create Profile

第二种方法,是我创建一个Profile类,并让其建立和User的一对一联系。使用这种方法,将会有另一张表格,通过外键和auth_user相连接。因此,如果要存储与身份验证无关的属性(用户个人信息,比如用户生日、地址等),建议使用这种方法。

此外,在不同的app中,我们也可以设计不同的Profile Model——在sales app中,我们可以用Customer来表示Profile,在hr app中,我们可以用Employee来表示Profile,在training app中,我们可以使用Student来表示Profile

由于第二种方法使用的场景更多,我们常常使用第二种方法作为客制化 User model的方法

Extending the User Model

我们观察到,在auth_user这张表格中,对于email并没有严格的限制,但是呢,事实上不会存在多个用户共同使用一个email的情况,因此,我们需要新建一张自己的user表格,在里面加上对email的唯一性限制

首先,我们应该把我们的 User 创建在那里,肯定不能是store,因为这时对整个项目来说的,不能是特定的app

因此我们可以新创建一个名为 core的app。然后,将其在 INSTALLED_APP中注册。

接着我们在 core>models.py中创建 User, 这个User需要继承自django.contrib.auth.models中的抽象类AbstractUser

1
2
3
4
5
6
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.

class User(AbstractUser):
email = models.EmailField(unique=True)

然后,我们需要在setting中确定AUTH_USER_MODEL属性,让其等于 core中的User

1
AUTH_USER_MODEL = 'core.User'

但这样一来,程序会起一个冲突:

原来,我们在第一部分写的Like app中,用到了django.contrib.auth.models中的User类,但是现在,我们将AUTH_USER_MODEL设置成了我们自己写的类,由此引发了冲突。为了修改这个冲突,我们可以对LikedItem Model进行修改:这里,根据系统的提示,我们隐式的确定USER_MODEL

1
2
3
4
from django.conf import settings
#...
class LikedItem(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

现在,冲突又出现了:由于我们的core还没有migrate,因此系统无法启动

1
2
raise ValueError("Dependency on app with no migrations: %s" % key[0])
ValueError: Dependency on app with no migrations: core

在makemigration之后,要进行migrate操作的时候,又发生了如下错误:

1
2
raise InconsistentMigrationHistory(
django.db.migrations.exceptions.InconsistentMigrationHistory: Migration admin.0001_initial is applied before its dependency core.0001_initial on database 'default'.

这个意思是说,我们在项目中期却打算修改原本的User模型,这一般是不被允许的,这涉及到数据库的底层设计。因此,我们需要重启数据库,将原有的库删除.然后重新migrate

1
2
DROP DATABASE  storefront2;
CREATE DATABASE storefront2;

所以,在今后我们最好在一个项目的一开始就创建自己的User类,可以这样写:

1
2
3
4
5
6
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.

class User(AbstractUser):
pass

现在,让我们重新创建一个superuser:python manage.py createsuperuser

进入admin页面后,我们发现,admin界面中,只有Groups page,没有Users page:

我们可以再 core>admin.py中作如下修改

1
2
3
4
5
6
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User
#...
@admin.register(User)
class UserAdmin(BaseUserAdmin):
pass

可以看到,现在有了用户界面,可以看到已经注册的用户(现在就我一个)

我们来到新建用户画面,发现必填字段只有下面这些

这是因为,我们继承的BaseUserAdmin类中,有一个属性是add_fieldsets, 里面规定的字段不包含email等,我们需要拿出来重写:在这里,我在原来的基础上加上了email、first_name,last_name

1
2
3
4
5
6
7
8
9
@admin.register(User)
class UserAdmin(BaseUserAdmin):
# pass
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'password1', 'password2', 'email', 'first_name', 'last_name'),
}), #这个逗号一定要加,否则会出bug
)

刷新后结果如下,我们发现,email现在已经成了必填项,而且有唯一性,First name和Last name是选填项

我们可以看到创建新用户后的样子如下:

与此同时,我们发现数据库中存放用户的表格从auth_user变成了core_user,里面保存了相关信息:

小结

现在我们来小结一下这种方法的基本步骤:

  1. 创建一个专门存放核心信息的app(最好叫core之类的)
  2. 在这个app中创建继承自AbstractUser的 User Model,里面确定要修改的字段
  3. 在settings.py 设置AUTH_USER_MODEL 属性
  4. 将之前引用Django.contrib.auth.models.User 的app修改为 settings.AUTH_USER_MODEL

Creating user Profiles

现在我们来学习第二种方法,之前,在store>model.py中,我们写了一个Customer Model,里面有名字、手机号、生日、会员等级等信息。我们要做的就是在此基础上增加一个和core.User的一对一联系.此外,模型中的first_name,last_name,email 都已经在 core.User中实现了,我们现在将这些冗余信息删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Customer(models.Model):
MEMBERSHIP_BRONZE = 'B'
MEMBERSHIP_SILVER = 'S'
MEMBERSHIP_GOLD = 'G'

MEMBERSHIP_CHOICES = [
(MEMBERSHIP_BRONZE, 'Bronze'),
(MEMBERSHIP_SILVER, 'Silver'),
(MEMBERSHIP_GOLD, 'Gold'),
]

phone = models.CharField(max_length=255)
birth_date = models.DateField(null=True, blank=True)
membership = models.CharField(
max_length=1, choices=MEMBERSHIP_CHOICES, default=MEMBERSHIP_BRONZE)
user = models.OneToOneField(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

接着,由于我们在__str__ 中返回的是f'{self.first_name} {self.last_name}' 这个字符串,在class Meta也规定按照['first_name','last_name']来排序,由此,我们需要对其进行修改:

1
2
3
4
5
def __str__(self):
return f'{self.user.first_name} {self.user.last_name}'

class Meta:
ordering = ['user__first_name', 'user__last_name']

接下来系统给我们报错:

1
<class 'store.admin.CustomerAdmin'>: (admin.E033) The value of 'ordering[0]' refers to 'first_name', which is not a field of 'store.Customer'.

原来,在第一章的admin模块中,我们在 store>admin中创建了 CustomerAdmin类,里面引用了 first_name.last_name,现在这些属性已经不属于 Customer了,我们要对其进行修改:

对于 ordering 属性,我们可以直接用user__first_nameuser__last_name 但是对于list_display属性,这种语法并不适用,所以我们保持原样,转向去 Customer Model去创建两个同名函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#store>models.py
class Customer(models.Model):
#...
def __str__(self):
return f'{self.user.first_name} {self.user.last_name}'

def first_name(self):
return self.user.first_name

def last_name(self):
return self.user.last_name
#store>admin.py
@admin.register(models.Customer)
class CustomerAdmin(admin.ModelAdmin):
#...
list_display = ['first_name', 'last_name', 'membership', 'orders']
ordering = ['user__first_name', 'user__last_name']

这样bug就完美解除了,现在我们来更新数据库。在 Customer界面,我们可以为已经存在的用户创建Profile:

我们为admin和JohnSmith分别创建一个profile,结果如下:

我们还可以给User添加排序:需要用@admin.display()修饰:

1
2
3
4
5
6
7
8
9
10
# store>model.py
from django.contrib import admin
class Customer(models.Model):
@admin.display(ordering='user__first_name')
def first_name(self):
return self.user.first_name

@admin.display(ordering='user__last_name')
def last_name(self):
return self.user.last_name

小结

要是用第二种方法,我们小结一下步骤:

  1. 创建Profile model(叫什么随意)
  2. 在这个model中,创建和 AUTH_USER_MODEL 的一对一联系

Groups and Permissions

Groups就是一些权限的集合,为了不给每一个用户都单独分配权限,我们可以将拥有相同权限的集合放到一个group中,这样就方便很多.

我们以superadmin登陆后,在 AUTHENTICATION AND AUTHORIZATION中点击 Groups可以新建一个组:在这里,我希望这个组内的成员拥有对customer (Profile)、Order和OrderItem的CRUD权利:

然后,我们可以在User界面,挑选一个用户,将它加到Customer Service组中,记得勾选 Staff status,这样他就可以登录到admin 后台了

我们退出admin,登录JasonBall后,可以看到,他只能对Customer和Order这两个Model进行修改了(OrderItem在Order中修改)。

Creating Custom Permissions

我们发现,有些权限是系统不能提供的,比如说我要取消订单,这是一种订单状态的修改,而不是删除订单。

要实现客制化,我们需要在Meta类中设置元数据 permission属性。这个属性是一个元组列表,每个元组代表特定的权限。元组中第一个元素是code name(需具备唯一性),第二个元素是对这个权限的描述

1
2
3
4
5
6
class Order(models.Model):
#...
class Meta:
permissions = [
('cancel_order','can_cancel_order')
]

那么关于这个权限是如何实现的,我们放在下面一节里细说

Securing APIs

在这一章,我们要对api进行安全认证

Token-based Authentication

对RESTful API,使用Token-base Authentication是一种基本操作. 基本流程如下:

  1. 新用户要使用我们的服务,需要发送request请求,里面有创建用户的信息
  2. server收到以后,为其创建一个账号
  3. 用户用刚注册的账号来访问接下来的api
    1. 如果后台认证成功(密码正确),那么就发送一个token
    2. 账号密码错误,返回一个error

token是什么我们就不用多说了。

Adding the Authentication Endpoints

虽然Django提供了用户认证系统,但是我们并没有为其注册url。现在我们要注册一系列url,以便让用户实现注册、登录等操作。

为了实现这层api,我们可以使用djoser 这个包。他提供给我们一系列Views:比如注册、登录、等等。在其官网中,我们可以找到一些教程

首先,我们pipenv install djoser 下载这个包,然后将其注册到 INSTALLED_APP中去

然后,我们将auth注册到urlpattern中:

1
2
3
4
5
urlpatterns = [
#...
path('auth/',include('djoser.urls'))
#...
]

djoser 要依赖与一个 Authentication Backend才能够实现,因为djoser只是一层api和view函数,我们需要确定项目的Auth engine,才能让djoser发挥其作用。

在Django中我们主要有两个 Auth Engines:

  • Token-based Authentication, 这是DRF中的认知引擎
  • JSON Web Token Authentication,这是一个独立的包,需要下载

那么两者有什么区别呢?

前者会使用一张独立的Token表格,来存放tokens,每次server收到一个请求后,都会去数据库中做验证,判断token是否合法。也就是每次请求,都需要访问数据库

后者则不需要依赖数据库,因为Token的架构不同,我们在server层就可以完成对用户的验证

在这里我们选择后者,因此,我们需要再下载一个包:pipenv install djangorestframework_simplejwt

然后,在settings.py中,我们要将项目的Auth Engine设为JWTAuthentication:

1
2
3
4
5
6
REST_FRAMEWORK = {
'COERCE_DECIMAL_TO_STRING': False,
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
}

接下来,我们要设置SIMPLE_JWT, 这项设置要求用户在请求头中需要加上JWT前缀

1
2
3
SIMPLE_JWT = {
'AUTH_HEADER_TYPES': ('JWT',),
}

按照教程,我们还需要在 storefront>urls.py中注册一个新的urlpattern:

1
2
3
4
5
6
urlpatterns = [
#...
path('auth/', include('djoser.urls')),
path('auth/', include('djoser.urls.jwt')),
#...
]

现在,djoser提供的endpoints就可以正常使用了:

  • /users/
  • /users/me/
  • /users/confirm/
  • /users/resend_activation/
  • /users/set_password/
  • /users/reset_password/
  • /users/reset_password_confirm/
  • /users/set_username/
  • /users/reset_username/
  • /users/reset_username_confirm/
  • /token/login/ (Token Based Authentication)
  • /token/logout/ (Token Based Authentication)
  • /jwt/create/ (JSON Web Token Authentication)
  • /jwt/refresh/ (JSON Web Token Authentication)
  • /jwt/verify/ (JSON Web Token Authentication)

比如说,我访问 http://localhost:9000/auth/users/

因为这些urls是不允许匿名用户访问的,我们必须有token才可以访问到信息。因此这里返回的状态码是401 Unauthorized。

Registering Users

接下来我们就来学怎么注册一个新用户,在这个endpoint,我们不仅有GET还有POST方法。因此是可以新建User的

如果我们的注册信息很简单:

1
2
3
4
5
{
"email": "user1@icloud.com",
"username": "user1",
"password": "1234"
}

我们看到是不能注册成功的

这是因为在settings.py中我们设置了一系列Validator: 有最短长度验证、简单密码验证、全数值密码验证等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]

当我们改进password后,就可以正常注册了

但我们有没有发现,这个POST函数并没有要求我们输入firstname和lastname(可以不填,但必须有),这是因为djoser默认提供的字段只有这些,因此我们需要客制化serializer

https://djoser.readthedocs.io/en/latest/settings.html?highlight=serializer#serializers 中,我们可以看到djoser提供的所有serializer,其中我们要重写的是user_create

我们将这个serializer写在core app中,首先,我们要导入djoser.serializers中的UserCreateSerializer
然后,我们要创建一个客制化类,让其继承自UserCreateSerializer
接着,我们要重写Meta 类中的fields属性,又不想重写Meta中所有的属性,所以我们让Meta也继承自原来类中的Meta类。
最后,我们确认fields属性中的字段,把first_name,last_name加上:

1
2
3
4
5
6
7
8
# core>serializers.py
from djoser.serializers import UserCreateSerializer as BaseUserCreateSerializer


class UserCreateSerializer(BaseUserCreateSerializer):
class Meta(BaseUserCreateSerializer.Meta):
fields = ['id', 'username', 'password',
'email', 'first_name', 'last_name']

此外,由于我们想用自己写的UserCreateSerializer,我们需要在settings中注明这一点:

1
2
3
4
5
6
# settings.py
DJOSER = {
'SERIALIZERS': {
'user_create': 'core.serializers.UserCreateSerializer'
}
}

最后结果如下所示,注意,first_name和last_name是选填的

Building the Profile API

但是,现在我们输入的都是core.user的字段,我们想在注册的时候添加birthday这种在profile中的字段,该怎么办?

我们当然可以在UserCreateSerializer中添加这个字段,但是这会让代码的耦合度变得很高。我们的程序设计理念是模块化。理想的状态是这样的:在前端,用户填了一系列表单,表单中的有些字段是属于core.user的,而有些字段则是属于store.customer的,为了将这些信息都保存下来,我们需要用户在提交时先访问 create user API,然后再发送一个Update Profile 请求访问Profile API,这样能让整个系统结构更加稳定。

由于Djoser并不提供/user/profile类似的api,我们需要自己实现:

1
2
3
4
5
6
7
8
9
# store>serializers.py

class CustomerSerializer(serializers.ModelSerializer):
user_id = serializers.IntegerField()
class Meta:
model = Customer
fields = ['id', 'user_id', 'phone', 'birth_date', 'membership']
# store>urls.py
router.register('customers',views.CustomerViewSet)

然后,在http://localhost:9000/store/customers/界面,我们就可以为特定的 User创建profile了:

Logging In

现在我们来讲用户认证。在Djoser中,下面两个endpoints是给 auth engine为DRF自带的Token Based Authentication使用的

  • /token/login/ (Token Based Authentication)
  • /token/logout/ (Token Based Authentication)

下面三个endpoints则是给 JSON Web Token Authentication使用的:

  • /jwt/create/ (JSON Web Token Authentication)
  • /jwt/refresh/ (JSON Web Token Authentication)
  • /jwt/verify/ (JSON Web Token Authentication)

其中/jwt/create/ 这个endpoint就是用来给用户登录的:如果我们输入的用户名和密码错误,如下所示

如果我们输入正确的用户名和密码,那么DJango会返回两个个Token :access token和refresh token。access token是short-lived(短暂的)token,用来访问需要安全认证的API的;当access token过期的时候,需要用到refresh token来获得一个新的access token

simplejwt的官网中,我们可以看到一系列设置,其中包括'ACCESS_TOKEN_LIFETIME','REFRESH_TOKEN_LIFETIME'也就是说,我们可以自己修改access token的寿命:

为了方便,我们这里就讲access token的有效时间也设为1天

1
2
3
4
5
#settings.py
SIMPLE_JWT = {
'AUTH_HEADER_TYPES': ('JWT',),
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
}

在一个前后端分离的系统中,当前段获得了一对token后,需要将它们存在浏览器里面。这里我们只能存放在一个文件里面,用来模拟前端的小型数据库,这样就算登录账户了。

那么怎么登出账户呢?很简单,就是将token从前端的小型数据库中移除即可。所以,在这个项目中,没有登出用户一说,因为这是前端干的事,和后端、数据库没有关系

Inspecting a JSON Web Token

现在,我们来学习解构JWT,需要用到这个网站: https://jwt.io/

我们在这个网站里,将我们刚才保存的token输进去,如下图所示,右图是这个token解构后的信息:

第一部分是header,里面有typ键,这里是JWT,因为我们采用了这种认证机制

第二部分是payload, 第一个键是token_type,这里是access token;第二个键是过期时间;第三个键是这个token的唯一认证机制。最后一个键是user_id

第三部分是Verify Signature,是根据前两部分生成的,your-256-bit-secret 会存在server里面。如果我们对Payload进行修改,那么Signature也会做相应的修改。因此,就算黑客获取了你的token,打算修改你的user_id,这时候signature也会改变,也就不是原来的那个token了。如果拿这个去访问后端,是会被拒绝的,因为后端也认出这个your-256-bit-secret 不是自己生成的。

Refreshing Tokens

那么如果Access token 过期了,没法访问Protected API, 而Refresh token 没有过期,会怎么样呢?我们需要用refresh_token向后端重新申请一个access token.

Djoser 为我们提供的这个api,是可以通过一个refresh token去获得一个新的access token的:

  • /jwt/refresh/ (JSON Web Token Authentication)

效果如下:

Getting the Current User

我们可以通过 users/me/ 这个endpoint 来获得当前的用户。

但是我们发现,如果直接访问http://localhost:9000/auth/users/me/,Django是不会给我们提供任何信息的,因为我们并没有在请求头里存放token信息。

因此,我们需要用到这个浏览器插件:Modheader, 利用这个插件,我们可以往请求头中添加信息。这里,我要添加一个Authorization的信息,信息内容是 JWT (Access Token) 。之所以这么写,是因为之前我们在settings中规定了认证的类型为JWT: 'AUTH_HEADER_TYPES': ('JWT',)

设置好后,我们刷新页面,就可以得到个人信息了。

但是现在,我们发现只能获得默认的三个字段:email,id,username,如果我想获得其他信息如first_name,last_name 应该怎么办?

还是老方法,修改默认的Serializer:我们找到 'current_user': 'djoser.serializers.UserSerializer', 发现这个UserSerializer 是负责当前用户信息的。因此我们来重写他

1
2
3
4
5
6
7
8
9
# core>serializers.py
from djoser.serializers import UserSerializer as BaseUserSerializer,\
UserCreateSerializer as BaseUserCreateSerializer
#...
class UserSerializer(BaseUserSerializer):

class Meta(BaseUserSerializer.Meta):
fields = ['id', 'username', 'password',
'email', 'first_name', 'last_name']

别忘了在 settings.py>Djoser中增加修改:

1
2
3
4
5
6
DJOSER = {
'SERIALIZERS': {
'user_create': 'core.serializers.UserCreateSerializer',
'current_user': 'core.serializers.UserSerializer',
}
}

注意了,Modheader会向所有的browser发送这个Authorization,因此可能会扰乱其它网站的登录(如果他们也用JWT的话),因此最好用完就删掉。

Getting Current Users Profile

上面我们获得了User的核心信息,现在如果我想获得User Profile,该怎么办?

我们希望,访问/store/customers/me 的时候,可以获取到用户的电话号码、出生日期,会员等级等信息。

首先,我们要导入 action decorator,用它来修饰/me 这个动作,这里,我们要设置detail的值,如果为False,那么说明这个action作用于list view,通过/store/customers/me即可访问;如果为True,说明这个action作用于detail view,需要通过/store/customers/<id>/me 来访问。在这里,由于只会筛选到一个customer的信息,因此我们设置为False即可

然后,由于token的Payload中有userid这个键,因此我们可以用request.user.id 找到特定的customer的profile信息

最后,我们将object传入Serializer并将serializer.data传回

1
2
3
4
5
6
7
8
9
10
11
12

from rest_framework.decorators import action

class CustomerViewSet(CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer

@action(detail=False)
def me(self, request):
customer = Customer.objects.get(user_id = request.user.id)
serializer = CustomerSerializer(customer)
return Response(serializer.data)

结果如下:

但是,现在我作为用户本人,只能查看个人的信息,并不能创建、修改更新个人信息。因为我们看到这边只允许GET方法,因此我们需要对CustomerView进行修改。

现在我们来捋一捋逻辑,我现在作为用户本人,已经登录了,现在在访问/store/customers/me/ 这个endpoint

  • 如果我有customer信息
    • 我可以GET来获取
    • 我可以PUT来更新
  • 如果我没有customer信息
    • 那么我可以创建相关信息
      • GET
      • PUT
  • 在使用PUT的时候,我不希望修改这个user的id,否则就乱了套了

根据这个逻辑,我们可以这样修改:

  • 首先规定action中的methods参数为GET和PUT
  • 然后,根据token中的id去数据库中获取customer对象,如果有则返回对象,如果没有,就创建一个实例
    • 如果方法是GET,那么就返回serializer.data
    • 如果方法是PUT,说明要更新,因此我们需要将customer实例和request.data传给CustomerSerializer,让它去验证信息准确性,如果没问题,就保存
  • 在CustomerSerializer中将user_id改为read_only=True, 这样一来,用户只有在访问/store/customers/me/ 的时候,才能创建或修改个人信息。在/store/customers/ 的时候,不能创建个人信息,因为此时是获取不到user_id的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# store>views.py
class CustomerViewSet(CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer

@action(detail=False, methods=['GET', 'PUT'])
def me(self, request):
(customer,created) = Customer.objects.get_or_create(user_id=request.user.id)
if request.method == 'GET':
serializer = CustomerSerializer(customer)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = CustomerSerializer(customer, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)

# stores>serialize.py
class CustomerSerializer(serializers.ModelSerializer):
user_id = serializers.IntegerField(read_only=True)

class Meta:
model = Customer
fields = ['id', 'user_id', 'phone', 'birth_date', 'membership']

注意,这里使用get_or_create 方法的时候,返回值是一个对象元组,我们需要用一个元组来接收,如果只赋值给customer会报如下错误:

1
2
3
Got AttributeError when attempting to get a value for field `user_id` on serializer `CustomerSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `tuple` instance.
Original exception text was: 'tuple' object has no attribute 'user_id'.

结果如下:

Applying Permissions

https://www.django-rest-framework.org/api-guide/permissions/ 中,我们可以看到各式各样的 Permission

比如 AllowAny,是对所有人都可以开放的权限,isAuthentication 是已经登陆的人的权限。我们也可以自己创建权限。

如果我们想给所有的viewset都加上 isAuthentication权限.可以直接修改settings.py

1
2
3
4
5
6
7
8
9
10
11
12
# settings.py

DJOSER = {
'SERIALIZERS': {
'user_create': 'core.serializers.UserCreateSerializer',
'current_user': 'core.serializers.UserSerializer',
},
'DEFAULT_PERMISSION_CLASSES':[
'rest_framework.permissions.IsAuthenticated'
]

}

如果我们希望有些 api可以由匿名用户访问,我们就不能采用这种一劳永逸的方法。可以在viewset里面设置

1
2
3
4
5
6
7
class CustomerViewSet(CreateModelMixin,
RetrieveModelMixin,
UpdateModelMixin,
GenericViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
permission_classes = [IsAuthenticated]

但是对于Generic View,好像用不了这个方法。我们需要在class上面加上一个decorator:

1
2
3
@permission_classes([IsAuthenticated])
class SearchBookView(ListAPIView):
#...

我们希望,在CustomerViewSet中,如果是未认证的用户,也可以查看他人的用户信息,但是只用认证后的用户才有权限更新Profile:

1
2
3
4
5
6
7
8
9
# store >views
class CustomerViewSet(CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
permission_classes = [IsAuthenticated]

def get_permissions(self):
if self.request.method == 'GET':
return [AllowAny]

如果我们不登录,那么只有GET方法

如果我们登陆了,那么可以调用PUT方法

Applying Custom Permissions

现在我们来自己创建Permissions类:

对于Products,我们希望只有admin可以修改有关product的信息,但是其他用户(不管有没有登录),都不能修改。但是DRF中写好的permission类只有IsAuthenticatedOrReadOnly,没有 IsAdminOrReadOnly,.因此我们要客制化一个Permission 类。

我们首先来看IsAuthenticated,其内部逻辑就是找到是否token里有user,并且这个user是否是认证的

1
2
3
4
5
6
class IsAuthenticated(BasePermission):
"""
Allows access only to authenticated users.
"""
def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated)

然后我们再来看看IsAdminUser的内部逻辑,就是找到是否有user,并且user是否是admin

1
2
3
4
5
6
7
class IsAdminUser(BasePermission):
"""
Allows access only to admin users.
"""

def has_permission(self, request, view):
return bool(request.user and request.user.is_staff)

接着我们就可以自己写permission了。逻辑如下:

  • 如果使用安全方法(GET,OPTION,HEAD)访问,那么就给这个权限
  • 如果用PUT,DELETE,POST方法来访问的话,那么就需要判断是否为admin了
1
2
3
4
5
6
7
8
from rest_framework import permissions


class IsAdminOrReadOnly(permissions.BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return bool(request.user and request.user.is_staff)

我们看到,如果我是staff(admin)我就可以新创建一个Product,除此之外只能是已读 的

Applying Model Permissions

现在我们只给admin操作Customer的权限。在之前我们创建的一个Group 叫做 Customer Service,如果我们想让拥有这个权限的用户进行操作,该怎么办?

我们可以使用DjangoModelPermissions

1
2
3
4
5
6
7
8
class CustomerViewSet(CreateModelMixin, 
RetrieveModelMixin,
UpdateModelMixin,
GenericViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
permission_classes = [DjangoModelPermissions]
#...

当使用这种权限方式,只有Groups中的用户才有权限对这个endpoint 进行操作.

比如说对于 JasonBall用户,他被加入到 customer service组了,那么现在他就拥有对customer的增删改查的权限:http://localhost:9000/store/customers/5/

当我们把他从这个组删除,那么我们看到,除了GET方法,它不能对customer进行PUT,POST和DELETE操作了

那么如果我甚至不想给他查看用户信息的权限,我们该怎么办?

看到DjangoObjectPermissions的源码,我们发现,在 perms_map这个列表中,只有POST、PUT、PATCH和DELETE是受保护的方法,而对GET方法没有权限方面的限制

1
2
3
4
5
6
7
8
9
perms_map = {
'GET': [],
'OPTIONS': [],
'HEAD': [],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}

因此,我们可以重写DjangoModelPermissions,在里面为GET方法加上权限设置。然后,将VIewSet中的permission_classes 修改成 我们自己写的 FullDjangoModelPermissions

1
2
3
4
5
6
7
8
9
10
11
# store>permissions
class FullDjangoModelPermissions(permissions.DjangoModelPermissions):
def __init__(self) -> None:
self.perms_map['GET'] = ['%(app_label)s.add_%(model_name)s']

# store>views
class CustomerViewSet(CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
permission_classes = [FullDjangoModelPermissions]
#...

结果如下,JasonBall现在连信息都看不了了

Applying Custom Model Permissions

现在我们客制化一个Model Permission,我们想实现的功能是,当用户拥有这个权限,那么他可以访问历史订单,否则看不见。因为我们还没有开始写订单API,所以我们先(意思意思)

首先,我们在customer下面定义 permission元组列表。migration后,我们就可以在/admin界面给用户添加这个权限了

1
2
3
4
5
6
7
class Customer(models.Model):
#...
class Meta:
ordering = ['user__first_name', 'user__last_name']
permissions = [
('view_history','Can view history')
]

接着,我们要为这个自定义的permission创建一个类,注意,这种自定义的类都需要继承自 BasePermission.在这个类中,我们要重写has_permission函数,里面返回的是一个判断用户是否拥有特定权限的布尔值。

这里,权限的命名方式是:app名字 . 自定义的权限名字

1
2
3
class ViewCustomerHistoryPermission(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.has_perm('store.view_history')

最后,我们把这个permission class加到我们想要的action里面去,注意,这里自定义的action需要self,request和pk三个参数。因为这个 action是为特定的customer服务的,因此需要用到pk参数

1
2
3
4
5
6
7
8
9
10
lass CustomerViewSet(	CreateModelMixin, 
RetrieveModelMixin,
UpdateModelMixin,
DestroyModelMixin,
GenericViewSet):
#...
@action(detail=True, permission_classes=ViewCustomerHistoryPermission)
def history(self, request, pk):
return Response({'OK': "History"})
#...

如果没有这个权限:

如果我们把这个权限加给JasonBall

注意点:

我们在给权限的时候永远不要一个一个加,就算只给一个权限,我们也要先创建一个Group,然后在把用户加到组里,这样不仅方便管理,而且能够轻易地通过组来筛选组内的用户。

如果一个一个加,项目大的话会非常难以管理

Designing and Building the Orders API

Designing the API

我们现在来创建一个下单的API:首先给个设计

METHOD url request Response
POST /orders/ {cartId} order
GET /orders/ {} order[]
GET /orders/1 {} order
PATCH /orders/1 —— ——
DELETE /orders/1 —— ——

Getting the Orders

首先我们在数据库里生成一些订单(因为创建订单的api我们还没写)

还是老套路,首先创建 serializer.我们最终想返回的是一个嵌套列表,形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[
{
"id": 2, # 订单号
"customer": 4, # 顾客id
"placed_at": "2021-12-14T16:07:53Z", # 创建时间
"payment_status": "P", # 订单状态
"items": [ # 订单物件
{
"id": 2,
"product": { # 物件信息
"id": 1,
"title": "Bread Ww Cluster",
"unit_price": 4.0
},
"unit_price": 10.0, #单价
"quantity": 10
},
{
"id": 3,
"product": {
"id": 2,
"title": "Island Oasis",
"unit_price": 84.64
},
"unit_price": 20.0,
"quantity": 20
}
]
}
]

我们看到这个嵌套对象还是蛮复杂的,最外层是order,第二层是orderitems,第三层是product

为了实现这个嵌套数组,我们可以这样来写serializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# serializer.py
class OrderItemSerializer(serializers.ModelSerializer):
product = SimpleProductSerializer()

class Meta:
model = OrderItem
fields = ['id', 'product', 'unit_price', 'quantity']

class OrderSerializer(serializers.ModelSerializer):

items = OrderItemSerializer(many=True)

class Meta:
model = Order
fields = ['id', 'customer', 'placed_at', 'payment_status', 'items']
# 注意,如果items = OrderItemSerializer(many=True),会报错,因为没有在model里面写related_name
# model.py
class OrderItem(models.Model):
order = models.ForeignKey(
Order, on_delete=models.PROTECT, related_name='items')
#...

最后,我们注册routers:

1
2
# urls.py
router.register('orders', views.OrderViewSet)

Applying Permissions

现在我们给 orders 添加权限,否则匿名用户也能对订单进行查看。而且每个人只能查看自己下的订单。因此,我们要对queryset进行一个重写:

如果用户是staff,那么就可以返回所有的订单

如果用户不是staff,那么,就筛选出该用户下的订单并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
# views.py
class OrderViewSet(ModelViewSet):
serializer_class = OrderSerializer

def get_queryset(self):
if self.request.user.is_staff:
return Order.objects.all()

(customer_id, created) = Customer.objects.only(
'id',).get_or_create(user_id=self.request.user.id)
Order.objects.filter(customer_id=customer_id)
# urls.py
router.register('orders', views.OrderViewSet, basename='orders')

Creating an Order

现在我们来理一下创建Order的逻辑。

首先,我们可以从 Token中获得user_id, 我们将其取出放入 context。然后,如果是POST方法,说明需要创建一个Order,因为创建Order需要同时创建item(这一部分暂未实现),内部实现逻辑更复杂,因此我们这里需要新建一个CreateOrderSerializer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class OrderViewSet(ModelViewSet):
permission_classes = [IsAuthenticated]

def get_serializer_context(self):
return {'user_id': self.request.user.id, }

def get_serializer_class(self):
if self.request.method == 'POST':
return CreateOrderSerializer
return OrderSerializer

def get_queryset(self):
if self.request.user.is_staff:
return Order.objects.all()

(customer_id, created) = Customer.objects.only(
'id',).get_or_create(user_id=self.request.user.id)
Order.objects.filter(customer_id=customer_id)

因为这个Serializer是要收入一个嵌套数组的(cart+cartItem),这不属于Model之一 ,因此我们这里使用Serializer,需要自己定义save函数。创建一个Order最基本的两个条件是购物车号码以及用户的id(当item为空的时候),因此这边我们根据id去找到customer,并用这个customer去创建Order

需要注意的是,cart_id 存放在 **validated_data里面,属于request.data中的内容;而user_id是view通过context(可以理解为小窗)传给Serializer的内容,不包含在validated_data里面

在调用save函数的时候,validated_data会自动传入,而context不会,因此我们要手动传入customer字段。

1
2
3
4
5
6
7
class CreateOrderSerializer(serializers.Serializer):
cart_id = serializers.UUIDField()

def save(self, **kwargs):
(customer, created) = Customer.objects.get_or_create(
user=self.context['user_id'],)
Order.objects.create(customer=customer)

Creating Order Items

上面所说的,只是创建一个Order对象,但是并没有创建订单中的物品信息。为此,我们需要在订单之后,再创建订单中的OrderItems对象

第一步,我们获取validated_data 中的cart_id信息。

第二步, 我们根据card_id找到隶属于这个购物车中的所有物品 cart_items

第三步,对于每个cart_item,我们都创建一个 order_item ;然后,作为数组成员放到 order_items 中去

第四步,调用 objects.bulk_create 方法,传入一个数组,这个方法就会为每一个数组中的成员创建一个对象,所以叫做 bulk,意思是大量创建

第五步,在创建订单后,购物车就没有用了,所以我们要删掉它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

class CreateOrderSerializer(serializers.Serializer):
with transaction.atomic():
cart_id = serializers.UUIDField()

def save(self, **kwargs):
(customer, created) = Customer.objects.get_or_create(
user=self.context['user_id'],)
order = Order.objects.create(customer=customer)
cart_id = self.validated_data['cart_id']
cart_items = CartItem.objects\
.select_related('product')\
.filter(cart_id=cart_id)\

order_items = [
OrderItem(
order=order,
product=item.product,
unit_price=item.product.unit_price,
quantity=item.quantity,
) for item in cart_items
]

OrderItem.objects.bulk_create(order_items)

Cart.objects.filter(pk=cart_id).delete()

需要注意的是,如果在订单创建一半的时候服务器崩掉了,怎么办?我们肯定需要回滚,因此我们可以将其视作一个数据库事务。为了实现事务功能,我们在代码前面加上 with transaction.atomic() 即可

结果如下,我们首先创建一个购物车。

然后,我们把购物车编号输入,用POST方法创建一个订单

然后,用GET方法就可以获得当前用户的所有订单。

Returning the Created Order

我们现在发现,当我们用一个购物车号码去创建一个订单的时候,返回的结果只是它的订单号,而我们希望的是直接返回这个创建好后的订单:

这是因为,在OrderViewSet 中,我们让其继承自 ModelViewSet,我们查看其源代码中的 CreateModelMixin类如下:

1
2
3
4
5
6
7
8
9
10
11
class CreateModelMixin:
"""
Create a model instance.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
#...

我们发现,首先它会获取到ViewSet中定义的serializer,然后创建一个对象。在Response中, 同样用这个serializer来构造返回信息。因此,在这个例子中,当使用 POST请求方法的时候,用到的是CreateOrderSerializer ,这个serializer只接收一个字段——购物车的号码,因此返回体中也只有这个购物车号这一个字段

为了修改这个bug,我们可以重写create函数,让其覆盖掉返回时候的那个serializer,从而返回刚刚创建的订单对象。 需要注意的是,由于CreateOrderSerializer 需要接受到user_id和请求体中的cart_id,所以我们要传入两组数据。

get_serializer_context 的作用就是给serializer带去“额外”(不在请求体之内的) 信息的,现在我们在重写create函数的时候直接传入了context,因此这个函数可以删去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

class OrderViewSet(ModelViewSet):
permission_classes = [IsAuthenticated]

def create(self, request, *args, **kwargs):
serializer = CreateOrderSerializer(data=request.data,
context={
'user_id': self.request.user.id, }
)
serializer.is_valid(raise_exception=True)
order = serializer.save()
serializer = OrderSerializer(order)
return Response(serializer.data)

def get_serializer_class(self):
if self.request.method == 'POST':
return CreateOrderSerializer
return OrderSerializer

# def get_serializer_context(self):
# return context={'user_id': self.request.user.id, })

def get_queryset(self):
if self.request.user.is_staff:
return Order.objects.all()

(customer_id, created) = Customer.objects.only(
'id',).get_or_create(user_id=self.request.user.id)
Order.objects.filter(customer_id=customer_id)

结果如下所示:

Data Validation

现在我们虽然已经实现了创建订单,返回订单,但是如果我们用一个cart_id重复创建订单的话,系统并不会给我们报错,而这是不被允许的——因为当订单创建后,购物车会被删除,此时cart_id 就不存在了。

我们需要排除的情况是:

  • 当前的UUID并不存在(购物车ID非法)
  • 当前的购物车中并没有任何物品

我们可以再Serializer Class中对特定的字段进行验证,格式为:函数 validate_{字段名} ,比如说我们对cart_id进行验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
class CreateOrderSerializer(serializers.Serializer):
with transaction.atomic():
cart_id = serializers.UUIDField()# 必须是在Serializer中的字段才可以被验证
def validate_cart_id(self, cart_id):
# 如果购物车号不存在,那么就会报错
if not Cart.objects.filter(pk=cart_id).exists():
raise serializers.ValidationError(
"No cart with id %s exists" % cart_id)
# 如果购物车中没有一件商品,也会报错
if CartItem.objects.filter(cart_id=cart_id).count() == 0:
raise serializers.ValidationError(
"The cart with id % s is EMPTY!" % cart_id)
return cart_id

结果如下

Revisiting the Permissions

如果我们想给不同的人不同的权限怎么办?在ViewSets里面怎么修改?

比如说,对于订单删除功能,我只想开放给Admin,对于一般的用户,是不能删除创建的订单的的,为此,我们可以重写get_permissions函数

1
2
3
4
5
6
7
class OrderViewSet(ModelViewSet):
http_method_names = ['get', 'patch','post' ,'delete', 'head', 'options']

def get_permissions(self):
if self.request.method in ['PATCH', 'DELETE']:
return [IsAdminUser()]
return [IsAuthenticated()]

Updating an Order

修改完权限后,就必须是管理员才能修改订单了,但是,在修改订单的时候,我们只希望修改订单的状态,其他字段我们希望它是只读的。但是,如果我们直接修改OrderSerializer的话,需要给除了payment_status 以外的字段都加上read_only,这是比较繁琐的,而且如果以后有新的字段进来,还是要修改OrderSerializer的。

因此,我们可以另外创建一个UpdateOrderSerializer,专门来更新Order

  • serializer.py
1
2
3
4
class UpdateOrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ['payment_status']
  • views.py> class OrderViewSet
1
2
3
4
5
6
def get_serializer_class(self):
if self.request.method == 'POST':
return CreateOrderSerializer
elif self.request.method == 'PATCH':
return UpdateCartItemSerializer
return OrderSerializer

Signals

之前,我们在OrderViewSetget_queryset 函数中,我们使用了 get_or_create 方法:

1
2
3
4
def get_queryset(self):
(customer_id, created) = Customer.objects.only(
'id',).get_or_create(user_id=self.request.user.id)
#...

因为在我们这个app中,先要创建一个User对象,然后, 再需要手动创建Customer,让其和User建立一对一的关系。因此存在忘记创建Customer的情况,使用了get_or_create之后,当检测到对应User的Customer未创建,就会自动创建一个Customer对象。

但这种方法毕竟是权宜之计,我们想要的理想状态是:当创建了User之后,会自动生成对应的Customer对象(默认),然后,用户可以去自行修改其Customer对象的信息。

由此,我们就需要用到Signal了。 顾名思义,信号允许应用程序在发生特定事件时得到通知 。比如说,我想在有人发表评论或对文章做出React时通知文章作者,我们就可以使用信号。只要指定的Model被保存下来(修改、更新),就会发送signal,接收器收到以后,会执行相应的动作

  • 信号系统包含以下三要素:

    • 发送者-信号的发出方
    • 信号-信号本身
    • 接收者-信号的接受者

Django内置了一整套信号,下面是一些比较常用的:

  • django.db.models.signals.pre_save & django.db.models.signals.post_save

在ORM模型的save()方法调用之前或之后发送信号

  • django.db.models.signals.pre_delete& django.db.models.signals.post_delete

在ORM模型或查询集的delete()方法调用之前或之后发送信号。

  • django.db.models.signals.m2m_changed

m2m_changed当多对多字段被修改时发送信号。

  • django.core.signals.request_started &django.core.signals.request_finished

当接收和关闭HTTP请求时发送信号。

监听信号

  • 接收器

首先我们要用到 @receiver这个修饰器,被它修饰的函数把这三者都集成到了一起。

在这个例子中,signalpost_save, 发送者是core.User , 如果是成功创建了的话,Customer就会创建出对应的对象

1
2
3
4
5
6
7
8
9
10
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Customer
from django.conf import settings


@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_customer_for_new_user(sender, **kwargs):
if kwargs['created']:
Customer.objects.create(user=kwargs['instance'])
  • 随后在apps/store/app.py的config类下重写ready方法,用来激活signals,因为这个app对core.User发出来的signal比较感兴趣。
1
2
3
4
5
6
class StoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'store'

def ready(self) -> None:
import store.signals

结果如下,我们看到,当创建了一个新用户的时候,对应的Customer也被创建了

Creating Custom Signals

此外,我们也可以自定义signals,比如说,当我创建了一个订单之后,我可以发送一个Signal。对这个signal感兴趣的app 就可以捕获它,实现提示用户等功能

由于情况变得复杂(这个app中既有系统信号又有自定义信号),我们把逻辑都移动到signals文件夹当中。其中,文件夹中的 handlers.py 用来存放 receiver的逻辑。__init__.py 用来存放新建的自定义信号。比如说,我想创建一个order_created信号,在订单创建时候发送

1
2
from django.dispatch import Signal
order_created = Signal()

然后,我们需要设计发送逻辑,我们希望在订单创建时候发送这个信号,就需要修改Serializer中的save函数:

1
2
3
4
5
6
7
8
class CreateOrderSerializer(serializers.Serializer):
with transaction.atomic():
#...
def save(self, **kwargs):
#...
# 用 signal.send
order_created.send_robust(self.__class__,order = order)
return order

信息发送有两个函数:send和send_robust

顾名思义,后者比较鲁棒,稳定性较强。

因为send函数,当其中一个receiver在接收信号的时候发生了错误,是不会影响其他receiver的,但send_robust 函数会捕获receiver发生的异常,并添加到返回的 responses数组中。

当store发出了这个signal之后,我们希望core app可以收到这个消息并做出一定的动作。那么,我们需要在core中也创建一个receiver。同样的,我们创建signals文件夹,里面再新建一个handlers.py

1
2
3
4
5
6
from store.signals import order_created
from django.dispatch import receiver
# 在这里我们没有指定sender,默认只要收到信号就执行
@receiver(order_created)
def on_order_created(sender, **kwargs):
print(kwargs['order'])
-------------本文结束,感谢您的阅读-------------