Django-高级教程-五-

136 阅读34分钟

Django 高级教程(五)

原文:Pro Django

协议:CC BY-NC-SA 4.0

十一、改进应用

Abstract

一旦一个站点有了一组正常工作的基本应用,下一步就是添加更多的高级功能来补充现有的行为。这有时可能只是简单地添加更多的应用,每个应用都为用户和员工提供新的功能。其他时候,有一些方法可以增强您现有的应用,让它们直接增加新的功能,而不需要一个独立的应用。

一旦一个站点有了一组正常工作的基本应用,下一步就是添加更多的高级功能来补充现有的行为。这有时可能只是简单地添加更多的应用,每个应用都为用户和员工提供新的功能。其他时候,有一些方法可以增强您现有的应用,让它们直接增加新的功能,而不需要一个独立的应用。

这些“元应用”或“子框架”的构建目标是使用已经提供的钩子轻松集成到现有的应用中。这本书举例说明了许多这样的挂钩,它们可以结合使用,以达到很好的效果。通常可以编写一个工具来执行许多任务,但只需要在现有的应用中添加一行代码。

添加 API

如今,大多数网站都有一个 API,允许程序员与网站的内容和功能进行交互,而不需要用户甚至网络浏览器。目标是使用结构化、可靠的技术,以简单的方式提供数据,以便代码处理您的数据。Django 基于类的视图提供了多种定制视图行为的方法,这对于生成 API 非常有用,无需自己编写大量新代码。

构建一个 API 需要做一些决定,但并不是所有的决定都需要马上做出。这里有一个小例子,展示了设计 API 时需要回答的一些常见问题。

  • 应该使用什么格式来传输数据?
  • 应该公开哪些类型的数据?
  • 应该如何组织这些数据?
  • 用户访问数据需要认证吗?
  • 用户可以自定义检索哪些数据吗?
  • 用户可以通过 API 修改数据吗?
  • 不同的 API 端点有单独的权限吗?

本章将在第十章中概述的房地产网站的背景下回答其中的一些问题。更好的是,您将看到一个简单框架的示例,它可以添加必要的 API 特性,而不需要直接向应用添加太多东西。可重用性是这些特性长期成功的关键,所以为这样的任务开发一个可配置的工具是理想的。

Caution

本章不会回答所有这些问题。特别是,本章中的例子没有使用任何认证或授权。Django 标准的基于会话的认证不太适合与 API 一起使用,但是您有几个选择。你可以简单地让你的 web 服务器处理认证 1 ,实现一个完整的 OAuth 提供者 2 或者使用其他对你的站点有意义的方法。那些决定和相关的说明超出了本书的范围。

序列化数据

一个很好的起点是为您的数据建立一种使用格式。如今,事实上的标准是 JSON,即 JavaScript 对象符号。它起源于一种在浏览器内部使用数据的简单方法,因为浏览器本身就理解 JavaScript 对象,但它后来成为一种简单、可读和可靠的跨平台数据格式。Python 有自己的工具可以直接读写它,许多其他编程语言也是如此。

事实上,Django 甚至有自己的工具,可以使用 JSON 编写模型实例并再次读取它们。因为它接受内存中的对象并将其转换为可以通过网络发送的字符序列,所以这个过程称为序列化。Django 的序列化工具位于django.core.serializers。使用get_serializer()函数获得 JSON 序列化器相当简单。

要获得序列化器,只需传入您想要使用的序列化方法的名称,您将获得一个可用于序列化对象的类。Django 支持三种序列化格式。

  • json —JavaScript 对象符号
  • xml—可扩展标记语言
  • YAML 不是一种标记语言,如果你安装了 PyYAML 3 就可以使用

>>> from django.core import serializers

>>> JSONSerializer = serializers.get_serializer('json')

无论您选择哪种格式,从这里返回的序列化程序类都以相同的方式工作。本章的其余部分将使用 JSON,但是您应该能够使用 XML 或 YAML,只需做相对较小的修改。

序列化器的用法与 Python 直接提供的更简单的json模块略有不同。Django 的序列化器没有使用dumps()loads()方法,而是分别提供了serialize()deserialize()方法来在 JSON 之间来回转换数据。此外,这些方法适用于适当的 Django 模型实例,而不仅仅是列表和字典这样的本地数据结构。现在,我们只看一下serialize()方法,从您的应用中获取数据,以便其他人可以使用它。

>>> serializer = JSONSerializer()

>>> from contacts.models import Contact

>>> serializer.serialize(Contact.objects.all())

'[{...}, {...}, {...}]'

如果您查看每个序列化联系人的实际输出,您会注意到一些您可能没有预料到的附加信息。Django 的序列化工具旨在产生可以反序列化的输出,而无需事先知道最初序列化了哪些模型和实例。为此,输出包括关于模型本身的一些信息,以及每个实例的主键。每个物体看起来都像这样。

{

"pk": 1

"model": "contacts.contact"

"fields": {

"user": 1

"address": "123 Main Street"

"city": "Los Angeles"

"state": "CA"

"zip_code": "90210"

"phone_number": "123-456-7890"

}

}

