Django 入门指南(六)
九、Django 模型表单和类视图
在前两章中,您学习了如何使用 Django 模型在关系数据库和 Django 项目之间移动数据。尽管这是 Django 模型的主要目的,但是 Django 模型实现的另一组重要功能并不直接绑定到数据库。
在这一章中,你将学习如何创建与 Django 模型不同的 Django 表单,这个过程进一步扩展了 Django 的 DRY(不要重复自己)原则。您将了解 Django 模型如何生成 Django 表单,包括它的字段、验证,以及将它的数据保存到数据库,所有这些都不需要编写第六章中描述的许多表单搭建逻辑。
接下来,您将了解 Django 基于类的模型视图。虽然你可以继续使用第二章中介绍的 Django 视图技术——就像第六章中的表单技术一样——但是在牢牢掌握 Django 模型之后,你可以进一步将 Django DRY 原则应用到视图中。您将学习如何创建基于类的视图来执行模型 CRUD 操作,从而减少将模型 CRUD 操作合并到视图中所需的逻辑数量。
Django 模型表单结构和工作流程
Django 模型代表了将数据移入和移出数据库的标准方式。但是正如您在前两章中所了解到的,将数据移入和移出数据库的阶段需要您以编程方式操作模型记录(例如,views.py文件中的内部视图方法),以在模型上执行所需的 CRUD 操作。
虽然这对于任何 web 框架来说都是非常有效的工作流,但是您可以通过将 Django 模型链接到更自然的输入/输出机制(表单)来改进将数据移入和移出数据库的过程。
一旦您在实际项目中以编程方式创建了足够多的 Django 模型记录,您将会看到一种模式的出现:大多数 Django 模型操作背后的逻辑是由用户交互决定的。最终用户创建Order模型记录,最终用户读取Store模型记录,管理员更新Menu模型记录,或者管理员删除Item模型记录。这些最终用户和管理员用什么来交流这些模型操作呢?确切地说,是用户界面(UI)中的表单。
现在让我们看看将表单链接到模型的另一面。在第六章中,您学习了 Django 表单,但是您是否意识到在表单数据被处理后,您最有可能对其进行什么操作呢?您最有可能将它保存到数据库中,这涉及到 Django 模型。
因此,本着 Django 的 DRY 原则的精神,模型表单提供了一种使用 Django 模型作为基础来生成 Django 表单以在 Django 模型上执行 CRUD 操作的方法。换句话说,不是创建一个独立的 Django 表单,然后创建必要的“粘合”代码来创建 Django 模型实例,反之亦然,而是创建一个独立的 Django 模型,然后创建必要的表单来对模型记录进行 CRUD 操作,Django 模型表单允许您不重复自己。
创建 Django 模型表单
回到第六章,您创建了一个 Django 表单来获取姓名、电子邮件和评论。接下来,我们将把这个表单重新设计成一个模型表单,以便能够快速地将数据保存到数据库中。
创建模型表单的第一步是创建一个模型作为数据的基础。清单 9-1 展示了一个 Django 模型类,紧接在从模型创建的 Django 模型表单之后。
Tip
参考本书附带的源代码来运行练习,以减少打字和自动访问测试数据。
from django import forms
class Contact(models.Model):
name = models.CharField(max_length=50,blank=True)
email = models.EmailField()
comment = models.CharField(max_length=1000)
class ContactForm(forms.ModelForm):
class Meta:
model = Contact
fields = '__all__'
Listing 9-1.Django model class and model form
清单 9-1 的第一个重要方面是 Django 模型遵循标准的模型语法,有三个字段使用模型字段来限制模型存储的数据类型。清单 9-1 中的模型保持简单,以便更好地说明模型形式,但是可以添加你在前两章中学到的任何其他模型功能(例如,验证器、清理方法、元选项)。
清单 9-1 中的下一个是代表表单的ContactForm类,它从django.forms.ModelForm类继承了它的行为,最后一个类使它成为一个模型表单。注意ContactForm类没有任何表单域,就像你在第六章的表 6-2 中学到的那样;相反,它声明了一个类似于模型中使用的Meta类部分。
ContactForm的Meta类部分指定了两个选项:model和fields。model选项指示使用哪个模型来生成表单,在本例中,Contact模型也在清单 9-1 中。fields选项指示使用哪些模型字段来生成表单,在这种情况下,'__all__'告诉 Django 使用模型中的所有模型fields。
清单 9-1 中ContactForm的强大之处在于它使用两条语句创建一个表单,该表单反映了与Contact模型相同的字段类型。这不仅避免了重复(例如,在显式表单域中键入),表单域还继承了模型的验证行为(例如,models.EmailField()被翻译成forms.EmailField)。但是在我描述完模型表单的基础知识之后,我将很快描述更多关于这个模型到表单继承行为的细节和选项。
一旦有了一个模型表单类,您可能想知道它的处理与标准的 Django 表单有什么不同?实际上很少;你在第六章学到的处理、验证和布局表单的概念对模型表单同样有效,如清单 9-2 所示。
# views.py method to process model form
def contact(request):
if request.method == 'POST':
# POST, generate bound form with data from the request
form = ContactForm(request.POST)
# check if it's valid:
if form.is_valid():
# Insert into DB
form.save()
# redirect to a new URL:
return HttpResponseRedirect('/about/contact/thankyou')
else:
# GET, generate unbound (blank) form
form = ContactForm()
return render(request,'about/contact.html',{'form':form})
# See chapter 6 for form layout template syntax in about/contact.html
Listing 9-2.Django model form processing
在清单 9-2 中,你可以看到视图方法序列遵循与标准 Django 表单相同的模式。当用户在 view 方法上发出 GET 请求时,会创建一个未绑定表单实例,并发送给用户,并通过about/contact.html模板呈现。接下来,当用户通过 POST 请求提交表单时,使用request.POST参数创建一个绑定表单,然后使用is_valid()方法对其进行验证。如果表单值无效,则有错误的绑定表单被返回给用户,以便他可以纠正错误,如果表单值有效,在清单 9-2 的特定情况下,用户被重定向到/about/contact/thankyou页面。
然而,在清单 9-2 中,模型表单有一个重要的处理差异。在表单的值被确定为有效之后,在模型表单实例上调用save()。这个save()方法被绑定到表单的支持模型save()方法,这意味着表单数据被结构化为模型记录并保存到数据库中。
如您所知,与创建和处理独立的表单和独立的模型相比,创建和处理模型表单的过程节省了大量时间。
Django 模型表单选项和字段映射
现在您已经了解了模型表单的基本操作,让我们来看看它的各种选项。大多数模型表单选项都在元类语句中声明,如清单 9-1 所示。但是,也可以声明常规的表单字段,或者覆盖默认的模型字段行为,或者包含新的表单字段。
模型表单必需选项:模型和字段或排除
模型表单从forms.ModelForm类继承它们的行为——而不是标准的forms.Form类——因此 Django 总是期望表单基于一个模型,这就是元model选项的目的。因此model选项值总是模型表单的一个要求。
Django 并不期望模型的结构与表单完美匹配,Django 还期望您明确告诉它支持模型的哪些字段应该或不应该成为模型表单的一部分。这可以通过fields选项——指示哪些模型字段成为模型表单的一部分——或exclude选项——指示哪些模型字段不应该成为模型表单的一部分来实现。总是需要fields或exclude选项,即使模型表单将包含支持模型的所有字段。注意清单 9-1 中的模型表单示例是如何声明选项fields='__all__'来创建一个模型表单,该表单捕获与它的支持模型相同的一组字段。
当您使用除了fields='__all__'(例如,模型字段的缩短列表)或exclude选项(例如,要在表单中省略的模型字段的列表)之外的东西来声明模型表单时,请注意您是有意地并且潜在地违反了模型的规则。例如,默认情况下,所有的模型字段都是必需的,因此如果您创建一个省略了某些字段的模型表单——使用fields或exclude——表单本身可以正常显示,但是模型表单永远不会成功完成其标准工作流,除非您手动添加这些省略的字段。在这种情况下,最终用户将看到“无效表单错误”,因为表单的模型部分由于所需的模型字段值而被破坏。关于模型表单验证和初始化的下一节将描述如何向模型表单中手动添加省略的字段值。
正如您所看到的,您可以创建一个比它的支持模型有更多或更少字段的模型表单。此外,还可以向模型表单添加新的字段——它不是支持模型的一部分——以及定制由模型字段生成的默认表单字段。
为了描述最后两种情况的解决方案,下一节将描述由每个模型字段生成的不同表单字段——这样您就可以确定是否需要自定义默认行为——随后的一节将描述如何自定义和向模型表单添加新字段。
模型表单默认字段映射
模型表单遵循一定的规则将表 7-1 中描述的模型字段数据类型转换为表 6-2 中描述的表单字段数据类型。在大多数情况下,模型字段数据类型被转换成镜像等价的表单字段数据类型。例如,如果模型字段使用models.CharField数据类型,则模型表单会将该字段转换为forms.CharField数据类型。
表 9-1 说明了模型数据类型和表单数据类型之间使用的模型表单映射。请注意,模型和表单之间具有镜像数据类型映射的数据类型包含在表 9-1 的第一行中。
表 9-1。
Model form data type mapping between models and forms
| 模型字段 | 表单字段 | | --- | --- | | 模特。布尔菲尔德 | 表格。布尔菲尔德 | | 模特。日期字段 | 表格。日期字段 | | 模特。日期 | 表格。日期 | | 模特。德西马菲尔德 | 表格。德西马菲尔德 | | 模特。邮箱 | 表格。邮箱 | | 模特。文件字段 | 表格。文件字段 | | 模特。文件路径字段 | 表格。文件路径字段 | | 模特。浮田 | 表格。浮田 | | 模特。像场 | 表格。像场 | | 模特。整数 | 表格。整数 | | 模特。IP 地址字段 | 表格。IP 地址字段 | | 模特。GenericIPAddressField | 表格。GenericIPAddressField | | 模特。NullBooleanField | 表格。NullBooleanField | | 模特。斯拉格菲尔德 | 表格。斯拉格菲尔德 | | 模特。时间字段 | 表格。时间字段 | | 模特。URLField | 表格。URLField | | 模型。自动字段模型。BigAutoField | 不在表单中显示,因为自动建模字段是由数据库生成的。 | | 模特们。毕希菲尔德 | 表格。IntegerField,最小值设置为-9223372036854775808,最大值设置为 9223372036854775807) | | 模特。类型 | 表格。CharField,如果 null=True,max_length 设置为模型字段的 max_length,empty_value 设置为 None | | 模特。CommaSeparatedIntegerField | 表格。类型 | | 模特。外键 | 表格。模型选择字段 | | 模特。多对多字段 | 表格。模型多重选择字段 | | 模特。正积分域 | 表格。整数字段,min_value 设置为 0 | | 模特。PositiveSmallIntegerField | 表格。整数字段,min_value 设置为 0 | | 模特。SmallIntegerField | 表格。整数 | | 模特。文本字段 | 表格。CharField,with widget=forms。文本区域 |正如你在表 9-1 中看到的,超过 50%的 Django 模型数据类型直接映射到等价的表单数据类型。大多数剩余的模型数据类型映射到稍微调整的表单数据类型,以更好地适应支持模型类型(例如,models.PositiveIntegerField映射到forms.IntegerField,但是表单min_value值为 0)。
只有表 9-1 中的四种模型数据类型没有直接映射到表 6-2 中第六章描述的表单数据类型。models.AutoField和models.BigAutoField模型数据类型从不在模型表单中表示,原因很简单,它们的值是由数据库自动分配的,所以它们在表单中没有输入位置。models.ForeignKey和models.ManyToManyField模型数据类型代表模型关系,这意味着它们的数据来自不同的模型。反过来,models.ForeignKey和models.ManyToManyField模型数据类型不映射到字符串或数字的常规表单字段,而是映射到表示其他模型数据的表单字段,这就是特殊表单数据类型:forms.ModelChoiceField和forms.ModelMultipleChoiceField的用途。这最后两个表单字段将在后面的带有关系的模型表单小节中描述。
Tip
要查阅表单域数据类型产生的 HTML(如查阅表 6-2 ,其中包含表单域和表单小部件之间的映射,最后一个小部件产生实际的表单 HTML 标记。
模型表单新的和定制的字段:小部件、标签、帮助文本、错误消息、字段类和本地化字段
现在您已经知道了如何将所有模型字段转换为模型表单中的表单字段,让我们来解决如何在模型表单中添加和自定义表单字段的问题。
向模型表单添加新的表单域就像声明一个表单域一样简单,就好像它是一个常规表单一样。还可以自定义模型字段数据类型使用的默认表单字段数据类型(即表 9-1 中的映射),方法是声明一个与模型字段同名的新表单字段,使其优先于默认的模型-表单字段映射。
清单 9-3 展示了清单 9-1 中的 Django 模型类和模型表单,更新后包括了一个新的表单字段和一个覆盖默认模型表单字段映射的表单字段。
from django import forms
def faq_suggestions(value):
# Validate value and raise forms.ValidationError for invalid values
pass
class Contact(models.Model):
name = models.CharField(max_length=50,blank=True)
email = models.EmailField()
comment = models.CharField()
class ContactForm(forms.ModelForm):
age = forms.IntegerField()
comment = forms.CharField(widget=forms.Textarea,validators=[faq_suggestions])
class Meta:
model = Contact
fields = '__all__'
Listing 9-3.Django model form with new and custom field
清单 9-3 首先添加了新的age表单字段来捕获表单中的整数值。尽管底层的Contact模型从来不知道age字段或值,但通过这种修改,模型表单将要求该字段作为表单工作流的一部分提供。
清单 9-3 中的下一个是comment表单字段,它覆盖了同名的底层模型字段。在这种情况下,覆盖comment表单字段的目的是添加一个自定义的widget,以及添加一个自定义的validators方法,以便在表单被认为有效之前验证comment值——注意widget和validators选项都是第六章中描述的标准表单选项。
清单 9-3 中的表单域覆盖机制有优点也有缺点。这样做的好处是您可以完全控制表单域来定义任何选项。缺点是传递给表单域的模型域选项(如max_length)丢失,需要作为新表单域语句的一部分重新声明。
为了保留模型字段的底层行为并仍然能够定制某些表单字段选项,除了model、fields和exclude选项之外,模型表单还支持额外的元类选项。清单 9-4 展示了一个模型表单的附加元选项来覆盖默认的模型表单字段映射,同时保持底层的模型字段行为。
from django import forms
class Contact(models.Model):
name = models.CharField(max_length=50,blank=True)
email = models.EmailField()
comment = models.CharField()
class ContactForm(forms.ModelForm):
class Meta:
model = Contact
fields = '__all__'
widgets = {
'name': models.CharField(max_length=25),
'comment': form.Textarea(attrs={'cols': 100, 'rows': 40})
}
labels = {
'name': 'Full name',
'comment': 'Issue'
}
help_texts = {
'comment': 'Provide a detailed account of the issue to receive a quick answer'
}
error_messages = {
'name': {
'max_length': "Name can only be 25 characters in length"
}
}
field_classes = {
'email': EmailCoffeehouseFormField
},
localized_fields = '__all__'
Listing 9-4.Django model form with meta options to override default form field behavior
清单 9-4 中的元模型表单选项最重要的方面是它们是表单域选项的复数名称,在第六章中有描述。清单 9-4 中突出显示的模型表单元选项是复数的,因为它们可以将多个表单字段的选项声明为一个字典,其中每个键代表表单字段名称,其值代表选项值。
例如,清单 9-4 中的widgets和labels元选项为name和comment模型表单字段定义了定制部件和标签。help_texts元选项为comment模型表单字段定义了help_text选项,而error_messages元选项为name模型表单字段上的max_length键错误声明了一个定制表单错误消息。
接下来,清单 9-4 中的field_classes元选项用于为email模型表单字段声明一个定制表单字段。最后,清单 9-4 中的localized_field元选项被设置为__all__以告诉 Django 本地化(即转换成不同的语言)所有模型表单字段。如果省略了localized_field选项,那么模型表单字段不会被本地化。值得一提的是,您可以通过将模型表单字段的列表传递给localized_field选项来选择性地本地化某些模型表单字段,就像使用fields和exclude选项一样。
有关系的 Django 模型表单
正如您在前两章中所了解到的,Django 模型之间可以有关系,这反过来使得模型具有引用另一个模型中的记录的数据类型(例如,ForeignKey,ManyToManyField)。
当在模型表单的上下文中使用包含这种数据类型的模型时,Django 依赖于两个特殊的表单字段。默认情况下,ForeignKey模型字段转换为ModelChoiceField表单字段,ManyToManyField模型字段转换为ModelMultipleChoiceField表单字段。
ModelChoiceField和ModelMultipleChoiceField表单字段的好处是它们基于 Django 模型查询生成表单字段。因此,ModelChoiceField和ModelMultipleChoiceField表单域生成一个包含模型记录的友好的 HTML <select>/<option>输入域,而不是用模型数据手工填充表单域。
模型选择字段和模型多重选择字段表单字段选项:queryset、empty_label、to_field_name 和 label_from_instance
Tip
ModelChoiceField和ModelMultipleChoiceField是标准表单字段,可用于任何需要模型数据的 Django 表单。默认情况下,它们被用在有关系的模型表单上,但是它们并不局限于模型表单(即,从forms.ModelForm继承的表单)。
Note
ModelChoiceField和ModelMultipleChoiceField是标准表单字段(即 Django forms包的一部分),也接受标准表单选项:必填、小部件、标签、初始、help_text 和 limit _ choices _ to——如第六章所述。
由于ModelChoiceField和ModelMultipleChoiceField表单字段使用模型记录来获取数据,因此它们明确需要模型查询。对于从forms.ModelForm继承其行为的模型表单,它们的基础模型包含一个ForeignKey或ManyToManyField模型字段,这个模型查询是自动设置的。例如,如果一个Item模型包含一个ForeignKey到一个Menu模型,一个Item模型表单在一个表单中显示所有的Menu记录,允许用户选择一个单独的Menu记录。类似地,如果一个Store模型包含一个ManyToManField到一个Amenity模型,一个Store模型表单在一个表单中呈现所有的Amenity记录,允许用户选择多个Amenity记录。
虽然这种行为在大多数情况下是可以接受的,但当您需要过滤默认行为以使用模型表单字段上的所有模型记录时,或者当这些表单字段用于常规表单(即继承了forms.Form)时,可能有必要提供对ModelChoiceField或ModelMultipleChoiceField表单字段的显式查询。
清单 9-5 展示了使用queryset选项在ModelChoiceField和ModelMultipleChoiceField表单字段上设置模型查询的两种技术。
from django import forms
from coffeehouse.stores.models import Amenity
class Menu(models.Model):
name = models.CharField(max_length=30)
def __str__(self):
return "%s" % (self.name)
class Item(models.Model):
menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
class ItemForm(forms.ModelForm):
menu = forms.ModelChoiceField(queryset=Menu.objects.filter(id=1))
class Meta:
model = Item
fields = '__all__'
class StoreForm(forms.Form):
name = forms.CharField()
address = forms.CharField()
amenities = forms.ModelMultipleChoiceField(queryset=None)
def __init__(self, *args, **kwargs):
super(StoreForm, self).__init__(*args, **kwargs)
self.fields['amenities'].queryset = Amenity.objects.filter(name__contains='W')
Listing 9-5.Django model form and standard form with custom query for ModelChoiceField and ModelMultipleChoiceField form fields
清单 9-5 中的第一项技术通过用ItemForm模型表单上的自定义ModelChoiceField()覆盖menu字段来定义内联queryset值。在这种情况下,不是ItemForm模型表单有一个包含所有Item记录的menu字段,而是将menu字段限制为只有包含id=1的Item记录。
清单 9-5 中的第二项技术在标准表单上定义了一个空的queryset值,该表单在amenities字段上使用了一个forms.ModelMultipleChoiceField()表单字段。但是在表单的__init__方法中,amenities字段被设置为一个查询,该查询将其记录限制为包含字母W的Amenity记录。
值得一提的是,清单 9-5 中展示的两种queryset技术在模型表单和常规表单中同样有效,同样有效的还有ModelChoiceField()和ModelMultipleChoiceField()表单字段。
默认情况下,不定义initial值的ModelChoiceField()表单字段生成时会将空的 HTML <option>---------</option>选项作为默认字段值。可以通过empty_label选项自定义该空选项的值(例如empty_label='Please select a value',输出<option>Please select a value</option>)。也可以用empty_label=None禁用这个空选项。
默认情况下,ModelChoiceField()和ModelMultipleChoiceField()表单字段都从模型记录的主键值(即id)和模型__str__方法表示)生成它们的 HTML <select>/<option>输入字段值。例如,给定清单 9-5 中的Menu模型定义,该模型的 HTML <select>/<option>输入字段将如下所示:
<select name="menu" required id="id_menu">
<option value="" selected>---------</option>
<option value="1">Breakfast</option>
<option value="2">Salads</option>
<option value="3">Sandwiches</option>
<option value="4">Drinks</option>
</select>
注意每个<option> value对应一个记录的主键id值,而<option>文本对应一个由模型的__str__方法返回的记录的name字段。
可以使用to_field_name选项定制ModelChoiceField()和ModelMultipleChoiceField()表单字段中使用的<option> value。例如,在最后一个代码片段的上下文中设置to_field_name='name',将 HTML <select>/<option>输入字段更改为格式<option value="Breakfast">Breakfast</option>。
Caution
使用 to_field_name 值会破坏基础模型表单保存到数据库的能力,因为模型的关系值被设置为与模型关系所期望的主键不同的值。
除了定制<option> value之外,通过覆盖ModelChoiceField()和ModelMultipleChoiceField()表单域中的label_form_instance方法,还可以将表单域中的<option>文本定制为除模型的__str__方法之外的东西。清单 9-6 展示了一个为此目的设计的定制表单域。
from django import forms
from django.forms import ModelChoiceField
class MenuModelChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return "Menu #%s) %s" % (obj.id,obj.name)
class ItemForm(forms.ModelForm):
menu = MenuModelChoiceField(queryset=Menu.objects.all())
class Meta:
model = Item
fields = '__all__'
# HTML menu form field output
<select name="menu" id="id_menu" required>
<option value="" selected>---------</option>
<option value="1">Menu #1) Breakfast</option>
<option value="2">Menu #2) Salads</option>
<option value="3">Menu #3) Sandwiches</option>
<option value="4">Menu #4) Drinks</option>
</select>
Listing 9-6.Django custom form field to customize <option> text for ModelChoiceField and ModelMultipleChoiceField form fields
清单 9-6 中的第一步创建了从ModelChoiceField表单域继承其行为的MenuModelChoiceField定制表单域,并定义了label_from_instance方法的实现。在这种情况下,label_from_instance方法告诉 Django 生成以Menu #静态字符串为前缀的<option>文本值,后跟模型的id和name。注意同样的技术可以用来定制一个ModelMultipleChoiceField,只要确保改变定制表单域的继承类。
接下来,通过menu字段将清单 9-6 中的MenuModelChoiceField定制表单字段添加到同一清单中的ItemForm模型表单中。因为MenuModelChoiceField是一个定制的ModelChoiceField表单字段,所以有必要指定一个显式的queryset值来填充表单字段,在本例中,它对应于所有的Menu模型记录。
最后,清单 9-6 展示了menu字段的 HTML <option>文本输出遵循自定义MenuModelChoiceField自定义表单字段中定义的模式。
Django 模型表单处理
现在您已经对各种模型表单选项有了坚实的理解,是时候更深入地了解模型表单处理了,这在清单 9-2 中有简要介绍。
在处理模型表单时要考虑的最重要的因素是您正在处理两个实体:表单和模型。在清单 9-2 中展示的模型表单处理示例中,这个事实并不太明显,主要是因为表单完全符合支持模型。但是,当您修改任何模型表单部件时,使用表示表单和模型的单个引用可能需要更多的考虑。
模型表单初始化:初始和实例
模型表单可以使用两个初始化参数:initial和instance。initial参数的工作方式就像标准的initial表单参数——在第六章中描述——为未绑定表单提供初始值。instance参数用于用一个模型实例初始化一个模型窗体,该模型实例也用于初始化一个未绑定窗体的值。
在所有的模型表单中,正如您在前面章节中所学的,表单定义优先于任何底层的模型定义。这意味着initial参数中的所有模型表单值优先于通过instance参数定义的值。清单 9-7 显示了使用initial和instance参数的模型表单初始化序列。
from coffeehouse.items.models import Item
preloaded_item = Item.objects.get(id=1)
# Model form from Listing 9-6, initialize with instance
form = ItemForm(instance=preloaded_item)
# Unbound form set up with instance values
form.as_p()
<p>
<label for="id_menu">Menu:</label>
<select name="menu" required id="id_menu">
<option value="">---------</option>
<option value="1" selected>Menu #1) Breakfast</option>
<option value="2">Menu #2) Salads</option>
<option value="3">Menu #3) Sandwiches</option>
<option value="4">Menu #4) Drinks</option>
</select>
</p>
<p>
<label for="id_name">Name:</label>
<input type="text" name="name"
value="Whole-Grain Oatmeal" required maxlength="30" id="id_name" />
</p>
# Remaining fields committed for brevity
# Model form from Listing 9-6, initialize with instance and override with initial
form2 = ItemForm(initial={'menu':3},instance=preloaded_item)
# Unbound form set up with instance values
form2.as_p()
# Unbound form set up with instance values, but overridden with initial
form2.as_p()
<p>
<label for="id_menu">Menu:</label>
<select name="menu" required id="id_menu">
<option value="">---------</option>
<option value="1">Menu #1) Breakfast</option>
<option value="2">Menu #2) Salads</option>
<option value="3" selected>Menu #3) Sandwiches</option>
<option value="4">Menu #4) Drinks</option>
</select>
</p>
# Remaining fields committed for brevity
Listing 9-7.Django model form initialization with initial and instance
清单 9-7 中的第一步是获取一个Item模型记录来填充ItemForm模型表单;在这种情况下,进行查询以获得带有id=1的Item模型记录。接下来,Item模型记录被用来用实例值初始化模型表单。在清单 9-7 中,表单是用标准的as_p()表单方法输出的,您可以确认表单字段是预先选择的,以反映底层的模型记录。
清单 9-7 中的下一个是同一个ItemForm模型表单的初始化序列,但是它也结合使用了initial参数和instance参数。在这种情况下,因为initial参数提供了'menu':3值,所以未绑定表单的menu字段被设置为值3,而不是模型记录的实例menu值1。从而确认initial参数值优先于instance参数值。
注意,只使用initial参数——而不使用instance参数——来初始化一个模型表单,就像它是一个常规表单一样,这同样有效。在模型表单的初始化阶段,表单的模型部分不知道任何值,直到模型表单进入验证阶段,底层模型才知道任何表单值。
模型表单验证
与模型表单初始化类似,模型表单验证可能看起来是交织在一起的,因为您处理的是一个既引用表单又引用模型的变量。但是只要你知道表单验证的基本步骤——在第六章中描述——和模型验证——在第七章中描述——模型表单验证就很简单。
回到清单 9-2 ,您学习了如何通过在视图方法(例如ContactForm(request.POST))中传递request.POST值来将模型表单转换为绑定表单(即包含用户数据的表单)。一旦有了绑定表单,标准的 Django 表单验证工作流就会继续应用于模型表单:对表单引用调用is_valid()方法,根据表单验证规则验证用户提交的数据。如果有任何表单规则不符合,一个errors字典将被添加到表单引用中,并给出原因,它将作为一个重新呈现的有错误的表单返回给用户。如果is_valid()方法成功,模型表单的处理逻辑就可以进入下一步。
一旦模型表单通过了is_valid()方法测试,您实际上可以使用相同的标准表单cleaned_data()方法来访问包含有效表单数据内容的字典(例如,form.cleaned_data()包含{'name': '...','email': '...','comment': '...', })。但是由于您使用的是模型表单,您更可能采取的步骤是使用表单数据来进一步与模型进行交互。
为了促进这一过程,Django 向表单引用添加了instance字段,其中包含了作为模型表单底层模型实例的表单数据。当您在尝试模型操作之前需要或必须操作模型数据时,instance字段尤其重要。这是模型表单验证过程中最关键的方面:即使在表单is_valid()方法通过并且数据被用于在instance字段中构造模型实例之后,这个模型实例数据仍然必须经过模型验证,否则就有被支持的模型验证规则拒绝的风险。
对于简单的模型表单场景,其中模型和表单直接相互映射——如清单 9-2 中所示——操纵instance字段是不必要的(例如,在表单is_valid()方法传递之后,您可以调用form引用上的save()方法来将模型实例保存在instance中)。但是对于模型和表单在字段数量上不同的模型表单,您需要在表单is_valid()方法传递之后和调用模型表单的save()方法之前执行额外的逻辑。
清单 9-8 展示了模型表单的两个验证过程,其中表单省略了底层模型中的字段。
from django import forms
from django.conf import settings
class Contact(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, default=None)
name = models.CharField(max_length=50,blank=True)
email = models.EmailField()
comment = models.CharField()
class ContactForm(forms.ModelForm):
class Meta:
model = Contact
exclude = ['user']
# Option 1) Form model processing with missing value assigned with instance
if form.is_valid():
# Check if user is available
if request.user.is_authenticated():
# Add missing user to model form
form.instance.user = request.user
# Insert into DB
form.save()
# Option 2) Form model processing with missing value assigned after model form sequence
if form.is_valid():
# Save instance but don't commit until model instance is complete
# form.save() returns a materialized model instance that has yet to be saved
pending_contact = form.save(commit=False)
# Check if user is available
if request.user.is_authenticated():
# Add missing user to model form
pending_contact.user = request.user
# Insert into DB
pending_contact.save()
Listing 9-8.Django model form with reduced form that requires model update before saving
清单 9-8 中的Contact模型类似于前面清单中使用的模型类,但是有额外的user字段来注册 Django 用户作为模型记录的一部分。接下来,在清单 9-8 中是ContactForm模型表单——基于最后一个Contact模型——它使用exclude选项从表单中省略user模型字段。
因为您有意从模型表单中省略了user字段,最终用户将无法提供它——即使在不太可能的情况下,他也知道他的内部用户。因此,作为验证过程的一部分,有必要更新模型以包含内部用户,这在视图方法的request引用中总是可用的。
在清单 9-8 中的第一个验证序列中,您可以看到在模型表单通过is_valid()方法后,会进行一个快速检查来确认用户是否通过了身份验证;如果是,则访问模型的instance引用来更新user模型字段。一旦完成,模型表单的支持实例包含一个被省略的user字段的值,并且在调用save()方法时,模型记录与它的所有模型字段的值一起被保存。
清单 9-8 中的第二个验证序列使用commit=False来具体化模型表单的模型实例,而不将其保存到数据库中。一旦完成了这些,模型表单的工作也就完成了,这样就剩下一个需要更新并保存到数据库中的基本模型记录实例。您可以在清单 9-8 中看到,进行了相同的检查来确认用户是否通过了身份验证,如果是,则更新未保存的模型记录user引用,并最终调用模型的save()方法将记录提交给数据库。
Django 模型表单集
正如标准的 Django 表单可以作为一个集合组合在一起成为一个表单集一样,Django 模型表单也可以组合成一个模型表单集。类似地,正如 Django 模型表单类似于标准 Django 表单一样,模型表单集也与标准表单集有很多相似之处。
接下来,我将基于第六章最后一部分介绍的关于标准表单集的知识来描述模型表单集的特性。因此,如果您不熟悉表单集术语(例如,工厂和管理表单),请回过头来阅读最后一节,因为下面的内容假设您已经了解这些基本的表单集概念。
模型表单集工厂
modelformset_factory()方法是使用模型表单集的核心。modelformset_factory()方法最多可以接受 19 个参数,其中 9 个与标准表单集相同,其余的特定于模型表单集。下面的代码片段展示了modelormset_factory()方法中每个参数的所有名称和默认值,粗体文本代表模型表单集特定选项。
modelformset_factory(model, queryset=model.objects.all(),
form=ModelForm,fields=None, exclude=None,
formset=BaseModelFormSet, extra=1, can_order=False, can_delete=False,
max_num=None, min_num=None, validate_max=False, validate_min=False,
widgets=None, localized_fields=None,labels=None,
help_texts=None, error_messages=None,
field_classes=None,formfield_callback=None)
正如您可以在这个代码片段中确认的那样,modelformset_factory()方法唯一需要的参数(即没有默认值)是model。每个参数的含义如下:
model。-定义要在其上创建表单集的模型类。
queryset。-定义 queryset 以创建表单集。默认情况下,所有的模型记录都被用来创建表单集(即model.objects.all() queryset,如果使用的话)。
fields。-定义模型表单字段作为模型的一部分,以创建模型表单集–就像fields元模型表单选项一样。
exclude。-定义要作为模型的一部分省略的模型表单字段,以创建模型表单集-就像exclude元模型表单选项一样。
widgets。-为模型表单定义覆盖小部件以创建模型表单集——就像widgets元模型表单选项一样。
localize_fields。-定义要本地化的模型表单字段(即支持多种语言)以创建表单集–就像localize_fields元模型表单选项一样。
labels。-为模型表单定义覆盖标签以创建模型表单集–就像labels元模型表单选项一样。
help_text。-为模型表单定义覆盖帮助文本以创建模型表单集-就像help_texts元模型表单选项一样。
error_messages。-为模型表单定义覆盖错误消息,以创建模型表单集–就像help_texts元模型表单选项一样。
field_classes。-为模型表单定义覆盖字段类以创建模型表单集——就像field_classes元模型表单选项一样。
formsetfield_callback。-定义在从模型字段创建表单字段之前要执行的方法。通常用于在表单集的上下文中自定义模型表单字段,如前面的模型表单部分所述。
Tip
参见第六章了解额外模型表单集 _ 工厂选项的详细信息,该选项已在标准表单集章节中描述。
鉴于模型表单集逻辑是前面描述的模型表单技术和第六章中描述的表单集技术的结合,我不会再重复同样的技术。你可以查看这本书的源代码,在online应用下的第九章的源代码中找到一个工作模型表单集的例子。
带有模型的基于类的视图
回到第二章,您学习了基于类的视图如何允许您创建符合面向对象编程(OOP)原则(例如封装、多态和继承)的视图,从而提高可重用性并缩短实现时间。现在你已经知道 Django 模型是如何工作的,我们可以处理基于类的视图,这些视图与表 2-10 的最后一部分描述的模型相集成。
不同于标准的 Django 视图(在第二章的前几节中讨论过)允许开放式逻辑处理请求并生成响应,基于类的模型视图以更模块化的方式封装了针对 Django 模型执行的逻辑。
例如,在标准视图方法中创建、读取、更新和删除模型实例的逻辑模式通常遵循非常一致的工作流:从 url 或表单中获取输入数据,对模型执行 CRUD 操作,然后将响应发送给模板。
本着 Django 的 DRY 原则,基于类的模型视图提供了一种减少标准视图方法中使用的样板代码的方法,并使用类字段和方法来定义用于 Django 模型 CRUD 操作的工作流。
使用基于类的视图 CreateView 创建模型记录
到目前为止,您已经了解到,在实际项目中创建 Django 模型实例伴随着一系列的构造,包括模型表单、GET/POST 请求处理、模板的使用等等。
Django CreateView基于类的视图是专门为减少创建模型记录所需的样板代码而设计的。清单 9-9 展示了使用CreateView类的基于类的视图。
# views.py
from django.views.generic.edit import CreateView
from .models import Item, ItemForm
from django.core.urlresolvers import reverse_lazy
class ItemCreation(CreateView):
model = Item
form_class = ItemForm
success_url = reverse_lazy('items:index')
# models.py
from django import forms
from django.db import models
class Menu(models.Model):
name = models.CharField(max_length=30)
class Item(models.Model):
menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
class ItemForm(forms.ModelForm):
class Meta:
model = Item
fields = '__all__'
widgets = {
'description': forms.Textarea(),
}
# urls.py
from django.conf.urls import url
from coffeehouse.items import views as items_views
urlpatterns = [
url(r'^new/$', items_views.ItemCreation.as_view(), name='new'),
]
# templates/items/item_form.html
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Create</button>
</form>
Listing 9-9.Django class-based view with CreateView to create model records
清单 9-9 的第一部分展示了基于ItemCreation类的视图,它从CreateView类继承了它的行为。注意这个视图缺少一个request引用、处理逻辑或者return语句,所有这些在第二章描述的标准视图方法中都很常见。那么ItemCreation基于阶级的观点实际上在做什么呢?
因为您预先知道您想要创建一个模型记录,所以由基于ItemCreation类的视图使用的CreateView类支持所有必要的样板文件逻辑,并且需要最少的代码集来实现它的模型记录创建逻辑。
清单 9-9 中的ItemCreation基于类的视图使用model字段告诉 Django 创建Item模型记录。此外,form_class字段指定了ItemForm表单——也在清单 9-9 中声明——用于创建模型记录。此外,当模型记录创建成功时,success_url字段用于将控制权返回给item:index url。
Why Reverse_Lazy in Class-Based Views, Instead of Reverse?
由于基于类的视图中模型、视图和 url 的同时导入顺序,使用标准的 reverse()方法来解析 URL 名称可能会导致错误:django . core . exceptions . improperly configured:包含的 URLconf '–-'中似乎没有任何模式。如果您在文件中看到有效的模式,那么问题可能是由循环导入引起的。
reverse_lazy 方法确保只有在所有模型、视图和 url 都被正确导入之后才尝试任何反向 URL 名称解析;因此,这是基于类的视图环境中的常见选择。
您可能仍然会疑惑,如果清单 9-9 中的ItemCreation基于类的视图正在创建模型记录,那么它的save()和is_valid()方法在哪里?默认情况下没有。因为您只想创建一个模型记录,所以父类CreateView负责这个支持逻辑。
清单 9-9 中的下一部分包含了带有钩子的urly.py文件,用于在应用中设置基于类的视图。在这种情况下,ItemCreation基于类的视图被设置为在/new url 上运行,并通过使用基于类的视图as_view()方法被声明为url()方法的一部分——注意,这种 url 设置技术对所有基于类的视图都是相同的,并在第二章末尾对无模型的基于类的视图进行了描述。
那么当用户访问/new url 时会发生什么呢?控制被发送到ItemCreation基于类的视图。因为这个视图的目的是创建一个Item模型记录,所以基于类的视图在遵循约定<app_name>/<model_name>_form.html的项目的TEMPLATES目录路径下寻找一个呈现表单的模板。
在清单 9-9 的最后一部分,您可以看到模板templates/items/item_form.html,其中templates表示一个TEMPLATES目录路径值,items表示应用名称,item表示为基于类的视图定义的模型名称。此外,请注意item_form.html模板的内容使用了标准的表单布局,您可以像任何 Django 表单一样对其进行调整。
那么清单 9-9 中的 POST 表单处理程序和错误处理在哪里呢?默认情况下,也没有。一旦用户得到清单 9-9 底部所示的未绑定表单,表单处理和验证就由ItemCreation基于类的视图在幕后处理。如果表单包含错误,模板将被重新呈现——像任何其他表单一样——使用清单 9-9 中的相同模板。如果表单数据有效,表单处理被认为是成功的,基于类的视图创建一个Item模型记录——类似于模型表单——将控制重定向到基于类的视图success_url字段中定义的item:index url。
正如您在这个例子中看到的,从CreateView类继承其行为的基于类的视图减少了创建模型记录所需的样板代码。
创建视图字段和方法
虽然乍一看,从CreateView类继承其行为的基于类的视图可能显得不灵活,但是可以像任何 Django 构造一样覆盖其默认行为。
事实证明,CreateView类从许多其他基于 Django 类的视图中继承了它的行为,这就是赋予了CreateView类及其实现子类(如清单 9-9 中基于类的视图)幕后的力量。CreateView类从以下基于类的视图类继承其行为:
django.views.generic.detail.SingleObjectTemplateResponseMixin
django.views.generic.base.TemplateResponseMixin
django.views.generic.edit.BaseCreateView
django.views.generic.edit.ModelFormMixin
django.views.generic.edit.FormMixin
django.views.generic.detail.SingleObjectMixin
django.views.generic.edit.ProcessFormView
django.views.generic.base.View
那么,为什么这些课程如此重要呢?因为它们为所有基于类的视图提供了默认行为。清单 9-9 中的ItemCreation基于类的视图中声明的稀疏字段对于CreateView基于类的视图来说只有三个字段。可以声明十几个以上的字段和方法——属于这个过去的类列表——来提供行为,比如使用另一个模板而不是<app_name>/<model_name>_form.html;指定响应的内容类型(例如,text/csv);模型表单有效或无效时运行的自定义方法;以及声明自定义方法来手动执行 GET 和 POST 工作流逻辑。
Note
基于类的视图从它的父类继承了许多字段和方法。以下选项是最常见的选项;要获得详尽的列表,请查阅每个CreateView父类。
基本的 CreateView 选项:模型、form_class 和 success_url 字段
正如您已经在清单 9-9 中看到的,基于CreateView类的视图实现的基本逻辑是使用一个表单创建一个模型记录,这反过来需要一组基本的参数。首先,model字段是整个操作的基础,因为基于CreateView类的视图必须预先知道要创建哪种类型的模型记录。其次,form_ class也是一个基本参数,因为必须向用户提供一个表单来捕获数据以创建模型记录。
最后,因为成功地创建一个模型记录需要通知用户这个动作并离开表单页面,所以success_url字段也是一个基于CreateView类的视图的基本部分,用来指示在创建一个模型记录后将用户重定向到哪里。
定制模板名称、MIME 类型和上下文:模板名称和内容类型字段以及 get_context_data()方法
有时依赖于CreateView基于类的视图模板命名约定<app_name>/<model_name>_form.html是不可行的,要么是因为您有一个预先存在的模板可以重用,要么是因为您不喜欢默认约定。您可以将template_name字段声明为基于CreateView类的视图的一部分来覆盖这个约定。类似地,也可以覆盖基于类的视图响应所使用的默认 MIME 类型——如果模板包含除了text/html(例如text/csv)之外的东西——使用content_type字段作为CreateView基于类的视图的一部分。
此外,您可以通过定义get_context_data()方法的实现来改变传递给基于类的视图模板的上下文数据。当你需要传递额外的数据到一个模板——除了form引用——或者改变实际的form引用到另一个名字时,最后一个过程是很常见的。
清单 9-10 展示了一个基于CreateView类的视图,它利用了template_name和content_type字段,以及get_context_data()方法。
# views.py
from django.views.generic.edit import CreateView
from .models import Item, ItemForm, Menu
class ItemCreation(CreateView):
template_name = "items/item_form.html"
context_type = "text/html"
model = Item
form_class = ItemForm
success_url = reverse_lazy('items:index')
def get_context_data(self,**kwargs):
kwargs['special_context_variable'] = 'My special context variable!!!'
context = super(ItemCreation, self).get_context_data(**kwargs)
return context
Listing 9-10.Django class-based view with CreateView with template_name, content_type
, and get_context_data()
您可以在清单 9-10 中看到,template_name和content_type字段被声明为基于类的视图的一部分。在这种特殊情况下,为简单起见,两个字段值都被赋予默认值,使它们变得多余;但是您可以根据自己的需要进行相应的调整。
清单 9-10 中的get_context_data()方法首先添加自定义special_context_variable键,使其可用于基于类的视图模板(即items/item_form.html)。接下来,调用父类的get_context_data()方法(即CreateView)来运行其上下文设置逻辑,包括设置模板中也使用的form引用。最后,get_context_data()方法返回带有所有模板上下文值的context引用,以便在模板内部使用。
正如您在清单 9-10 中看到的,通过简单地在一个基于CreateView类的视图中添加字段和覆盖方法,您可以很容易地开始改变它的默认行为。
自定义表单初始化和验证:Initial field、get_initial()、get_form()、form_valid()和 form_invalid()方法
基于CreateView类的视图上的initial字段就像标准表单的initial参数一样为未绑定表单指定默认值。例如,在基于CreateView类的视图上声明字段initial = {'size':'L'}会将其表单的size字段设置为L。
表单初始化有时可能需要比单行语句更复杂的需求,其中基于类的视图还提供了方法。get_initial()方法的功能类似于标准表单中使用的__init__方法——因为您可以引入开放式逻辑来设置默认值——但它的唯一目的是设置initial值并返回一个值字典——不像__init__方法,您可以引入除默认表单值之外的其他操作(例如,更改小部件、添加验证)。
清单 9-11 展示了在基于CreateView的视图上使用initial参数和get_initial()方法。
# views.py
from django.views.generic.edit import CreateView
from .models import Item, ItemForm, Menu
class ItemCreation(CreateView):
initial = {'size':'L'}
model = Item
form_class = ItemForm
success_url = reverse_lazy('items:index')
def get_initial(self):
initial_base = super(ItemCreation, self).get_initial()
initial_base['menu'] = Menu.objects.get(id=1)
return initial_base
Listing 9-11.Django class-based view with CreateView with initial and get_initial()
清单 9-11 中的第一步是设置initial字段,将基于类的视图的表单size字段设置为L。接下来,声明get_initial()方法,为基于类的视图表单添加另一个默认值。
在get_initial()方法内部,第一步是调用父类的get_initial()方法(即CreateView)来运行它的初始设置逻辑;这确保了类别的initial字段值(即{'size':'L'})被考虑为initial值的一部分。接下来,更新initial_base引用,将表单的menu字段设置为带有id=1的Menu记录。最后,清单 9-11 中的get_initial()方法返回一个字典,其中包含由基于类的视图设置的initial字段值和在其主体中设置的自定义表单值。
基于CreateView类的视图的get_form()方法旨在利用表单的完整初始化过程,而不仅仅是像initial字段和get_initial()方法那样设置其默认值。这使得get_form()方法适合于执行更广泛的表单初始化任务,比如设置表单小部件和验证初始化,就像在标准表单的__init__方法中所做的那样。包含地,可以使用get_form()方法来指定默认的表单值,并且完全放弃使用initial和get_initial()。清单 9-12 展示了在基于CreateView类的视图中get_form()方法的使用。
# views.py
from django.views.generic.edit import CreateView
from .models import Item, ItemForm, Menu
class ItemCreation(CreateView):
initial = {'size':'L'}
model = Item
form_class = ItemForm
success_url = reverse_lazy('items:index')
def get_form(self):
form = super(ItemCreation, self).get_form()
initial_base = self.get_initial()
initial_base['menu'] = Menu.objects.get(id=1)
form.initial = initial_base
form.fields['name'].widget = forms.widgets.Textarea()
return form
Listing 9-12.Django class-based view with CreateView with get_form()
清单 9-12 中的get_form()方法的第一步是调用父类的get_form()方法(即CreateView)来获取基础表单。接下来,调用基于类的视图的get_initial()方法来获取其initial表单值,并使用模型查询为表单的menu字段设置默认值。在这种情况下,表单初始化表单字典的值与清单 9-11 中的值相同。
接下来,使用标准的initial表单引用将表单初始化字典分配给基础表单。最后,在返回 form 类的实例之前,在表单的name字段上设置一个自定义小部件,覆盖表单name字段的默认小部件。
除了初始化任务,还可以定制基于类的视图的表单验证过程。form_valid()和form_invalid()方法分别用于访问基于类的视图表单被认为成功或错误的点。清单 9-13 展示了一个在基于CreateView类的视图中使用form_valid()和form_invalid()方法的例子。
# views.py
from django.views.generic.edit import CreateView
from django.http import HttpResponseRedirect
from django.contrib import messages
from .models import Item, ItemForm, Menu
class ItemCreation(CreateView):
initial = {'size':'L'}
model = Item
form_class = ItemForm
success_url = reverse_lazy('items:index')
def form_valid(self,form):
super(ItemCreation,self).form_valid(form)
# Add action to valid form phase
messages.success(self.request, 'Item created successfully!')
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self,form):
# Add action to invalid form phase
return self.render_to_response(self.get_context_data(form=form))
Listing 9-13.Django class-based view with CreateView with form_valid() and form_invalid()
正如您在清单 9-13 中看到的,form_valid()方法通过它的form输入参数获得对表单实例的访问,这反过来允许您在表单通过验证阶段时在表单上执行操作。在这种情况下,没有对表单本身采取任何操作来简化事情,但是添加了一个额外的逻辑来说明定制过程。
清单 9-13 中form_valid()的第一步是调用父类的form_valid()方法(即CreateView)来运行其表单验证设置逻辑,这确保了基类的表单验证首先运行,以验证任何违反表单规则的情况(例如,向表单添加错误)。如果对父类的form_valid()方法的调用检测到了违反规则的情况,那么该类的form_valid()方法(即列出 9-13 的方法)就会短路并退回到form_invalid()方法。如果父方法form_valid()通过,清单 9-13 中的逻辑继续添加一个 Django 消息框架成功消息以呈现给用户。
Tip
还可以通过 mixin 将 Django 消息框架成功消息添加到基于类的视图表单验证过程中。本章的最后一节描述了基于类的视图的混合。
最后,因为您正在处理一个有效的表单工作流,清单 9-13 中的form_valid()方法必须显式地将用户重定向到一个位置来完成它的工作。在这种情况下,标准 Django HttpResponseRedirect方法与基于类的视图的get_success_url()方法一起使用,最后一个方法获取基于类的视图success_url字段值。
清单 9-13 中的form_invalid()方法并不特别处理无效表单。它只是通过基于类的视图方法完成最少量的工作,即将控制返回到同一个模板位置,并添加包含有错误的表单的上下文以呈现给用户。
正如您所看到的,基于CreateView类的视图中的form_valid()和form_invalid()方法的好处是它们允许您定制表单验证工作流,而不需要修改CreateView工作流的其他部分(例如,将表单数据保存到数据库)。
自定义视图方法工作流:get()和 post()方法
除了前面对基于类的视图的定制选项之外,还可以使用get()或post()方法对基于类的视图完成的工作流进行绝对控制。get()方法用于接入与基于类的视图相关联的 HTTP GET 工作流,而post()方法用于接入与基于类的视图相关联的 HTTP POST 工作流。
虽然使用get()或post()方法可以为基于类的视图提供最大的灵活性,但是它们也需要显式声明基于类的视图的初始化、验证和重定向序列。这意味着使用get()或post()方法的基于类的视图可以更类似于标准的开放式 Django 视图——在第二章中描述——而不是之前在清单 9-9 中呈现的简洁的基于类的视图。
尽管如此,有时基于类的视图的吸引力如此之大,使用get()或post()方法是有保证的。清单 9-14 展示了一个在基于CreateView的视图中使用get()和post()方法的例子。
# views.py
from django.views.generic.edit import CreateView
from django.shortcuts import render
from django.contrib import messages
class ItemCreation(CreateView):
initial = {'size':'L'}
model = Item
form_class = ItemForm
success_url = reverse_lazy('items:index')
template_name = "items/item_form.html"
def get(self,request, *args, **kwargs):
form = super(ItemCreation, self).get_form()
# Set initial values and custom widget
initial_base = self.get_initial()
initial_base['menu'] = Menu.objects.get(id=1)
form.initial = initial_base
form.fields['name'].widget = forms.widgets.Textarea()
# return response using standard render() method
return render(request,self.template_name,
{'form':form,
'special_context_variable':'My special context variable!!!'})
def post(self,request,*args, **kwargs):
form = self.get_form()
# Verify form is valid
if form.is_valid():
# Call parent form_valid to create model record object
super(ItemCreation,self).form_valid(form)
# Add custom success message
messages.success(request, 'Item created successfully!')
# Redirect to success page
return HttpResponseRedirect(self.get_success_url())
# Form is invalid
# Set object to None, since class-based view expects model record object
self.object = None
# Return class-based view form_invalid to generate form with errors
return self.form_invalid(form)
Listing 9-14.Django class-based view with CreateView with get() and post()
您可以在清单 9-14 中看到,get()和post()方法都可以访问一个request输入——一个HttpRequest实例——就像标准视图方法一样。这个request引用允许基于类的视图访问请求数据,例如 HTTP 元数据(例如,用户的 IP 地址),这是在第二章中详细描述的主题。
清单 9-14 中get()方法的第一步是使用父类的get_form()方法(即CreateView)创建一个未绑定表单。因为get()方法让您可以完全控制工作流,所以使用标准表单语法(例如form = ItemForm())创建未绑定表单同样有效,但是因为它是基于类的视图,所以该示例利用了基于类的视图构造。一旦创建了未绑定表单,就会在表单上设置一系列初始值和一个自定义小部件,就像在前面基于类的视图示例中所做的那样。
一旦未绑定表单准备好了,注意到get()方法的return语句使用了常规视图方法中使用的标准render()方法。在这种情况下,render()方法将控制重定向到基于类的视图模板,并用未绑定的form和一个额外的special_context_variable变量设置模板上下文,以便在模板中使用。
接下来,清单 9-14 中的post()方法负责处理包含用户数据的表单。post()方法的第一步是使用基于类的视图get_form()类获得一个绑定表单实例。类似于get()方法,使用标准表单语法(如form = ItemForm(request.POST))创建绑定表单同样有效。
对于绑定的表单实例,将进行检查以验证用户提供的表单数据是否有效,就像对标准表单(例如,form.is_valid())所做的那样。如果表单数据有效,则调用父类的is_valid()方法(即CreateView),确保在表单有效时执行基于类的视图的核心逻辑(例如将表单数据保存到数据库)。这里可以再次使用任何标准的模型构造,但是调用父类的is_valid()方法更容易执行这个例程逻辑来创建一个模型对象记录。一旦例程验证逻辑完成,就会添加一条成功消息呈现给最终用户,并重定向到基于类的视图的成功 url。如果表单数据无效,基于类的视图的对象字段被设置为None——因为基于类的视图期望在后期处理中处理一个对象记录实例——并且使用负责底层细节的基于类的form_invalid()方法返回控制,而不是使用render()方法创建标准的视图方法响应。
正如您现在从清单 9-14 中的例子所理解的,基于CreateView类的视图可以像标准视图方法一样灵活。这只是了解和理解基于类的视图所支持的不同字段和方法的问题。当然,如果你觉得基于CreateView类的视图的定制逻辑变得过于笨拙,你可以随时回到第二章中介绍的标准视图方法。
使用基于类的视图 ListView 和 DetailView 读取模型记录
与创建模型记录的过程类似,读取模型记录的过程也遵循一个对所有模型几乎相同的过程:创建一个查询来获取模型记录,然后使用一个模板来显示模型记录。Django ListView和DetailView基于类的视图是专门为减少分别显示 Django 模型记录列表和单个 Django 模型记录所需的样板代码而设计的。
ListView基于类的视图可以快速建立一个模型记录列表的查询,并在模板中显示它们。清单 9-15 展示了一个基于类的视图,它使用了ListView基于类的视图。
# views.py
from django.views.generic.list import ListView
from .models import Item
class ItemList(ListView):
model = Item
# urls.py
from django.conf.urls import url
from coffeehouse.items import views as items_views
urlpatterns = [
url(r'^$',items_views.ItemList.as_view(),name="index"),
]
# templates/items/item_list.html
{% regroup object_list by menu as item_menu_list %}
{% for menu_section in item_menu_list %}
<li>{{ menu_section.grouper }}
<ul>
{% for item in menu_section.list %}
<li>{{item.name|title}}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
Listing 9-15.Django class-based view with ListView to read list of records
清单 9-15 中的第一个定义是ItemList类,它继承了基于ListView类的视图类的行为。设置为Item的ItemList类中的model字段告诉 Django 生成所有Item模型记录的列表(例如Item.objects.all())。
接下来,使用as_view()方法将ListView基于类的视图连接到一个根 url 正则表达式–r'^$',最后一个方法可用于所有基于类的视图,并且在上一节中也用于设置一个CreateView基于类的视图。
最后,清单 9-15 的最后一部分展示了在object_list引用上生成循环的模板item_list.html,最后一个是默认的上下文变量,由包含模型记录列表的ListView基于类的视图使用。
概括一下,ListView基于类的视图最重要的默认行为如下:
记录列表由model选项中定义的模型的所有记录组成。
呈现记录列表的模板使用项目的TEMPLATES目录路径下的约定<app_name>/<model_name>_list.html。
传递给模板(即包含记录的模板)的上下文变量被命名为object_list。
正如您在这个例子中看到的,一个基于ListView类的视图减少了向一个字段显示模型记录列表所需的样板代码。
DetailView基于类的视图是另一个记录读取构造,旨在快速建立单个记录查询并在模板中呈现结果。清单 9-16 展示了使用DetailView类的基于类的视图。
# views.py
from django.views.generic. import DetailView
from .models import Item
class ItemDetail(DetailView):
model = Item
# urls.py
from django.conf.urls import url
from coffeehouse.items import views as items_views
urlpatterns = [
url(r'^(?P<pk>\d+)/$',items_views.ItemDetail.as_view(),name="detail"),
]
# templates/items/item_detail.html
<h4> {{item.name|title}}</h4>
<p>{{item.description}}</p>
<p>${{item.price}}</p>
<p>For {{item.get_size_display}} size: Only {{item.calories}} calories
{% if item.drink %}
and {{item.drink.caffeine}} mg of caffeine.</p>
{% endif %}
</p>
Listing 9-16.Django class-based view with DetailView to read model record
清单 9-16 中的第一个定义是ItemDetail类,它继承了基于DetailView类的视图类的行为。ItemDetail类中的model字段指向Item,它告诉 Django 获取一个Item模型记录。与清单 9-15 中读取所有模型记录的ListView基于类的视图类不同,DetailView基于类的视图类必须总是将其模型查询限制在单个记录中,这就是基于类的视图 url 定义发挥作用的地方。
清单 9-16 中的DetailView基于类的视图被挂接到一个根 url 正则表达式——r'^$'——使用as_view()方法——就像其他基于类的视图一样——但是请注意 url 定义包含了(?P<pk>\d+) url 参数,该参数被传递给基于类的视图以将模型查询限定为一条记录。
例如,如果在 url /items/1/上发出一个请求,1被分配给pk参数,该参数被传递给基于类的视图,以构建模型查询Item.objects.get(pk=1),该模型查询通过pk=1获得Item模型记录——注意pk字段代表主键,通常相当于id字段。
以这种方式,当在DetailView基于类的视图上作出的 url 请求改变时(例如/items/2/、/items/3/),对模型 a 记录作出的后备查询也改变,并且记录返回以显示在模板中。
最后,清单 9-16 的最后一部分展示了模板item_detail.html,它输出代表模型记录的item引用的各个字段。在这种情况下,使用item引用是因为DetailView基于类的视图为模型记录使用的默认上下文变量是模型本身的名称。
概括一下,DetailView基于类的视图最重要的默认行为如下:
模型记录是基于model选项和 url pk参数确定的,该参数根据模型的主键将查询限定为单个记录。
呈现记录的模板使用项目的TEMPLATES目录路径下的约定<app_name>/<model_name>_detail.html。
传递给模板(即包含记录的模板)的上下文变量以基于类的视图的model命名(例如,如果model=Item,则上下文变量命名为item)。
正如您在这个例子中看到的,从DetailView类继承其行为的基于类的视图减少了呈现单个模型记录所需的样板代码。
ListView 字段和方法
类似于前面介绍的CreateView基于类的视图,可以覆盖ListView基于类的视图的许多默认行为。
事实证明,ListView基于类的视图继承了许多其他 Django 基于类的视图的行为,如下所示:
django.views.generic.list.MultipleObjectTemplateResponseMixin
django.views.generic.base.TemplateResponseMixin
django.views.generic.list.BaseListView
django.views.generic.list.MultipleObjectMixin
django.views.generic.base.View
Note
基于类的视图从它的父类继承了许多字段和方法。以下选项是最常见的选项;要获得详尽的列表,请查阅每个ListView父类。
基本 ListView 选项:模型字段
正如您在清单 9-15 中看到的,基于ListView类的视图实现的基本逻辑是从一个模型中创建一个记录列表。因此,model字段选项对于指示在哪个模型上创建记录列表至关重要。
接下来的部分描述了如何定制一个基于ListView类的视图,包括如何为一个记录列表定界和生成多个页面,以及如何覆盖其他默认行为。
自定义模板上下文引用名称:上下文对象名称
在清单 9-15 中,您可以看到ListView基于类的视图的模板使用了不太友好的上下文变量object_list。这是默认行为,但是为了帮助模板编辑器,可以使用不同的上下文变量来包含记录列表。
context_object_name字段用于定义一个定制的上下文变量名,以操作模板内的记录列表(如context_object_name = 'item_list')。
Tip
基于ListView类的视图也从许多与前面描述的CreateView类相同的类中继承它的行为。因此,你也可以使用template_name字段来指定一个自定义模板名;用于指定 MIME 类型的content_type字段;和get_context_data()方法来改变模板使用的上下文。
自定义记录列表:查询集和排序字段以及分页行为
默认情况下,一个基于ListView类的视图为属于一个模型的所有记录生成一个列表。虽然这种行为是合理的,但也有必要创建一个基于ListView类的视图来返回更有限的标准,在这种情况下,有必要对支持模型查询进行定界。queryset字段用于定义一个定制查询来生成一个记录列表。
自定义记录列表的另一个有用选项是指定生成记录列表的字段顺序。类似于模型查询中使用的标准order_by()方法,基于ListView类的视图可以指定ordering字段来定义记录列表的排序顺序。
清单 9-17 展示了在基于ListView类的视图中queryset和ordering字段的使用。
# views.py
from django.views.generic.list import ListView
from .models import Item
class ItemList(ListView):
model = Item
queryset = Item.objects.filter(menu__id=1)
ordering = ['name']
Listing 9-17.Django class-based view with ListView to reduce record list with queryset
正如您在清单 9-17 中看到的那样,queryset字段被分配了一个标准模型查询来生成带有menu id=1的Item记录。通过这种方式,由ListView基于类的视图传递给模板的结果记录列表只包含符合这些标准的Item记录。此外,清单 9-17 还利用了ordering = ['name']字段,在这种情况下,它确保生成记录列表的查询按照Item模型的name字段排序。
虽然queryset选项有助于限定基于ListView类的视图所显示的记录列表的大小,但有时有必要处理一个大的记录列表,并且仍然能够限定模板中显示的结果数量。对于这种情况,您可以使用分页将大型记录列表拆分到多个页面上。
由于根据定义分页依赖于多个页面的使用,这迫使您不仅要调整基于类的视图定义,还要调整基于类的视图使用的 url 结构和模板以支持多个页面。清单 9-18 展示了一个基于ListView类的视图的分页示例,它基于清单 9-15 中的示例。
# views.py
from django.views.generic.list import ListView
from .models import Item
class ItemList(ListView):
model = Item
paginate_by = 5
# urls.py
from django.conf.urls import url
from coffeehouse.items import views as items_views
urlpatterns = [
url(r'^$',items_views.ItemList.as_view(),name="index"),
url(r'^page/(?P<page>\d+)/$',items_views.ItemList.as_view(),name="page"),
]
# templates/items/item_list.html
{% regroup object_list by menu as item_menu_list %}
{% for menu_section in item_menu_list %}
<li>{{ menu_section.grouper }}
<ul>
{% for item in menu_section.list %}
<li>{{item.name|title}}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
{% if is_paginated %}
{{page_obj}}
{% endif %}
Listing 9-18.Django class-based view with ListView to read list of records with pagination
清单 9-18 中的相关分页逻辑以粗体显示。首先,paginate_by = 5被添加到基于类的视图定义中,它告诉 Django 将记录列表限制为每页 5 条记录。由于ListView基于类的视图将需要页码来显示查询的前 5 条记录之外的记录——因为页面被限制为 5 个——获取页码的自然位置是通过 url(例如,/page/1/创建查询的前 5 条记录的列表,/page/2/创建记录 6 到 10 的列表,等等)。).
接下来在清单 9-18 中,您可以看到一个新的 url 定义,其中的(?P<page>\d+)参数连接到了ListView基于类的视图。这个 url 定义允许一个匹配正则表达式模式(例如,/page/1/,/page/2/)的请求将 url page参数值传递给基于类的视图。当基于ListView类的视图检测到存在paginate_by选项的 url page参数值时,它调整对记录列表的查询,以基于页面将适当的记录集返回给模板(例如,/page/1/生成整个查询的 1 到 5 的记录列表,/page/2/生成整个查询的 6 到 10 的记录列表,等等)。).
最后,清单 9-18 中的最后一部分代表了使用分页的ListView基于类的视图的支持模板。{% is_paginated %}标签和{{page_obj}}上下文引用用于让用户知道他们在整个记录列表的哪一页。
Tip
一个ListView基于类的视图也可以使用get()基于类的视图方法来获得对视图工作流的完全控制。参见上一节关于CreateView基于类的视图,它描述了如何使用它及其含义。
详细视图字段和方法
像其他基于类的视图一样,DetailView基于类的视图也可以用定制字段和方法来创建,以覆盖它们的默认行为。
事实证明,DetailView类从许多其他基于 Django 类的视图中继承了它的行为,这些视图在下面的列表中描述:
django.views.generic.detail.SingleObjectTemplateResponseMixin
django.views.generic.base.TemplateResponseMixin
django.views.generic.detail.BaseDetailView
django.views.generic.detail.SingleObjectMixin
django.views.generic.base.View
Note
基于类的视图从它的父类继承了许多字段和方法。以下选项是最常见的选项;要获得详尽的列表,请查阅每个DetailView父类。
基本详细信息视图选项:带 pk 参数的模型字段和 url
正如您在清单 9-16 中看到的,基于DetailView类的视图实现的基本逻辑是获取一个模型记录并在模板中显示它。因此,model字段是这种基于类的视图的重要部分之一,必须始终提供。
为了获得一个特定的模型记录,一个基于类的视图还必须定义一个 url 参数来帮助它选择一个模型记录。默认情况下,这个 url 参数必须被命名为pk,它的值用于使用pk字段对model值执行查询,这个字段通常相当于模型的id字段。
Tip
一个基于类的视图也从前面描述的许多基于类的视图类中继承了它的行为。因此,您也可以使用template_name字段来指定一个自定义模板名称;用于指定 MIME 类型的content_type字段;改变模板使用的上下文的get_context_data()方法;以及context_object_name字段来声明不同的上下文变量以访问模板中的记录。
自定义 url 和查询参数:pk_url_kwarg,slug_field 和 slug_url_kwarg
默认情况下,DetailView基于类的视图期望一个名为pk的 url 参数来决定通过主键查询哪个模型记录。然而,如果你已经有了一个预先存在的 url 或者只是不喜欢这个不明确的 url 参数名,你可以用pk_url_kwarg字段指定一个自定义的 url 参数。
例如,在基于DetailView类的视图上设置pk_url_kwarg='item_id',允许将 url 定义为url(r'^(?P<item_id>\d+)/$',items_views.ItemDetail.as_view())而不是默认的url(r'^(?P<pk>\d+)/$',items_views.ItemDetail.as_view())。
尽管使用pk字段执行基于DetailView类的视图的记录查询是一种常见的做法——考虑到pk字段通常是一个带有数据库索引的整数字段,以提高查找性能——有时可能有必要使用另一个模型字段执行基于DetailView类的记录查询(例如,创建更多用户/SEO 友好的 URL,如/item/capuccino/,而不是/item/1/)。
清单 9-19 展示了一个基于DetailView类的视图,它使用slug_field选项来允许对模型字段而不是pk进行模型记录查询。
# views.py
# views.py
from django.views.generic import DetailView
from .models import Item
class ItemDetail(DetailView):
model = Item
slug_field = 'name__iexact'
# urls.py
from django.conf.urls import url
from coffeehouse.items import views as items_views
urlpatterns = [
url(r'^(?P<slug>\w+)/$',items_views.ItemDetail.as_view(),name="detail"),
]
# Template identical to listing to template in Listing 9-16
Listing 9-19.Django class-based view with DetailView and slug_field option
清单 9-19 中的第一个重要方面是,url 定义有一个名为slug的 url 参数,由于使用了w+正则表达式,它可以匹配任何单词模式。这意味着像/items/espresso/和/items/latte/这样的 url 匹配这个 url,不像以前的DetailView基于类的 URL 定义使用pk参数来匹配数值和d+正则表达式。
当一个DetailView基于类的视图接收到一个名为slug的 url 参数时,它会通知基于类的视图,模型记录查询应该在一个模型字段而不是pk上进行。因为这个slug值可能是不明确的(例如,它可以表示几个模型字段值中的一个,如名称、成本或描述),所以有必要限定slug值表示哪个模型字段,这就是slug_field字段的目的。
在清单 9-19 的情况下,slug_field字段被设置为name__iexact,这告诉DetailView基于类的视图使用Item模型name字段上的slug值执行不区分大小写的模型查询。不区分大小写的查询由__iexact查找提供,在数据库记录值大小写混合的情况下很重要(例如,url /items/capuccino/将匹配Item名称记录capuccino、Capuccino,或CAPUCCINO)。如果没有大小写发明,Item名称记录将需要精确地capuccino来匹配 url)。
尽管清单 9-19 中的示例中没有提供,基于DetailView类的视图的另一个可用选项是slug_url_kwarg选项,它与前面描述的pk_url_kwarg选项类似。
默认情况下,使用 slug 查询(即非pk查询)的基于DetailView类的视图期望一个名为slug的 url 参数来确定对哪个模型字段进行查询以获得记录。但是,如果您已经有一个预先存在的 url 或者只是不喜欢这个不明确的 url 参数名称,您可以使用slug_url_ kwarg字段指定一个自定义的 url 参数。
例如,在基于DetailView类的视图上设置slug_url_kwarg='item_name',允许将 url 定义为url(r'^(?P<item_name>\w+)/$',items_views.ItemDetail.as_view())而不是默认的url(r'^(?P<slug>\w+)/$',items_views.ItemDetail.as_view())。
Tip
一个DetailView基于类的视图也可以使用get()基于类的视图方法来获得对视图工作流的完全控制。参见上一节关于CreateView基于类的视图,描述了如何使用它及其含义。
用基于类的视图更新模型记录
当您更新 Django 模型记录时,典型的过程包括从数据库中获取想要更新的模型记录,在表单中显示模型数据以便用户可以进行更改,最后将更改保存回数据库。基于 Django UpdateView类的视图是专门为减少更新 Django 模型记录所需的样板代码而设计的。
由于其功能性,UpdateView基于类的视图的操作类似于通过表单创建模型记录的CreateView和显示模型记录的DetailView的组合。清单 9-20 展示了一个UpdateView基于类的视图的例子。
# views.py
from django.views.generic import UpdateView
from .models import Item
class ItemUpdate(UpdateView):
model = Item
form_class = ItemForm
success_url = reverse_lazy('items:index')
# urls.py
from django.conf.urls import url
from coffeehouse.items import views as items_views
urlpatterns = [
url(r'^edit/(?P<pk>\d+)/$', items_views.ItemUpdate.as_view(), name='edit'),
]
# templates/items/item_form.html
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">
{% if object == None%}Create{% else %}Update{% endif %}
</button>
</form>
Listing 9-20.Django class-based view with UpdateView to edit a record
清单 9-20 中的第一个定义是从基于UpdateView类的视图类继承其行为的ItemUpdate类。ItemUpate类中的model字段为Item设置一个值,它告诉 Django 更新Item模型记录。此外,因为您正在处理一个更新操作,Item模型记录必须以一种形式呈现——这是from_class选项的目的——以及定义一个成功的 url 来在更新成功时重定向用户,这是success_url选项的目的。
清单 9-20 中的下一个是链接到UpdateView基于类的视图类的 url 定义。因为基于UpdateView类的视图必须提供一个特定的模型记录来编辑,所以它依赖于一个 url 参数来限制对单个记录的查询。在这种情况下,注意 url 包含传递给基于类的视图的pk url 参数——就像用DetailView基于类的视图一样。
例如,如果请求 url /items/edit/1/,1作为pk参数被传递给UpdateView基于类的视图,该视图又执行查询Item.objects.get(pk=1)来检索记录,并以表格的形式呈现出来以供编辑。
最后,清单 9-20 中的最后一部分展示了模板item_form.html,它显示了要编辑的表单中的记录。默认的UpdateView基于类的视图模板的一个重要方面是它与CreateView基于类的视图使用的默认模板相同。这是因为两个基于类的视图共享该功能的相同的基于类的父视图类,更不用说表单的目的是相同的(例如,CreateView基于类的视图中的表单是空的,供用户填写新的记录数据,而UpdateView基于类的视图中的表单是用预填充的记录发送的,以更新记录数据)。
出于这个原因,清单 9-20 中的模板与清单 9-9 中用于基于CreateView类的视图的模板几乎相同。微小的区别是,如果模板在其上下文中检测到object变量的存在,这意味着UpdateView基于类的视图正在传递一个记录进行编辑,并且表单按钮被设置为Update;否则,如果没有出现object变量,这意味着CreateView基于类的视图正在调用模板,表单按钮被设置为Create。
概括一下,UpdateView基于类的视图最重要的默认行为:
要更新的模型记录是根据model选项和 url pk参数确定的,该参数根据模型的主键将查询限定为单个记录。
呈现表单以更新记录的模板使用项目的TEMPLATES目录路径下的约定<app_name>/<model_name>_form.html,注意它与CreateView基于类的视图使用的默认模板相同,对于这种情况,您可以依靠object上下文变量的存在来确定是否是UpdateView调用了模板。
传递给模板(即包含记录的模板)的上下文变量被命名为object,尽管表单会自动填充这个记录值。
正如您在这个例子中看到的,从UpdateView类继承其行为的基于类的视图减少了编辑模型记录所需的样板代码。
更新视图字段和方法
像其他基于类的视图一样,UpdateView基于类的视图也可以用定制字段和方法来创建,以覆盖它们的默认行为。
事实证明,UpdateView类继承了许多其他基于 Django 类的视图的行为,如下所示:
django.views.generic.detail.SingleObjectTemplateResponseMixin
django.views.generic.base.TemplateResponseMixin
django.views.generic.edit.BaseUpdateView
django.views.generic.edit.ModelFormMixin
django.views.generic.edit.FormMixin
django.views.generic.detail.SingleObjectMixin
django.views.generic.edit.ProcessFormView
django.views.generic.base.View
Note
基于类的视图从它的父类继承了许多字段和方法。以下选项是最常见的选项;要获得详尽的列表,请查阅每个UpdateView父类。
基本更新视图选项:模型、form_class 和 success_url 字段,以及带有 pk 参数的 url
正如您在清单 9-20 中看到的,基于UpdateView类的视图实现的基本逻辑是获取一个模型记录,并以一种形式显示出来以供编辑。因此,model字段是这种基于类的视图的重要部分之一,必须始终提供。此外,因为记录是通过表单更新的,所以还需要通过form_class选项指定一个表单,以及当更新成功时通过success_url选项指定一个成功 url。
为了获得一个特定的模型记录,一个基于UpdateView类的视图还必须定义一个 url 参数来帮助它选择一个模型记录进行编辑。默认情况下,这个 url 参数必须被命名为pk,它的值用于使用pk字段对model值执行查询,这个字段通常相当于模型的id字段。
Tip
一个基于类的视图也从前面描述的许多基于类的视图类中继承了它的行为。因此,您也可以使用template_name字段来指定一个自定义模板名称;用于指定 MIME 类型的content_type字段;改变模板使用的上下文的get_context_data()方法;以及context_object_name字段来声明不同的上下文变量以访问模板中的记录。
此外,因为一个基于UpdateView类的视图需要获得一个模型记录,所以它也可以使用pk_url_kwarg字段来接受一个不同于pk的 url 参数;slug_field字段指定一个备选记录查询字段;和slug_url_kwarg字段来接受不同于slug命名的不同 url 参数。
最后,UpdateView基于类的视图也可以使用get()和post()基于类的视图方法来获得对视图工作流的完全控制。前面的基于类的视图部分描述了如何使用所有这些基于类的视图字段和方法。
使用基于类别的视图删除视图删除记录
当您删除 Django 模型记录时,除了执行实际的删除操作之外,向用户显示一个确认页面通常是一个很好的实践。Django DeleteView基于类的视图是专门为减少删除模型记录所需的样板代码量而设计的,同时显示一个确认页面。清单 9-21 展示了一个DeleteView基于类的视图示例。
# views.py
from django.views.generic.edit import DeleteView
from .models import Item
class ItemDelete(UpdateView):
model = Item
success_url = reverse_lazy('items:index')
# urls.py
from django.conf.urls import url
from coffeehouse.items import views as items_views
urlpatterns = [
url(r'^delete/(?P<pk>\d+)/$', items_views.ItemDelete.as_view(), name='delete'),
]
# templates/items/item_confirm_delete.html
<form method="post">
{% csrf_token %}
Do you really want to delete "{{ object }}"?
<button class="btn btn-primary" type="submit">Yes, remove it!</button>
</form>
Listing 9-21.Django class-based view with DeleteView to delete record
清单 9-21 中的第一个定义是ItemDelete类,它继承了基于DeleteView类的视图类的行为。ItemDelete类中的model字段为Item设置一个值,它告诉 Django 删除Item模型记录。此外,因为您希望用户确认删除操作,所以还必须声明success_url选项,以指定删除操作成功后将用户带到哪里。
清单 9-21 中的下一个是链接到DeleteView基于类的视图类的 url 定义。因为基于类的视图必须被告知删除哪个模型记录,所以它依赖 url 参数来提供这个查询分隔符。在这种情况下,注意 url 包含传递给基于类的视图的pk url 参数——就像用DetailView基于类的视图一样。
例如,如果请求 url /items/delete/1/,1作为pk参数被传递给DeleteView基于类的视图,后者接着执行查询Item.objects.get(pk=1)来准备删除记录。
最后,清单 9-21 的最后一部分展示了用于面向用户的删除序列的item_confirm_delete.html模板。注意,这个模板包含一个伪表单(即没有表单字段),带有一个问题和提交按钮。这种伪表单的原因是item_confirm_delete.html模板具有双重功能。
如果在一个基于DeleteView类的视图上发出一个 HTTP GET 请求,那么一个带有这个伪表单的页面将返回给用户,向他提出问题Do you really want to delete "{{ object }}"?,其中object是要删除的模型记录。如果用户点击这个伪表单提交按钮,它会向同一个基于DeleteView类的视图发出一个 HTTP POST 请求——注意<form method="post">标记——调用实际的删除过程。以这种方式,这种具有伪形式的模板允许用户在点击 url 删除链接(例如/items/delete/1/)之后确认他是否真的想要删除记录,而不是在点击 url 删除链接时立即删除记录。
概括一下,DeleteView基于类的视图最重要的默认行为如下:
要删除的模型记录是根据model选项和 url pk参数确定的,该参数根据模型的主键将查询限定为单个记录。
呈现表单以更新记录的模板使用项目的TEMPLATES目录路径下的约定<app_name>/<model_name>_confirm_delete.html。
传递给模板(即包含要删除的记录的模板)的上下文变量被命名为object。
对基于DeleteView类的视图的 HTTP GET 请求显示来自<app_name>/<model_name>_confirm_delete.html模板的确认页面。基于DeleteView类的视图上的 HTTP POST 请求执行实际的删除过程。
正如您在这个例子中看到的,从DeleteView类继承其行为的基于类的视图减少了删除模型记录所需的样板代码。
删除视图字段和方法
像其他基于类的视图一样,DeleteView基于类的视图也可以用定制字段和方法来创建,以覆盖它们的默认行为。
事实证明,DeleteView类继承了许多其他基于 Django 类的视图的行为,如下所示:
django.views.generic.detail.SingleObjectTemplateResponseMixin
django.views.generic.base.TemplateResponseMixin
django.views.generic.edit.BaseDeleteView
django.views.generic.edit.DeletionMixin
django.views.generic.detail.BaseDetailView
django.views.generic.detail.SingleObjectMixin
django.views.generic.base.View
Note
基于类的视图从它的父类继承了许多字段和方法。以下选项是最常见的选项;要获得详尽的列表,请查阅每个DeleteView父类。
基本删除视图选项:模型和 success_url 字段以及带有 pk 参数的 url
正如您在清单 9-21 中看到的,基于DeleteView类的视图实现的基本逻辑是获取一个模型记录,并在显示确认页面时删除它。因此,model字段是这种基于类的视图的重要部分之一,必须始终提供。此外,因为记录被设置为删除,所以还需要通过success_url选项指定一个成功的 url,以便在记录被删除后重定向用户。
为了获得一个特定的模型记录,一个基于类的视图还必须定义一个 url 参数来帮助它选择一个要删除的模型记录。默认情况下,这个 url 参数必须被命名为pk,它的值用于使用pk字段对model值执行查询,这个字段通常相当于模型的id字段。
Tip
一个基于类的视图也从前面描述的许多基于类的视图类中继承了它的行为。因此,您也可以使用template_name字段来指定一个自定义模板名称;用于指定 MIME 类型的content_type字段;改变模板使用的上下文的get_context_data()方法;以及context_object_name字段来声明不同的上下文变量以访问模板中的记录。
此外,因为一个基于DeleteView类的视图需要获得一个模型记录,所以它也可以使用pk_url_kwarg字段来接受一个不同于pk的 url 参数;slug_field字段指定一个备选记录查询字段;和slug_url_kwarg字段来接受不同于slug命名的不同 url 参数。
最后,DeleteView基于类的视图也可以使用get()和 post()基于类的视图方法来获得对视图工作流的完全控制。前面的基于类的视图部分描述了如何使用所有这些基于类的视图字段和方法。
带有混合的基于类的视图
尽管所有操作模型的基于类的视图通常遵循相同的工作流程来创建、读取、更新和删除模型记录,但是公平地说,在看到前面的基于类的视图部分之后,您会发现自己经常调整基于类的视图的默认行为。
虽然多次调整基于类的视图的方法和字段是完全合理的,但是如果您需要一次又一次地在几十个或几百个基于类的视图中获得相同的功能,这可能会令人厌烦。
当您被迫在基于类的视图中定义一个get()或post()方法,以包含一些在基于类的视图中不支持的功能(例如,CreateView、ListView、DetailView、UpdateView、DeleteView)时,这一点尤为明显,这需要输入一个冗长的工作流,如果重复多次,结果可能会是重复的。为了减少基于类的视图上下文中的重复定制,可以使用 mixins。
首先,您已经在基于类的视图环境中使用了 mixins,即使您没有意识到这一点。如果您仔细观察为前面章节中描述的基于类的模型视图提供行为的基于类的视图类,您可能会注意到许多视图类的名称中都包含术语 mixin(例如,ModelFormMixin、FormMixin、SingleObjectMixin)。
软件 mixin 是一个允许类之间的类继承行为的构造,特别强调类继承。当您使用类继承时,父子类关系通常用“是一个”术语来描述(例如,如果饮料类从项目类继承其行为,则饮料是一个项目)。另一方面,mixin 类允许一个类采用 mixin 类的行为,而没有“是 a”行为。
换句话说,mixin 类是向类添加功能的一种方式,mixin 类充当可重用的组件。例如,您可以拥有一个由 Store 类和 OnlineStore 类使用的 mixin Checkout 类,这允许在任何其他类中重用 mixin 类的功能。注意,对于 mixin 类,继承“是 a”行为并不适用,你不能说 Store 是 Checkout 或 OnlineStore 是 Checkout,它更多的是“使用 a”行为。因此,尽管 mixin 类——从语义上来说——用于继承行为,但从技术上来说,它们不使用软件工程中通常所知的继承,因此使用了术语“类继承”。
那么为什么 mixins 对基于类的视图很重要呢?原来,您在上一节中学习的所有基于基类的视图类都是建立在基于类的视图混合之上的。这不仅意味着您可以混合和匹配 mixin 来创建定制的基于类的视图,而且您还可以创建或重用其他 mixin 来增强基于类的视图的功能。
早些时候,当你学习了基于类的视图时,你可能还记得清单 9-13 中的例子,9-14 增加了一个 Django 框架成功消息,当一个记录被创建时通知用户。在清单 9-13 的情况下,这需要接入form_valid()基于类的视图方法来添加这个消息,而在清单 9-14 的情况下,这需要接入post()基于类的视图方法。尽管这两种方法都非常有效,但是为了添加 Django 框架成功消息的唯一目的,在基于类的视图中声明这两种方法中的任何一种都需要做大量的工作。
通过在基于类的视图上使用 mixin,您可以简化将基于类的视图中的 Django 框架成功消息添加到单个字段,而不需要定制更复杂的方法和/或将逻辑添加到基于类的视图。
清单 9-22 展示了一个CreateView基于类的视图,该视图使用 mixin 来支持将 Django 框架成功消息添加到基于类的视图响应中的功能。
# views.py
from django.views.generic.edit import CreateView
from django.contrib.messages.views import SuccessMessageMixin
from .models import Item, ItemForm
class ItemCreation(SuccessMessageMixin,CreateView):
model = Item
form_class = ItemForm
success_url = reverse_lazy('items:index')
success_message = "Item %(name)s created successfully"
Listing 9-22.Django class-based view with CreateView and mixin class
Caution
在多重继承的基于类的视图(例如class ItemCreation(SuccessMessageMixin,CreateView))的上下文中,Mixin 类应该总是首先声明,以优先于粗粒度的基于类的视图类。
清单 9-22 的第一个重要方面是SuccessMessageMixin mixin 类的import语句,这使得任何基于类的视图都能够轻松添加成功消息。接下来,SuccessMessageMixin mixin 类和CreateView基于类的视图被添加到ItemCreation类中,为基于类的视图提供核心功能。注意,mixin 类是使用标准 Python 继承语法添加到基于类的视图中的。
一旦ItemCreation基于类的视图获得了对SuccessMessageMixin mixin 类行为的访问,生成成功消息所需要的就是在success_message字段中定义实际的消息。以这种方式,当在基于类的视图的上下文中发生成功操作时(例如,success_url被触发),基于类的视图自动将success_message值添加到请求中以显示给终端用户。
正如您在清单 9-22 中看到的,将 Django 框架成功消息添加到基于类的视图的过程被大大简化了,并通过 mixin 类实现了可重用性。当您将它与清单 9-13 和 9-14 中采用的方法进行比较时,这一点尤其正确,清单 9-13 和清单 9-14 中采用的方法由覆盖基于类的视图方法组成,这些方法需要大量的输入(许多是重复的)来完成一个简单的任务。
十、Django 用户管理
在这一章中,你将学习 Django 管理用户、组和权限。您将了解如何根据用户有条件地显示内容,如何限制 URL 和基于类的视图,以及如何基于用户权限管理 Django 模型记录的 CRUD(创建-读取-更新-删除)权限。
此外,您还将了解如何定制 Django 的内置用户模型以支持额外的数据字段,以及如何自动化用户管理任务,如注册、密码提醒和密码重置电子邮件。最后,您将了解如何创建定制的身份验证后端,以使用不同的技术验证用户凭证,包括允许用户使用脸书、谷歌或 Twitter 帐户访问 Django 应用。
Django 用户系统介绍
Django 用户系统基于内置于 Django 框架中的django.contrib.auth包。在本节中,您将了解这个包提供的核心概念,它是各种 Django 应用(包括 Django admin)使用的默认用户系统。
用户类型、子类型、组和权限
Django 用户类主要有两种类型:User和AnonymousUser。如果用户验证了自己的身份(即提供了有效的用户名/密码),Django 会将其识别为User。另一方面,如果用户只是在没有任何认证的情况下浏览应用,Django 会将他识别为AnonymousUser。
任何User都可以进一步分为各种子类型中的一种:
- 超级用户。-在 Django admin 中拥有创建、读取、更新和删除数据权限的最强大的用户,包括模型记录和其他用户。
- 工作人员。-标记为 staff 的用户可以访问 Django admin。但是在 Django admin 中创建、读取、更新和删除数据的权限必须明确授予用户。默认情况下,超级用户被标记为 staff。
- 活跃。-所有信誉良好的用户都被标记为活跃用户。被标记为不活跃的用户无法验证自己,这是一种常见的状态,如果有一个待定的注册后步骤(例如,确认电子邮件)或用户被禁止,而您不想删除他的数据。
Django 还提供了一个Group类的概念来授予一组用户相同的权限,而不必单独分配给他们。例如,您可以将权限授予某个组,然后将用户分配到该组,以使权限管理更容易。通过这种方式,您可以在一个步骤中撤销或添加一组用户的权限,并快速授予新用户相同的权限。
此外,您可以将 Django 权限粒度地分配给 a User或Group,以便它们在 Django 模型上执行 CRUD(创建-更新-删除)记录,这是一个通过权限模型记录完成的过程。或者,您也可以在 URL/视图方法或模板内容上分配粗粒度的 Django 权限,以授予User、Group,甚至Permission受让人访问权限。
现在您已经了解了 Django 用户系统背后的基本概念,让我们更详细地探索与 Django 用户相关的更常见的操作。
创建用户
您想要创建的第一个用户是超级用户。如果你已经在第一章中设置了 Django 管理员,那么你已经有了一个项目超级用户。无论哪种方式,我都会在这里重述这个过程,因为您可以拥有任意数量的超级用户。清单 10-1 展示了创建超级用户的各种方法。
Tip
参考本书附带的源代码来运行练习,以减少打字和自动访问测试数据。
[user@coffeehouse ∼]$ python manage.py createsuperuser
Username (leave blank to use 'admin'):
Email address: admin@coffeehouse.com
Password:
Password (again):
Superuser created successfully.
[user@coffeehouse ∼]$ python manage.py createsuperuser --username=bigboss
--email=bigboss@coffeehouse.com
Password:
Password (again):
Superuser created successfully.
[user@coffeehouse ∼]$ python manage.py shell
Python 2.7.3 (default, Apr 10 2013, 06:20:15)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> user = User.objects.create_superuser(username='angelinvestor',
email='angelinvestor@coffeehouse.com',
password='seedfunding')
>>>
Listing 10-1.Create Django superuser
Note
Django 用户名必须是唯一的;先前存在的用户名被拒绝。
正如您在清单 10-1 中看到的,您可以在manage.py实用程序中使用createsuperuser命令创建一个超级用户,在这里您需要输入用户名、电子邮件。和密码。你也可以用同样的createsuperuser命令创建一个超级用户,使用内嵌参数--username和--email,在这种情况下,你只需要输入密码。
此外,还可以通过 Django shell 使用带有create_superuser()方法的User模型类来创建超级用户。注意create_superuser()方法是如何要求相同的用户名、电子邮件和密码参数的。
当您以任何一种方式创建超级用户时,该用户也会被自动设置为职员并标记为活动用户,因此您不需要采取任何额外的步骤来访问 Django admin(需要职员权限)或继续进行身份验证(需要将用户标记为活动用户)。
有时候你只想创建一个普通用户,这种情况下你可以使用 Django 的 shell 实用程序,通过User模型直接创建一个用户。清单 10-2 说明了这一过程。
[user@coffeehouse ∼]$ python manage.py shell
Python 2.7.3 (default, Apr 10 2013, 06:20:15)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> user = User.objects.create_user(username='downtownbarista',
email='downtownbarista@coffeehouse.com',
password='cappuccino')
>>> user.is_staff
False
>>> user.is_active
True
>>> user.is_superuser
False
Listing 10-2.Create regular Django user through shell
正如您在清单 10-2 中看到的,在您获得对 Django shell 的访问权之后,您导入User模型类,并使用用户名、电子邮件和密码参数调用create_user()方法。create_user()方法的结果包含新创建的用户。
为了确认create_user()方法生成了一个普通用户,您可以在清单 10-2 中看到一个对各种User模型属性的调用,确认用户既不是员工也不是超级用户,只是被标记为活动的。
最后,还可以通过 Django admin 创建用户。要做到这一点,你首先需要确保你拥有 Django admin 的超级用户权限。一旦你进入 Django admin,你会看到一个如图 10-1 所示的屏幕,点击“用户”链接。“用户”链接将你带到如图 10-2 所示的屏幕,点击右上角的“添加用户+”按钮。点击“添加用户+”按钮,您将进入如图 10-3 所示的屏幕,在这里您可以介绍新用户的凭证。
图 10-3。
Django admin to create new user
图 10-2。
Django admin Users list
图 10-1。
Django admin site home page
如果您希望更改以这种方式创建的用户的子类型(即超级用户、职员),您也可以在 Django admin 中完成,这个过程将在下一节中描述。
管理用户
一旦用户在 Django 应用中,您将最终管理他。这种管理可以是撤销他的特权、增加他的特权,或者甚至编辑他的简档信息。您可以通过两种方式管理 Django 用户,在 Django admin 中或者通过在 Django shell 中或者直接在您的应用中操作User模型。
管理用户最简单的方法是直接在 Django admin 中。一旦你进入 Django admin,你会看到如图 10-1 所示的屏幕,如果你点击“用户”链接,你会被带到如图 10-2 所示的屏幕,其中包含 Django 用户列表。图 10-2 中显示的每个 Django 用户都有一个用户名链接,如果你点击这个链接,你会被带到用户页面,如图 10-4 、 10-5 和 10-6 所示,在这里你可以编辑用户的个人资料。
图 10-6。
Django admin change user page - Part 3
图 10-5。
Django admin change user page - Part 2
图 10-4。
Django admin change user page - Part 1
您可以编辑的用户资料的第一部分如图 10-4 所示。您可以在这里编辑他的用户名和密码——通过点击小文本末尾的“此表格”链接——他的名字、姓氏以及电子邮件。此外,您可以看到有三个复选框,可以在其中更改用户的活动、员工和超级用户状态。
如果你向下滚动,你会看到你可以编辑的用户资料的第二部分,如图 10-5 所示。在这里,您可以将一个用户分配到不同的组,以及分配一个用户对 Django 模型的 CRUD 权限。在这里,我建议您仔细评估为用户分配单独的 CRUD 权限的必要性;一种更灵活的方法是创建组,并为它们分配 CRUD 权限,然后将用户分配到组中,这样权限就更容易跟踪,并可供其他用户重用。
如果你继续向下滚动到最后,你会看到你可以编辑的用户资料的第三部分,如图 10-6 所示。在这里,您可以查看和更新用户的上次登录,以及用户的创建日期。在页面的右下角,您可以看到各种保存按钮,用于保存对页面所做的任何更改。此外,在左下角有一个“删除”按钮,可以完全删除用户;但是,我建议您考虑取消选中用户的活动状态来限制访问。这最后一步足以阻止用户再次访问应用,并保持他的其他数据不变,以防您想要撤销该操作。
修改用户资料的另一个选项是直接操作他的User模型记录。如清单 10-3 所示,首先查询所需的用户,然后修改模型属性或执行User模型助手方法之一。
[user@coffeehouse ∼]$ python manage.py shell
Python 2.7.3 (default, Apr 10 2013, 06:20:15)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> user = User.objects.get(id=1)
>>> user.username = 'superadmin'
>>> user.save()
>>> userbig = User.objects.get(username='bigboss')
>>> userbig.is_superuser
True
>>> userbig.superuser = False
>>> userbig.first_name = 'Big'
>>> userbig.last_name = 'Boss'
>>> userbig.save()
>>> userbig.is_superuser
False
>>> userbig.get_full_name()
u'Big Boss'
>>> userbarista = User.objects.get(email='downtownbarista@coffeehouse.com')
>>> userbarista.email ='barista@coffeehouse.com'
>>> userbarista.save()
>>> userbarista.set_password('mynewpass')
>>> userbarista.check_password('oldpass')
False
>>> userbarista.check_password('mynewpass')
True
Listing 10-3.Manage Django user through shell
正如您在清单 10-3 中看到的,您可以修改图 10-4 、 10-5 和 10-6 中 Django admin 中呈现的相同的User配置文件值。请注意,因为您正在进行查询,所以对字段所做的任何更改之后都必须调用对要持久化的字段的引用的save()方法。表 10-1 和表 10-2 包含User模型上可用的字段和方法的完整列表。
表 10-2。
Django django.contrib.auth.models.User methods
| 方法 | 描述 | | --- | --- | | 获取用户名() | 返回用户的用户名。由于用户模型可以更改为另一个用户模型,因此推荐使用这种方法,而不是直接引用 username 属性。 | | is_anonymous() | 对于用户,该方法总是返回 False 这只是用来区分用户和匿名用户的一种方式。 | | is_authenticated() | 对于用户来说,该方法总是返回 True,因为它仅用于确定用户是否已经通过了 AuthenticationMiddleware(表示当前登录的用户)。 | | 获取全名() | 返回名和姓字段,中间有一个空格。 | | get_short_name() | 返回名字。 | | 设置密码(原始密码) | 将用户的密码设置为给定的原始字符串,注意密码哈希。请注意,当 raw_password 为 None 时,密码被设置为不可用的密码,就像使用 set_unusable_password()一样。 | | 检查密码(原始密码) | 如果给定的原始字符串是用户的正确密码,则返回 True,并注意进行比较的密码哈希。 | | set_unusable_password() | 将用户标记为未设置密码。请注意,这不同于使用空白字符串作为密码。该用户的 check_password()永远不会返回 True。如果针对现有外部源(例如,LDAP 目录)进行身份验证,这将很有帮助。 | | has_usable_password() | 如果为用户调用了 set_unusable_password(),则返回 False。 | | 获取组权限(obj =无) | 为用户返回一组组权限字符串。如果 obj 被传递,则仅返回特定对象的组权限。 | | 获取所有权限(obj =无) | 为用户返回一组组和用户权限字符串。如果 obj 被传递,则仅返回特定对象的组权限。 | | has_perm(perm,obj=None) | 如果用户具有指定的权限,则返回 True,其中 perm 的格式为。”。注意如果用户处于非活动状态,该方法总是返回 False。如果 obj 被传递,检查发生在特定的对象上,而不是模型上。 | | has_perms(perm_list,obj=None) | 如果用户拥有每个指定的权限,则返回 True,其中每个 perm 的格式为。”。注意如果用户处于非活动状态,该方法总是返回 False。如果 obj 被传递,检查发生在特定的对象上,而不是模型上。 | | has_module_perms(包名) | 如果用户拥有给定包(即 Django 应用标签)中的权限,则返回 True。如果用户不活动,该方法总是返回 False。 | | email_user(主题,消息,from _ email =无,**kwargs) | 向用户发送电子邮件。如果 from_email 为 None,Django 使用 settings.py 中的缺省 _FROM_EMAIL。还要注意,该方法依赖于 Django 的 send_mail()方法,它向该方法传递**kwargs 参数。有关 send_mail()方法和**kwargs 值的更多详细信息,请参见 Django 电子邮件快捷方式方法, |表 10-1。
Django django.contrib.auth.models.User fields
| 田 | 描述 | | --- | --- | | 用户名 | (必需)30 个字符或更少,可以包含字母数字、_、@、+、。和-字符。 | | 名字 | (可选)30 个字符或更少。 | | 姓氏 | (可选)30 个字符或更少。 | | 电子邮件 | (可选)电子邮件地址。 | | 密码 | (必需)密码的散列和元数据。注意,Django 不存储原始密码。 | | 组 | 与 django . contrib . auth . models . group 的多对多关系 | | 用户权限 | 与 django.contrib.auth.Permission 的多对多关系。 | | is_staff(布尔值) | 指定用户是否可以访问管理网站。 | | is_active(布尔值) | 指定用户是否被视为活动用户。 | | is _ 超级用户 | (布尔值)指定用户是否拥有所有权限,而无需显式分配这些权限。 | | 最后一次登录 | 用户上次登录的日期时间,如果用户从未登录,则设置为 NULL。 | | 日期 _ 加入 | 指定帐户创建时间的日期时间。创建帐户时,默认设置为当前日期/时间。 |Tip
用户模型数据存储在数据库表 auth_user 中。
Password Strength Options
默认情况下,Django 强制密码遵循某些规则,比如不要与用户名相似,包含最少数量的字符,避免常用词,以及强制密码不仅仅由数字组成。这些密码规则是在项目的 settings.py 文件的 AUTH_PASSWORD_VALIDATORS 变量中定义的,如下所示:
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',
},
]
这个验证器列表可以被编辑以适应项目的需要,通过删除某些规则或者包含使它们对选项更严格。 1
创建和管理组
Django 组可以在 Django 管理中创建。用一个超级用户帐号,进入 Django admin,你会看到一个类似图 10-1 的屏幕,点击“Groups”链接。“组”链接将你带到如图 10-7 所示的屏幕,然后点击右上角的“添加组+”按钮。点击“添加群组+”按钮,您将进入如图 10-8 所示的屏幕,在这里您可以创建一个新群组并介绍其名称。
图 10-8。
Django admin to create new group
图 10-7。
Django admin Groups list
正如你在图 10-8 中看到的,创建一个组所需要的只是一个名字,你可以选择指定给这个组的权限,在应用中对 Django 模型进行 CRUD 操作。
组的管理比用户简单,也可以完全从 Django admin 中完成。大多数时候,您最终要做的是将用户分配到组中。
要将用户分配到一个组,当您编辑用户时,您会看到一个选择网格,如图 10-5 所示。要编辑一个组的属性-名称和创建-删除-更新 Django 模型权限-你可以在如图 10-8 所示的创建它的同一个页面上进行。要从图 10-7 所示的群组列表中删除一个群组,选择您想要删除的群组,并从下拉列表中选择动作,如图 10-9 所示。
图 10-9。
Django admin to delete group Tip
组模型数据存储在数据库表 auth_group 中。和用户组关系存储在数据库表 auth_user_groups 中。
权限类型
默认情况下,Django 项目中的权限会强制执行某些操作。在上一节中,您了解了如何给予用户超级用户、职员和活动状态,所有这些都是用于 Django 管理和 Django 项目的整个访问工作流的权限。此外,您还了解了如何赋予用户或组创建、删除或更新 Django 模型记录的能力,所有这些都是分配给各个 Django 模型的权限。
在这一节中,我将扩展权限的主题,并描述权限的默认行为,以及如何为其他目的创建自己的权限。
用户权限:超级用户、员工和活跃用户
在上一节中,您学习了如何为 Django 用户分配不同的子类型:超级用户、职员和活动用户。以下是与这些用户子类型相关联的权限列表:
- 超级用户。-在 Django admin 中创建、读取、更新和删除用户的权限,以及通过 Django admin 创建、读取、更新和删除项目中所有 Django 模型记录的权限。
- 工作人员。-允许访问 Django admin。即使用户被标记为超级用户,用户也必须被标记为职员才能访问 Django admin。标记为 staff 的用户必须在每个 Django 模型上获得额外的权限来执行任务。
- 活跃。-允许登录应用(Django admin 或其他地方)。一个不活跃的用户被有效地禁止使用他的凭证来认证一个 Django 项目。例如,为了访问 Django admin,除了 staff 之外,用户必须被标记为 active。此外,Django 项目的任何部分(例如视图或模板)都可以标记为需要活动状态,迫使用户登录。
所有三个用户子类型都作为User模型的一部分存储在数据库中,这些子类型可以通过使用属于User模型的表 10-2 中的方法来确认。虽然User权限在 Django admin 中被大量使用,但这并不意味着它们仅限于此。例如,可以依靠User权限,如超级用户、职员或 active,来允许或拒绝对 Django 项目其他部分的访问(例如,模板中的链接、url/view 方法或其他操作)。
关于权限实施的下一节将详细描述这些过程。接下来,我将描述与 Django 模型相关的另一种类型的权限。
模型权限:添加、更改、删除和自定义
模型权限与创建、删除或更新 Django 模型记录的能力相关联。在本章的第一节中,你可以在图 10-5 中看到一个选择网格,它将单个模型上的add、change和delete权限分配给 Django 项目中的每个用户。在图 10-8 中,您可以看到一个等价的选择网格,为 Django 项目中的每个组分配相同类型的add、change和delete对单个模型的权限。
这些add、change和delete权限通过权限模型记录在数据库级别进行管理,这些记录是在您运行 Django 模型的第一次迁移时自动创建的。一旦创建了这些类型的权限,管理它们是非常简单的,因为分配是直接在用户上进行的——如图 10-5 所示——或者如图 10-8 所示的组。
Tip
模型权限数据存储在数据库表 auth_permission 中,该表还引用 django_content_type 表,该表维护已安装的 django 模型的列表。此外,模型权限-用户关系存储在数据库表 auth_user_user_permissions 中,模型权限-组关系存储在数据库表 auth_group_permissions 中。
与用户权限的行为类似,模型权限不一定局限于模型操作。例如,如果一个用户或组有一定的模型权限(例如,如果一个User有能力创建一个Store模型或改变一个Item模型,并允许访问一个页面或链接),那么允许或拒绝访问一个视图或模板部分是完全可能的。
但是,下一节关于权限实施的内容将详细描述这些过程。接下来,我将描述如何为 Django 模型定制模型权限。
模型元权限选项:默认权限和权限
有时可能需要更改模型的默认权限。默认情况下,所有 Django 模型都被赋予了add、change和delete权限。尽管授予用户和组这些权限是由管理员决定的,但有时可能有必要从 Django 模型中删除部分或全部权限(例如,从数据敏感模型中删除change和delete权限)。
此外,有时可能还需要向不符合默认add、change和delete权限的模型添加自定义权限(例如,give_refund或can_hire等权限,用于分配用户/组访问应用某些部分的权限)。
要改变给予模型的默认权限(例如,add、change和delete,您可以使用模型的元类default_permissions字段。此外,您可以通过模型的元类permissions字段向模型的权限添加自定义权限。
清单 10-4 展示了一个利用模型元permissions和default_permissions字段的模型类。
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30,unique=True)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
class Meta:
default_permissions = ('add',)
permissions = (('give_refund','Can refund customers'),('can_hire','Can hire employees'))
Listing 10-4.Customize a Django model’s permissions with default_permissions and permissions
注意,在清单 10-4 中,Store模型的default_permissions字段被设置为('add',),这实际上删除了change和delete权限。如果您添加这个模型元语句,您会注意到Store模型的默认权限发生了变化,如 UI 图 10-10 所示。
图 10-10。
Custom default model permissions and custom model permissions
此外,在清单 10-4 中,请注意模型元权限字段设置了两个自定义权限:give_refund和can_hire,其中每个自定义权限由一个元组组成,第一个元素表示代码(在数据库中使用),第二个元素表示友好的 UI 名称。如果您添加这个模型元语句,您会注意到Store模型添加了这两个自定义权限,如 UI 图 10-10 所示。
权限检查和实施
现在您已经了解了不同类型的 Django 权限,我们可以探索如何在它们的主要上下文之外利用这些权限(例如,Django 管理之外的用户权限和 Django CRUD 操作之外的模型权限)。
接下来,您将学习如何在视图、URL、模板、模型和基于类的视图中更改和实施 Django 权限。
查看方法权限检查
因为视图方法处理传入的请求并将响应分派给最终用户,所以它们代表了实施权限检查的理想位置。例如,您可以使用查看方法的权限检查来返回不同的内容,这取决于用户是否登录(即,是User还是AnonymousUser)或者是超级用户、职员还是活动子类型。
清单 10-5 展示了一个视图方法逻辑内部的权限检查,根据用户的登录状态返回不同的结果,以及另一个使用装饰器根据用户的登录状态限制视图方法访问的变体。
# Internal check to see if user is anonymous or not
def homepage(request):
if request.user.is_anonymous():
# Logic for AnonymousUser
else:
# Logic for User
# Method check to see if user is logged in or not (i.e. a User)
from django.contrib.auth.decorators import login_required
@login_required
def profile(request):
# Logic for profile
Listing 10-5.Permission check in view methods with internal checks and @login_required
Note
由于 Django 默认启用的AuthenticationMiddleware,所有视图方法请求(例如request.user)都可以使用User模型。关于 Django 中间件的更多细节,请参见第二章。
清单 10-5 中的第一个例子使用了所有User车型上可用的is_anonymous()方法——如表 10-2 所述。如果最后一种方法确定发出请求的用户是匿名的——这意味着他没有登录过——就会采取某种行动和响应。另一方面,如果is_anonymous()方法确定发出请求的用户已经登录,就会采取另一种行动和响应。
Tip
除了 is_anonymous 方法之外,您还可以使用任何用户模型字段或方法——在表 10-1 或表 10-2 中——来执行检查(例如,is_staff,is_superuser)。
清单 10-5 中的第二个例子使用@login_required()装饰器将整个视图方法限制为登录用户(即,具有User的请求和具有AnonymousUser的阻塞请求)。当预先知道 view 方法不需要像清单 10-5 中的第一个例子那样的条件许可工作流时,最后一个技术是很有帮助的。
还可以对视图方法执行权限检查,以验证用户是否符合特定的权限测试。例如,可以阻止属于特定组的用户或具有特定模型权限的用户使用查看方法。清单 10-6 展示了使用@user_passes_test和@permission_required装饰器的三个额外的视图方法权限检查。
# Method check to see if User belongs to group called 'Barista'
from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.models import Group
@user_passes_test(lambda u: Group.objects.get(name='Baristas') in u.groups.all())
def dashboard(request):
# Logic for dashboard
# Explicit method check, if User is authenticated and has permissions to change Store model
# Explicit method with test
def user_of_stores(user):
if user.is_authenticated() and user.has_perm("stores.change_store"):
return True
else:
return False
# Method check using method
@user_passes_test(user_of_stores)
def store_manager(request):
# Logic for store_manager
# Method check to see if User has permissions to add Store model
from django.contrib.auth.decorators import permission_required
@permission_required('stores.add_store')
def store_creator(request):
# Logic for store_creator
Listing 10-6.Permission check in view methods with @user_passes_test and @permission_required
清单 10-6 中的第一个例子使用了@user_passes_test装饰器并定义了一个内嵌测试。代码片段lambda u: Group.objects.get(name='Baristas') in u.groups.all()获取名为Baristas的Group模型记录,并检查请求用户是否属于这个组。如果请求用户不属于Baristas组,则测试失败,访问被拒绝;否则,允许用户运行视图方法。
第二个例子也使用了@user_passes_test装饰器,但是它依赖于user_of_stores()方法来执行测试逻辑,而不是定义一个内联测试。如果测试很复杂,与常规方法相比,很难遵循内联逻辑,这一点尤其有用。正如您在清单 10-6 中看到的,user_of_stores()验证用户是否通过了身份验证,以及他是否拥有对Store模型的更新权限——注意字符串stores.change_store是 Django 的权限模型记录使用的语法。
清单 10-6 中的最后一个例子使用了@permission_required装饰器,用于验证用户是否有给定的Permission记录。在这种情况下,注意装饰器有一个输入字符串stores.add_store,它表明只有有权限添加Store模型的用户才能运行 view 方法。
What Happens When a User Fails a Permission Check?
对于内部验证检查(例如,在方法体中进行的检查,如if request.user.is_anonymous():)您拥有绝对控制权,因此您可以将用户重定向到任何页面,或者添加 flash 消息以显示在模板上。
对于装饰者验证检查@login_required、@user_passes_test和@permission_required,默认的失败行为是将用户重定向到 Django 的登录页面。Django 的默认登录页面 URL 是/account/login/,这个值可以用settings.py中的LOGIN_URL变量覆盖,我将在下一节中提供细节。
对于@permission_required装饰器,也可以通过添加raise_exception=True属性(例如@permission_required('stores.add_store',raise_exception=True))将失败的测试重定向到 Django 的 HTTP 403(禁止)页面。
URL 权限检查
在某些情况下,您可以拥有一个 Django 工作流,它不涉及视图方法,只是简单地将控制从 URL 直接发送到静态模板。在这种情况下,也可以直接对 URL 定义进行权限检查。清单 10-7 展示了类似清单 10-5 和 10-6 中应用于urls.py中 URL 定义的验证检查。
from django.conf.urls import include, url
from django.views.generic import TemplateView
from django.contrib.auth.decorators import login_required,permission_required,user_passes_test
from django.contrib.auth.models import Group
urlpatterns = [
url(r'^online/baristas/',
user_passes_test(lambda u: Group.objects.get(name='Baristas') in u.groups.all())
(TemplateView.as_view(template_name='online/baristas.html')),name="onlinebaristas"),
url(r'^online/dashboard/',
permission_required('stores.add_store')
(TemplateView.as_view(template_name='online/dashboard.html')),name="onlinedashboard"),
url(r'^online/',
login_required(TemplateView.as_view(template_name='online/index.html')),name='online'),
]
Listing 10-7.Permission checks in urls.py for static templates
正如您在清单 10-7 中看到的,导入所需的装饰器后,您只需要在 URL 定义中集成验证测试。@user_passes_test和@permission_required装饰器被声明为独立的方法,后跟 URL 定义(例如,user_pass_test()(TemplateView.as_view...))。@login_required装饰器将TemplateView语句作为其输入。应该指出的是,清单 10-7 中失败测试的行为与清单 10-5 和 10-6 中的行为相同,在侧栏“当用户权限检查失败时会发生什么?"
验证检查的另一种可能性是在一组视图方法/URL 上执行它们,这样就不用给每个单独的视图方法添加装饰器了——如清单 10-6 所示——你只需要为整个组做一次。当通过include()方法在urls.py中定义 URL 时,这种视图方法/URL 分组过程尤其常见。清单 10-8 展示了如何对使用include()方法的 URL 集合进行验证检查。
from django.conf.urls import include, url
from django.core.urlresolvers import RegexURLResolver, RegexURLPattern
class DecoratedURLPattern(RegexURLPattern):
def resolve(self, *args, **kwargs):
result = super(DecoratedURLPattern, self).resolve(*args, **kwargs)
if result:
result.func = self._decorate_with(result.func)
return result
class DecoratedRegexURLResolver(RegexURLResolver):
def resolve(self, *args, **kwargs):
result = super(DecoratedRegexURLResolver, self).resolve(*args, **kwargs)
if result:
result.func = self._decorate_with(result.func)
return result
def decorated_includes(func, includes, *args, **kwargs):
urlconf_module, app_name, namespace = includes
patterns = getattr(urlconf_module, 'urlpatterns', urlconf_module)
for item in patterns:
if isinstance(item, RegexURLPattern):
item.__class__ = DecoratedURLPattern
item._decorate_with = func
elif isinstance(item, RegexURLResolver):
item.__class__ = DecoratedRegexURLResolver
item._decorate_with = func
return urlconf_module, app_name, namespace
from django.contrib.auth.decorators import login_required,permission_required,user_passes_test
from django.contrib.auth.models import Group
from coffeehouse.items.urls import urlpatterns as drinks_url_patterns
urlpatterns = [
url(r'^items/',
decorated_includes(login_required,include(items_url_patterns,namespace="items"))),
url(r'^stores/',
decorated_includes(permission_required('stores.add_store'),
include('coffeehouse.stores.urls',namespace="stores"))),
url(r'^social/',
decorated_includes(user_passes_test(lambda u: Group.objects.get(name='Baristas') in u.groups.all()),
include('coffeehouse.social.urls',namespace="social"))),
]
Listing 10-8.Permission checks in urls.py for include() definitions
因为 Django 在include()定义中没有内置的权限检查支持,你可以在清单 10-8 中看到,我们首先定义了两个自定义类,然后是自定义方法decorated_includes。如果您按照清单 10-8 中的顺序,您可以看到decorated_includes()方法接受两个输入参数,首先是权限测试(例如login_required、permission_required),然后是带有 URL 定义的标准include()方法。还应该指出的是,清单 10-8 中失败测试的行为与之前清单中的行为相同,在侧栏“当用户权限检查失败时会发生什么?”
模板权限检查
Django 项目中另一个可用的权限检查是在模板中,如果您想根据用户的权限显示/隐藏内容(例如链接),这个过程会很有帮助。清单 10-9 展示了一系列 Django 模板语法示例。
{% if user.is_authenticated %}
{# Content for authenticated users #}
{% endif %}
{% if perms.stores.add_store %}
{# Content for users that can add stores #}
{% endif %}
{% for group in user.groups.all %}
{% if group.name == 'Baristas' %}
{# Content for users with 'Baristas' group #}
{% endif %}
{% endfor %}
Listing 10-9.Permission checks in templates
Note
由于 Django 的auth上下文处理器,用于在模板中执行权限检查的user和 perms 变量是可用的,该处理器在默认情况下是启用的——参见第三章关于 Django 上下文处理器使用的详细信息。
您可以在清单 10-9 中看到,所有模板语法示例都使用了user和perms变量来执行条件权限检查。第一个例子检查用户是否通过了is_authenticated的认证。第二个例子使用 Django 的权限语法stores.add_store验证持有用户权限的perms是否有权创建Store模型记录。清单 10-9 中的第三个例子遍历一个用户组,检查用户是否在Baristas组中;如果是,它将为这类用户输出内容。
Tip
Django 模板中检查属性的循环效率非常低。尽管清单 10-9 中的最后一个例子可以工作,但这是一个非常低效的机制。更好的解决方案是创建一个自定义过滤器,并在过滤器中执行直接查询(例如,{ % if user|has_group:"Baristas" %},其中has_group过滤器包含大部分逻辑检查)。在这种情况下,我选择了清单 10-9 中的语法,将所有内容放在一个地方,但是要知道,对于这种类型的逻辑,更有效的解决方案是使用第三章中描述的自定义过滤器。
基于类的查看权限检查
正如你在第二章和第九章的结尾所学到的,基于类的视图为视图方法中包含的逻辑提供了更好的可重用性和封装。然而,由于基于类的视图的组成方式,它们需要一种不同于清单 10-5 和 10-6 中常规视图方法的方法来检查权限。
虽然从技术上来说,您可以在基于类的视图中检查权限——就像在标准视图方法中一样——通过定义基于类的视图的get()和/或post()方法,这种技术也迫使您声明视图的大部分逻辑,如果您试图做的只是合并权限检查,这在基于类的视图中是低效的。
清单 10-10 展示了在基于类的视图中执行权限检查的一系列选项。
from django.views.generic import ListView
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.utils.decorators import method_decorator
from django.core.urlresolvers import reverse_lazy
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.decorators import login_required,user_passes_test
from django.contrib.auth.models import Group
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin, PermissionRequiredMixin
class ItemList(LoginRequiredMixin,ListView):
model = Item
context_object_name = 'items'
template_name = 'items/index.html'
class ItemDetail(UserPassesTestMixin,DetailView):
model = Item
pk_url_kwarg = 'item_id'
template_name = 'items/detail.html'
def test_func(self):
return self.request.user.is_authenticated
class ItemCreation(PermissionRequiredMixin,SuccessMessageMixin,CreateView):
model = Item
form_class = ItemForm
success_url = reverse_lazy('items:index')
success_message = "Item %(name)s created successfully"
permission_required = ('items.add_item',)
@method_decorator(login_required, name='dispatch')
class ItemUpdate(SuccessMessageMixin,UpdateView):
model = Item
pk_url_kwarg = 'item_id'
form_class = ItemForm
success_url = reverse_lazy('items:index')
success_message = "Item %(name)s updated successfully"
@method_decorator(user_passes_test(lambda u: Group.objects.get(name='Baristas') in u.groups.all()), name='dispatch')
class ItemDelete(DeleteView):
model = Item
pk_url_kwarg = 'item_id'
success_url = reverse_lazy('items:index')
Listing 10-10.Permission checks in class-based views
清单 10-10中的第一个基于类的视图使用LoginRequiredMixin mixin 来强制只有登录的用户才能访问基于类的视图。第二个基于类的视图ItemDetail使用另一个名为UserPassesTestMixin的 mixin 来确保只有通过权限测试的用户才被允许访问基于类的视图。要为使用UserPassesTestMixin mixin 的基于类的视图定义权限测试,请注意使用了test_func()方法。最后一个方法通过self.request获得对用户的访问权,并基于测试返回一个True或False值,在清单 10-10 中只需调用is_authenticated即可。
清单 10-10 ItemCreation中的第三个基于类的视图使用PermissionRequiredMixin mixin 来强制只有拥有特定模型权限的用户才被允许访问基于类的视图。为了在使用PermissionRequiredMixin mixin 的基于类的视图上声明模型权限,使用了带有权限元组值的permission_required选项。在清单 10-10 的情况下,('items.add_item',)值遵循模型权限语法<app_name>.<app_permission>,确保只有有权向items应用添加Item记录的用户才被允许访问基于类的视图。
清单 10-10和ItemDelete中最后两个基于类的视图利用@method_decorator装饰器来实施基于类的权限。@method_decorator装饰器是专门为将标准视图方法装饰器——如清单 10-5 和 10-6 中使用的那些——应用于基于类的视图方法而设计的,它接受两个参数:一个装饰器和基于类的视图方法,在该方法上应用装饰器。在清单 10-10 的例子中,您可以看到ItemUpdate基于类的视图将login_required装饰器应用于dispatch()方法,只允许登录的用户访问基于类的视图。而ItemDelete基于类的视图将user_passes_test装饰器应用于dispatch()方法,只允许属于Baristas组的用户访问基于类的视图。
用户认证和自动管理
在本章的第一节中,您学习了如何创建用户。但是回想一下,要使这个过程正常工作,您要么必须使用 Django 命令行工具,要么必须使用 Django admin。这些技术虽然有效,但并不是为访问应用的最终用户设计的,也不会扩展。
类似地,到本章的这一点为止,用户登录和注销的唯一位置是通过 Django admin 认证表单。最后一种形式对最终用户来说也不是理想的位置,不仅因为它缺少为应用设计的布局,还因为它不适合从不打算与项目数据库交互的用户。
如果您计划让最终用户在应用中进行身份验证,那么您需要为用户提供一种注册、登录和注销的方式,以及让用户记住和更改密码的方式。支持User模型的同一个django.contrib.auth包还包括一系列用于创建用户认证工作流的预构建结构。
第一个可用于验证用户身份的预建机制是一组 URL,允许用户登录和注销应用,允许用户更改他们的密码,以及允许用户通过电子邮件通知重置他们的密码。
清单 10-11 展示了如何将全套的django.contrib.authURL 添加到主urls.py文件中——使用一条include语句——以及等效的单个url语句,以防您想要有选择地挑选在项目中使用哪些 URL。
from django.conf.urls import url
from django.contrib.auth import views
# Option 1 to include all urls (See option 2 for included urls)
urlpatterns = [
url(r'^accounts/', include('django.contrib.auth.urls')),
]
# Option 2) (Explicit urls, all included in django.contrib.auth)
urlpatterns = [
url(r'^accounts/login/$', views.LoginView.as_view(), name='login'),
url(r'^accounts/logout/$', views.LogoutView.as_view(), name='logout'),
url(r'^accounts/password_change/$', views.PasswordChangeView.as_view(), name='password_change'),
url(r'^accounts/password_change/done/$', views.PasswordChangeDoneView.as_view(), name='password_change_done'),
url(r'^accounts/password_reset/$', views.PasswordResetView.as_view(), name='password_reset'),
url(r'^accounts/password_reset/done/$', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
url(r'^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
url(r'^accounts/reset/done/$', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]
Listing 10-11.Configure urls from django.contrib.auth package
清单 10-11 中的第一个选项在accounts url 上配置所有的django.contrib.authURL。这引发了一个问题,为什么是accounts的网址?因为,默认情况下,所有的django.contrib.auth动作都使用这个 url 作为它们的根(例如,在前面的章节中,回想一下试图访问需要登录的资源会执行到/accounts/login的重定向)。因此,通过将这个 url include()语句与django.contrib.auth.urls一起使用,所有的django.contrib.auth动作都获得了一个有效的 url,它也得到必要的视图方法逻辑的支持!但是稍后将更多地讨论视图方法逻辑。
因为 url include()语句有一个‘使用所有 url 或者不使用’的行为,清单 10-11 中的第二个选项代表使用粒度 url 语句来配置相同的django.contrib.authURL。如果您想要禁用某些django.contrib.authURL,但仍保持其他django.contrib.authURL 活动(例如,禁用密码更改 URL,但保持登录和注销 URL),最后一个选项很有帮助。
正如你所看到的,使用清单 10-11 中的任一选项都可以为你提供一个快速的解决方案,将django.contrib.auth动作连接到 URL,后者也被连接到默认的基于类的视图,这些视图也是django.contrib.auth包的一部分。
登录和注销工作流
登录和注销工作流的入口点——如清单 10-11 所示——分别是/accounts/login/和/accounts/logout/URL。如果在/accounts/login/ url 上进行调用,则触发django.contrib.auth.views.LoginView基于类的视图,如果在/accounts/logout/ url 上进行调用,则触发django.contrib.auth.views.LogoutView基于类的视图。
像所有 Django 内置的基于类的视图一样,LoginView和LogoutView基于类的视图在代码和配置方面几乎不需要任何东西。它们唯一需要的是一个显示登录表单的模板和一个显示注销成功消息的模板。
LoginView基于类的视图在定义为settings.py中TEMPLATES/DIRS变量一部分的目录下寻找模板registration/login.html。并且LogoutView基于类的视图寻找模板registration/logout.html,也在定义为settings.py中TEMPLATES/DIRS变量的一部分的目录下。
Tip
关于registration/login.html和registration/logout.html模板所需的布局和字段,请参见本书附带的源代码。
登录工作流在settings.py中提供了两个配置选项,允许您修改其行为,而无需定制LoginView基于类的视图。LOGIN_URL变量默认为/accounts/login/,当用户试图访问需要认证的资源但没有登录时,它被用作重定向到的 url。LOGIC_REDIRECT变量默认为/accounts/profile/,是用户成功登录后被重定向到的 url。django.contrib.auth包没有为/accounts/profile/ url 提供入口点,因此您必须要么配置这个 url,要么简单地将其更改到不同的位置(例如,LOGIN_REDIRECT='/'在成功登录后将用户重定向到主页)。
注销工作流支持settings.py中的LOGOUT_REDIRECT变量来定义用户注销时被带到哪里。LOGOUT_REDIRECT变量默认为/accounts/logout/,但是可以更新以将用户重定向到不同的位置(例如,LOGOUT_REDIRECT='/'在用户注销后将用户重定向到主页)。
除了创建模板、设置 url——如清单 10-11 所述——以及可选地更改登录 URL 位置和登录/退出重定向行为,您不需要做任何其他事情来启用由django.contrib.auth包提供的登录和注销工作流。如果你想让用户登录,只需将他们指向/accounts/login/ url,如果你想让他们退出,将他们指向/accounts/logout/ url。所有其他工作流细节(例如,身份验证、密码验证、错误表单处理、会话到期)由LoginView和LogoutView基于类的视图负责。
密码更改工作流
密码更改工作流需要两个 url 入口点:触发PasswordChangeView基于类的视图的/accounts/password_change/ url 和触发PasswordChangeDoneView基于类的视图的/accounts/password_change/done/ url。
PasswordChangeView基于类的视图在定义为settings.py中的TEMPLATES/DIRS变量的一部分的目录下寻找一个包含表单的模板,以更改registration/password_change_form.html中的密码。并且PasswordChangeDoneView基于类的视图在registration/password_change_done.html中寻找包含成功消息的模板,也在定义为settings.py中TEMPLATES/DIRS变量的一部分的目录下。
Tip
关于registration/password_change_form.html和registration/password_change_done .html模板所需的布局和字段,请参见本书附带的源代码。
除了创建模板和设置 urls 如清单 10-11 所述——您不需要做任何其他事情来启用django.contrib.auth包提供的密码更改工作流。如果你想让用户修改他的密码,只需将他们指向/accounts/password_change/ url。所有其他工作流细节(例如,密码验证、数据库更新、表单错误处理)由PasswordChangeView和PasswordChangeDoneView基于类的视图负责。
密码重置工作流程
密码重置工作流由两个子工作流组成。第一个子工作流捕获用户的电子邮件,并向他发送一封包含重置密码链接的电子邮件。第二个子工作流处理重置链接并验证用户的新密码。
第一个子工作流使用/accounts/password_reset/ url,它触发PasswordResetView基于类的视图,以及/accounts/password_reset/done/ url,它触发PasswordResetDoneView基于类的视图。第二个子工作流使用/accounts/reset/ url,它触发PasswordResetConfirmView基于类的视图,以及/accounts/reset/done/ url,它触发PasswordResetCompleteView基于类的视图。
基于PasswordResetView类的视图在registration/password_reset_form.html中寻找包含重置用户密码表单的模板,基于PasswordResetDoneView类的视图在registration/password_reset_done.html中寻找成功消息模板,这两个视图都位于定义为settings.py中TEMPLATES/DIRS变量一部分的目录下。对于第二个子工作流,PasswordResetConfirmView基于类的视图在registration/password_reset_confirm.html中寻找带有引入新用户密码的表单的模板,而PasswordResetCompleteView基于类的视图在registration/password_reset_complete.html中寻找成功消息模板,这两个视图都位于定义为settings.py中TEMPLATES/DIRS变量的一部分的目录下。
默认情况下,密码重置工作流会生成一封电子邮件,其中包含将用户带到第二个子工作流以重置其密码的说明。同样默认情况下,重置电子邮件包含一个到域localhost:8000的链接,可以通过安装 Django 站点django.contrib.sites应用进行定制(例如,将django.contrib.sites添加到INSTALLED_APPS和settings.py中的SITE_ID=1,并在 Django 管理中更新 id 为 1 的站点以反映一个新的域)。此外,您可以在registration/password_reset_email.html模板中定义一个定制的电子邮件布局,并将其放在一个目录下,该目录被定义为settings.py中TEMPLATES变量的一部分。
Tip
关于registration/password_reset_form.html, registration/password_reset_done.html、registration/password_reset_confirm.html、registration/password_reset_complete.html和registration/password_reset_email.html模板所需的布局和字段,请参见本书随附的源代码。
除了创建模板和设置 urls 如清单 10-11 所述——以及可选地更改默认的电子邮件布局之外,您不需要做任何其他事情来启用或允许用户使用django.contrib.auth包提供的工作流记住他们的密码。如果你想让用户记住他的密码,只需将他们指向/accounts/password_reset/ url。所有其他工作流细节(例如,电子邮件令牌验证、密码验证、数据库更新、表单错误处理)由PasswordResetView、PasswordResetDoneView、PasswordResetConfirmView和PasswordResetCompleteView基于类的视图负责。
用户注册工作流
用户可以在来自django.contrib.auth包的一些构造的帮助下自动注册一个 Django 应用。尽管用户注册工作流不像以前的用户相关工作流那样嵌入到django.contrib.auth包中,但是仍然很容易创建。
创建用户注册工作流的第一步是配置一个 url 入口点,以允许用户创建一个帐户,如清单 10-12 所示。
# urls.py main
from django.conf.urls import url
from django.contrib.auth import views
from coffeehouse.registration import views as registration_views
urlpatterns = [
url(r'^accounts/', include('django.contrib.auth.urls')),
url(r'^accounts/signup/$',registration_views.UserSignUp.as_view(),name="signup"),
]
Listing 10-12.Configure url for user sing up workflow
清单 10-12 展示了一个在/accounts/signup/可访问的 url,此外还有由django.contrib.auth包提供的所有 URL,包括之前与用户相关的工作流。请注意,/accounts/signup/ url 被设置为由来自coffeehouse.registration应用的UserSignUp基于类的视图处理,这意味着您需要为项目创建一个基于类的视图。
清单 10-13 展示了负责用户注册工作流的UserSignUp基于类的视图,它自动创建User模型记录,这在以前是使用 Django 命令行工具或 Django admin 完成的。
from django.core.urlresolvers import reverse_lazy
from django.http import HttpResponseRedirect
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login
class UserSignupForm(UserCreationForm):
email = forms.EmailField(required=True)
class Meta:
model = User
fields = ("username", "email", "password1", "password2")
class UserSignUp(SuccessMessageMixin,CreateView):
model = User
form_class = UserSignupForm
success_url = reverse_lazy('items:index')
success_message = "User created successfully"
template_name = "registration/signup.html"
def form_valid(self, form):
super(UserSignUp,self).form_valid(form)
# The form is valid, automatically sign-in the user
user = authenticate(self.request, username=form.cleaned_data['username'],
password=form.cleaned_data['password1'])
if user == None:
# User not validated for some reason, return standard form_valid() response
return self.render_to_response(self.get_context_data(form=form))
else:
# Log the user in
login(self.request, user)
# Redirect to success url
return HttpResponseRedirect(self.get_success_url())
Listing 10-13.Signup workflow fulfilled by custom CreateView class-based view
清单 10-13 中的UserSignUp基于类的视图是一个标准的CreateView基于类的视图,它使用 mixinSuccessMessageMixin——如果你不熟悉这种基于类的视图来创建模型记录或 mixin 的概念,请参阅上一章,其中解释了这两个主题。
UserSignUp基于类的视图将模型选项设置为User,告诉 Django 创建django.contrib.auth.models.User记录。接下来,将form_class选项设置为UserSignupForm,这也在清单 10-13 中定义。接下来是success_url和success_message选项,用于在创建User记录时指示 url 和成功消息,以及template_name选项,用于指定一个带有表单的模板,以捕获User字段。
清单 10-13 中的自定义UserSignupForm模型表单基于django.contrib.auth包中的UserCreationForm。从实用的角度来看,UserSignUp基于类的视图可以使用UserCreationForm作为它的form_class,然而,这个最后的内置表单只包含username和password字段。在这种情况下,作为用户注册过程的一部分,定制的UserSignupForm模型表单将email字段添加到基础UserCreationForm表单类中。请注意,因为支持User模型已经包含了一个email字段,所以不需要对模型进行修改。
为了在创建了User模型记录后自动登录用户,UserSignUp基于类的视图在其form_valid()方法中包含了定制逻辑。在清单 10-13 的例子中,来自django.contrib.auth包的authenticate方法被表单值调用,验证凭证是否有效。如果凭证是有效的——给定工作流应该是 100%的时间,除非发生不可预见的黑客事件——authenticate方法返回一个User实例并立即执行login方法——也来自django.contrib.auth包——创建一个用户会话(即登录),之后控制被重定向到基于类的视图的成功页面。在不可预见的事件中,authenticate方法没有返回一个User实例,form_valid()方法返回它的标准有效载荷,也就是经过验证的形式。
从清单 10-12 和 10-13 中可以看到,您可以创建一个用户注册工作流,让用户创建自己的帐户,并减轻从 Django admin 或 Django 命令行工具创建用户帐户的管理负担。
Tip
清单 10-13 中的注册工作流是许可的,因为它不验证电子邮件并自动让用户登录。这样做是为了简单,并且可能适用于大多数项目。但是您可以在创建User记录之前或之后,通过将此逻辑添加到不同的基于类的视图方法中,为注册工作流添加额外的保护措施(例如,发送验证链接,将用户设置为非活动,直到验证链接被点击)。
Tip 2
Django allauth 包(将在本章后面介绍)内置了对电子邮件验证的支持。
自定义用户模型字段
默认的 Django User模型(即django.contrib.auth.models.User类)使用最小的一组数据字段,包括username、email、first_name、last_name、date_joined和last_login;除了权限相关字段password、is_superuser、is_staff和is_active。
虽然最后这些字段对于 Django 的内置用户功能来说已经足够了,但是如果您希望存储额外的用户数据(例如年龄、电话、地址),它们就不够用了。有两种方法可以支持额外的用户数据:创建一个单独的模型来存储额外的数据,并创建一个与之相关的用户关系(例如,一个带有ForeignKey的User)或者覆盖默认的django.contrib.auth.models.User类来创建一个定制的用户类。
清单 10-14 通过创建一个额外的模型来存储额外的用户数据并创建一个与django.contrib.auth.models.User类的关系来展示第一种技术。
from django.contrib.auth.models import User
from django.db import models
class UserExtra(models.Model):
user = models.ForeignKey(User)
age = models.IntegerField(blank=True,null=True)
telephone = models.CharField(max_length=15,blank=True,null=True)
Listing 10-14.Model with extra user fields related to default Django user model
清单 10-14 中的UserExtra模型的功能类似于任何其他具有ForeignKey数据类型的模型,因此,在这些情况下使用时,会涉及一些性能和维护问题。另一方面,用户数据分布在两个数据库表中。一个表用于User记录,另一个表用于UserExtra记录,这不可避免地需要两个查询或一个连接查询来获取给定用户的所有数据——参见第七章了解跨模型关系的查询。从积极的方面来看,这种技术保持了项目的默认 Django User模型类的完整性,不需要额外的配置或开发工作。
接下来,让我们看看第二种技术,它包括创建一个定制的用户类,这个过程在清单 10-15 中进行了说明。
# models.py (app registration)
from django.contrib.auth.models import AbstractUser
from django.db import models
class CoffeehouseUser(AbstractUser):
age = models.IntegerField(blank=True,null=True)
telephone = models.CharField(max_length=15,blank=True,null=True)
# admin.py (app registration)
from django.contrib import admin
from .models import CoffeehouseUser
class CoffeehouseUserAdmin(admin.ModelAdmin):
pass
admin.site.register(CoffeehouseUser, CoffeehouseUserAdmin)
# settings.py
AUTH_USER_MODEL = 'registration.CoffeehouseUser'
Listing 10-15.Custom User model to override default Django User model
清单 10-15 中的第一部分显示了用作定制用户类的CoffeehouseUser类。注意这个类从AbstractUser类继承了它的行为,这个类给了它与默认的User类相同的字段和行为(例如username、email等)。).接下来,CoffeehouseUser类使用标准模型字段声明了age和telephone字段,为定制用户类提供了比默认User类多两个字段。
因为自定义的CoffeehouseUser类将覆盖项目的默认User类,这意味着默认的django.contrib.auth.models.User类不再被使用。因此,您必须配置 Django,以便在任何需要用户逻辑的地方使用定制的CoffeehouseUser类。
清单 10-15 中的第二部分展示了从 Django admin 访问新的定制用户类所必需的 Django admin 配置,并且能够创建、读取、更新和删除用户,就像用标准的django.contrib.auth.models.User模型类所做的一样。注意下一章将更详细地介绍 Django 管理配置。
最后,清单 10-15 中的第三部分显示了 Django settings.py文件,其中AUTH_USER_MODEL设置为registration.CoffeehouseUser,其中registration表示应用名称,CoffeehouseUser表示定制用户模型。这最后一个配置确保整个项目中的用户逻辑是在CoffeehouseUser模型而不是默认的User模型上生成的。
一旦您使用清单 10-15 中的定制用户模型类和配置运行迁移操作,您将看到 Django 项目用户获得了两个额外的字段。
Caution
类似于清单 10-15 中的定制用户模型实现应该只在项目开始时进行(例如,第一批项目迁移之一)。
因为用户模型在管理对 Django 项目的访问中起着如此重要的作用,所以您不应该试图在项目中途实现定制的用户模型,如清单 10-15 中所示。这样做有破坏依赖关系(外键)的风险,这些依赖关系被其他模型用来引用默认的django.contrib.auth.models.User模型记录;此外,这还会更改存储用户数据的底层数据库表。通过在项目开始时实现自定义用户模型,您可以保证对用户进行的任何可能的模型依赖都是针对自定义用户记录进行的,并且用户数据从一开始就存储在单个数据库表中。
事实上,如果您计划使用自定义用户模型,但不知道一开始要添加哪些新字段,您可以创建一个占位符用户模型,如下所示:
class CoffeehouseUser(AbstractUser):
pass
这个代码片段创建了一个与默认的django.contrib.auth.models.User类相同的模型,但是为在定制模型上创建用户依赖关系打下了基础,同时允许向定制模型添加字段,因为像任何其他模型一样,标准迁移需要这些字段。
最后,使用定制用户模型时要考虑的另一个因素——在清单 10-15 中并不明显——是如何在项目的其他地方引用定制用户模型。当您使用默认的User类时,语句from django.contrib.auth.models import User在models.py和views.py文件中引用用户是常见的,但是对于定制模型,这种引用不再适用。
为了支持定制用户模型,Django 提供了助手方法get_user_model,该方法返回对在settings.py的AUTH_USER_MODEL变量中定义的任何用户模型的引用。以这种方式,您可以使用语句from django.contrib.auth import get_user_model从项目中的任何地方获得对项目用户模型的引用。下一节包含一个使用get_user_model方法的例子。
自定义身份验证后端
身份验证过程非常重要,因为它决定了允许哪些用户访问应用。Django 使用的默认认证过程包括将用户名和密码(在 web 表单上提供)与数据库中的User记录进行比较。如果用户名和密码与User记录匹配,则认证过程被认为是成功的,但是如果值不匹配,则认证过程被认为是失败的。
Django 本身包括一系列内置的认证后端类 2 来支持这个认证过程的变化。此外,在本章的最后一节,我将向您介绍 allauth Django 包,它支持一系列的认证后端(例如,针对社交媒体帐户的认证)。
但是为了从头开始说明自定义身份验证后端的概念,我将创建一个简单的身份验证后端,它依赖于电子邮件进行身份验证,并且可以使用任何用户类型(即自定义用户模型或默认的User模型)。
默认情况下,Django 项目使用django.contrib.auth.backends.ModelBackend身份验证后端类,用于将最终用户提供的用户名和密码集与数据库中的项目用户进行比较。现在问问你自己,你认为登录凭证、用户名或电子邮件哪个更容易记住?如果你和这个时代的大多数人一样,你很可能已经回复了邮件。
清单 10-16 展示了一个定制的认证后端,它能够通过电子邮件凭证而不是默认用户名来认证用户。
# models.py (registration app)
from django.contrib.auth import get_user_model
class EmailBackend(object):
def authenticate(self, request, username=None, password=None, **kwargs):
User = get_user_model()
try:
user = User.objects.get(email=username)
except User.DoesNotExist:
return None
else:
if getattr(user, 'is_active', False) and user.check_password(password):
return user
return None
def get_user(self, user_id):
User = get_user_model()
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
# setting.py
AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend',
'coffeehouse.registration.models.EmailBackend']
Listing 10-16.Custom authentication back end to support authentication with email
清单 10-16 为定制认证后端声明了EmailBackend类。像所有定制认证后端类一样,您必须至少声明authenticate()和get_user()方法,其中第一个方法用于定义认证逻辑,第二个方法用于返回请求的用户。
作为基本身份验证工作流的一部分,authenticate()方法可以访问终端用户提供的username和password字段。接下来,您可以看到authenticate()方法依靠get_user_model()方法助手获得了对项目用户模型的访问,这确保了即使项目使用定制用户模型,认证工作流也是在正确的用户类上完成的,如前一节所述。
一旦获得了对项目的用户类的引用,请注意,authenticate()方法使用提供的username在email字段上为用户执行查询,这有效地允许将用户提供的username值作为电子邮件字段进行身份验证。如果用户匹配作为username值提供的电子邮件,那么清单 10-16 中的try-except-else块调用check_password()来验证匹配用户的密码。如果密码匹配,authenticate()返回认证用户,如果密码不匹配,authenticate()返回None。
清单 10-16 中的最后一部分是settings.py中的AUTHENTICATION_BACKENDS变量,它被分配了一个认证后端类列表。在这种情况下,保留默认的django.contrib.auth.backends.ModelBackend类,以确保首先尝试用户名/密码的默认认证工作流。接下来,添加清单 10-16 中的自定义EmailBackend身份验证后端类,以确保身份验证工作流将输入数据视为电子邮件/密码集。
从清单 10-16 中的例子可以看出,通过添加这个简单的定制身份验证后端类,您可以允许用户输入他们的用户名或电子邮件来在 Django 应用中进行身份验证。
Tip
如果您使用清单 10-16 中的自定义身份验证后端类,请将登录表单中的用户名标签更改为‘用户名/电子邮件’,以通知用户他们可以同时使用这两个类别。
Django allauth 的用户管理
正如您在本章中所看到的,django.contrib.auth包提供了大量管理用户和组、权限以及认证工作流的功能。但是您可能也意识到了,django.contrib.auth包也可能会受到很多遗留行为的影响,这些行为在当今时代不适用于 web 应用。例如,django.contrib.auth不支持社交认证之类的东西——这几乎是当今互联网的一项要求——此外,django.contrib.auth还被设计为使用开箱即用的用户名,而不是电子邮件——这也是一种非常过时的做法。
仍然因为django.contrib.auth包被构建到 Django 中,所以经常有许多 Django 包(例如,Django admin 和其他第三方包)假定 Django 项目使用django.contrib.auth包及其功能。
因此,一方面,您必须继续使用django.contrib.auth包来维护跨其他 Django 包的用户管理兼容性,这些 Django 包需要使用django.contrib.auth,但另一方面,您不希望被 2005 年的做法所束缚,即要求用户提供用户名,并且不允许他们使用社交媒体帐户进行身份验证。
在众多可用于解决 Django 用户管理集成的第三方包和潜在解决方案中,Django allauth 包提供了最佳功能集之一(例如,基于社交认证和电子邮件的用户),以及与django.contrib.auth包的最佳集成。接下来,我将描述 Django allauth 包的设置过程。
安装并设置 django-allauth
要安装 Django allauth 包,使用下面的pip语句:
pip install django-allauth
安装完成后,让我们创建一个基本的 Django allauth 配置,以实现以下用户管理特性:
- 使用 email 作为主要的用户标识符,但是保留用户名凭证以与其他包兼容(例如 Django admin)。
- 要求电子邮件验证,以避免垃圾用户。
- 为添加 Django 社交认证(脸书、谷歌、Twitter)奠定基础。)
清单 10-17 展示了对项目的settings.py文件进行必要的添加,以使一个基本 Django allauth 配置具备这些特性。
# Ensure the 'django.contrib.sites' is declared in INSTALLED_APPS
# And also add the allauth, allauth.account and allauth.socialaccount to INSTALLED_APPS
INSTALLED_APPS = [
# Django sites app required
'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
]
# Ensure SITE_ID is set sites app
SITE_ID = 1
# Add the 'allauth' backend to AUTHENTICATION_BACKEND and keep default ModelBackend
AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend']
# EMAIL_BACKEND so allauth can proceed to send confirmation emails
# ONLY for development/testing use console
EMAIL_BACKEND='django.core.mail.backends.console.EmailBackend'
# Custom allauth settings
# Use email as the primary identifier
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_REQUIRED = True
# Make email verification mandatory to avoid junk email accounts
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
# Eliminate need to provide username, as it's a very old practice
ACCOUNT_USERNAME_REQUIRED = False
Listing 10-17.Base Django allauth settings.py configuration
除了清单 10-17 到settings.py文件的变化之外,您还需要在主urls.py文件中注册 Django allauth url 入口点。清单 10-18 展示了您需要对主urls.py文件进行的修改。
urlpatterns = [
...
url(r'^accounts/', include('allauth.urls')),
...
]
Listing 10-18.Django allauth url configuration urls.py
正如您在清单 10-18 中看到的,url 正则表达式告诉 Django 在/accounts/路径下挂载 Django allauthallauth.urlsURL,就像清单 10-11 中对标准 Django django.contrib.auth包 URL 所做的那样。
Django allauth 使用与标准django.contrib.auth包几乎完全相同的 url 模式和行为。这意味着 Django allauth 将其登录页面配置在/accounts/login/ url,注销页面配置在/accounts/logout/ url。Django allauth 确实在清单 10-18 的include()语句中包含了一系列新的 URL(例如,电子邮件验证),但是我将在我们继续讨论时描述这些新的 URL。
类似地,标准django.contrib.auth包用于认证目的的相同settings.py变量也适用于 Django allauth。例如,您可以设置LOGIN_URL来覆盖默认的/accounts/login/ url 位置,也可以设置默认为/accounts/profile/ url 的LOGIN_REDIRECT_URL。事实上,就像django.contrib.auth包一样,Django allauth 不包含/accounts/profile/ url 入口点,所以您也可以覆盖settings.py中的LOGIN_REDIRECT_URL变量以指向另一个 url(例如,LOGIN_REDIRECT_URL='/'在成功登录后将用户重定向到主页)。
最后,一旦您完成了这些配置步骤,您需要执行以下其他步骤来确保 Django allauth 正确运行:
- 从命令行运行
python manage.py migrate,确保 Django allauth 所需的所有数据库表都已创建。 - 确保
django.contrib.sites包反映了应用的域。默认情况下,django.contrib.sites默认为域example.com,您可以在 Django admin 中更改该值,以确保某些特性(如电子邮件)使用正确的应用域。
首先用 Django allauth 中的超级用户登录和注销
首先,使用清单 10-1 中本章开头概述的任何技术创建一个 Django 超级用户,并记下电子邮件。接下来,直接进入/accounts/login/网址,你会看到一个如图 10-11 所示的页面。
图 10-11。
Django allauth defult login screen
现在让我们暂停一下,思考一下 Django·阿劳斯刚刚提供了什么。图 10-11 展示了一个登录页面,与django.contrib.auth登录工作流不同,你甚至不必为其创建模板。此外,请注意图 10-11 中的登录表单要求提供电子邮件凭证,您甚至不需要创建一个自定义认证后端来支持该功能。
接下来,在图 10-11 中的登录表单中输入超级用户的电子邮件/密码,并点击“登录”按钮,如果凭证正确,您将被重定向到“验证您的电子邮件地址”页面。这是有意的,请记住清单 10-17 中的基本 Django allauth 配置强制电子邮件验证(ACCOUNT_EMAIL_VERIFICATION = 'mandatory');因此,在用户的电子邮件地址被验证之前,访问是被拒绝的。
如果您正在使用清单 10-17 中描述的相同电子邮件设置——向控制台发送电子邮件——您将会看到类似于清单 10-18 中运行 Django 开发服务器的电子邮件。
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
Subject: [coffeehouse.com] Please Confirm Your E-mail Address
From: webmaster@localhost
To: cashier@coffeehouse.com
Date: Wed, 12 Aug 2018 00:57:50 -0000
Message-ID: <20180812005750.15177.32621@laptop>
Hello from coffeehouse.com!
You're receiving this e-mail because user daniel at example.com has given yours as an e-mail address to connect their account.
To confirm this is correct, go to http://localhost:8000/accounts/confirm-email/1aflixsbb6sn14hptkntiyrgvk1r0pppqsurx6fxrs6wmlb96u8y3gotxzep0qie/
Thank you from coffeehouse.com!
coffeehouse.com
Listing 10-18.Confirm email for new user in django-allauth
将电子邮件中的验证链接复制并粘贴到您的浏览器中。一旦你点击验证链接,你将被要求确认用户的电子邮件。当您点击“确认”按钮进行最终验证时,您将再次被发送到/accounts/login/ url,显示一条简短的消息,表明帐户已被确认。
现在再次在图 10-11 的登录表单中重新输入超级用户邮箱/密码,并点击“登录”按钮。这一次你将被重定向到/accounts/profile/ url 或者在settings.py的LOGIN_REDIRECT_URL变量中定义的 url。
此时,您使用 Django allauth 提供的登录工作流表单作为超级用户登录到应用。接下来,直接进入 Django admin /admin/ url,你可以确认你能够直接访问它!在这种情况下,不需要使用 Django admin 表单重新认证自己,因为您已经使用 Django allauth 工作流登录。
最后,为了退出应用,您可以访问/accounts/logout/ url,在这里您会看到一个确认问题,询问您是否确定要退出,然后单击“退出”按钮实现退出操作。
正如您在本练习中所看到的,Django allauth 提供了与 Django admin 和django.contrib.auth包所使用的标准登录工作流的紧密集成,此外还提供了已经概述的特性:电子邮件验证、内置模板、内置电子邮件身份验证工作流,以及与用户名的向后兼容性。
Django allauth 用户注册
如果你查看图 10-11 中所示的/accounts/login/网址,你可以看到有一个“注册”链接带你到网址/accounts/signup/。点击这个链接,你会看到一个表单,要求输入电子邮件和密码来创建一个用户帐户。
填写完表格后,点击“注册”按钮。如果提交成功,您将被重定向到“验证您的电子邮件地址”页面。这与上一节描述的行为相同,因为 Django allauth 要求在允许访问应用之前进行电子邮件验证。
类似地,继续检查 Django allauth 生成的电子邮件,并复制粘贴验证链接以完成用户注册过程。接下来,您可以继续使用这个用户的凭据登录,默认情况下是一个普通用户。
关于这个注册过程的一个有趣的点可能不是很明显,Django allauth 创建了一个只使用他的电子邮件的用户,这提出了一些问题:如果这个用户后来成为超级用户或职员来访问 Django admin 会发生什么?依赖用户名的 Django admin 登录表单会失效吗?没有这样的事;它将按预期工作。
在幕后,Django allauth 创建了一个常规的 Django 用户,并将其与 Django allauth 功能(例如,电子邮件登录、社交认证)集成在一起。这种内置的集成是一个非常好的特性,因为您可以获得 Django allauth 的所有好处,而且用户可以保留 Django 的默认用户管理,同一个用户可以获得 Django admin 访问、超级用户和员工权限、属于组的能力以及权限分配。
Django allauth 创建用户名的惯例是将电子邮件的本地部分(即@)作为用户名句柄。对于多个同名电子邮件创建一个账户的情况,Django allauth 会为用户名分配一个数字(如nancy@coffeehouse.com=nancy、nancy@hotmail.com=nancy2、nancy@gmail.com=nancy3)。
使用 Django allauth 重置和更改密码
用户需要的最常见的管理任务之一通常与密码有关,无论是因为忘记密码而重置密码,还是出于安全原因而更改密码。Django allauth 为这两种密码场景提供了内置支持。如果你转到/accounts/password/reset/网址,你会看到一个表单,用户可以在其中输入自己的电子邮件来重置密码。
一旦你在最后一个表单上输入了一封电子邮件,点击“重置我的密码”按钮,Django allauth 就会发送一封确认邮件,就像清单 10-19 中的那封一样,带有一个密码重置链接。
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
Subject: [coffeehouse.com] Password Reset E-mail
From: webmaster@localhost
To: nancy@coffeehouse.com
Date: Wed, 19 Aug 2018 03:05:09 -0000
Message-ID: <20180819030509.15780.98825@laptop>
Hello from coffeehouse.com!
You're receiving this e-mail because you or someone else has requested a password for your user account at coffeehouse.com.
It can be safely ignored if you did not request a password reset. Click the link below to reset your password.
http://localhost:8000/accounts/password/reset/key/5-44f-0f6fbf1251fd33ee4b40/
Thank you for using coffeehouse.com!
coffeehouse.com
-------------------------------------------------------------------------------
Listing 10-19.Reset email for new password django-allauth
接下来,如果您单击电子邮件中的重置链接,您将被带到另一个页面,在那里您可以输入新密码。单击“更改密码”按钮后,如果该过程成功,您将看到一条密码确认提示消息,指示用户密码已更新。
django-allauth 中另一个与密码相关的选项是允许用户在登录时修改密码。如果你转到/accounts/password/change/网址,你会看到一个屏幕,上面有一个介绍新密码的表格。单击“更改密码”按钮后,您将在同一屏幕上看到一条确认消息,表明用户密码已更新。
使用 Django allauth 添加和更改用户电子邮件
为了帮助用户改变他们最初的电子邮件注册地址,Django allauth 有一个专门的页面来管理电子邮件地址。如果你转到/accounts/email/网址,你会看到如图 10-12 所示的页面。
图 10-12。
Django allauth email management
如图 10-12 所示,除了可以向帐户添加其他电子邮件之外,用户还可以更改其主要电子邮件、重新发送电子邮件验证,甚至删除与帐户相关的电子邮件。
更改 Django allauth 的模板
Django allauth 内置模板提供了您在前面章节中看到的基本功能。所有内置模板都从名为base.html的模板中继承它们的行为,它们的内容包含在{% block content %}{% block %}中。这意味着您可以在您的 Django 项目中创建一个名为base.html的模板,其中包含您想要的所有元素(例如,自定义颜色、标题菜单),并在其中声明{% block content %}{% block %},Django allauth 内置模板将在此上下文中呈现。
如果您想完全定制 Django allauth 使用的模板(例如,包括移动友好的表单或一些其他的深层变化),您可以在您的 Django 项目中提供覆盖模板。
Django allauth 依赖超过 15 个 HTML 模板和 6 个电子邮件模板;出于这个原因,如果您将 Django allauth 默认模板复制到您的 Django 项目中,然后根据需要修改它们,会更容易。您应该确保 Django allauth 模板与它们原来的account子文件夹一起被复制,这些子文件夹应该可以在settings.py中的TEMPLATES变量的DIRS值下被访问。根据您的 Python 安装,默认的 Django allauth 模板可以在路径/ lib/python3.5/site-packages/allauth/templates/中找到。
Tip
请参阅该书附带的源代码,其中包括所有 Django allauth 模板的布局。
Django allauth 背后的模型和数据库表
虽然 Django allauth 利用了 Django 的默认用户模型django.contrib.auth.models.User或自定义用户模型(如果它作为AUTH_USER_MODEL配置的一部分提供的话),但是 Django allauth 依靠一系列新模型来支持其高级用户管理特性。图 10-13 展示了 Django admin 展示的 Django allauth 系列模型。
图 10-13。
Django admin with models for django-allauth
图 10-13 所示的第一个 Django allauth 包含对应于“账户”应用,包括“电子邮件地址”模型。“电子邮件地址”模型跟踪电子邮件、它们与用户模型记录的关联(例如django.contrib.auth.models.User)、主要电子邮件状态以及电子邮件的验证状态。应注意,“电子邮件地址”模型记录存储在account_emailaddress数据库表中。
同样值得一提的是,在图 10-13 中,您可以看到django.contrib.auth.models.User包中的标准“用户”和“组”模型。Django allauth 继续利用项目的用户模型来存储核心用户数据(如密码)
图 10-13 中包含的第二组 Django allauth 模型对应于社交账户,用于允许 Django 在社交媒体网站(如脸书、谷歌、Twitter)上进行身份验证,这是本章下一节也是最后一节的主题。
Django allauth 的社会认证
社交认证包括在第三方网站(如脸书、谷歌、Twitter)上创建工作流(也称为应用),让这些网站的用户能够重用他们的身份来访问外部资源,在我们的例子中就是 Django 应用。这种能力在 Django 应用中是一个非常有价值的特性,因为它允许用户点击几次就可以注册,而不是经历一个更复杂的注册过程。
在最基本的层面上,社会认证过程涉及用户授权的令牌,然后在第三方站点和请求方(即您的 Django 应用)之间交换令牌以表明用户的同意。此外,根据应用及其配置,请求方(即 Django 应用)可以从第三方站点请求基本用户信息(例如电子邮件)以及更详细的信息(例如脸书应用中用户的朋友)。
Note
下面几节假设您已经设置了一个基本的 Django allauth 配置,如前一节所述。
为不同的社会服务提供者设置 Django allauth
Django allauth 支持许多社交提供者,每个提供者都通过其自己的包得到支持,这些包需要添加到settings.py中的INSTALLED_APPS列表中,如清单 10-20 所示。
INSTALLED_APPS = [
# Django sites framework is required
'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.facebook',
'allauth.socialaccount.providers.google',
'allauth.socialaccount.providers.twitter',
]
SOCIALACCOUNT_PROVIDERS = {'facebook': {}, 'google':{}, 'twitter':{}}
Listing 10-20.Social provider app installation for Django allauth in settings.py
正如你在清单 10-20 中看到的,清单INSTALLED_APPS中的最后三个应用对应于三个社交提供商:脸书、谷歌和推特。将这些提供者添加到INSTALLED_APPS是使他们能够通过 Django allauth 进行社交认证的第一步。
除了google、facebook和twitter应用,Django allauth 还支持来自不同提供商 3 的 50 多个其他社交认证工作流,在同一个allauth.socialaccount.providers包下。
在清单 10-20 中,您还可以看到SOCIALACCOUNT_PROVIDERS变量,它有一个对应于每个社交提供者的键字典。每个键都有一个空的字典值,该值最终将包含特定于提供者的配置选项。在接下来的部分中,我将描述如何设置每个社交提供者并填写这些配置值。
Caution
在获得社交提供商的真实连接参数之前,不要将社交提供商配置添加到settings.py。由于登录页面和其他工作流身份验证页面中缺少配置,您会得到错误。
除了SOCIALACCOUNT_PROVIDERS中的配置参数,您还需要通过 Django admin 添加一个社交提供商的应用凭证作为“社交应用”模型记录。进入 Django admin,点击“社交应用”模型,如图 10-13 所示。
接下来,您将看到如图 10-14 所示的屏幕,您可以在此编辑和添加社交应用,并点击右上角的“添加社交应用”按钮。在此页面上,您将看到如图 10-15 所示的屏幕,您可以在此选择社交提供商并引入与社交应用相关的各种值(如姓名、客户 id、密钥)。目前我们还没有这些值,但是让这个 Django 管理页面保持打开,因为接下来我将描述从哪里获得您添加到清单 10-20 中的INSTALLED_APPS变量的三个社交提供者的值。
图 10-15。
Django social application create/edit page
图 10-14。
Django social application list page
和 Django·阿劳斯一起建立脸书
要在 Django allauth 设置脸书社交认证,您需要一个脸书帐户来创建他们网站上的应用。前往developers.facebook.com/apps。如果你之前已经创建了一个脸书应用,你会看到如图 10-16 所示的屏幕,但是如果你从未创建过应用,那么你不会看到应用列表,但是你仍然会看到右上角的“添加新应用”绿色按钮,如图 10-16 所示。
图 10-16。
Facebook application list page
点击图 10-16 右上角的“添加新应用”按钮,您将看到如图 10-17 所示的弹出窗口。填写应用名称(如 Coffeehouse)和联系电子邮件的两个选项,然后点击“创建应用 ID”按钮。
图 10-17。
Facebook create application pop-up
当你点击“创建应用 ID”按钮时,应用创建完毕,你将进入应用的主页。接下来,让我们回到图 10-16 所示的应用列表页面,点击左栏下拉菜单中的“查看所有应用”按钮或再次访问 https://developers.facebook.com/apps 网址,稍后你可以点击应用列表页面上的应用返回应用主页。
在脸书应用列表页面(图 10-16 )上,将鼠标指针悬停在您刚刚创建的应用的右上角,直到出现一个箭头,单击该箭头,并从弹出菜单中单击“创建测试应用”选项,如图 10-18 所示。
图 10-18。
Facebook created test application pop-up menu
接下来,会出现一个弹出窗口,您可以在其中为测试应用指定一个名称或保留默认名称;据此选择。终止后,您将看到如图 10-19 所示的屏幕,显示所有应用配置参数。如果没有看到图 10-19 ,回到图 10-16 的脸书 app 列表页面,再次悬停在右上角,从弹出菜单中选择测试应用。
图 10-19。
Facebook test app main page
在图 10-19 中,您可以看到有一个“应用 ID”值,以及一个隐藏的“应用秘密”值,您可以通过单击“显示”按钮来查看。此外,在左侧,您可以看到一系列选项卡,用于进一步定制应用的参数。点击左侧栏中的“设置”/“基本”菜单。
在“设置”/“基本”菜单上,在页面底部中间你会看到一个大按钮“+添加平台”,如图 10-20 所示。点击“+添加平台”按钮,并在弹出菜单中选择“网站”选项。这将在“+添加平台”按钮上方添加一个“网站”框,在该框的输入字段中添加您的 Django 应用的 url(如http://localhost)以告知脸书接受来自该域的登录请求——如图 10-20 所示——此外,在页面顶部附近的“应用域”输入字段中也添加您的 Django 应用的 URL——如图 10-20 所示。
图 10-20。
Facebook test app configuration for authorized domain requests
接下来,转到 Django admin,在“社交应用”模型创建/编辑页面——如图 10-15 所示——通过从提供商列表中选择“脸书”来创建一个脸书应用,在“名称”框中引入一个友好的名称,在“客户端 Id”框中引入脸书的“应用 ID”值,在“密钥”框中引入脸书的“应用秘密”值。
接下来,打开 Django 项目的settings.py文件,用清单 10-21 中所示的脸书配置参数更新SOCIALACCOUNT_PROVIDERS变量。
SOCIALACCOUNT_PROVIDERS = { 'facebook':
{'METHOD': 'oauth2',
'SCOPE': ['email'],
'AUTH_PARAMS': {'auth_type': 'reauthenticate'},
'LOCALE_FUNC': lambda request: 'en_US',
'VERSION': 'v2.4'
}
}
Listing 10-21.Facebook social provider configuration for Django allauth in settings.py
清单 10-21 中的设置是运行脸书认证的最基本的一组值,但是您可以在 Django allauth 文档中查阅许多其他选项。接下来,启动 Django 应用并访问/accounts/login/页面尝试登录,您将看到如图 10-21 所示的页面。
图 10-21。
Django login with social authentication options
正如你在图 10-21 中看到的,与基于 Django allauth 的配置中的登录页面不同,现在有了向脸书、谷歌和 Twitter 注册的选项。点击脸书链接,你将被重定向到脸书页面进行授权,如图 10-22 所示。
图 10-22。
Facebook social authorization pop-up
如图 10-22 所示,用户被告知该操作将在确认后共享其公共档案和电子邮件。如果用户单击“继续为...”按钮脸书用一个令牌和用户的信息(即,公共配置文件信息和电子邮件)联系 Django 应用。此时,Django allauth 创建一个用户,并让他通过常规的 Django allauth 用户创建工作流。
由于之前的 Django allauth 基本配置(此社交配置基于该配置)强制进行电子邮件验证,因此无论脸书认证工作流如何,用户在通过 Django allauth 验证其电子邮件帐户之前,将无法登录应用。一旦用户通过 Django allauth 发送的验证链接确认其电子邮件,用户将被标记为已验证,并可以继续登录应用-请注意,此电子邮件验证过程不是特定的,也不是社交认证所必需的,您可以禁用它。如果您希望在脸书返回响应后立即允许自动登录。
脸书认证和电子邮件验证完成后,用户可以随时点击“脸书”登录链接登录 Django 应用。
在幕后,Django allauth 在“社交应用令牌”和“社交帐户”模型中跟踪社交认证令牌和帐户,这两个模型都可以在 Django admin 中访问。
此外,Django allauth 还通过脸书工作流创建了一个普通的 Django 用户(例如,django.contrib.auth.models.User类),因此同一个用户能够利用 Django 的标准认证系统(例如成为超级用户或职员)。但是请注意,这种类型的用户依赖于脸书授权令牌——并且没有密码——因此认证必须始终通过/accounts/login/中的社交脸书认证链接来完成,Django admin 不为这种类型的用户工作,因为它需要输入密码。
请记住,除了多个特定于脸书的配置 Django allauth 选项超出了本讨论的范围之外,还有其他社交认证概念(例如,令牌撤销、回叫 URL)需要您自己探索和配置——假设它们不是特定于 Django 的——以便为最终用户提供简化的社交认证流程。
Note
前面的程序是作为脸书‘测试应用’完成的——参见图 10-18 。对于 Django 应用的实时版本,您必须在同一个脸书应用的主(即非测试)通道上执行相同的过程。
与 Django·阿劳斯一起建立谷歌
要在 Django allauth 设置 Google social authentication,你需要一个 Google 帐户在他们的网站上创建一个应用。头转向 https://console.developers.google.com/ 。如果你以前创建了谷歌项目,你将登陆一个默认项目。虽然您可以在任何 Google 项目上创建社交认证工作流,但我建议您为此创建一个新项目。
在顶部菜单栏上——在“Google APIs”徽标的右侧——有一个带有默认项目名称的菜单:单击它,会出现一个弹出窗口,其中包含项目列表。在最后一个弹出窗口中,搜索框旁边是一个名为“创建项目”的“+”(加号)按钮。点击“创建项目”按钮,您将被带到图 10-23 所示的页面。介绍一个项目名称,然后单击“创建”按钮。
图 10-23。
Google create project page
点击“创建按钮”后,您将进入项目主页,如图 10-24 所示。确保您处于刚刚创建的项目中,验证顶部菜单栏中选定项目的名称——位于“Google APIs”徽标的右侧。如果不是正确的项目,单击 project 并从弹出的项目列表中选择合适的项目。
图 10-24。
Google project main page
在图 10-24 的左侧菜单中,点击“凭证”选项。接下来,在页面中央,点击“添加凭证”按钮,并从下拉菜单中选择“OAuth 客户端 ID”选项,如图 10-25 所示。
图 10-25。
Google project Oauth client ID option
点击“OAuth 客户端 ID”选项后,您将进入“创建客户端 ID”页面,如图 10-26 所示。您应该选择“Web 应用”选项;但是,请注意页面顶部的警告,指示“要创建 OAuth 客户端 ID,您必须首先在同意屏幕上设置产品名称”。让我们先来看看这个同意屏幕,点击图 10-26 中的“配置同意屏幕”按钮,进入图 10-27 中的页面。
图 10-27。
Google project OAuth consent screen
图 10-26。
Google project create client ID page
图 10-27 中的同意屏幕用于定制当最终用户从一个应用(在我们的例子中是 Django 应用)寻求授权时,Google 如何接待最终用户。填写必填的“向用户显示的产品名称”字段,并保存更改。一旦保存同意屏幕,您将被带回到图 10-26 中的屏幕,在那里您可以选择“网络应用”选项。
在“Web 应用”页面中,“名称”字段被赋予默认的“Web 客户端 1”值,您可以相应地进行调整。将“授权 JavaScript origins”选项设置为 Django 应用的域(如http://localhost),并将“授权重定向 URIs”选项设置为 Django allauth url /accounts/google/login/callback/,这是 Google 在成功认证后联系应用的地方——注意,最后一个值必须使用完全限定的域(如http://localhost/accounts/google/login/callback/)。
在“Web 应用”页面中输入必要的值后,单击“创建”按钮,您将看到如图 10-28 所示的弹出窗口,其中显示了应用客户端 ID 和客户端密码——如果您关闭该弹出窗口,稍后您可以在左侧的主“凭证”菜单中访问相同的值。
图 10-28。
Google project Client ID and Client Secret
接下来,转到 Django admin,在“社交应用”模型创建/编辑页面(如图 10-15 所示)中,通过从提供商列表中选择“Google”来创建一个 Google 应用,在“name”框中引入一个友好名称,在“Client ID”框中引入 Google“Client ID”值,并在“Secret key”框中引入 Google“Client Secret”值
接下来,打开 Django 项目的settings.py文件,用清单 10-22 中所示的 Google 配置参数更新SOCIALACCOUNT_PROVIDERS变量。
SOCIALACCOUNT_PROVIDERS = { 'google':
{ 'SCOPE': ['email'],
'AUTH_PARAMS': { 'access_type': 'online' }
}
}
Listing 10-22.Google social provider configuration for django-allauth in settings.py
清单 10-22 中的设置是运行 Google 认证最基本的一组值,但是你可以在 Django allauth 文档中查阅许多其他选项。接下来,启动 Django 应用并访问图 10-21 中的/accounts/login/页面,尝试登录 Google。点击“谷歌”注册链接,你将被重定向到谷歌授权页面,如图 10-29 所示。
图 10-29。
Google social authorization page
如图 10-29 所示,用户被告知他将在确认后分享他是谁以及他的电子邮件。如果用户点击“Accept ”, Google 会用一个令牌和用户信息(即公开的个人资料信息和电子邮件)联系 Django 应用。此时,Django allauth 创建了一个用户,这样他每次只需点击“Google”登录链接就可以登录 Django 应用。
因为谷歌本身需要有效的电子邮件才能工作,这意味着通过谷歌认证的用户使用的是有效的电子邮件,因此 Django allauth 跳过了电子邮件验证过程,并自动将用户标记为已验证——不像脸书认证,用户可以继续使用脸书的陈旧和潜在无效的电子邮件。
在幕后,Django allauth 在“社交应用令牌”和“社交帐户”模型中跟踪社交认证令牌和帐户,这两个模型都可以在 Django admin 中访问。
此外,Django allauth 还通过 Google workflow 创建了一个普通的 Django 用户(例如,django.contrib.auth.models.User类),因此同一个用户能够利用 Django 的标准认证系统(例如成为超级用户或职员)。然而,请注意,这种类型的用户依赖于 Google 授权令牌——并且没有密码——因此认证必须始终通过/accounts/login/中的社交 Google 认证链接来完成,Django admin 不为这种类型的用户工作,因为它需要输入密码。
与 Django allauth 一起创建 Twitter
要在 Django allauth 中设置 Twitter 社交认证,您需要一个 Twitter 帐户来在他们的网站上创建一个应用。前往 https://apps.twitter.com/app/new ,你会看到一个如图 10-30 所示的屏幕,创建一个 Twitter 应用。
图 10-30。
Twitter create app page
填写新 Twitter 应用所需的详细信息,确保“回调 URL”字段设置为http://localhost:8000/accounts/twitter/login/callback/,这是 Twitter 在成功认证后联系 Django 应用的地方。创建应用,您将被重定向到 Twitter 应用的主页。接下来,单击“密钥和访问令牌”选项卡,您将看到如图 10-31 所示的屏幕。
图 10-31。
Twitter app Key and Access Tokens page
接下来,转到 Django admin,在“社交应用”模型创建/编辑页面——如图 10-15 所示——从提供者列表中选择“Twitter ”,创建一个 Twitter 应用,在“name”框中引入一个友好名称,在“Client Id”框中引入 Twitter“Consumer Key(API Key)”值,在“Secret key”框中引入 Twitter“Consumer Secret”值。
Django allauth 不支持任何 Twitter 配置选项作为 settings.py 中的SOCIALACCOUNT_PROVIDERS变量的一部分,所以您可以跳过这个配置步骤。
接下来,启动您的 Django 应用并访问图 10-21 中的/accounts/login/页面,尝试 Twitter 登录。点击“Twitter”登录链接,您将被重定向到 Twitter 授权页面,如图 10-32 所示。
图 10-32。
Twitter social authorization page
正如你在图 10-32 中看到的,用户被告知他将在确认后分享推文和其他信息。如果用户点击“授权应用”,Twitter 会用一个令牌和用户的信息(即公开的个人资料信息)联系 Django 应用。此时,用户被重定向回网站,Django allauth 向用户索要电子邮件以完成帐户创建过程,如图 10-33 所示。
图 10-33。
Django allauth email request after Twitter social authorization
请注意,图 10-33 中的最后一个电子邮件请求是必要的,因为 Twitter 不提供带有社交认证的电子邮件——尽管很难相信,但这是已知的 Twitter 社交认证限制。
因为基本 Django allauth 配置强制执行电子邮件验证,以避免垃圾邮件,所以用户会收到一封验证电子邮件,在邮件中他必须单击一个链接才能完成激活。
一旦用户通过点击验证电子邮件链接确认了他的电子邮件,他就可以在任何时候通过点击“Twitter”登录链接来登录 Django 应用。在幕后,Django allauth 在“社交应用令牌”和“社交帐户”模型中跟踪社交认证令牌和帐户,这两个模型都可以在 Django admin 中访问。
此外,Django allauth 还使用 Twitter 工作流创建了一个普通用户(例如,django.contrib.auth.models.User类),因此该用户能够利用 Django 的标准认证系统(例如成为超级用户或职员)。但是请注意,这种类型的用户依赖 Twitter 授权令牌——并且没有密码——因此认证必须始终通过/accounts/login/中的社交 Twitter 认证链接来完成,Django admin 不为这种类型的用户工作,因为它需要输入密码。
Footnotes 1
https://docs.djangoproject.com/en/1.11/topics/auth/passwords/#included-validators
2
https://docs.djangoproject.com/en/1.11/ref/contrib/auth/#module-django.contrib.auth.backends
3
http://django-allauth.readthedocs.io/en/latest/providers.html