对于一个 API,您已经知道您正在使用什么模型和 ID,因为它们将被映射为 URL 的一部分。理想情况下,我们可以从中获取fields字典,然后来回发送。虽然没有记录,但是 Django 的序列化器确实提供了一种方法来覆盖它们的行为。首先要意识到get_serializer()的结果实际上是一个类,而不是一个实例对象。这允许您在实例化它之前创建一个子类,产生在子类上重写单个方法的所有好处。我们将把这段代码放在一个名为serializers.py的文件中,因为我们在本章后面还会添加一些文件,所以我们将在一个名为api的包中创建这段代码。最后,我们将能够把这个代码作为api.serializers导入。

from django.core import serializers

class QuerySetSerializer(serializers.get_serializer('json')):

pass

理解如何重写序列化程序需要了解一些序列化程序的工作原理。serialize()方法接受 QuerySet 或任何产生模型实例的 iterable。它遍历该对象,对于找到的每个对象,它遍历其字段,在每一步输出值。通过查看沿途调用的方法,可以很容易地看到整个过程。

  • start_serialization()—设置列表以保存将在流中输出的对象。
  • start_object(obj)—设置一个字典来收集单个对象的信息。
  • handle_field(obj, field)—每个字段被单独添加到对象的字典中。
  • handle_fk_field(obj, field)—使用单独的方法处理外键关系。
  • 与外键一样,多对多关系使用它们自己的方法来处理。
  • end_object(obj)—一旦所有字段都被处理,对象就有机会为其数据完成字典。这是将模型信息和主键值添加到字段中的地方,产生前面显示的输出。
  • get_dump_object(obj)—在 end_object()内部调用,负责定义每个被序列化的对象的实际结构。
  • end_serialization()—一旦所有的对象都被处理,这个方法就完成了流。

我们将应用的第一个定制是简化输出的结构。因为我们将使用 URL 来指示我们正在处理的对象的类型以及它的 ID,所以我们在序列化输出中需要的只是字段的集合。正如我们刚刚看到的过程中所暗示的,这是由get_dump_object()方法处理的。除了它所提供的对象之外,get_dump_object()还可以访问已经由字段处理方法组装的当前数据。该数据存储在 _current 属性中。

get_dump_object()的默认实现将字段数据包装在一个字典中,同时包装的还有对象的主键及其模型的路径和名称。我们在重写的方法中需要做的只是返回当前的字段数据。

class QuerySetSerializer(serializers.get_serializer('json')):

def get_dump_object(self, obj):

return self._current

有了这个简单的方法,您已经可以看到输出的改进。

{

"user"

"address": "123 Main Street"

"city": "Los Angeles"

"state": "CA"

"zip_code": "90210"

"phone_number": "123-456-7890"

}

输出单个对象

上一节末尾显示的例子只是列表中的一个条目,因为serialize()只对 iterables 进行操作。对于一个 API 来说,您可能更倾向于从一个细节视图中输出一个单独的对象,它不应该被包装在一个列表中。我们需要一个SingleObjectSerializer,而不是一个QuerySetSerializer。它仍然基于QuerySetSerializer,所以我们可以重用我们在那里添加的所有功能,但只需做足够的修改来处理单个对象。

首先要覆盖的是serialize()方法,因此它可以接受单个对象,但是为了重用所有的序列化行为,它需要用一个列表而不是单个对象来调用它的父方法。这是一个相当简单的覆盖。

class SingleObjectSerializer(QuerySetSerializer):

def serialize(self, obj, **options):

# Wrap the object in a list in order to use the standard serializer

return super(SingleObjectSerializer, self).serialize([obj], **options)

不幸的是,因为这将对象包装在一个列表中,并返回没有任何其他更改的输出,这实际上将仍然输出一个 JSON 字符串中的列表。为了只输出列表中一个对象的值,有必要去掉它周围的列表字符。结果是一个字符串,所以可以使用字符串的strip()方法删除这些字符。

我们可以将这段代码直接放在serialize()方法中,在调用父方法之后,但在最后返回字符串之前,但是 Django 的序列化器还有一个定制点,我们还没有研究过。一旦所有对象都被组装成序列化程序可以处理的格式,就会要求getvalue()方法返回完全序列化的输出。这是放置我们的定制的更好的地方,因为它符合原始方法的意图。将您的重写与原始实现的意图保持一致是一个很好的方法,可以确保将来的更改不会以意想不到的方式破坏您的代码。

class SingleObjectSerializer(QuerySetSerializer):

def serialize(self, obj, **options):

# Wrap the object in a list in order to use the standard serializer

return super(SingleObjectSerializer, self).serialize([obj], **options)

def getvalue(self):

# Strip off the outer list for just a single item

value = super(SingleObjectSerializer, self).getvalue()

return value.strip('[]\n')

这就是我们得到一个新的完全能够处理单个对象的序列化器所需要的。现在,您可以序列化一个对象本身,并获得该对象的输出作为回报。

>>> serializer = SingleObjectSerializer()

>>> from contacts.models import Contact

>>> serializer.serialize(Contact.objects.get(pk=1))

'{...}'

处理关系

查看当前的输出,您会注意到与联系人关联的用户仅由其主键表示。因为 Django 的序列化程序旨在一次重构一个模型,所以它们只包含每个对象所必需的数据。对于 API 来说,包含相关对象的一些细节更有用,最好是在嵌套字典中。

这部分输出由handle_fk_field()方法管理,默认实现只是输出数字 ID 值。我们可以覆盖它来提供对象的细节,但是这需要一个有趣的方法,因为有一个你可能没有想到的问题。Django 的序列化器包装了更多的通用序列化器,并添加了处理 Django 模型所必需的行为,但是这些添加的行为只适用于第一层数据。任何试图在一级 iterable 之外序列化的 Django 模型都会引发一个TypeError,表明它不是一个可序列化的对象。

乍一看,答案似乎是分别序列化相关对象,然后将它们附加到结构的其余部分。这方面的问题是序列化程序的输出是一个字符串。如果您将该输出附加到self._current字典,它将被序列化为一个单独的字符串,其中恰好包含另一个序列化的对象。

所以我们不能让对象不序列化,我们也不能完全序列化它。幸运的是,Django 通过另一个通常没有记录的序列化程序提供了两者之间的路径。'python'序列化器可以接受 Django 对象并生成原生 Python 列表和字典,而不是字符串。这些列表和字典可以在可序列化结构中的任何地方使用,并将产生您所期望的结果。

我们现在需要两个序列化器:一个用于输出整体结构,包括相关对象,另一个用于将字符串输出为 JSON 或您喜欢的任何其他格式。Python 序列化程序将完成大部分工作,我们可以通过将其与基本的 JSON 序列化程序相结合来构建一个更有用的序列化程序。下面是我们现有实现的样子。

class DataSerializer(serializers.get_serializer('python')):

def get_dump_object(self, obj):

return self._current

class QuerySetSerializer(DataSerializer, serializers.get_serializer('json')):

pass # Behavior is now inherited from DataSerializer

注意,get_dump_object()移到了新的DataSerializer中,因为它实际上与 JSON 输出没有任何关系。它的唯一目的是定义输出的结构,这适用于任何输出格式。那也是被覆盖的handle_fk_field()的归属。它有三项任务要完成。

  • 检索相关对象
  • 将其转换成原生 Python 结构
  • 将其添加到主对象的数据字典中

第一点和第三点很简单,但中间的一点看起来有点棘手。我们不能只调用self.serialize(),因为每个序列化器通过_current属性在整个过程中维护状态。我们需要实例化一个新的序列化器,但是我们也需要确保总是使用DataSerializer,而不是意外地获得 JSON 序列化器的一个实例。这是确保它输出原生 Python 对象而不是字符串的唯一方法。

class DataSerializer(serializers.get_serializer('python')):

def get_dump_object(self, obj):

return self._current

def handle_fk_field(self, obj, field):

# Include content from the related object

related_obj = getattr(obj, field.name)

value = DataSerializer().serialize([related_obj])

self._current[field.name] = value[0]

关于这个新方法,另一个值得注意的有趣的事情是,它在序列化相关对象之前将它包装在一个列表中。Django 的序列化器只对 iterable 进行操作,所以当处理单个对象时,您总是需要将它包装在 iterable 中,比如 list。在 Python 序列化器的情况下,输出也是一个列表,所以当把它赋回给self._current时,我们只需要从那个列表中获取第一项。

这样,典型联系人的序列化输出如下所示。

{

"user": {

"username": "admin"

"first_name": "Admin"

"last_name": "User"

"is_active": true

"is_superuser": true

"is_staff": true

"last_login": "2013-07-17T12:00:00.000Z"

"groups": []

"user_permissions": []

"password": "pbkdf2_sha256$10000$..."

"email": "admin@example.com"

"date_joined": "2012-12-04T17:46:00.000Z"

}

"address": "123 Main Street"

"city": "Los Angeles"

"state": "CA"

"zip_code": "90210"

"phone_number": "123-456-7890"

}

只需在一个方法中添加几行额外的代码,我们现在就有能力在其他方法中嵌套对象,而且因为它使用了DataSerializer,所以它们可以嵌套任意多的深度。但是在一个User对象中有很多信息,其中大部分并不需要包含在 API 中,并且其中的一些信息——比如密码散列——不应该被泄露。

控制输出字段

Django 再次适应了这种情况,这一次是通过向serialize()方法提供一个fields参数。只需传入一个字段名列表,只有那些字段会被handle_*_field()方法处理。例如,我们可以通过完全排除用户来简化我们的Contact模型的输出。

SingleObjectSerializer().serialize(Contact.objects.get(pk=1), fields=[

'phone_number'

'address'

'city'

'state'

'zip_code'

])

有了这些,输出肯定会变得更简单。

{

"address": "123 Main Street"

"city": "Los Angeles"

"state": "CA"

"zip_code": "90210"

"phone_number": "123-456-7890"

}

当然,从输出中删除用户并没有真正的帮助。我们真正需要做的是限制用户对象上的字段,而不是联系人。不幸的是,这是 Django 的序列化程序的意图稍微妨碍我们的另一种情况。就像我们必须拦截handle_fk_field()中用户对象的序列化一样,这也是我们必须为其对serialize()方法的调用提供fields参数的地方。但是每次我们想要指定字段时,都需要重写方法,并对我们想要处理的每个模型进行特殊处理。

一个更通用的解决方案是创建一个模型及其相关字段的注册表。然后,handle_fk_field()方法可以使用它找到的字段列表检查它接收到的每个对象的注册表,如果模型没有注册,则返回到标准序列化。设置注册中心非常简单,注册模型和字段列表组合的功能也是如此。

field_registry = {}

def serialize_fields(model, fields):

field_registry[model] = set(fields)

Note

字段可以作为任何 iterable 传入,但是在内部被显式地放入一个集合中。字段的顺序对于序列化过程来说无关紧要,集合可以更小更快,因为它不担心排序问题。此外,在这一章的后面,我们将能够利用集合的特定行为来使实现的某些部分更容易使用。

有了这个,我们就可以在任何需要为模型指定字段列表的地方导入它,并简单地用适当的映射调用它一次,稍后将需要使用它。使用这个注册表的代码实际上不会放入handle_fk_field()中,因为它只会应用于相关的对象,而不是最外层的对象本身。为了使使用模式更加一致,如果您可以在注册表中指定字段,并为您序列化的每个对象使用这些注册的字段,无论它是否是关系,这将是理想的。

为了支持这个更一般的用例,读取字段注册表的代码可以放在serialize()方法中。它是主对象和相关对象的主要入口点,因此是提供这种额外行为的好地方。

它需要做的第一个任务是确定正在使用的模型。因为可以传入 QuerySet 或标准 iterable,所以有两种方法可以获得传入的对象的模型。最直接的方法利用了 QuerySets 也是可迭代的这一事实,因此您总是可以只获得第一项。

class DataSerializer(serializers.get_serializer('python')):

def serialize(self, queryset, **options):

model = queryset[0].__class__

return super(DataSerializer, self).serialize(queryset, **options)

# Other methods previously described

这对于两种情况都适用,但是它将为每个传入的 QuerySet 进行额外的查询,因为获取第一条记录实际上是一种不同于迭代所有结果的操作。当然,对于非 QuerySet 输入,我们无论如何都需要这样做,但是 query set 有一些我们可以使用的额外信息。每个 QuerySet 上还有一个model属性,它已经包含了用于查询记录的模型,所以如果该属性存在,我们可以使用它来代替。

class DataSerializer(serializers.get_serializer('python')):

def serialize(self, queryset, **options):

if hasattr(queryset, 'model'):

model = queryset.model

else:

model = queryset[0].__class__

return super(DataSerializer, self).serialize(queryset, **options)

# Other methods previously described

因为这不是专门检查一个QuerySet对象,而只是检查是否存在model属性,如果你碰巧有其他产生模型的 iterable,只要 iterable 也有一个model属性,它也能正确工作。

有了模型,很容易在字段列表注册表中执行查找,但重要的是,我们只有在没有提供fields参数时才这样做。像这样的全局注册应该总是很容易在特定情况下被覆盖。如果提供了 fields 参数,它将出现在选项字典中,这也是我们放置从注册表中找到的字段列表的地方。因此,添加这部分流程也变得非常简单。

class DataSerializer(serializers.get_serializer('python')):

def serialize(self, queryset, **options):

if hasattr(queryset, 'model'):

model = queryset.model

else:

model = queryset[0].__class__

if options.get('fields') is None and model in field_registry:

options['fields'] = field_registry[model]

return super(DataSerializer, self).serialize(queryset, **options)

# Other methods previously described

Note

第二个if块可以一眼看上去很奇怪,但是不能简单的查看'fields'是否存在于options字典中。在某些情况下,可以显式传入一个None,其行为应该与参数被完全忽略的情况相同。考虑到这一点,我们使用get(),如果没有找到,则返回到None,然后我们手动检查None,以确保我们捕捉到所有正确的案例。特别是,提供一个空列表仍然应该覆盖任何已注册的字段,所以我们不能只使用布尔值not

现在serialize()将自动为它已经知道的任何模型注入一个字段列表,除非被定制的fields参数覆盖。这意味着在尝试序列化任何东西之前,你必须确保注册你的字段列表,但是正如你将在本章后面看到的,这在你的 URL 配置中很容易做到。还要注意的是,如果模型没有被分配一个字段列表,你自己也没有指定一个,那么这个更新将简单地不指定fields参数,回到我们之前看到的默认行为。

有了这些,我们可以轻松地为我们的ContactUser模型定制字段列表。我们不需要特别定制Contact,因为我们想要包含它的所有字段,但是出于演示的目的,这里也包含了它。此外,显式比隐式好,在这里指定一切有助于记录 API 的输出。

from api import serialize_fields

from contacts.models import Contact

from django.contrib.auth.models import User

serialize_fields(Contact, [

'phone_number'

'address'

'city'

'state'

'zip_code'

'user'

])

serialize_fields(User, [

'username'

'first_name'

'last_name'

'email'

])

有趣的是,这些字段列表大多与我们在第十章中创建的表单中已经提供的字段相匹配。表单还保留了一个自己的字段列表,所以我们实际上可以使用表单字段名称来重写这些注册,这有助于我们避免重复。这让表单主要负责哪些字段对最终用户有用,API 只是简单地跟着做。我们需要做的唯一改变是添加回Contact模型的user属性,因为在表单场景中处理方式不同。

from api import serialize_fields

from contacts.models import Contact

from django.contrib.auth.models import User

from contacts.forms import ContactEditorForm, UserEditorForm

serialize_fields(Contact, ContactEditorForm.base_fields.keys() + ['user'])

serialize_fields(User, UserEditorForm.base_fields.keys())

现在,当我们使用SingleObjectSerializer序列化一个Contact对象时,有了这些新的变化,它最终看起来像你所期望的那样。

{

"user": {

"username": "admin"

"first_name": "Admin"

"last_name": "User"

"email": "admin@example.com"

}

"address": "123 Main Street"

"city": "Los Angeles"

"state": "CA"

"zip_code": "90210"

"phone_number": "123-456-7890"

}

多对多关系

到目前为止,该 API 将输出您可能需要的几乎所有内容,缺少的主要特性是多对多关系。handle_fk_field()方法将只处理指向每个记录一个对象的简单外键,而多对多关系将产生一个相关对象的列表,所有这些对象都需要序列化并插入到 JSON 字符串中。

正如本章前面所概述的,序列化器也有一个handle_m2m_field()方法,我们可以用它来定制它们如何处理这些更复杂的关系。从技术上来说,这些关系已经得到了轻微的处理,但只是以与外键最初相同的方式。每个相关的对象将仅仅产生它的主键值,其他什么都没有。为了从这些关系中获得更多信息,我们需要应用一些与外键相同的步骤。

外键处理的第一个变化是引用相关对象的属性不是 object 或 QuerySet 本身;这是一个 QuerySet 管理器。这意味着它本身是不可迭代的,因此不能被直接序列化,所以我们必须调用它的all()方法来获得一个 QuerySet。然后,我们可以直接通过标准的serialize()方法传递它,而不是将它包装在一个列表中。

class DataSerializer(serializers.get_serializer('python')):

# Other methods previously described

def handle_m2m_field(self, obj, field):

# Include content from all related objects

related_objs = getattr(obj, field.name).all()

values = DataSerializer().serialize(related_objs)

self._current[field.name] = values

准备好之后,如果我们将'groups'字段添加到User对象的注册表中,联系人看起来会是什么样子。

{

"user": {

"username": "admin"

"first_name": "Admin"

"last_name": "User"

"email": "admin@example.com"

"groups": [

{

"name": "Agents"

"permission_set": [...]

}

{

"name": "Buyers"

"permission_set": [...]

}

{

"name": "Sellers"

"permission_set": [...]

}

]

}

"address": "123 Main Street"

"city": "Los Angeles"

"state": "CA"

"zip_code": "90210"

"phone_number": "123-456-7890"

}

当然,在这种情况下,权限没有多大意义,所以您可能希望从User对象的字段列表中删除这些权限,但是除此之外,这看起来是另一个非常简单的解决方案,可以帮助我们继续前进。不幸的是,Django 中的多对多关系还有一个特点,它让事情变得更加复杂。

当指定多对多关系时,可以选择指定一个“直通”模型,该模型可以包含有关该关系的一些附加信息。这些信息并不直接附属于任何一个模型,而是两者之间关系的一部分。我们刚刚为handle_m2m_field()应用的简单方法完全忽略了这个特性,所以我们的输出中不会包含任何额外的信息。

记得从第十章的中,我们的Property模型通过多对多的关系与FeatureContact相关联,并且它们中的每一个都使用了through参数来包含一些额外的信息字段。如果你试图用我们现在的代码序列化一个Property对象,你会看到下面的内容。

{

"status": 2

"address": "123 Main St."

"city": "Anywhere"

"state": "CA"

"zip": "90909"

"features": [

{

"slug": "shed"

"title": "Shed"

"definition": "Small outdoor storage building"

}

{

"slug": "porch"

"title": "Porch"

"definition": "Outdoor entryway"

}

]

"price": 130000

"acreage": 0.25

"square_feet": 1248

}

正如您所看到的,所列出的特性只包括关于所提到的特性的生成类型的信息。这个定义简单地解释了棚子和门廊的一般含义,但是对于这个特殊的属性没有任何具体的特征。这些细节只存在于PropertyFeature关系表中,该表目前被忽略。让我们来看看我们希望用它来做什么,以便更好地理解如何到达那里。我们正在寻找的字段存储在一个中间的PropertyFeature模型中,但是我们希望将它们包含进来,就好像它们直接在Feature模型中一样。这需要将Feature实例和PropertyFeature实例的属性合并到一个字典中。

获取适当的字段

我们遇到的第一个问题是,PropertyFeature模型上的字段比我们真正想要包含的要多。它包括与PropertyFeature相关的两个ForeignKey字段,这实际上只是为了支持关系,并没有添加任何有用的信息,这些信息是我们用前面显示的简单方法无法获得的。我们不想包括那些,或者它的自动主键。其他的都是有用的信息,但是我们需要一种方法来识别哪些字段是有用的,哪些是无用的。

为了获得这些信息,我们将从一个助手方法开始,该方法可以查看PropertyFeature模型中的字段,并根据它们的用途组织它们。有四种类型的字段,每一种都可以通过我们可以在代码中自省的不同东西来标识。

  • 自动递增主键将是AutoField的一个实例。这个字段对我们没有任何用处,所以一旦找到它,可以放心地忽略它。
  • 一个外键指向我们正在使用的主模型。在这种情况下,它指向Property模型。这可以被识别为ForeignKey的一个实例,其rel.to属性的值与传入的对象的类相匹配。我们称之为source场。还有一个指向相关模型的外键,在本例中是Feature。这可以被识别为ForeignKey的一个实例,其rel.to属性的值与传递给handle_m2m_field()ManyToMany字段上的rel.to属性相匹配。让我们称之为target字段。
  • 最后,不属于其他三个类别的任何其他字段包含关于关系本身的信息,这些是我们正在努力收集的信息。我们称之为extra字段。

有了这些规则,新的get_through_fields()方法就相当简单了。它只需要查看关系模型上的所有字段,并根据这些规则识别每个字段,返回我们需要在handle_m2m_field()中处理的字段。

from django.db.models import AutoField, ForeignKey

class DataSerializer(serializers.get_serializer('python')):

# Other methods previously described

def get_through_fields(self, obj, field):

extra = set()

for f in field.rel.through._meta.fields:

if isinstance(f, AutoField):

# Nothing to do with AutoFields, so just ignore it

continue

if isinstance(f, ForeignKey):

# The source will refer to the model of our primary object

if f.rel.to == obj.__class__:

source = f.name

continue

# The target will be the same as on the ManyToManyField

if f.rel.to == field.rel.to:

target = f.name

continue

# Otherwise this is a standard field

extra.add(f.name)

return source, target, extra

获取关系信息

现在我们已经有了我们需要的字段,这个过程的核心是分别找到每个关系,并从中提取适当的信息。我们将一次构建这个新版本的handle_m2m_field()几行代码,这样就更容易看到所有的部分在这个过程中组合在一起。

首先,我们需要检索适用于该任务的所有字段信息。上一节为从关系模型中获取信息做好了准备,但是我们还需要包含在序列化输出中的字段列表。我们没有使用标准流程序列化PropertyFeature,所以它不能像其他模型一样使用字段注册表。此外,我们将在由Feature引用的结构中一起返回来自FeaturePropertyFeature的所有数据,所以如果我们允许配置在指定Feature时指定两个模型的所有字段,那会更好。例如,要获取特性的标题及其对当前属性的描述,我们可以在一行中注册它们。

api.serialize_fields(Feature, ['title', 'description'])

title字段将来自Feature模型,而description来自PropertyFeature,但是这允许实现细节被更好地隐藏起来。调用get_through_fields()非常容易,检索注册字段列表与在handle_fk_field()中一样,只有一个小的例外。如果没有已经注册的字段列表,我们可以使用从调用get_through_fields()返回的extra字段。我们必须确保在默认情况下指定一些东西,因为否则自动主键和那两个额外的外键也会被序列化,即使它们在这里没有用。

class DataSerializer(serializers.get_serializer('python')):

# Other methods previously described

def handle_m2m_field(self, obj, field):

source, target, extra_fields = self.get_through_fields(obj, field)

fields = field_registry.get(field.rel.to, extra_fields)

接下来,我们准备迭代当前对象的所有关系。这种方式的工作原理与看起来有点不同,因为访问多对多关系的简单方法不会返回任何额外的关系信息。为此,我们需要直接查询关系模型,只过滤那些source引用了传递给handle_m2m_field()的对象的结果。同时,我们还可以建立一个列表来存储从这些关系中检索到的数据。

class DataSerializer(serializers.get_serializer('python')):

# Other methods previously described

def handle_m2m_field(self, obj, field):

source, target, extra_fields = self.get_through_fields(obj, field)

fields = field_registry.get(field.rel.to, extra_fields)

# Find all the relationships for the object passed into this method

relationships = field.rel.through._default_manager.filter(**{source: obj})

objects = []

现在我们已经准备好开始遍历这些关系,并从每一个关系中提取必要的信息。第一步是根据我们之前找到的字段列表实际序列化相关的模型。例如,这将添加来自Feature模型的title数据。

class DataSerializer(serializers.get_serializer('python')):

# Other methods previously described

def handle_m2m_field(self, obj, field):

source, target, extra_fields = self.get_through_fields(obj, field)

fields = field_registry.get(field.rel.to, extra_fields)

# Find all the relationships for the object passed into this method

relationships = field.rel.through._default_manager.filter(**{source: obj})

objects = []

for relation in relationships.select_related():

# Serialize the related object first

related_obj = getattr(relation, target)

data = DataSerializer().serialize([related_obj])[0]

请注意,我们需要将对象包装在一个列表中进行序列化,然后从结果列表中获取第一项。我们之前创建了一个SingleObjectSerializer,但这只是为了作为一个更加公共的接口与 JSON 输出一起工作。我们只在一种方法中这样做,所以不值得创建另一种单对象变体来处理原生 Python 数据结构。

sourcetarget字段已经被证明是有用的,我们现在有了一个data字典,包含了我们需要的一些内容。为了从关系模型中获得其余的信息,我们查看extra字段。然而,我们不一定需要所有的人。我们只需要获得那些也包含在字段列表注册表中的。这就是我们将它们都存储为集合变得非常有用的地方。我们可以使用&操作符来执行一个简单的交集操作,只得到两个地方都有的字段。对于我们找到的每一个值,我们只需将它与其他值一起添加到data字典中。

class DataSerializer(serializers.get_serializer('python')):

# Other methods previously described

def handle_m2m_field(self, obj, field):

source, target, extra_fields = self.get_through_fields(obj, field)

fields = field_registry.get(field.rel.to, extra_fields)

# Find all the relationships for the object passed into this method

relationships = field.rel.through._default_manager.filter(**{source: obj})

objects = []

for relation in relationships.select_related():

# Serialize the related object first

related_obj = getattr(relation, target)

data = DataSerializer().serialize([related_obj])[0]

# Then add in the relationship data, but only

# those that were specified in the field list

for f in fields & extra_fields:

data[f] = getattr(relation, f)

现在剩下的就是将所有这些数据添加到对象列表中,并将整个集合添加到序列化程序用来跟踪当前对象的字典中。

class DataSerializer(serializers.get_serializer('python')):

# Other methods previously described

def handle_m2m_field(self, obj, field):

source, target, extra_fields = self.get_through_fields(obj, field)

fields = field_registry.get(field.rel.to, extra_fields)

# Find all the relationships for the object passed into this method

relationships = field.rel.through._default_manager.filter(**{source: obj})

objects = []

for relation in relationships.select_related():

# Serialize the related object first

related_obj = getattr(relation, target)

data = DataSerializer().serialize([related_obj])[0]

# Then add in the relationship data, but only

# those that were specified in the field list

for f in fields & extra_fields:

data[f] = getattr(relation, f)

objects.append(data)

self._current[field.name] = objects

现在,当我们序列化我们之前看到的同一个Property对象时,您可以看到它将来自FeaturePropertyFeature的信息包含在一个字典中,这使它成为我们系统中更有用的数据表示。

{

"status": 2

"address": "123 Main St."

"city": "Anywhere"

"state": "CA"

"zip": "90909"

"features": [

{

"title": "Shed"

"description": "Small woodshed near the back fence"

}

{

"title": "Porch"

"description": "Beautiful wrap-around porch facing north and east"

}

]

"price": 130000

"acreage": 0.25

"square_feet": 1248

}

检索数据

有了我们的数据结构,下一个合乎逻辑的步骤是从数据库中检索数据并适当地呈现它。这是视图的工作,特别是我们可以从基于类的视图中获得很多好处。在内部,基于类的视图由几个插件组成,这些插件根据需要组合在一起以构建更有用的类。我们将从创建自己的混音开始,名为ResourceView。这段代码将放在api包中一个名为views.py的新文件中,与前面章节中的序列化代码放在一起。

资源视图

与大多数基于类的视图一样,这里的大部分工作是允许为每个用例定制它的行为。因为ResourceView的目的是序列化一个或多个对象,所以我们可以赋予它接受用于执行该步骤的serializer的能力。另外,我们还将通过添加它自己的serialize()方法使它更容易使用,这样你就不用担心直接访问序列化器了。

from django.views.generic import View

class ResourceView(View):

serializer = None

def serialize(self, value):

return self.serializer.serialize(value)

注意,默认情况下,serializer被设置为标准的 JSON 序列化程序。对于 QuerySets 和单个对象,我们有不同的序列化器,此时,没有办法知道使用哪一个。与其抛硬币来决定哪种用法更常见,不如暂时不要定义它,而需要在子类或单独的 URL 配置中指定它。

现在,serialize()调用缺少的一点是指定输出字段的能力。我们的序列化代码中有相当一部分是为了支持该特性而设计的,所以ResourceView应该将该行为暴露给各个 URL 进行定制。在这里使用None作为缺省值将会自动序列化所提供的任何模型上的所有可用字段。

from django.views.generic import View

class ResourceView(View):

serializer = None

fields = None

def get_fields(self):

return self.fields

def serialize(self, value):

return self.serializer.serialize(value, fields=self.get_fields())

我们在这里使用了一个get_fields()方法,而不仅仅是原始属性访问,因为 mix-in 旨在以我们可能不期望的方式被子类化。在本章的后面部分,您将看到一个子类,它需要通过添加一个在没有指定fields时使用的回退来改变字段的检索方式。我们可以考虑在子类中使用一个property来代替一个方法,但是如果它的未来子类需要再次覆盖那个行为,特别是如果它想要建立在它的父类的行为之上,那么这会导致它自己的一系列问题。一般来说,方法是处理子类化行为的一种更直接的方式,所以它们非常适合像这样的混合类。

资源列表视图

现在我们可以开始处理一个实际上有用的真实视图了。因为ResourceView只是一个提供一些新选项和方法的组合,所以我们可以将它与我们想要使用的几乎任何其他 Django 视图结合起来。对于最基本的情况,我们可以使用 Django 自己的ListView来提供一个对象集合,并简单地将它们序列化,而不是呈现一个模板。

因为ListView是基于TemplateView的,所以它已经包含了一个方法,通过渲染模板的方式将给定的context字典渲染到HttpResponse中。我们没有呈现模板,但是我们确实需要返回一个HttpResponse,上下文已经给了我们这样做所需要的一切。这允许我们使用定制的render_to_response()来使用 JSON 序列化程序代替模板渲染器以获得正确的结果。

首先,我们需要指定我们想要使用的序列化程序,因为默认的ResourceView没有指定序列化程序。

from django.views.generic import View, ListView

from api import serializers

# ResourceView is defined here

class ResourceListView(ResourceView, ListView):

serializer = serializers.QuerySetSerializer()

接下来,我们可以重写render_to_response()方法。这将需要执行三个步骤:

  • 从提供的上下文中获取对象列表
  • 将这些对象序列化为一个 JSON 字符串
  • 返回一个合适的HttpResponse

鉴于我们已经具备的特性,前两步很容易做到。不过最后一步不能只是一个标准的HttpResponse。我们需要定制它的Content-Type,向 HTTP 客户端表明内容由一个 JSON 值组成。我们需要的值是application/json,可以使用HttpResponsecontent_type参数设置。所有这些步骤组合成一个非常短的函数。

from django.http import HttpResponse

from django.views.generic import View, ListView

from api import serializers

# ResourceView is defined here

class ResourceListView(ResourceView, ListView):

serializer = serializers.QuerySetSerializer()

def render_to_response(self, context):

return HttpResponse(self.serialize(context['object_list'])

content_type='application/json')

信不信由你,这就是通过你的新 API 提供一个 JSON 对象列表的全部内容。所有对ListView可用的选项在这里也是可用的,唯一的区别是输出将是 JSON 而不是 HTML。下面是一个相关联的 URL 配置可能的样子,因此您可以看到各种序列化特性是如何组合起来实现这一点的。

from django.conf.urls import *

from django.contrib.auth.models import User, Group

from api.serializers import serialize_fields

from api.views import ResourceListView

from contacts.models import Contact

from contacts import forms

serialize_fields(Contact, forms.ContactEditorForm.base_fields.keys() + ['user'])

serialize_fields(User, forms.UserEditorForm.base_fields.keys())

serialize_fields(Group, ['name'])

urlpatterns = patterns(''

url(r'^$'

ResourceListView.as_view(

queryset=Contact.objects.all()

), name='contact_list_api')

)

资源详细视图

接下来,我们需要为我们的模型提供一个详细的视图,它的工作方式与上一节描述的一样。事实上,只有三点不同:

  • 我们需要子类化DetailView而不是ListView
  • 我们用SingleObjectSerializer代替QuerySetSerializer
  • 我们需要的上下文变量被命名为'object'而不是'object_list'

有了这三个变化,这里的ResourceListViewResourceDetailView一起在api包的views.py文件中。

from django.http import HttpResponse

from django.views.generic import View, ListView, DetailView

from api import serializers

# ResourceView is defined here

class ResourceListView(ResourceView, ListView):

serializer = serializers.QuerySetSerializer()

def render_to_response(self, context):

return HttpResponse(self.serialize(context['object_list'])

content_type='application/json')

class ResourceDetailView(ResourceView, DetailView):

serializer = serializers.SingleObjectSerializer()

def render_to_response(self, context):

return HttpResponse(self.serialize(context['object'])

content_type='application/json')

此外,这里是上一节 URL 配置的延续,扩展后还包含了对ResourceDetailView的引用。

from django.conf.urls import *

from django.contrib.auth.models import User, Group

from api.serializers import serialize_fields

from api.views import ResourceListView, ResourceDetailView

from contacts.models import Contact

from contacts import forms

serialize_fields(Contact, forms.ContactEditorForm.base_fields.keys() + ['user'])

serialize_fields(User, forms.UserEditorForm.base_fields.keys())

serialize_fields(Group, ['name'])

urlpatterns = patterns(''

url(r'^$'

ResourceListView.as_view(

queryset=Contact.objects.all()

), name='contact_list_api')

url(r'^(?P<slug>[\w-]+)/$'

ResourceDetailView.as_view(

queryset=Contact.objects.all()

slug_field='user__username'

), name='contact_detail_api')

)

现在怎么办?

本章展示的 API 只是一个开始,它提供了对一些模型的匿名只读访问。您可以在许多不同的方向上扩展它,添加诸如认证、外部应用授权之类的东西,甚至使用 Django 的表单通过 API 更新数据。

本书中讨论的工具和技术远远超出了 Django 官方文档的范围,但是仍然有很多内容没有探索。使用 Django 和 Python 还有很多其他的创新方法。

当您开发自己的应用时,一定要考虑回馈 Django 社区。该框架可用是因为其他人决定免费分发它;通过这样做,你可以帮助更多的人发现更多的可能性。

Footnotes 1

http://prodjango.com/remote-user

  2

http://prodjango.com/oauth

  3

http://prodjango.com/pyyaml