OverIQ-中文系列教程-三-

OverIQ 中文系列教程(三)

原文:OverIQ Tutorials

协议:CC BY-NC-SA 4.0

Django ORM 基础第 1 部分

原文:https://overiq.com/django-1-11/django-orm-basics-part-1/

最后更新于 2020 年 7 月 27 日


学习了创建模型的艺术后,现在让我们将注意力转移到如何插入和访问数据库中的数据。Django ORM 提供了一种优雅而强大的与数据库交互的方式。ORM 代表对象关系映射器。它只是描述如何以面向对象的方式访问存储在数据库中的数据而不是执行 SQL 的一个花哨的词。

使用shell命令启动 DjangoShell。

$ ./manage.py shell
Python 3.5.2 (default, Nov 23 2017, 16:37:01) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

要在 shell 中使用数据库,首先,我们必须导入必要的模型。在本章中,我们将处理 djangobin 应用中存储的模型,因此让我们从 djangobin 应用中导入所有模型开始。

>>>
>>> from djangobin.models import *
>>>

此时,这些模型对应的表是空的。

让我们创建一个Author对象。

>>>
>>> a = Author(name='tom', email='tom@email.com', active=True)
>>>

尝试打印变量a,您将获得以下输出:

>>>
>>> a
<Author: tom : tom@email.com>
>>>
>>> print(a)
tom : tom@email.com
>>>

请注意,作者的字符串表示来自Author模型中的__str__()方法。如果我们没有定义__str__()方法,我们将得到如下输出:

<Author: Author object>

您可以使用点(.)运算符来访问对象的属性。

>>>
>>> a.name
'tom'
>>>
>>> a.email
'tom@email.com'
>>>
>>> a.active
True
>>>

请注意,在创建Author对象时,我们没有为created_onlast_logged_in字段提供任何值,因为这些字段将auto_now_addauto_now设置为True。因此,当您将对象保存到数据库时,Django 将自动提供当前日期和时间。然而,如果我们没有设置auto_now_addauto_now参数,那么我们将不得不向created_onlast_logged_in字段传递值,如下所示:

>>>
>>> from django.utils import timezone
>>>
>>> r = Author(name="root", email="root@mail.com", active=True, 
... created_on=timezone.now, last_logged_in=timezone.now)
>>>
>>>

请注意,这里我使用的是django.utils.timezone模块,而不是 Python 内置的datetime模块。这是因为django.utils.timezone模块默认生成一个时区感知datetime.datetime对象(由于settings.py文件中的USE_TZ = True)。

在前面的片段中需要注意的另一件重要的事情是,我们只是将函数的名称(timezone.now)传递给created_onlast_logged_in参数,而不是实际调用它。这允许 Django 在插入新记录时调用timezone.now()

此时,变量a指向的对象只存在于 Django shell 内部。要将对象保存到数据库,请调用save()方法。

>>>
>>> a.save()
>>>

回想一下,我们定义的每个模型都继承自models.Model类,这就是save()方法的来源。

要查看这个新添加的对象,请打开 Navicat 中的djangobin_author表。

类似地,models.Model类也提供了一个delete()方法来从数据库中删除一个对象。

>>>
>>> a.delete()
(1, {'djangobin.Author': 1})
>>>

这段代码将作者tom从数据库中删除。然而,它仍然存在于 Shell 内部。

>>>
>>> a
<Author: tom : tom@email.com>
>>>

让我们通过调用save()方法再次保存它。

保存对象时,会自动分配主键。可以使用idpk属性引用主键。

>>>
>>> a.id
2
>>> a.pk
2
>>>

如果你想改变一个对象的属性,只要分配新的值,然后再次调用save()方法。

>>>
>>> a.name = 'tommy'
>>> a.email = 'tommy@example.com'
>>>
>>> a
<Author: tommy : tommy@example.com>
>>>

这些更改尚未保存到数据库中。为此,调用save()方法。

>>>
>>> a.save()
>>>

通过管理器访问数据库

默认情况下,Django 会为每个模型类添加一个名为objects的管理器。objects管理器帮助我们以各种方式查询数据库。

要访问objects管理器类型模型类,后跟(.)点运算符,然后是objects管理器本身。例如:

>>>
>>> Author.objects
<django.db.models.manager.Manager object at 0x7f84da83a1d0>
>>>
>>> type(Author.objects)
<class 'django.db.models.manager.Manager'>
>>>

可以看到,objects只是django.db.models.manager.Manager类的一个实例。objects管理器提供了一整套方法,让我们可以轻松地与数据库交互。

我们来讨论一下objects经理的一些重要方法。

create()方法

create()方法允许我们一次创建对象并将其提交给数据库,而不是单独调用save()方法。例如:

>>>
>>>
>>> a2 = Author.objects.create(name='jerry', email='jerry@mail.com')
>>>
>>> a2
<Author: jerry : jerry@mail.com>
>>>
>>> a2.pk
3
>>>
>>>

bulk_create()方法

bulk_create()方法允许我们创建和提交多个对象。它接受对象列表。例如:

>>>
>>> 
>>> Author.objects.bulk_create([
...     Author(name='spike', email='spike@mail.com'),
...     Author(name='tyke', email='tyke@mail.com'),
...     Author(name='droopy', email='droopy@mail.com'),
... ])
[<Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>, <Author: droopy : droopy@mail.com>]
>>> 
>>>

此时,djangobin_author表应该是这样的:

all()方法

all()方法从表中获取所有记录。例如:

>>>
>>> Author.objects.all()
<QuerySet [<Author: tommy : tommy@example.com>, <Author: jerry : jerry@mail.com>, <Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>, <Author: droopy : droopy@mail.com>]>
>>>

该查询返回djangobin_author表中的所有记录。

all()方法返回一个QuerySet对象。一个QuerySet对象看起来很像一个列表,但它不是一个实际的列表,然而,在某些方面,它的行为就像一个列表。例如,您可以使用索引号访问QuerySet对象中的单个成员。

>>>
>>> l = Author.objects.all()
>>>
>>> type(l)
<class 'django.db.models.query.QuerySet'>
>>> 
>>> l
<QuerySet [<Author: tommy : tommy@example.com>, <Author: jerry : jerry@mail.com>, <Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>, <Author: droopy : droopy@mail.com>]>
>>> 
>>>
>>> l[0]
<Author: tommy : tommy@example.com>
>>>
>>> l[1]
<Author: jerry : jerry@mail.com>
>>>
>>> l[2]
<Author: spike : spike@mail.com>
>>>

虽然,变量l指向类型为QuerySet的对象,但是l[0]l[1]l[2]等等都指向类型为Author的对象。

>>>
>>> type(l[0])
<class 'djangobin.models.Author'>
>>>
>>> type(l[1])
<class 'djangobin.models.Author'>
>>>
>>> type(l[3])
<class 'djangobin.models.Author'>
>>>

一个QuerySet物体在 Django 特别重要。我们用它来过滤、排序和切片结果。正如我们将看到的,还有许多其他方法返回一个QuerySet对象。

QuerySet对象就像列表一样是可重复的。您可以使用 for 循环遍历其中的所有对象。

>>>
>>> l = Author.objects.all()
>>>
>>> for a in l:
...    print("Author: {0}".format(a.name))
...
Author: tommy
Author: jerry
Author: spike
Author: tyke
Author: droopy
>>>

count()方法

count()方法返回查询返回的记录总数。

>>>
>>> Author.objects.count()
5
>>>

这个查询没有任何限定,这就是为什么它返回表中所有记录的计数。前面的查询在功能上等同于下面的查询:

Author.objects.all().count()

使用 filter()方法筛选记录

大多数情况下,您只想处理数据的子集。在 Django,这就是filter()法的工作。它接受字段名作为关键字参数,并返回一个包含符合给定条件的对象的QuerySet

>>>
>>> Author.objects.filter(name='tommy')
<QuerySet [<Author: tommy : tommy@example.com>]>
>>>
>>> Author.objects.filter(name='johnny')
<QuerySet []>
>>>

查询Author.objects.filter(name='tommy')大致翻译成 SQL 如下:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    "djangobin_author"."name" = tommy

由于数据库只有一条名为'tommy'的记录,QuerySet对象只包含一个Author对象。如果我们有两个名称为'tommy'的记录,那么filter()会返回一个包含两个Author对象的QuerySet

类似地,Author.objects.filter(name='johnny')大致翻译成 SQL 如下:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    "djangobin_author"."name" = johnny

由于没有名称为'johnny'的记录,返回一个空的QuerySet

我们也可以使用QuerySet对象的query属性直接打印 Django 用来查询数据库的原始 SQL。

>>>
>>> print(Author.objects.filter(name='tommy').query)
SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    "djangobin_author"."name" = tommy
>>>

使用关键字参数执行的匹配区分大小写。

>>>
>>> Author.objects.filter(email='jerry@mail.com')
<QuerySet [<Author: jerry : jerry@mail.com>]>
>>>
>>> Author.objects.filter(email='JERRY@mail.com')
<QuerySet []>
>>>

最后一个查询返回一个空的QuerySet,因为没有电子邮件是"JERRY@mail.com"的记录,但是有一个电子邮件是"jerry@mail.com"的记录。

也可以将多个关键字参数传递给filter()方法。当我们这样做的时候,关键字参数被放在一起。

>>>
>>> Author.objects.filter(name='spike', email='spike@mail.com')
<QuerySet [<Author: spike : spike@mail.com>]>
>>>

该查询大致转换为如下 SQL:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    (
        "djangobin_author"."name" = spike
        AND "djangobin_author"."email" = spike@mail.com
    )

使用 exclude()方法排除记录

exclude()法与filter()法正好相反。它返回一个QuerySet,只包含与给定参数不匹配的对象。

>>> 
>>> Author.objects.exclude(name='spike', email='spike@mail.com')
<QuerySet [<Author: tommy : tommy@example.com>, <Author: jerry : jerry@mail.com>, <Author: tyke : tyke@mail.com>, <Author: droopy : droopy@mail.com>]>
>>> 
>>>

该查询大致转换为如下 SQL:

>>> print(Author.objects.exclude(name='spike', email='spike@mail.com').query)
SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    NOT (
        "djangobin_author"."email" = spike@mail.com
        AND "djangobin_author"."name" = spike
    )

按主键获取对象

如前所述,Django 会自动向每个模型类添加一个名为id的主键字段。您可以使用id字段或其别名pk通过其主键来访问对象。例如:

>>>
>>> Author.objects.filter(id=2)
<QuerySet [<Author: tommy : tommy@example.com>]>
>>>
>>> Author.objects.filter(pk=2)
<QuerySet [<Author: tommy : tommy@example.com>]>
>>>

字段查找

除了传递字段名作为关键字参数。您也可以使用一种叫做查找的方法来过滤结果。查找由一个模型字段和两个下划线(__)以及查找名称组成。查找也作为关键字参数传递。

包含查找

contains查找执行区分大小写的包容测试。例如:

>>>
>>> Author.objects.filter(name__contains="ke")
<QuerySet [<Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>]>
>>>
>>>

在这个查询中,contains查找找到所有对象,其中name字段在开始、结束或两者之间包含字符串"ke"

查询Author.objects.filter(name__contains="ke")大致翻译成 SQL 如下:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    "djangobin_author"."name" LIKE '%ke%'

通过contains查找执行的匹配区分大小写。

图标包含查找

工作方式与contains类似,但执行不区分大小写的匹配。

>>>
>>> Author.objects.filter(name__icontains="KE")
<QuerySet [<Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>]>
>>>

SQLite 数据库不支持区分大小写的LIKE语句。因此,containsicontains查找将返回相同的结果。

从查找开始

startswith查找执行区分大小写的开头。例如:

>>>
>>> Author.objects.filter(name__startswith="t")
<QuerySet [<Author: tommy : tommy@email.com>, <Author: tyke : tyke@mail.com>]>
>>>

在这个查询中,startswith查找找到了所有以字符串"t"开头的name字段的记录。

上述查询的等效 SQL 是:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    "djangobin_author"."name" LIKE 't%'

还有一个互补的查找叫做endswith

查找结束

它执行区分大小写的以。例如:

>>>
>>> Author.objects.filter(email__endswith="com")
<QuerySet [<Author: tom : tom@email.com>, <Author: jerry : jerry@mail.com>, <Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>, <Author: droopy :
droopy@mail.com>]>
>>>
>>>

这里endswith查找找到所有电子邮件以"com"结尾的记录。

上述查询的等效 SQL 是:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    "djangobin_author"."email" LIKE "%com"

startswithendswith都区分大小写。它们的不区分大小写的对等词是istartswithiendswith

gt 查找

大于:

>>>
>>> Author.objects.filter(id__gt=3)
<QuerySet [<Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>, <Author: droopy : droopy@mail.com>]>
>>>

在这个查询中,gt查找找到所有的id或主键(pk)大于3的记录。

上述查询的等效 SQL 是:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    "djangobin_author"."id" > 3

还有一个互补查找叫做lt(小于)。

lt 查找

小于:

>>>
>>> Author.objects.filter(id__lt=3)
<QuerySet [<Author: tommy : tommy@example.com>]>
>>>
>>>

这里lt查找查找主键小于 3 的所有记录。

上述查询的等效 SQL 是:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    "djangobin_author"."id" < 3

还有两个类似的查找分别叫做gte(大于等于)和lte(小于等于),分别查找大于等于和小于等于的记录。

您也可以一次传递多个查找。当我们这样做时,查找和 and 一起进行。

>>>
>>> Author.objects.filter(id__lt=5, email__endswith="com")
<QuerySet [<Author: tommy : tommy@example.com>, <Author: jerry : jerry@mail.com>, <Author: spike : spike@mail.com>]>
>>> 
>>>

上述查询的等效 SQL 是:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    (
        "djangobin_author"."id" < 5
        AND "djangobin_author"."email" LIKE "%com"
    )

精确查找

exact查找执行区分大小写的精确匹配:例如:

>>>
>>> Author.objects.filter(name__exact="spike")
<QuerySet [<Author: spike : spike@mail.com>]>
>>>

该查询返回任何名称为spike的对象。该查询的 SQL 等价物如下:

>>> print(Author.objects.filter(name__exact="spike").query)
SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    "djangobin_author"."name" = spike
>>>

需要记住的重要一点是,如果您没有在查询中指定任何查找,Django 将隐式应用exact查找。这意味着前面的查询也可以编写如下:

>>>
>>> Author.objects.filter(name="spike")
<QuerySet [<Author: spike : spike@mail.com>]>
>>>

iexact 查找

exact查找相同,但执行不区分大小写的匹配。

>>> 
>>> Author.objects.filter(name__exact="SPIKE")
<QuerySet []>
>>> 
>>> Author.objects.filter(name__iexact="SPIKE")
<QuerySet [<Author: spike : spike@mail.com>]>
>>>

isnull 查找

isnull查找取TrueFalse,分别添加IS NULLIS NOT NULL操作符到查询中。

>>>
>>> Author.objects.filter(name__isnull=True)
<QuerySet []>
>>> 
>>> Author.objects.filter(name__isnull=False)
<QuerySet [<Author: tommy : tommy@example.com>, <Author: jerry : jerry@mail.com>, <Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>, <Author: droopy : droopy@mail.com>]>
>>>

前面两个查询的 SQL 等价物如下:

>>> 
>>> Author.objects.filter(name__isnull=True).query
>>> print(Author.objects.filter(name__isnull=True).query)
SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    "djangobin_author"."name" IS NULL
>>> 

>>> print(Author.objects.filter(name__isnull=False).query)
SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    "djangobin_author"."name" IS NOT NULL
>>>

查找中

in查找找到列表中指定的所有值。例如:

>>> 
>>> Author.objects.filter(name__in=['spike', 'tyke'])
<QuerySet [<Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>]>
>>>

前面的查询返回namespiketykeAuthor对象。

该查询的 SQL 等价物是:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    "djangobin_author"."name" IN ('spike', 'tyke')

日期、月份和年份查找

daymonthyear查找分别执行精确的日、月和年匹配。例如:

>>>
>>> Author.objects.filter(created_on__year=2018)
<QuerySet [<Author: tommy : tommy@example.com>, <Author: jerry : jerry@mail.com>, <Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>, <Author: droopy : droopy@mail.com>]>
>>>

该查询返回 2018 年创建的所有Author对象。

>>> 
>>> Author.objects.filter(created_on__month=3, created_on__year=2018)
<QuerySet [<Author: tommy : tommy@example.com>, <Author: jerry : jerry@mail.com>, <Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>, <Author: droopy : droopy@mail.com>]>
>>>

该查询返回 2018 年 3 月创建的所有对象。

>>> 
>>> Author.objects.filter(created_on__day=24, created_on__month=3, created_on__year=2018)
<QuerySet [<Author: jerry : jerry@mail.com>, <Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>, <Author: droopy : droopy@mail.com>]>
>>>

该查询返回 2018 年 3 月 24 日创建的所有对象。

链接查询集

也可以链接多个QuerySet对象,以获得您想要的东西。例如:

>>> 
>>> Author.objects.filter(id__gt=1).\
...     exclude(name='spike').\
...     filter(name__icontains="o")
<QuerySet [<Author: tommy : tommy@example.com>, <Author: droopy : droopy@mail.com>]>
>>>

在这个查询中,我们首先创建一个包含主键大于1的对象的QuerySet。然后我们排除所有名称为spike的物体。最后,我们只过滤在它们的name中包含字符'o'的对象。

当您在构建查询时应用一些逻辑时,链接特别有用。例如:

q = Author.objects.filter(created_on__year=2018)
if True:  # some logic
    q = q.filter(active=False)

得到第一个和最后一个结果

first()last()方法分别从QuerySet返回第一个和最后一个结果。

>>>
>>> Author.objects.filter(created_on__year=2018)
<QuerySet [<Author: tommy : tommy@example.com>, <Author: jerry : jerry@mail.com>, <Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>, <Author: droopy : droopy@mail.com>]>
>>> 
>>> 
>>> Author.objects.filter(created_on__year=2018).first()
<Author: tommy : tommy@example.com>
>>> 
>>>
>>> Author.objects.filter(created_on__year=2018).last()
<Author: droopy : droopy@mail.com>
>>> 
>>>

使用 get()方法检索单个对象

上节描述的filter()方法返回一个QuerySet,有时候我们只想从表中取一条记录。为了处理这些情况,objects经理提供了一种get()方法。get()方法接受与filter()方法相同的参数,但它只返回一个对象。如果它发现多个对象,它会引发MultipleObjectsReturned异常。如果没有找到任何对象,就会引发DoesNotExist异常。

>>>
>>> Author.objects.get(name="tommy")
<Author: tommy : tommy@email.com>
>>>
>>> Author.objects.filter(name="tommy")
<QuerySet [<Author: tommy : tommy@example.com>]>
>>>

注意get()filter()方法输出的差异。对于同一个参数,它们都返回两个不同的结果。get()方法返回一个Author的实例,而filter()方法返回一个QuerySet对象。

让我们看看如果get()方法遇到多条记录会发生什么。

>>>
>>> Author.objects.filter(name__contains="ke")
<QuerySet [<Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>]>
>>>
>>>
>>> Author.objects.get(name__contains="ke")
Traceback (most recent call last):
...
djangobin.models.MultipleObjectsReturned: get() returned more than one Author -- it returned 2!
>>>

这里get()方法引发MultipleObjectsReturned异常,因为数据库中有多个对象与给定参数匹配。

同样,如果您试图访问一个不存在的对象,那么get()方法将引发DoesNotExist异常。

>>>
>>> Author.objects.get(name__contains="captain planet")
Traceback (most recent call last):
...
djangobin.models.DoesNotExist: Author matching query does not exist.
>>>

排序结果

为了排序结果,我们使用order_by()方法,就像filter()一样,它也返回一个QuerySet对象。它接受要作为位置参数进行排序的字段名。

>>>
>>> Author.objects.order_by("id")
<QuerySet [<Author: tommy : tommy@example.com>, <Author: jerry : jerry@mail.com>, <Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>, <Author: droopy : droopy@mail.com>]>
>>>

该代码以升序检索由id排序的所有Author对象。该查询的 SQL 等价物是:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
ORDER BY
    "djangobin_author"."id" ASC

这里还有一个例子,我们首先对结果进行过滤,按照name升序排序。

>>>
>>> Author.objects.filter(id__gt=3).order_by("name")
<QuerySet [<Author: droopy : droopy@mail.com>, <Author: spike : spike@mail.com>,
 <Author: tyke : tyke@mail.com>]>
>>>

该查询的 SQL 等价物是:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    "djangobin_author"."id" > 3
ORDER BY
    "djangobin_author"."name" ASC

要反转排序顺序,请在字段名称前添加减号(-),如下所示:

>>>
>>> Author.objects.filter(id__gt=3).order_by("-name")
<QuerySet [<Author: tyke : tyke@mail.com>, <Author: spike : spike@mail.com>, <Au
thor: droopy : droopy@mail.com>]>
>>>

该查询的 SQL 等价物是:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    "djangobin_author"."id" > 3
ORDER BY
    "djangobin_author"."name" DESC

您也可以像这样按多个字段对结果进行排序。

>>>
>>> Author.objects.filter(id__gt=3).order_by("name", "-email")
<QuerySet [<Author: droopy : droopy@mail.com>, <Author: spike : spike@mail.com>,
 <Author: tyke : tyke@mail.com>]>
>>>

该查询首先按name升序对结果排序,然后按email降序对结果排序

选择字段

当您像这样查询数据库时:

>>>
>>> Author.objects.filter(name__contains='foo').order_by("name")
>>>

它返回所有字段(列)中的数据。如果我们只需要一两个字段的数据呢?对象管理器为此作业提供了一种values_list()方法。values_list()方法接受一个或多个我们想要数据的字段名,并返回一个QuerySet。如果您没有向values_list()方法传递任何值,那么将返回所有字段的数据。例如:

>>>
>>> Author.objects.values_list("id", "name")
<QuerySet [(2, 'tommy'), (3, 'jerry'), (4, 'spike'), (5, 'tyke'), (6, 'droopy')]>
>>>

注意values_list()方法返回一个QuerySet,其中每个元素都是一个元组。并且元组只包含来自我们在values_list()方法中指定的字段的数据。

下面是另一个例子:

>>>
>>> Author.objects.filter(id__gt=3).values_list("id", "name")
<QuerySet [(4, 'spike'), (5, 'tyke'), (6, 'droopy')]>
>>>
>>> r = Author.objects.filter(id__gt=3).values_list("id", "name")
>>>
>>> r
<QuerySet [(4, 'spike'), (5, 'tyke'), (6, 'droopy')]>
>>>
>>> r[0]
(4, 'spike')
>>>
>>> r[0][0]
4
>>>
>>> r[0][1]
'spike'
>>>

objects管理器还提供了一个名为values()的相同方法,其工作原理与values_list()完全相同,但它返回一个QuerySet,其中每个元素都是字典而不是元组。

>>>
>>> r = Author.objects.filter(id__gt=3).values("id", "name")
>>>
>>> r
<QuerySet [{'name': 'spike', 'id': 4}, {'name': 'tyke', 'id': 5}, {'name': 'droo
py', 'id': 6}]>
>>>
>>> type(r[0])
<class 'dict'>
>>>
>>> r[0]
{'name': 'spike', 'id': 4}
>>>
>>> r[0]['name']
'spike'
>>>
>>> r[0]['id']
4
>>>

切片结果

您可以使用 Python 列表切片语法([start:end])将您的QuerySet对象限制为一定数量的结果。

例 1:

>>>
>>> Author.objects.order_by("id")[1]  
<Author: tyke : tyke@mail.com>
>>>

此查询按 id 按升序对结果进行排序,然后只返回 QuerySet 中的第一个对象。相当于查询的 SQL 是:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
ORDER BY
    "djangobin_author"."id" DESC
LIMIT 1 OFFSET 1

例 2:

>>>
>>> Author.objects.order_by("-id")[:3]
<QuerySet [<Author: droopy : droopy@mail.com>, <Author: tyke : tyke@mail.com>, <
Author: spike : spike@mail.com>]>
>>>
>>>

该查询按 id 降序对结果进行排序,并从 QuerySet 返回前三个对象。这段代码大致翻译成 SQL 如下:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
ORDER BY
    "djangobin_author"."id" DESC
LIMIT 3

例 3:

>>>
>>> # returns objects from 3rd index to 5th index after sorting the result
>>>
>>> Author.objects.filter(id__gt=1).order_by("-id")[2:5]
<QuerySet [<Author: spike : spike@mail.com>, <Author: jerry : jerry@mail.com>, <Author: tommy : tommy@example.com>]>
>>> 
>>>

该查询返回从索引 2 到索引 5 的对象。该查询的 SQL 等价物如下:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
ORDER BY
    "djangobin_author"."id" DESC
LIMIT 3 OFFSET 2

不幸的是,我们不能使用负索引来分割 queryset。

>>>
>>> Author.objects.order_by("-id")[-1]
Traceback (most recent call last):
#...
AssertionError: Negative indexing is not supported.
>>>
>>>

带有 Q 对象的复杂查询

我们已经看到,当我们传递多个关键字参数来查找函数(即filter()exclude()get())时,它们是被 and 在一起的。有时您需要使用OR运算符而不是AND运算符来组合两个或多个条件。这就是Q物体发挥作用的地方。

一个Q对象只是一个关键字参数的集合,您通常会将其传递给查找函数(filter()exclude()get()方法)。例如:

from django.db.models import Q
Q(name__contains="tom")

这个Q对象封装了一个关键字参数。但是我们可以传递任意多的关键字参数。

from django.db.models import Q
Q(name__icontains="tom", email__icontains="example", created_on__year=2018)

一旦我们使用Q对象创建了一个条件,我们就可以在代码中多次重用它,而无需一次又一次地重新创建它。

请注意,在前面的示例中,传递给Q()构造函数的多个关键字参数被一起 and。这意味着以下两个查询是等价的。

>>> 
>>> from django.db.models import Q
>>>
>>> Author.objects.filter(Q(name__icontains="tom", email__icontains="example", created_on__year=2018))
<QuerySet [<Author: tommy : tommy@example.com>]>
>>> 
>>>
>>> Author.objects.filter(name__icontains="tom", email__icontains="example", created_on__year=2018)
<QuerySet [<Author: tommy : tommy@example.com>]>
>>> 
>>>

该查询的 SQL 等价物是:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    (
        "djangobin_author"."created_on" BETWEEN 2018 - 01 - 01 00 : 00 : 00
        AND 2018 - 12 - 31 23 : 59 : 59.999999
        AND "djangobin_author"."name" LIKE "%tom%"
        AND "djangobin_author"."email" LIKE "%example%"
    )

我们可以使用&(按位“与”)和|(按位“或”)运算符组合Q对象。当我们这样做时,一个新的Q对象被创建。&|运算符允许我们分别使用ANDOR条件创建 SQL 查询。例如:

>>>
>>> Author.objects.filter(Q(name__iexact="tommy") | Q(name__iexact="jerry"))
<QuerySet [<Author: tommy : tommy@example.com>, <Author: jerry : jerry@mail.com>]>
>>> 
>>>

该查询返回其name字段为tomjerry的所有对象。该查询的 SQL 等价物是:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    (
        "djangobin_author"."name" = 'tommy'
        OR "djangobin_author"."name" = 'jerry'
    )

我们可以将多个Q对象作为位置参数传递给查找函数(例如filter()exclude()get()等)。如果我们这样做的话,Q的物体将会聚集在一起。例如:

>>> 
>>> Author.objects.filter(
...     Q(created_on__year=2018),  
...     Q(name__iexact="tommy")| Q(name__iexact="jerry")
... )
<QuerySet [<Author: tommy : tommy@example.com>, <Author: jerry : jerry@mail.com>]>
>>>

该查询的 SQL 等价物是:

SELECT
    "djangobin_author"."id",
    "djangobin_author"."name",
    "djangobin_author"."email",
    "djangobin_author"."active",
    "djangobin_author"."created_on",
    "djangobin_author"."last_logged_in"
FROM
    "djangobin_author"
WHERE
    (
        "djangobin_author"."created_on" BETWEEN 2018 - 01 - 01 00 : 00 : 00
        AND 2018 - 12 - 31 23 : 59 : 59.999999
        AND (
            "djangobin_author"."name" LIKE "tommy"
            OR "djangobin_author"."name" LIKE "jerry"
        )
    )

最后,我们还可以在调用查找函数时将关键字参数与Q对象混合。在这样做的时候,请记住查找函数的所有参数将被加在一起,并且所有Q对象必须出现在任何关键字参数之前。这意味着以下是有效的查询:

>>> 
>>> Author.objects.filter(
...    Q(name__iexact="tommy") | Q(name__iexact="jerry"), 
...    active=True
... )
<QuerySet [<Author: tommy : tommy@example.com>]>
>>>

但下一个不是。

>>>
>>> Author.objects.filter(
...    active=True,
...    Q(name__iexact="tommy") | Q(name__iexact="jerry"), 
... )
  File "<console>", line 3
SyntaxError: positional argument follows keyword argument
>>>

更新多个对象

回想一下,更新对象的一种方法是在更新其属性后调用save()方法。例如:

>>>
>>>
>>> a = Author.objects.get(pk=2)
>>> a
<Author: tommy : tommy@email.com>
>>>
>>> a.name = 'tom'
>>> a.email = 'tom@mail.com'
>>>
>>> a.save()
>>>
>>> a
<Author: tom : tom@mail.com>
>>>
>>>

objects管理器提供一种称为update()的方法,一步更新一条或多条记录。就像filter()方法一样,它接受一个或多个关键字参数。如果更新成功,它将返回更新的行数。

>>>
>>> Author.objects.filter(id__gt=3).update(active=True, name='x')
3
>>>

该查询修改主键大于 3 的所有作者的activename字段。

该查询的 SQL 等价物是:

UPDATE djangobin_author 
SET active=1, name='x'
WHERE id > 3;

这里还有一个例子,将所有对象的active属性修改为False

>>>
>>>
>>> Author.objects.update(active=False)
5
>>>
>>>

该查询相当于以下内容:

Author.objects.all().update(active=False)

上述查询的等效 SQL 是:

UPDATE djangobin_author SET
active=0

删除记录

正如本课前面所讨论的,我们可以使用模型的delete()方法从数据库中删除一个对象。例如:

>>>
>>> a = Author.objects.get(pk=2)
>>>
>>> a
<Author: tom : tom@mail.com>
>>>
>>> a.delete()
(1, {'djangobin.Author': 1})
>>>
>>>

要一次删除多个记录,请使用QuerySet对象提供的delete()方法。例如:

>>>
>>> Author.objects.all().delete()
(4, {'djangobin.Author': 4})
>>>
>>>

现在您应该对 Django ORM 有了一个坚实的了解。在下一课中,我们将讨论如何使用 Django ORM 访问相关表中的数据。



Django ORM 基础第 2 部分

原文:https://overiq.com/django-1-11/django-orm-basics-part-2/

最后更新于 2020 年 7 月 27 日


在前一课中,我们已经介绍了使用 Django ORM 与数据库交互的所有基础知识。我们已经创建、修改和删除了许多对象。到目前为止,我们处理的对象类型都是可以独立存在的简单对象。在本课中,我们将学习如何插入和访问相关数据,但首先,我们将在表中填充一些数据。

如果您正在密切关注课程,此时,djangobin 应用的所有表都应该是空的。在我们继续之前,让我们分别给djangobin_authordjangobin_language表添加一些AuthorLanguage对象。通过执行shell命令打开 Django shell,从 djangobin 应用导入所有模型。

$ ./manage.py shell
Python 3.5.2 (default, Nov 23 2017, 16:37:01) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> 
>>> from djangobin.models import *
>>>

将以下代码复制并粘贴到 Django shell 中,以创建一些作者和语言。

Author.objects.bulk_create([
    Author(name='tom', email='tom@mail.com'),
    Author(name='jerry', email='jerry@mail.com'),
    Author(name='spike', email='spike@mail.com'),
    Author(name='tyke', email='tyke@mail.com'),
])

Language.objects.bulk_create([
    Language(name='Python', lang_code='python', slug='python', mime='text/x-cython', file_extension=".py"),
    Language(name='PHP', lang_code='php', slug='php', mime='text/x-php', file_extension=".php"),
    Language(name='Java', lang_code='java', slug='java', mime='text/x-java', file_extension=".java"),
    Language(name='JavaScript', lang_code='js', slug='js', mime='application/javascript', file_extension=".js"),
])

>>>
>>>
>>> Author.objects.bulk_create([
...     Author(name='tom', email='tom@mail.com'),
...     Author(name='jerry', email='jerry@mail.com'),
...     Author(name='spike', email='spike@mail.com'),
...     Author(name='tyke', email='tyke@mail.com'),
... ])
[<Author: tom : tom@mail.com>, <Author: jerry : jerry@mail.com>, <Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>]
>>> 
>>> Language.objects.bulk_create([
...     Language(name='Python', lang_code='python', slug='python', mime='text/x-cython', file_extension=".py"),
...     Language(name='PHP', lang_code='php', slug='php', mime='text/x-php', file_extension=".php"),
...     Language(name='Java', lang_code='java', slug='java', mime='text/x-java', file_extension=".java"),
...     Language(name='JavaScript', lang_code='js', slug='js', mime='application/javascript', file_extension=".js"),
... ])
[<Language: Python>, <Language: PHP>, <Language: Java>, <Language: JavaScript>]
>>> 
>>>

现在让我们将注意力转移到Snippet模型上。以下是Snippet模型的定义:

djangobin/django _ project/djangobin/models . py

#...

class Snippet(models.Model):
    title = models.CharField(max_length=200, blank=True)
    original_code = models.TextField()
    highlighted_code = models.TextField()
    expiration = models.CharField(max_length=10, choices=Pref.expiration_choices)
    exposure = models.CharField(max_length=10, choices=Pref.exposure_choices)
    hits = models.IntegerField(default=0)
    slug = models.SlugField()
    created_on = models.DateTimeField(auto_now_add=True)

    language = models.ForeignKey(Language, on_delete=models.CASCADE)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    tag = models.ManyToManyField('Tag')

    class Meta:
        ordering = ['-created_on']

#...

这就是djangobin_snippet表的样子:

Snippet模型有两种一对多的关系。第一款是Author车型,第二款是Language车型。

一对多关系的数据将存储在djangobin_snippet表中。此外,Snippet模型的authorlanguage字段不接受空值,因此,Snippet对象必须与AuthorLanguage对象相关联。试图创建没有AuthorLanguage对象的Snippet对象将引发IntegrityError异常。

Snippet模型与Tag模型也有多对多的关系。但是由于多对多关系的数据存储在单独的表(djangobin_snippet_tags)中,因此在创建Snippet对象时,我们不需要提供这些数据。一旦我们创建了一个Snippet对象,我们可以在以后给它添加标签。我们将在本章后面看到如何做到这一点。

让我们尝试在不提供作者和语言的情况下创建一个Snippet对象,看看会发生什么:

>>>
>>>
>>> import time
>>>
>>> s = Snippet(
...     original_code = 'some code',
...     highlighted_code = '<p>some code</p>',
...     expiration="never",
...     exposure = "public",
...     slug=str(time.time()).replace(".", "")
... )
>>> 
>>>
>>> s.save()
>>>
Traceback (most recent call last):
...
django.db.utils.IntegrityError: NOT NULL constraint failed: djangobin_snippet.author_id

>>>

不出所料,调用save()方法会引发类型为IntegrityError的异常。如果您尝试使用objects管理器的create()方法创建一个Snippet对象,您会得到相同的异常。

错误是告诉我们author_id列有NOT NULL约束。因此,它不能接受NULL的价值观。

我们在author_id列而不是language_id列上得到IntegrityError的原因是djangobin_snippet表中author_id出现在language_id列之前。

我们可以通过将null=True传递给模型类中的字段构造器来允许author字段接受NULL值。目前,没有理由这么做。

好的,让我们再次尝试创建Snippet对象,但是这次我们将提供一个Author和一个Language对象。

>>>
>>> 
>>> Author.objects.values_list("id", "name")
<QuerySet [(7, 'tom'), (8, 'jerry'), (9, 'spike'), (10, 'tyke')]>
>>> 
>>> Language.objects.values_list("id", "name")
<QuerySet [(3, 'Java'), (4, 'JavaScript'), (2, 'PHP'), (1, 'Python')]>
>>> 
>>>
>>> a1 = Author.objects.get(pk=7)
>>>
>>> a1
<Author: tom : tom@mail.com>
>>>
>>> a2 = Author.objects.get(pk=8) 
>>>
>>> a2
<Author: jerry : jerry@mail.com>
>>>
>>> l1 = Language.objects.get(pk=1)
>>>
>>> l1
<Language: Python>
>>> 
>>> 
>>> s1 = Snippet(
...     original_code = 'some code',
...     highlighted_code = '<p>some code</p>',
...     expiration="never",
...     exposure = "public",
...     slug=str(time.time()).replace(".", ""),
...     author = a1,
...     language = l1
... )
>>> 
>>> s1
<Snippet: Untitled - Python>
>>>  
>>> s1.save()
>>>

可以看到,这次操作成功了。您也可以使用objects管理器的create()方法来创建一个Snippet对象,而不是调用Snippet()构造函数。

>>>
>>> s2 = Snippet.objects.create(
...     original_code = 'another snippet',
...     highlighted_code = '<p>another snippet</p>',
...     expiration="never",
...     exposure = "public",
...     slug=str(time.time()).replace(".", ""),
...     author = a1,
...     language = l1
... )
>>> 
>>> s2
<Snippet: Untitled - Python>
>>>

现在,我们可以使用点(.)操作符使用s1s2变量来获取关于Snippet对象以及该对象所属的AuthorLanguage对象的信息。

>>>
>>>
>>> s1.title
''
>>> 
>>> s1.original_code
'some code'
>>> 
>>> s1.highlighted_code
'<p>some code</p>'
>>> 
>>> s1.expiration
'never'
>>> 
>>> s1.created_on
datetime.datetime(2018, 3, 26, 8, 19, 40, 824422, tzinfo=<UTC>)
>>> 
>>> s1.slug
'1522052373360689' 
>>>
>>> s1.language     # get the Language instance attached to s1
<Language: Python>
>>> 
>>> s1.language.slug    
'python'
>>> 
>>> s1.author       # get the Author instance attached to s1 
<Author: tom : tom@mail.com>
>>> 
>>> s1.author.email
'tom@mail.com'
>>> 
>>>

请注意,我们如何能够在不编写 SQL JOIN 查询的情况下访问存储在不同表中的数据。这就是 Django ORM 的力量。

在创建Snippet对象时,您也可以传递作者的主键,而不是传递Author对象,但是您必须将其分配给author_id关键字参数,而不是author。同样,我们可以将Language对象的主键传递给language_id关键字参数。

>>>
>>> 
>>> Author.objects.values_list('id', 'name')
<QuerySet [(7, 'tom'), (8, 'jerry'), (9, 'spike'), (10, 'tyke')]>
>>> 
>>> Language.objects.values_list('id', 'name')
<QuerySet [(3, 'Java'), (4, 'JavaScript'), (2, 'PHP'), (1, 'Python')]>
>>> 
>>>
>>> s3 = Snippet.objects.create(
...     original_code = 'cool snippet',
...     highlighted_code = '<p>cool snippet</p>',
...     expiration="never",
...     exposure = "public",
...     slug=str(time.time()),
...     author_id = 7,
...     language_id = 2
... )
>>>
>>>

在我们访问下一部分之前,让我们向数据库中添加一些Tag对象。

>>>
>>> t1 = Tag.objects.create(name="django", slug="django")
>>>
>>> t2 = Tag.objects.create(name="flask", slug="flask")
>>>

其他经理

objects并不是 Django 唯一可用的经理。原来,在处理多对多关系时,Django 使用一个名为 related manager 的管理器来连接数据。在Snippet模型tags领域就是这样一个管理者。您可以通过在 Django shell 中键入以下代码来验证这一点。

>>>
>>> type(s1.tags)
<django.db.models.fields.related_descriptors.create_forward_many_to_many_manager.<locals>.ManyRelatedManager object at 0x7fc981e36e10>
>>>

由于tags是一个经理,你可以使用我们在Django ORM 基础部分 1 一课中学到的所有管理方法。例如,要查看与s1s2帖子相关联的所有标签,请键入以下代码:

>>>
>>> s1.tags.all()
<QuerySet []>
>>>
>>> 
>>> s2.tags.all()
<QuerySet []>
>>>

目前,片段s1s2没有与任何标签相关联,这就是为什么返回空的QuerySet的原因。

那么我们如何给现有的Snippet对象添加标签呢?

所有的关系管理器都自带add()方法,可以用来连接对象。

>>>
>>> t1
<Tag: django>
>>>
>>> s1.tags.add(t1)
>>>

现在片段s1django标签相关联。上述代码在djangobin_snippet_tags表中添加了以下记录。

>>>
>>> s1.tags.all()
<QuerySet [<Tag: django>]>
>>>

您也可以通过将多个参数传递给add()方法,将多个标签与一个Snippet对象相关联。

>>>
>>> s2.tags.add(t1, t2)
>>> 
>>> s2.tags.all()
<QuerySet [<Tag: django>, <Tag: flask>]>
>>> 
>>> s2.tags.order_by("-name")
<QuerySet [<Tag: flask>, <Tag: django>]>
>>> 
>>>

反向访问

当您在模型类中使用类似ForeignKeyManyToManyField等字段定义关系时。该模型的每个实例都将具有访问相关对象的属性。例如,给定一个Snippet对象s1,我们可以使用s1.author访问其作者,使用s1.language访问其语言,使用s1.tags访问其标签。

但是,我们如何反过来访问数据呢?简单地说,我们如何访问来自Author实例或Tag实例的片段?

Django 自动在关系的另一端添加一个
<related_model>_set形式的关系管理器。更具体地说,给定一个Author对象a1,与该作者相关的片段由a1.snippet_set给出。snippets_set属性是类型ManyRelatedManager的一个实例,继承自django.db.models.manager.Manager,因此,我们在一章中讨论的大多数管理器方法都可以使用。例如:

>>>
>>> a1.snippet_set.all()
<QuerySet [<Snippet: Untitled - PHP>, <Snippet: Untitled - Python>, <Snippet: Untitled - Python>]>
>>> 
>>> a1.snippet_set.order_by("-created_on")
<QuerySet [<Snippet: Untitled - PHP>, <Snippet: Untitled - Python>, <Snippet: Untitled - Python>]>
>>> 
>>> a1.snippet_set.order_by("-created_on").values_list('id', 'created_on')
<QuerySet [(4, datetime.datetime(2018, 3, 26, 8, 48, 48, 956850, tzinfo=<UTC>)), (3, datetime.datetime(2018, 3, 26, 8, 23, 23, 534384, tzinfo=<UTC>)), (2, datetime.datetime(2018, 3, 26, 8, 19, 40, 824422, tzinfo=<UTC>))]>
>>> 
>>>

类似地,给定一个Tag实例t1,与这个标签相关联的片段由t1.snippet_set给出。

>>> 
>>> t1.snippet_set.all()
<QuerySet [<Snippet: Untitled - Python>, <Snippet: Untitled - Python>]>
>>> 
>>> 
>>> t1.snippet_set.filter(exposure='public')
<QuerySet [<Snippet: Untitled - Python>, <Snippet: Untitled - Python>]>
>>> 
>>>

如果由于某种原因<related_model>_set属性看起来不直观或不可读。我们可以通过将related_name参数传递给模型类中的ForeignKeyManyToManyField构造函数来更改它。例如:

class Snippet(models.Model):
    #...

    language = models.ForeignKey(Language, on_delete=models.CASCADE, )
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='snippets')
    tags = models.ManyToManyField('Tag', related_name='snippets')

对于AuthorTag实例,该代码将关系名称从snippet_set更改为snippets。给定一个Tag实例t1,我们现在可以使用t1.snippets访问相关的片段。类似地,给定一个Author实例a1,我们可以使用a1.snippets访问相关的片段。

跨越关系的查找

到目前为止,关键字参数(有或没有查找)我们一直传递给查找函数(例如filter()exclude()get()等;)仅限于我们当前操作的模型类。

事实证明,我们还可以将相关模型类的字段名传递给查找函数。为此,请键入小写的型号名称,后跟两个下划线,然后是字段名。以下是一般格式:

Model.objects.filter(related_model__field=some_value)

这个跨度可以是你想要的深度。此外,您还可以使用我们在第 1 课 Django ORM 基础知识第 1 部分中学到的所有查找。这种技术减少了执行给定任务所需的临时查询。例如,假设我们只想从名字以om结尾的作者那里找到片段。这是找到所有这些片段的漫长道路。

>>>
>>> al = Author.objects.filter(name__iendswith="om")
>>>
>>> al
<QuerySet [<Author: tom : tom@mail.com>]>
>>> 
>>> sl = Snippet.objects.filter(author__in=al)  # using in lookup
>>>
>>> sl
<QuerySet [<Snippet: Untitled - PHP>, <Snippet: Untitled - Python>, <Snippet: Untitled - Python>]>
>>> 
>>> sl.count()
3
>>>
>>>

使用可以跨越关系的查找,我们可以在一行中编码前面的查询。

>>>
>>> sl = Snippet.objects.filter(author__name__iendswith="om")
>>> 
>>> sl
<QuerySet [<Snippet: Untitled - PHP>, <Snippet: Untitled - Python>, <Snippet: Untitled - Python>]>
>>> 
>>> sl.count()
3
>>>

这项技术在两个方向都有效。例如,以下查询检索 2018 年创建片段的所有作者。

>>> 
>>> Author.objects.filter(snippet__created_on__year=2018)
<QuerySet [<Author: tom : tom@mail.com>, <Author: tom : tom@mail.com>, <Author: tom : tom@mail.com>]>
>>>

请注意,前面的查询返回了同一个Author的三个实例,这是因为在我的 SQLite 数据库中tom已经创建了三个片段。为了过滤掉重复的结果,我们可以使用distinct()方法。distinct()方法在删除所有重复项后返回一个QuerySet

>>>
>>> Author.objects.filter(snippet__created_on__year=2018).distinct()
<QuerySet [<Author: tom : tom@mail.com>]>
>>>

get_object_or_404()函数

大多数时候,我们的视图功能是这样的:

  1. 代码尝试和除块。
  2. 在 try 块中查询数据库。
  3. 如果抛出异常,在 except 块中捕获异常并显示一个 404 页。

这种模式非常普遍,以至于 Django a 提供了一个名为
get_object_or_404()的简短函数。它的语法是:

get_object_or_404(klass, **kwargs)

klass可以是模特、经理或查询者。

**kwargs表示所有的关键字参数以及我们在查找函数中使用的查找参数(get()filter()exclude()等)。

成功后,它返回给定模型的单个对象,如果找不到任何记录,它就会引发 Http404 异常。

这个方法在内部调用objects管理器的get()方法,所以你必须一直使用这个方法来获取一个单一的对象。

要使用get_object_or_404(),首先使用以下代码从django.shortcuts导入。

from django.shortcuts import get_object_or_404

以下示例显示了如何将get_object_or_404()函数用于模型、queryset 和 managers。它还显示,当没有找到匹配的记录时,get_object_or_404()会引发Http404异常。

例 1: 其中klass为模型

>>>
>>> from django.shortcuts import get_object_or_404
>>> 
>>> s1 = get_object_or_404(Snippet, pk=1)
>>>
>>> s1
<Snippet: Untitled - Python>
>>> 
>>>
>>> s1 = get_object_or_404(Snippet, pk=200)
Traceback (most recent call last):
  ...    
django.http.response.Http404: No Snippet matches the given query.
>>>

该代码相当于:

>>>
>>> try:
...   s1 = Snippet.objects.get(pk=1)
... except Snippet.DoesNotExist:
...   raise Http404("Post not found")
...
>>>

例 2: 其中klass为 queryset

>>> 
>>> queryset = Author.objects.filter(active=False)
>>> 
>>> get_object_or_404(queryset, email="tom@mail.com")
<Author: tom : tom@mail.com>
>>> 
>>> get_object_or_404(queryset, email="bob@mail.com")
...  
django.http.response.Http404: No Author matches the given query.
>>> 
>>>
Traceback (most recent call last):

例 3: 其中klass是经理

>>> 
>>> s2 = Snippet.objects.get(id=2)
>>> 
>>> s2.tags.all()
<QuerySet [<Tag: django>, <Tag: flask>]>
>>>
>>> type(s1.tags)
<class 'django.db.models.fields.related_descriptors.create_forward_many_to_many_manager.<locals>.ManyRelatedManager'>
>>> 
>>> get_object_or_404(s2.tags, name='flask')
<Tag: flask>
>>> 
>>>

get_list_or_404()方法

get_list_or_404()的工作方式与get_object_or_404()函数类似,但它返回的不是单个模型实例,而是一个Queryset。如果没有找到匹配的结果,则引发Http404异常。

>>>
>>> from django.shortcuts import get_list_or_404
>>> 
>>> queryset = Author.objects.filter(active=False)
>>> 
>>> get_list_or_404(queryset, name__icontains="ke")
[<Author: spike : spike@mail.com>, <Author: tyke : tyke@mail.com>]
>>>  
>>> get_list_or_404(queryset, name__icontains="jo")
Traceback (most recent call last):
...
django.http.response.Http404: No Author matches the given query.
>>>



Django 管理员应用

吴奇珍:t0]https://overiq . com/django-1-11/django-admin-app/

最后更新于 2020 年 7 月 27 日


如今,管理网站是任何网站不可或缺的一部分。管理员、作者和工作人员使用管理站点来管理主站点的内容。Django 提供了一个管理内容的管理网站,这样你就可以专注于构建你需要的功能,而不是创建旧的无聊的管理网站。然而,这并不意味着你不能推出自己的管理网站。如果 Django 管理网站不符合您的要求,您可以从头开始创建自己的管理网站。然而,在本系列中,我们不会这样做。如果你有兴趣建立自己的管理网站,你有两个选择。首先是等待这个系列完成,到那时你将有足够的知识来创建自己的管理网站。另一个选项是签出我的 Django 1.10 系列,其中我从头开始创建了一个管理站点。二话不说。让我们开始使用 Django 管理应用。

创建超级用户

要使用管理站点,我们首先必须创建一个超级用户。在终端中输入以下命令。

$ ./manage.py createsuperuser

首先,系统会提示您输入用户名。

Username (leave blank to use 'overiq'): admin

接下来,输入电子邮件地址。这是可选的。

Email address: admin@overiq.com

最后,输入密码,并通过重新键入来确认。

Password:
Password (again):
Superuser created successfully.

现在,您可以登录到 Django 管理网站。

Django 管理网站-第一眼

如果尚未运行,启动开发服务器,并将浏览器指向
http://localhost:8000/admin。您将看到如下登录页面:

输入用户名和密码,然后点击回车。成功后,您将被重定向到 Django 管理站点的索引页面。

索引页显示了项目中可用模型的列表。目前,它只显示来自django.contrib.auth应用的GroupUser型号(在INSTALLED_APPS设置中的第二个应用)。要从我们的djangobin应用显示模型,我们必须做一点配置,这是下一节的主题。

Group模型用于组织权限,User模型代表站点用户。管理权限超出了本系列的范围。因此,我们的讨论将仅限于User模型。

单击“用户”链接,您将进入用户列表页面,如下所示:

列表页面显示特定模型的对象列表。用户列表页面显示了数据库中User对象的列表。从上图中可以看到,目前数据库中只有一个User对象,这是我们在上一节中创建的。

除了显示User对象,用户列表页面还允许我们执行以下任务:

  • 添加新用户。
  • 修改现有用户。
  • 搜索和过滤用户。
  • 通过单击列标题对用户数据进行排序。
  • 删除用户。

Django 管理网站是非常不言自明的。任何非技术用户都应该能够毫无问题地使用它。尝试自己浏览网站。添加一些新用户并修改现有用户,这将让你更好地了解 Django 管理网站的工作方式。在下一节中,我们将把 djangobin 应用中的模型添加到 Django 管理站点。

向 Django 管理添加模型

默认情况下,Django 管理网站不会从我们创建的应用中加载任何模型。要将模型添加到 Django 管理中,您必须修改每个应用目录中可用的admin.py文件。打开 djangobin 应用内的admin.py,即djangobin/django_project/djangobin。此时,admin.py应该是这样的:

决哥/决哥 _ project/决哥/决哥/admin.py】

from django.contrib import admin

# Register your models here.

按如下方式修改文件:

决哥/决哥 _ project/决哥/决哥/admin.py】

from django.contrib import admin
from . import models

# Register your models here.

admin.site.register(models.Author)
admin.site.register(models.Language)
admin.site.register(models.Snippet)
admin.site.register(models.Tag)

在第 2 行,我们从 djangobin 应用导入模型。要将模型添加到 Django 管理站点,请将模型的名称传递给admin.site.register()方法。

打开浏览器,访问http://localhost:8000/admin/。您应该会在 djangobin 应用中看到如下模型:

Django 管理的重要特征

在这一节中,我们将讨论 Django 管理站点的一些重要方面。我们的讨论将限于以下几点:

  • __str__()方法。
  • 模型关系。
  • 数据验证。
  • Django 追踪系统。
  • 小部件。

str()方法

点击 Django 管理主页中的“片段”链接,导航至片段列表页面(即http://localhost:8000/admin/djangobin/snippet/)。您将看到如下页面:

那么是什么让 Django admin 以这种形式显示Snippet对象呢?

默认情况下,Django admin 显示模型类中定义的__str__()方法返回的值。对于Snippet车型,__str__()方法定义如下:

djangobin/django _ project/djangobin/models . py

#...
class Snippet(models.Model):
    #...

    def __str__(self):
        return (self.title if self.title else "Untitled") + " - " + self.language.name
#...

如下修改Snippet模型的__str__()方法:

#...
class Snippet(models.Model):
    #...

    def __str__(self):
        return self.language.name
#...

刷新代码片段列表页面(http://localhost:8000/admin/djangobin/snippet/),现在应该如下所示:

正如您所看到的,Django 管理站点已经接受了我们在__str__()方法中的更改,并开始显示代码片段的语言。我们不希望这些更改是永久性的,因为在下一节中将在单独的列中显示代码片段的语言。所以让我们回到我们最初的__str__()定义。

#...
class Snippet(models.Model):
    #...

    def __str__(self):
        return (self.title if self.title else "Untitled") + " - " + self.language.name
#...

再次刷新代码片段列表页面,它应该会恢复原样。

Django 管理网站中的模型关系

这是值得解释的最重要的事情之一。回想一下,我们的
Snippet模型有以下关系:

  1. Author模型的一对多关系。
  2. Language模型的一对多关系。
  3. Tag模型的多对多关系。

作为参考,Snippet模型就是这样定义的。

djangobin/django _ project/djangobin/models . py

#...

class Snippet(models.Model):
    title = models.CharField(max_length=200, blank=True)
    original_code = models.TextField()
    highlighted_code = models.TextField()
    expiration = models.CharField(max_length=10, choices=Pref.expiration_choices)
    exposure = models.CharField(max_length=10, choices=Pref.exposure_choices)
    hits = models.IntegerField(default=0)    
    slug = models.SlugField()
    created_on = models.DateTimeField(auto_now_add=True)

    language = models.ForeignKey(Language, on_delete=models.CASCADE)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    tags = models.ManyToManyField('Tag')

#...

我们可以看到 Django 管理站点是如何通过添加一个新片段或编辑一个现有片段来管理模型关系的。

让我们通过点击代码片段列表页面右上角的“添加代码片段+”按钮来添加一个新的代码片段。

Django 使用单次选择<select>框来表示一对多的关系。我们有两种一对多的关系。第一个在SnippetLanguage之间,第二个在SnippetAuthor之间。使用单一选择<select>元素表示Snippet模型的languageauthor字段,如下所示:

您也可以通过点击<select>标签旁边的+链接来添加新的语言/作者。单击+图标会打开一个弹出窗口,允许您将新对象添加到数据库中。

一旦选择了语言或作者,您也可以通过单击下拉列表旁边的黄色铅笔图标来编辑它们。

同样,Django 使用多重选择<select>框来表示多对多关系。由于一个片段可以有一个或两个以上的标签,多重选择<select>框允许您在选择时按住 Ctrl 键选择多个标签。

也可以通过点击+链接添加新标签。

Snippet模型的languageauthor字段是通过on_delete参数设置为models.CASCADE来定义的。因此,删除作者或语言也会删除其关联的片段。

Django 管理站点处理验证

Django 管理站点自动提供输入验证。请尝试保存一个空白代码段,或者在 Slug 字段中输入一些无效字符。您将看到如下错误:

在添加片段(和更改片段)表单中,除了title之外,所有字段都是必需的(我们没有考虑设置了auto_now_add参数Truecreated_on字段)。title字段是可选的,因为我们在Snippet模型中定义它时设置了blank=True

此外,请注意,Django 管理网站以粗体显示必填字段。

Django 追踪系统。

Django 会记录您对对象所做的更改。要查看更改日志,请单击对象编辑页面中的“历史记录”链接。

如果您没有对该对象进行任何更改,那么您将收到以下消息。

小工具

小部件指的是模型字段在表单中的显示方式。Django 根据模型中定义的字段类型提供小部件。例如,CharField显示为<input>元素,ForeignKey显示为选择框,BooleanField显示为复选框等等。我们将在Django 表单基础一章中了解更多关于小部件以及如何覆盖它们的信息。

自定义列表页面

打开浏览器,点击 Django 管理网站索引页面上的“语言”链接,访问语言列表页面(http://localhost:8000/admin/djangobin/language/)。

该页面与用户列表页面(http://localhost:8000/admin/auth/user/)相似,但有细微的区别。

  1. 与用户列表页面不同,它没有搜索栏。
  2. 右侧没有过滤器。
  3. Language模型由 7 个字段组成(namelang_codeslugmimefile_extensioncreated_onupdated_on),但只有name可见。
  4. 我们不能通过点击列标题来排序。

要自定义列表页面上模型的外观,我们使用ModelAdmin类。ModelAdmin类提供了几个属性,允许我们更改模型在列表页面中的显示方式。所有这些属性都是在应用的admin.py文件中的ModelAdmin子类中定义的。以下是ModelAdmin的一些常见属性列表。

  • list_display -控制列表页面显示哪些模型字段。它接受字段名称的列表或元组。除了显示字段,它还使字段可排序。例如,

    list_display = ('name', 'email', 'created_on',)
    
    

    这将显示来自模型类的nameemailcreated_on字段的数据,并使它们可排序。

  • search_fields -该属性启用列表页面上的搜索功能。它接受您想要搜索的字段名称列表或元组。它执行不区分大小写的搜索。例如:

    search_fields = ('name', 'email',)
    
    
  • ordering -指定列表页面中对象列表的排序方式。它采用字段名称的列表或元组。例如:

    ordering = ['-name']
    
    

    这将通过name以降序显示对象列表。请注意,该选项将覆盖内部Meta类的ordering属性。

  • list_filter -该属性激活列表页面右侧的过滤栏。它接受字段名称的列表或元组。Django 根据字段的类型自动提供不同的快捷方式来过滤对象。例如,如果字段类型为DateField,则 Django 提供TodayPast 7 daysThis monthThis year快捷方式。同样,如果字段类型为BooleanField,则 Django 提供AllYesNo快捷方式。

  • date_hierarchy -该属性专门设计为在操作选择框正上方提供智能的基于日期的向下钻取导航。它需要一个字符串,而不是一个列表或元组。由于date_hierarchy创建基于日期的过滤器,您只能指定类型为DateFieldDateTimeField的字段。下面是这个属性在列表页面上的样子。

让我们测试一些属性。在 djangobin 应用中打开admin.py,创建一个名为LanguageAdmin的类,该类继承自admin.ModelAdmin类,如下所示:

决哥/决哥 _ project/决哥/决哥/admin.py】

from django.contrib import admin
from . import models

# Register your models here.

class LanguageAdmin(admin.ModelAdmin):
    list_display = ('name', 'lang_code', 'slug', 'mime', 'created_on')
    search_fields = ['name', 'mime']
    ordering = ['name']
    list_filter = ['created_on']
    date_hierarchy = 'created_on'

admin.site.register(models.Author)
admin.site.register(models.Language, LanguageAdmin)
admin.site.register(models.Snippet)
admin.site.register(models.Tag)

为了注册LanguageAdmin类概述的变化,我们将其名称作为第二个参数传递给注册其相应模型的register()方法。

刷新语言列表页面,它将如下所示:

在我们继续之前,让我们在admin.py文件中添加一些更多的类来定制SnippetTag模型的外观。

决哥/决哥 _ project/决哥/决哥/admin.py】

#...
class LanguageAdmin(admin.ModelAdmin):
    #...
    date_hierarchy = 'created_on'

class SnippetAdmin(admin.ModelAdmin):
    list_display = ('language', 'title', 'expiration', 'exposure', 'author')
    search_fields = ['title', 'author']
    ordering = ['-created_on']
    list_filter = ['created_on']
    date_hierarchy = 'created_on'

class TagAdmin(admin.ModelAdmin):
    list_display = ('name', 'slug',)
    search_fields = ('name',)

admin.site.register(models.Author)
admin.site.register(models.Language, LanguageAdmin)
admin.site.register(models.Snippet, SnippetAdmin)
admin.site.register(models.Tag, TagAdmin)

自定义表单

为了定制表单,admin.ModelAdmin类提供了以下属性。

  1. fields -默认情况下,表单中字段的出现顺序与模型类中字段的出现顺序相同。要更改顺序,请按您想要的列表或元组顺序列出字段。例如,

    fields = ['title', 'created_on', 'author']
    
    

    这将显示"title"场,然后是"created_on"场,最后是"author"场。

    除了对字段进行排序之外,您还可以使用该属性来删除一个或多个字段,使其不被完全编辑/添加。

  2. filter_horizontal -该属性只能与ManyToManyField一起使用。默认情况下,使用多重选择<select>框显示ManyToManyField字段。从一个小列表中选择记录很容易,但是如果有成百上千条记录呢?为了便于选择,Django 提供了filter_horizontal属性。它接受类型为ManyToManyField的字段名列表或元组。然后,它创建了一个良好的界面,允许搜索记录,以及查看可用的记录和选择的记录。

    让我们使用该属性来更改片段模型的tags字段的渲染方式。打开admin.py并在SnippetAdmin类末尾添加filter_horizontal属性,如下所示:

    决哥/决哥 _ project/决哥/决哥/admin.py】

    class SnippetAdmin(admin.ModelAdmin):
       #...
       date_hierarchy = 'pub_date'
       filter_horizontal = ('tags',)
    
    

    访问添加代码段或更改代码段页面。您将看到这样一个小部件:

  3. raw_id_fields -毫无疑问filter_horizontal让选录变得更加容易。但是,如果您有数百或数千条记录,一次加载所有这些可能需要一段时间。解决办法是用raw_id_fields代替filter_horizontal

    它接受类型为ManyToManyFieldForeignKey的字段名称列表或元组,并创建一个输入框(<input type="text" ... />),您可以在其中输入记录的主键。

    打开admin.py文件,注释掉上一步添加到SnippetAdmin类的filter_horizontal属性。然后在它的正下方添加raw_id_fields属性。

    决哥/决哥 _ project/决哥/决哥/admin.py】

    class SnippetAdmin(admin.ModelAdmin):
        #...
        # filter_horizontal = ('tags',)
        raw_id_fields = ('tags',)
    
    

    再次访问添加代码段或更改代码段页面。你应该这样看tags场。

    要输入数据,请单击输入框旁边的搜索图标,将会打开一个弹出窗口,允许您选择和搜索记录。

  4. prepopulated_fields -该属性自动向字段添加内容。它接受一个字典,将字段名称映射到它应该预先填充的字段。它通常用于制造鼻涕虫。例如:

    prepopulated_fields = {'slug': ('title',)}`
    
    

    这将使用 JavaScript 从title字段预填充slug字段。

    打开admin.py并将prepopulated_fields属性添加到TagAdmin类,如下所示:

    决哥/决哥 _ project/决哥/决哥/admin.py】

    class TagAdmin(admin.ModelAdmin):
        list_display = ('name', 'slug',)
        search_fields = ('name',)
        prepopulated_fields = {'slug': ('name',)}
    
    

    访问添加标签页面(http://localhost:8000/admin/djangobin/tag/add/)并在标题字段中输入一些数据。您会注意到,slug 字段会实时自动填充。

    prepopulated_fields属性仅在添加新记录时预填充字段,当您更新记录时,它不会预填充字段。

  5. readonly_fields -该属性使字段成为只读。它接受字段名作为列表或元组。

    就目前的情况来看,当我们创建一个新的片段时,我们需要在highlighted_codeslughits字段中输入数据。如果您考虑一下,无论您是添加一个代码片段还是更新一个现有的代码片段,向这些字段中输入数据都没有多大意义。对于highlighted_codeslug领域尤其如此。前者包含由 Pygments 包生成的 HTML 代码,后者包含访问代码片段的唯一标识符。更好的方法是将这些字段设为只读,并在保存代码片段时自动向它们提供数据。

    让我们从将这些字段设为只读开始。再次打开admin.py,像这样给SnippetAdmin添加readonly_fields属性:

    决哥/决哥 _ project/决哥/决哥/admin.py】

    class SnippetAdmin(admin.ModelAdmin):
        #...    
        raw_id_fields = ('tags',)
        readonly_fields = ('highlighted_code', 'hits', 'slug', )
    
    

    保存文件并访问添加片段(或更改片段)页面。您应该会看到这样的页面:

    请注意,所有三个字段都被推到了页面的末尾,这是因为 Django 首先显示可编辑字段,然后是只读字段。我们可以使用本节前面讨论的fields属性轻松更改这种行为。在SnippetAdmin类中的readonly_fields属性之后,添加fields属性如下:

    决哥/决哥 _ project/决哥/决哥/admin.py】

    class SnippetAdmin(admin.ModelAdmin):
        #...
        readonly_fields = ('highlighted_code', 'hits', 'slug', )
        fields = ('title', 'original_code', 'highlighted_code', 'expiration', 'exposure',
              'hits', 'slug', 'language', 'author', 'tags' )
    
    

    刷新“添加片段”页面,您将看到所有字段都按照fields属性中指定的顺序列出。

    既然是这样,就让我们将 Tag 模型的slug字段设为只读。在admin.py中,注释掉prepopulated_fields = {'slug': ('name',)},然后将
    readonly_fields属性添加到TagAdmin,如下所示:

    决哥/决哥 _ project/决哥/决哥/admin.py】

    class TagAdmin(admin.ModelAdmin):
        list_display = ('name', 'slug',)
        search_fields = ('name',)
        # prepopulated_fields = {'slug': ('name',)}
        readonly_fields = ('slug',)
    
    

    我们已将一些字段设为只读。在下一节中,我们将看到如何自动向他们提供数据。

    在我们离开这一部分之前,重要的是要提到类型为DateFieldDateTimeField的模型字段,它们的auto_now_addauto_now参数设置为True将不会出现在 Django 管理站点中,即使您将它们的名称添加到fields属性中。

    fields = ('title', 'slug', 'pub_date', 'content', 'author', 'category', 'tags',)
    
    

    事实上,这样做会引发FieldError异常。

通过重写 save()方法自定义对象创建

每次创建或更新对象时都会调用models.Model类的save()方法。要定制对象创建,我们可以覆盖模型类中的save()方法。

让我们从覆盖Snippet模型的save()方法开始。

打开models.py,在get_absolute_url()方法后添加save()方法,如下所示:

djangobin/django _ project/djangobin/models . py

#...
from django.shortcuts import reverse
import time

#...

class Snippet(models.Model):
    #...
    tags = models.ManyToManyField('Tag')

    def get_absolute_url(self):
        return reverse('djangobin:snippet_detail', args=[self.slug])

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = str(time.time()).replace(".", "")
        self.highlighted_code = self.highlight()
        if not self.title:
            self.title = "Untitled"
        super(Snippet, self).save(*args, **kwargs)  # Call the "real" save() method.

#...

第 15 行,if 语句检查self.slug是否存在。如果没有,则意味着调用save()方法来创建代码片段。另一方面,如果self.slug存在,则调用save()方法更新片段。这个条件是必须的,否则save()方法会在每次更新后改变弹头。

在第 17 行,我们调用Snippet模型的highlight()方法进行实际的高亮显示,然后将其结果分配给highlighted_code字段。

在第 18 行,我们正在检查self.title是否包含值。如果没有,我们设置一个默认值"Untitled"

最后,在第 20 行,我们调用被覆盖的save()方法将结果保存到数据库中。

完成这些更改后,请访问“添加代码片段”页面,并尝试提交一两个代码片段。

请注意数据是如何自动填充到highlighted_codeslughits字段中的。每次创建或更新Snippet对象时,都会运行save()方法。

您可能已经注意到,您仍然需要提交至少一个标签来创建一个Snippet对象。Snippet模型与Tag模型具有多对多的关系。我们可以通过设置blank=True使Tag字段可选。在 djangobin app 中打开models.py,更新如下:

class Snippet(models.Model):
    #...
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    tags = models.ManyToManyField('Tag', blank=True)

    def __str__(self):
        return (self.title if self.title else "Untitled") + " - " + self.language.name
    #...

要提交更改,请使用makemigrations命令创建迁移:

$ ./manage.py makemigrations djangobin
Migrations for 'djangobin':
  djangobin/migrations/0009_auto_20180328_1409.py
    - Alter field tags on snippet

然后,使用migrate命令提交迁移:

$ ./manage.py migrate djangobin
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, djangobin, sessions
Running migrations:
  Applying djangobin.0009_auto_20180328_1409... OK

再次访问“添加代码片段”页面,并创建一个新的代码片段。您会发现不再需要提交标签来创建新的代码片段。

现在让我们将注意力转移到Tag模型上。

就像Snippet对象一样,我们希望在不在slug字段中输入任何数据的情况下创建Tag对象。在前一节中,我们已经将slug设为只读。剩下的唯一事情就是覆盖Tag模型中的save()方法,并在那里设置slug字段的值。打开model.py文件,更新为包含save()方法,如下所示:

#...
import time
from django.utils.text import slugify
from .utils import Preference as Pref

#...

class Tag(models.Model):
    name = models.CharField(max_length=200, unique=True)
    slug = models.SlugField(max_length=200, unique=True)

    #...

    def get_absolute_url(self):
        return reverse('djangobin:tag_list', args=[self.slug])

    def save(self, *args, **kwargs):        
        self.slug = slugify(self.name)
        super(Tag, self).save(*args, **kwargs)  # Call the "real" save() method.

save()方法中,我们使用 Django 提供的slugify()功能从name字段创建鼻涕虫。回想一下,slug 只能包含字母、数字、下划线和连字符。如果name字段的值为"enter the dragon",则slugify()功能将返回"enter-the-dragon"。另外,请注意,我们没有测试slug字段是否包含任何值,因为我们希望save()方法在每次更新后重新创建 slug。

从现在开始,您将能够创建新的标签,而无需在 slug 字段中输入任何值。

更改字段标签

Django 管理站点中的表单标签使用模型中的字段名称。如果模型中的字段名是name,那么表单标签就是Name。如果标签由多个像pub_date一样用下划线分隔的单词组成,那么表单标签将是Pub date

在模型类中定义字段时,我们可以使用verbose_name参数明确指定字段标签。打开models.py文件,将verbose_name添加到Language模型中的name字段,如下所示:

djangobin/django _ project/djangobin/models . py

#...
class Language(models.Model):
    name = models.CharField(max_length=100)
    lang_code = models.CharField(max_length=100, unique=True, verbose_name='Language Code')
    slug = models.SlugField(max_length=100, unique=True)
    #...

如往常一样,使用makemigrationsmigrate命令提交更改。

$ ./manage.py makemigrations djangobin
Migrations for 'djangobin':
  djangobin/migrations/0011_auto_20180329_0636.py
    - Alter field lang_code on language

$ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, djangobin, sessions
Running migrations:
  Applying djangobin.0011_auto_20180329_0636... OK

访问添加语言(或更改语言)页面查看更改的标签。

向代码段表单添加帮助文本

Django 模型基础一课中,我们讨论了一个名为help_text的字段选项。如果你还记得的话,help_text选项会选择一个字符串作为字段的描述。

访问添加代码片段(或更改代码片段)页面,您将获得如下表单:

如您所见,“添加代码段”表单由 3 个只读字段组成:突出显示的代码、命中和分段。第一次访问这个页面的人可能会对如何使用这些字段感到困惑。因为我们是自动向这些字段提供数据的,所以我们应该明确地告诉用户,这些字段是只读的,并且将在保存代码片段时自动填充。

要添加help_text参数,请在 djangobin 应用中打开models.py文件,并修改Snippet模型,如下所示:

djangobin/django _ project/djangobin/models . py

#...
class Snippet(models.Model):
    #...
    title = models.CharField(max_length=200, blank=True)
    original_code = models.TextField()
    highlighted_code = models.TextField(blank=True, help_text="Read only field. Will contain the"
                                    " syntax-highlited version of the original code.")
    expiration = models.CharField(max_length=10, choices=expiration_choices)
    exposure = models.CharField(max_length=10, choices=exposure_choices)
    hits = models.IntegerField(default=0, help_text='Read only field. Will be updated after every visit to snippet.')
    slug = models.SlugField(help_text='Read only field. Will be filled automatically.')
    created_on = models.DateTimeField(auto_now_add=True)
    #...

再次运行makemigrationsmigrate命令提交更改。刷新“添加代码段(或更改代码段)”页面以查看帮助文本:



Django 认证框架基础

原文:https://overiq.com/django-1-11/django-authentication-framework-basics/

最后更新于 2020 年 7 月 27 日


Django 认证(或auth)应用为用户管理提供了广泛的工具,从认证用户到重置密码。

auth app 挺大的。它由 URL 模式、视图、表单、装饰器、模型、中间件等组成。为了使事情简单易懂,我们的讨论将仅限于以下主题:

  1. User模型
  2. AnonymousUser模型
  3. 密码散列系统
  4. 登录/注销用户
  5. 创建用户
  6. 修改口令
  7. 重置密码
  8. 限制访问
  9. 扩展User模型

我们将从基础开始,然后在后面的章节中继续讨论更复杂的主题。

设置身份验证框架

认证框架被实现为'django.contrib.auth'应用,但它也依赖于'django.contrib.contenttype'应用和一些中间件来正确工作。

打开settings.py文件,确保在INSTALLED_APPS列表中有'django.contrib.auth'
T2,如下所示:

djangobin/django _ project/django _ project/settings . py

#...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'djangobin',
]
#...

如果你现在正在添加'django.contrib.auth''django.contrib.contenttype',运行./manage.py migrate命令。该命令将在数据库中创建必要的表。

那么django.contrib.contenttype是什么呢?

django.contrib.contenttype应用用于跟踪项目中不同类型的内容。这个应用有高级用例,所以我们不会在这个初学者教程中介绍它。

Django auth应用也在幕后使用会话。因此,您必须在settings.pyMIDDLEWARE列表中拥有以下两个中间件。

  1. django.contrib.sessions.middleware.SessionMiddleware
  2. django.contrib.auth.middleware.AuthenticationMiddleware

django.contrib.sessions.middleware.SessionMiddleware负责生成唯一的会话标识。并且django.contrib.auth.middleware.AuthenticationMiddlewarerequest对象添加名为user的属性。在接下来的章节中,这一点的作用将变得更加明显。

最后,MIDDLEWARE列表应该是这样的:

djangobin/django _ project/django _ project/settings . py

#...
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
#...

用户类型

Django 有两种类型的用户:

  1. 匿名用户
  2. 用户

匿名用户

未登录到应用的用户称为匿名用户。例如,经常来我们网站消费内容的用户是匿名用户。这类用户使用AnonymousUser模型表示。

用户

登录到应用的用户称为用户。这类用户使用User模型表示。

除了 web 请求之外,AnonymousUser模型并不常用,但是User模型在很多地方都有使用。因此,我们将大部分时间花在User模式上。以下部分解释了您需要了解的关于User型号的一切。

用户模型

User模型被认为是 Django 认证框架的核心。我们使用User模型存储应用用户的数据。要使用它,您必须首先从
django.contrib.auth.models导入它。

>>>
>>> from django.contrib.auth.models import User
>>>

创建用户对象

Django 在objects管理器上提供了一个名为create_user()的自定义方法来创建用户。它接受至少三个参数用户名、密码和电子邮件。

>>>
>>> u = User.objects.create_user(
... 'noisyboy',
... 'noisyboy@mail.com',
... 'password'
... )
>>>
>>>
>>> u
<User: noisyboy>
>>>
>>>

下表列出了User模型的所有字段。

属性 描述
username 存储用户名的 30 个字符的必填字段。它只能包含字母数字字符,即字母、数字和下划线。
password 存储密码的必填字段。
first_name 30 个字符的可选字段,用于存储用户的first_name
last_name 30 个字符的可选字段,用于存储用户的last_name
email 存储电子邮件地址的可选字段。
is_active 一个布尔值字段,它指示该帐户是否可以用于登录。
is_superuser 一个布尔字段,如果设置为True,用户拥有在 Django 管理站点执行任何 CRUD(创建、读取、更新和删除)操作的完全权限。
is_staff 一个布尔字段,如果设置为True,用户可以访问 Django 管理站点,但是在获得明确的权限之前不能执行任何 CRUD 操作。默认情况下,对于超级用户(其is_superuser属性为True的用户)is_staff也是True
date_joined 存储帐户创建日期和时间的字段。该字段会自动填入创建帐户的当前日期和时间。
last_login 包含上次登录日期和时间的字段。

create_user()方法创建一个非超级用户,并将is_staff属性设置为True。这意味着用户可以登录到 Django 管理站点,但不能执行任何 CRUD 操作。要创建一个超级用户,使用create_superuser()方法,其工作原理与create_user()完全相同,但创建一个超级用户。

要获取新创建的User实例的字段值,请执行以下查询:

>>>
>>> User.objects.filter(username='noisyboy').values()
<QuerySet 
[{
    'username': 'noisyboy', 
    'password': 'pbkdf2_sha256$36000$JuRTV4Vec59R$ueTUvEcLH/i/eESGTIiwk7O3iEZfW+BtpnhCmtpYK48=', 
    'is_staff': False, 
    'first_name': '', 
    'id': 2, 
    'email': 'noisyboy@mail.com', 
    'last_name': '', 
    'is_active': True, 
    'is_superuser': False, 
    'last_login': None, 
    'date_joined': datetime.datetime(2018, 4, 3, 4, 51, 8, 674638, tzinfo=<UTC>)
}]>
>>>

可以看到,目前noisyboy不是超级用户。我们可以通过将is_superuser属性设置为True,然后调用save()方法来轻松改变这一点:

>>>
>>> u.is_superuser = True
>>>
>>> u.is_superuser
True
>>> u.save()
>>>

这里要注意的另一件事是前面查询的输出中password字段的值。它看起来像这样:

'password': 'pbkdf2_sha256$36000$JuRTV4Vec59R$ueTUvEcLH/i/eESGTIiwk7O3iEZfW+BtpnhCmtpYK48=',

回想一下,在创建noisyboy用户时,我们使用字符串"password"作为帐户密码。为什么会变得这么长?

不将密码以纯文本形式存储到数据库中是一种常见的安全措施。相反,我们使用一个数学函数将我们的密码转换成一个长字符串,如上图所示。这种函数称为散列函数,它返回的长字符串称为密码散列。哈希函数背后的思想是这样的——从密码创建密码哈希很容易,但反向过程是不可能的。

默认情况下,Django 使用 PBKDF2 算法创建密码哈希。

这就是为什么我们使用create_user()方法来创建User对象,而不是使用create()bulk_create()方法的原因。create_user()方法自动将密码转换为散列。如果我们使用create()bulk_create()方法,它会将密码以纯文本形式保存到数据库中。我们可以通过使用create()方法创建新用户来验证这一点,如下所示:

>>>
>>> test_user = User.objects.create(
...     username='test',
...     email='test@mail.com',
...     password='pass'
... )
>>>
>>>
>>> test_user.password
'pass'
>>>

Django 将用户数据存储在auth_user表中。执行上述代码后,auth_user表应该如下所示:

以下是User模型提供的一些方法:

方法 描述
get_username() 返回用户的username。要获取用户名,您应该使用这个属性,而不是直接引用username属性。
get_full_name() 返回first_namelast_name属性的值,中间有一个空格
check_password(pass) 如果传递的字符串是正确的密码,则返回True,否则返回False。它首先将密码转换为密码哈希,然后将其与保存在数据库中的密码进行比较。
set_password(passwd) 它用于更改用户的密码。它负责密码散列。注意set_password()没有保存User对象,你必须调用save()方法将更改提交给数据库。
is_authenticated() 如果用户通过认证(登录),返回True,否则返回False
is_anonymous() 如果用户匿名则返回True,否则返回False

get_username()

>>>
>>> u.get_username()
'noisyboy'
>>> u.get_full_name()
''
>>>

检查 _ 密码()

>>>
>>> u.check_password("mypass")
False
>>> u.check_password("password")
True
>>>

set_password()

>>>
>>> u.set_password("pass")
>>>

要提交更改,请调用save()方法。

>>>
>>> u.save()
>>>

is_authenticated()

>>>
>>> u.is_authenticated()
True
>>>

这里is_authenticated()方法返回True。这并不意味着noisyboy目前已经登录了 Django 管理网站。事实上,在 DjangoShell 中,当在User实例上使用时,is_authenticated()总是返回True。我们可以通过创建一个新用户,然后检查is_authenticated()方法的返回值来验证这个事实。

>>>
>>> new_user = User.objects.create_user(
... 'newtestuser',
... 'newtestuser@mail.com',
... 'pass'
... )
>>>
>>> new_user.is_authenticated()
True
>>>

Django Shell 里面is_authenticated()没用。正如我们将在后续课程中看到的,它的真正效用在视图和模板中发挥作用。

is_anonymous()

>>>
>>> u.is_anonymous()
False
>>>

对象uUser类的一个实例,这就是为什么is_anonymous()返回False。如果它属于AnonymousUser级,它就会返回True

匿名用户模型

Django 还有一个名为AnonymousUser的模型,代表未登录的用户。换句话说,经常来我们网站消费内容的用户是匿名用户。AnonymousUser模型的方法和领域与User模型几乎相同,区别如下。

  • idpk属性始终包含None
  • username属性将始终为空字符串,即''
  • is_anonymous()True而不是False
  • is_authenticated()False而不是True
  • is_staffis_superuser属性永远是False
  • is_active永远是False
  • AnonymousUser对象上调用save()delete()将引发NotImplementedError异常。

需要注意的是AnonymousUser模型与User模型没有任何一种关系。它是一个单独的类,有自己的方法和字段。UserAnonymousUser模型唯一的相似之处就是大部分领域和方法都是一样的。这不是设计缺陷,这是故意的,目的是让事情变得更容易。

在本章的前面,我们已经讨论了
django.contrib.auth.middleware.AuthenticationMiddleware中间件向request对象添加了一个user属性。request.user返回AnonymousUserUser模型的实例。因为UserAnonymousUser类都实现了相同的接口,所以我们可以使用相同的字段和方法来获取相关信息,而不用担心对象的实际类型。

下面的例子完美地描述了我们如何利用这种行为来达到我们的目的。test_logged_on_or_not()视图测试用户是否使用is_authenticated()方法登录。我们能够编写这个代码是因为is_authenticated()AnonymousUser模型和User模型中都实现了。

def test_logged_on_or_not(request):
    if request.user.is_authenticated():
        return HttpResponse("You are logged in.")
    else:
        return redirect("login")

以上代码是这样工作的:

如果is_authenticated()方法返回True,那么用户将会得到"You are logged in."的响应。否则,用户将被重定向到登录页面。

实际上,您可能永远不需要创建类型为AnonymousUser的对象。以防万一,你很好奇,下面的代码告诉你怎么做。

>>>
>>> from django.contrib.auth.models import AnonymousUser
>>> au = AnonymousUser()
>>>

一旦我们访问了AnonymousUser实例,我们就可以使用任何字段或方法来获取任何相关信息。

>>>
>>> print(au.id)
None
>>> print(au.pk)
None
>>>
>>> au.username
''
>>>
>>> au.is_authenticated()
False
>>>
>>> au.is_anonymous()
True
>>>
>>> au.is_active
False
>>>
>>> au.is_superuser
False
>>>
>>>
>>> au.delete()
...  
NotImplementedError: Django doesn't provide a DB representation for AnonymousUser.
>>>
>>>
>>> au.save()
Traceback (most recent call last):
...
NotImplementedError: Django doesn't provide a DB representation for AnonymousUser.
>>>

如前所述,在AnonymousUser对象上调用save()delete()会引发NotImplementedError异常。

扩展用户模型

Django 仅在User模型中提供了最少的字段来帮助您入门,但它也为您提供了扩展User模型的全部能力来满足您的应用需求。

回想一下,默认情况下User模型包含以下字段:

  1. 用户名
  2. 名字
  3. 姓氏
  4. 电子邮件
  5. 密码
  6. 上次登录时间
  7. 不活动的
  8. is_staff
  9. 是超级用户
  10. 加入日期

在这一点上,您可能想知道User模型是否用于存储用户信息,那么定义Author模型有什么意义。

如果你是这么想的。你是对的。

Author模型的所有字段也存在于User模型中。这意味着我们根本不需要Author模型。

但是如果我们想存储一些关于用户的附加数据呢?比如出生日期、喜欢的车、娘家姓等;

为了存储关于用户的附加数据,我们必须扩展我们的User模型。

扩展User模型的第一步是创建一个新模型,其中包含您想要存储的所有附加字段。接下来,将我们的新模型与User模型相关联,定义一个OneToOneField字段,该字段包含对新模型中User模型的引用。

在我们的例子中,我们希望存储关于用户的以下信息:

描述
default_language 创建代码片段的默认语言
default_exposure 违约风险(即公共、私人和未上市)
default_expiration 默认到期时间(即从不、1 周、1 个月、6 个月、1 年)
private 一个布尔字段,指示其他用户是否可以查看此配置文件
views 一个整数字段,用于存储访问配置文件页面的次数。

预计到我们将要对Author模型进行的更改,让我们执行一些数据库清理。现在djangobin_author表中有一些独立于User模型的用户。为了避免冲突,最好从数据库中删除所有Author对象。请注意,删除作者也将删除与其关联的所有片段。

>>>
>>> from djangobin.models import *
>>>
>>> Author.objects.all().delete()
(17, {'djangobin.Author': 4, 'djangobin.Snippet': 7, 'djangobin.Snippet_tags': 6})
>>> 
>>>

注意:并不总是可以从表中删除数据。这就是为什么在开始编写应用之前,应该正确规划数据库。

我们现在可以对Author模型进行更改。打开models.py并删除Author型号。在Language模型下面,重新定义新的Author模型如下:

djangobin/django _ project/djangobin/models . py

#...
from django.contrib.auth.models import User
from .utils import Preference as Pref

class Language(models.Model):
    #...

def get_default_language():
    lang = Language.objects.get_or_create(
        name='Plain Text',
        lang_code='text',
        slug='text',
        mime='text/plain',
        file_extension='.txt',
    )

    return lang[0].id

class Author(models.Model):
    user = models.OneToOneField(User, related_name='profile')
    default_language = models.ForeignKey(Language, on_delete=models.CASCADE,
                                         default=get_default_language)
    default_exposure = models.CharField(max_length=10, choices=Pref.exposure_choices,
                                        default=Pref.SNIPPET_EXPOSURE_PUBLIC)
    default_expiration = models.CharField(max_length=10, choices=Pref.expiration_choices,
                                        default=Pref.SNIPPET_EXPIRE_NEVER)
    private = models.BooleanField(default=False)
    views = models.IntegerField(default=0)

    def __str__(self):
        return self.user.username

    def get_absolute_url(self):
        return reverse('djangobin:profile', args=[self.user.username])

    def get_snippet_count(self):
        return self.user.snippet_set.count()

class Snippet(models.Model):
    #...

这里没有什么新内容,除了我们如何设置default_language字段的默认值。我们第一次使用可调用来设置默认值。每次创建新的Author实例时,都会调用可调用的。

可调用程序使用了一种名为get_or_create()objects管理器的新方法。get_or_create()法是get()create()法的融合。它首先检查数据库中是否存在匹配的对象,如果不存在,则创建一个。

get_or_create()方法返回一个形式为(object, created)的元组,其中object是指检索或创建的实例,created是一个布尔值,指定是否创建新对象。

可调用返回名称为Plain Text的语言的主键。Plain Text将用于创建没有任何突出显示的片段。

default_languagedefault_expiration字段的默认值和选项来自我们在 Django 的模型基础一课中定义的Preference课程

接下来,使用./manage.py makemigrations命令创建一个新的迁移文件,您将得到如下提示:

$ ./manage.py makemigrations
Did you rename author.active to author.private (a BooleanField)? [y/N] n

输入Nn,继续。之后,您将收到另一个提示:

You are trying to add a non-nullable field 'user' to author without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py
Select an option:

问题是我们试图向djangobin_author表中添加一个带有 UNIQUE 和 NOT NULL 约束的外键。Django 认为djangobin_author表中可能有一些行,需要知道这些行的外键(用户)列要填什么。虽然djangobin_author表中没有实际作者,但 Django 对此完全不知情。

让我们选择选项 1,并提供一次性默认值None:

Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> None
Migrations for 'djangobin':
  djangobin/migrations/0013_auto_20180403_1059.py
    - Change Meta options on tag
    - Remove field active from author
    - Remove field created_on from author
    - Remove field email from author
    - Remove field last_logged_in from author
    - Remove field name from author
    - Add field default_expiration to author
    - Add field default_exposure to author
    - Add field default_language to author
    - Add field private to author
    - Add field user to author
    - Add field views to author

最后,使用migrate命令提交迁移:

$ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, djangobin, sessions
Running migrations:
  Applying djangobin.0013_auto_20180404_0457... OK

请记住,扩展User模型并不意味着每次创建新的User实例时都会自动创建关联的Author实例。您可以使用 Django ORM 或者从 Django 管理站点创建Author实例。

要在创建User实例时自动触发Author实例的创建,我们可以使用信号。我们将在本章后面讨论如何做到这一点。

此外,数据库中已经存在的User实例没有关联的Author实例。如果您尝试在应用的完成版本中使用这些帐户登录,这将导致错误。我们可以通过使用以下代码为每个User实例创建Author来轻松解决这个问题:

>>>
>>> from django.contrib.auth.models import User
>>> from djangobin.models import *
>>>
>>> ul = User.objects.all()
>>> 
>>> ul
<QuerySet [<User: admin>, <User: noisyboy>, <User: test>, <User: newtestuser>]>
>>> 
>>> for u in ul:
...     Author.objects.get_or_create(user=u)
... 
(<Author: admin>, True)
(<Author: noisyboy>, True)
(<Author: test>, True)
(<Author: newtestuser>, True)
>>> 
>>>

执行上述代码后,djangobin_author表将如下所示:

在进入下一部分之前,让我们用User模型的外键替换Snippet模型的外键author,如下所示:

djangobin/django _ project/djangobin/models . py

#...

class Snippet(models.Model):
    title = models.CharField(max_length=200, blank=True)
    original_code = models.TextField()
    highlighted_code = models.TextField(blank=True, help_text="Read only field. Will contain the"
                                    " syntax-highlited version of the original code.")
    expiration = models.CharField(max_length=10, choices=Pref.expiration_choices)
    exposure = models.CharField(max_length=10, choices=Pref.exposure_choices)
    hits = models.IntegerField(default=0, help_text='Read only field. '
                                                    'Will be updated after every visit to snippet.')
    slug = models.SlugField(help_text='Read only field. Will be filled automatically.')
    created_on = models.DateTimeField(auto_now_add=True)

    language = models.ForeignKey(Language, on_delete=models.CASCADE)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    tags = models.ManyToManyField('Tag', blank=True)

    class Meta:
        ordering = ['-created_on']

#...

接下来,更新admin.py中的SnippetAdmin类以使用user字段,如下所示:

决哥/决哥 _ project/决哥/决哥/admin.py】

#...

class SnippetAdmin(admin.ModelAdmin):
    list_display = ('language','title', 'expiration', 'exposure', 'user')
    search_fields = ['title', 'user']
    ordering = ['-created_on']
    list_filter = ['created_on']
    date_hierarchy = 'created_on'
    # filter_horizontal = ('tags',)
    raw_id_fields = ('tags',)
    readonly_fields = ('highlighted_code', 'hits', 'slug', )
    fields = ('title', 'original_code', 'highlighted_code', 'expiration', 'exposure',
              'hits', 'slug', 'language', 'user', 'tags')

#...

要创建一个新的迁移运行./manage.py makemigrations djangobin命令并提供None的一次性值。

$ ./manage.py makemigrations djangobin
You are trying to add a non-nullable field 'user' to snippet without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> None
Migrations for 'djangobin':
  djangobin/migrations/0006_auto_20180616_0826.py
    - Remove field author from snippet
    - Add field user to snippet

最后,通过键入以下命令提交迁移:

$ ./manage.py migrate djangobin
Operations to perform:
  Apply all migrations: djangobin
Running migrations:
  Applying djangobin.0006_auto_20180616_0826... OK

使用用户模型显示作者模型

如果你访问 Django 管理网站,你会发现UserAuthor模型显示在各自独立的页面上。由于Author模型只是User模型的扩展,如果我们将Author模型的数据与User模型一起显示会好得多。为此,我们必须对 djangobin 应用的admin.py文件进行一些更改。

在 Django 管理站点中有两种表示模型的方式:

  1. 模型管理
  2. inline modeldadmin

我们已经在Django 管理应用一章中看到了ModelAdmin是如何工作的。InlineModelAdmin允许我们在同一个页面上编辑父模型和子模型。InlineModelAdmin有两个子类:

  1. 白板线
  2. StackedInline

这些类控制 Django 管理站点中模型字段的布局。前者以表格形式显示字段,而后者以堆叠形式显示字段(每行一个字段)。

要显示Author模型的字段和User模型的字段,在应用的admin.py文件中定义一个InlineModelAdmin(使用TabularInlineStackedInline)并将其添加到UserAdmin类,该类在 Django 管理器中注册User类。最后,注销旧的User型号,并随着更改再次注册。

决哥/决哥 _ project/决哥/决哥/admin.py】

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from . import models

#...

class TagAdmin(admin.ModelAdmin):
    list_display = ('name', 'slug',)
    search_fields = ('name',)
    # prepopulated_fields = {'slug': ('name',)}
    readonly_fields = ('slug',)

class AuthorInline(admin.StackedInline):
    model = models.Author

class CustomUserAdmin(UserAdmin):
    inlines = (AuthorInline, )    

admin.site.unregister(User) # unregister User model
admin.site.register(User, CustomUserAdmin) # register User model with changes
admin.site.register(models.Language, LanguageAdmin)
admin.site.register(models.Snippet, SnippetAdmin)
admin.site.register(models.Tag, TagAdmin)

请注意,我们已经删除了在 Django 管理站点中注册Author模型的行(admin.site.register(models.Author))。

保存文件并访问更改用户页面。您应该会看到这样的页面:

在下一节中,我们将学习如何在创建User时使用信号自动触发Author对象的创建。

Django 信号

把信号想象成 JavaScript 中的事件,它允许我们监听动作并做些什么。

下表列出了一些常用信号:

信号 描述
django.db.models.signals.pre_save 在调用模型的save()方法之前发送
django.db.models.signals.post_save 调用模型的save()方法后发送
django.db.models.signals.pre_delete 在调用模型的delete()方法或QuerySetdelete()方法之前发送
django.db.models.signals.post_delete 在模型的delete()方法或QuerySetdelete()方法被调用后发送
django.core.signals.request_started 当 Django 开始处理 HTTP 请求时发送。
django.core.signals.request_finished 当 Django 处理完一个 HTTP 请求时发送。

出于我们的目的,我们希望在保存User实例时得到通知,以便我们可以创建相关的Author对象。这意味着我们有兴趣聆听来自User模型的post_save信号。

一旦我们知道了我们想听的信号,下一步就是定义一个接收器。接收器可以是一个 Python 函数或方法,每当发送信号时都会被调用。接收器函数包含我们想要执行的逻辑。每个接收器函数必须接受两个参数,sender**kwargs

sender参数包含信号的发送方,**kwargs包含一些与信号发送方相关的信息。post_save信号发送的两个常见信息是:

  1. instance -已保存实例的副本。
  2. created -一个布尔值,指定对象是被创建还是被更新。

post_save信号发出许多其他的论点,但是对于我们的目的来说,这两个就足够了。打开models.py并在Author类的正下方添加接收器功能,如下所示:

djangobin/django _ project/djangobin/models . py

#...

class Author(models.Model):
    #...

    def get_snippet_count(self):
        return self.user.snippet_set.count()

def create_author(sender, **kwargs):
    if kwargs.get('created', False):
        Author.objects.get_or_create(user=kwargs.get('instance'))

接收器功能就位后。下一步是将接收器连接到信号。我们这样做,借助receiver装饰师。receiver装饰器接受要连接的信号或信号列表,以及接收器函数中流行的kwargs参数的可选关键字参数。更新models.py使用receiver装饰器,如下所示:

djangobin/django _ project/djangobin/models . py

#...
from django.contrib.auth.models import User
from django.dispatch import receiver
from django.db.models.signals import post_save
from .utils import Preference as Pref

# Create your models here.

#...

@receiver(post_save, sender=User)
def create_author(sender, **kwargs):    
    if kwargs.get('created', False):
        Author.objects.get_or_create(user=kwargs.get('instance'))

只有保存了User实例时才会调用create_author()函数。

如果现在创建一个新的User实例,相关的Author实例将自动创建。

>>> 
>>> 
>>> u = User.objects.create(
...   username='foo',
...   email='foo@mail.com',
...   password='password'
... )
>>>
>>> u
<User: foo>
>>> 
>>> u.profile   ## access Author instance
<Author: foo>
>>> 
>>> 
>>> u.profile.default_language
<Language: Plain Text>
>>> 
>>> 
>>> u.profile.private
False
>>> 
>>>



Django 的数据迁移

原文:https://overiq.com/django-1-11/data-migrations-in-django/

最后更新于 2020 年 7 月 27 日


此时,我们的 SQLite 数据库由少量语言组成。然而,我们的目标是允许用户为各种语言创建代码片段。就目前情况而言,除了手动逐个创建Language对象,我们没有更好的方法来填充这些记录。

此外,在部署中,如果我们选择使用更健壮的数据库,如 PostgreSQL,那么我们必须再次逐个输入语言。

我们需要的是一种自动加载“系统数据”的方法,这样无论我们是在开发还是生产中,我们的应用都可以成功运行。

数据迁移

数据迁移类似于我们在课程迁移基础知识中学习的普通迁移,但是它不是改变数据库模式,而是改变数据库中的数据。

以下是数据迁移的两个常见使用案例:

  1. 加载基本数据,以便您的应用能够正确运行(这是我们需要的)。
  2. 当数据需要更新时,在模型改变之后。

如果您尝试通过makemigrations命令创建新的迁移,您将获得“未检测到任何更改”。

$ ./manage.py makemigrations
No changes detected

这是因为自上次运行makemigrations命令以来,我们没有进行任何更改。

那么我们如何创建数据迁移呢?

我们可以强制 Django 创建一个空的迁移文件,使用--empty选项,后跟应用的名称。

$ ./manage.py makemigrations --empty djangobin
Migrations for 'djangobin':
  djangobin/migrations/0017_auto_20180430_1637.py

这将在 djangobin 应用的migrations子目录中创建一个带时间戳的空迁移。

我们刚刚创建的迁移文件对我们来说具有特殊的意义。但是,迁移文件的名称给人一种错误的印象,即它是一个普通的迁移。让我们重命名文件以反映它的功能。

$ cd djangobin/migrations
$ mv 0017_auto_20180430_1637.py language_data.py

打开迁移文件,应该是这样的:

djangobin/django _ project/djangobin/migrations/language _ data . py

# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-30 16:37
from __future__ import unicode_literals

from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ('djangobin', '0016_auto_20180430_1618'),
    ]

    operations = [
    ]

正如我们在 Django移民一章中讨论的那样,operations列表是移民的主要部分,但目前它是空的。Django 有很多内置操作。我们已经在 Django 的移民一章中看到了其中的一些。我们感兴趣的操作叫做RunPython

RunPython操作允许我们从迁移文件中运行任意 Python 代码。它有两个功能:正向功能和反向功能。应用迁移时执行正向功能,回滚迁移时执行反向功能。

我们来定义RunPython操作。打开language_data.py,修改如下:

djangobin/django _ project/djangobin/migrations/language _ data . py

from django.db import migrations

LANGUAGES  = [
    {
        'name': 'Bash',
        'lang_code': 'bash',
        'slug': 'bash',
        'mime': 'application/x-sh',
        'file_extension': '.sh',
    },
    {
        'name': 'C',
        'lang_code': 'c',
        'slug': 'c',
        'mime': 'text/x-chdr',
        'file_extension': '.c',
    },
    {
        'name': 'C#',
        'lang_code': 'c#',
        'slug': 'c-sharp',
        'mime': 'text/plain',
        'file_extension': '.aspx,',
    },
    {
        'name': 'C++',
        'lang_code': 'c++',
        'slug': 'cpp',
        'mime': 'text/x-c++hdr',
        'file_extension': '.cpp',
    },
    #...
]

# forward function 
def add_languages(apps, schema_editor):
    Language = apps.get_model('djangobin', 'Language')

    for lang in LANGUAGES:
        l = Language.objects.get_or_create(
            name = lang['name'],
            lang_code = lang['lang_code'],
            slug = lang['slug'],
            mime = lang['mime'],
            file_extension = lang['file_extension'],
        )

        print(l)

# backward function
def remove_languages(apps, schema_editor):
    Language = apps.get_model('djangobin', 'Language')

    for lang in LANGUAGES:
        l = Language.objects.get(
            lang_code=lang['lang_code'],
        )

        l.delete()

class Migration(migrations.Migration):

    # adjust the dependencies list to refer to the correct migration file

    dependencies = [
        ('djangobin', '0016_auto_20180430_1618'),
    ]

    operations = [
        migrations.RunPython(
            add_languages,
            remove_languages
        )
    ]

注意:代码被截断以节省空间。请记住,您总是可以在 Github repo 中看到可以查看的完整源代码。

前向和后向函数有两个参数,app registry(是django.apps.registry.Apps的一个实例)和SchemaEditor

应用注册表包含加载到其中的所有模型的历史版本,以匹配迁移在历史中的位置。而SchemaEditor是 Django 用来和数据库通信的。

在数据迁移中,您应该始终使用模型的历史版本,因为模型的当前版本可能在此期间发生了变化。Django 使用迁移文件构建了这个历史模型。要加载模型的历史版本,我们使用get_model()方法,该方法以 app 和模型名称为参数。

Django 一直使用历史模型,但这是我们第一次需要了解它是如何工作的。

每当您运行makemigrations命令时,Django 都会将模型的当前版本与迁移文件中存储的模型的历史版本进行比较,以找出需要添加、更新或从数据库中删除的内容,然后根据遇到的更改创建一个迁移文件。

我们的数据迁移已经就绪。要应用它,请执行以下命令:

$ ./manage.py migrate

输出如下所示:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, djangobin, sessions
Running migrations:
  Applying djangobin.0008_language_data...(<Language: Language object>, False)
(<Language: Language object>, True)
(<Language: Language object>, True)
(<Language: Language object>, True)
(<Language: Language object>, True)
(<Language: Language object>, True)
(<Language: Language object>, True)
(<Language: Language object>, False)
(<Language: Language object>, False)
(<Language: Language object>, True)
(<Language: Language object>, False)
(<Language: Language object>, False)
(<Language: Language object>, True)
(<Language: Language object>, True)
(<Language: Language object>, True)
(<Language: Language object>, True)
 OK



Django 表单基础

原文:https://overiq.com/django-1-11/django-form-basics/

最后更新于 2020 年 7 月 27 日


在 Django 中创建表单的语法几乎与创建模型的语法相似,唯一的区别是:

  1. Django 形态继承自forms.Form而非models.Model
  2. 每个表单域继承forms.FieldName而不是models.FieldName

让我们从创建一个LanguageForm类开始。

创建一个名为forms.py的新文件,如果 djangobin 应用中还没有这个文件的话,添加下面的代码。

djangobin/django _ project/djangobin/forms . py

from django import forms

class LanguageForm(forms.Form):
    name = forms.CharField(max_length=100)
    lang_code = forms.CharField()
    slug = forms.SlugField()  
    mime = forms.CharField()
    created_on = forms.DateTimeField()
    updated_on = forms.DateTimeField()

表单域

表单字段负责验证数据并将其转换为 Python 类型。但是,与模型字段不同,表单字段没有相应的 SQL 类型。换句话说,表单字段不知道如何在数据库中表示自己。

小部件

小部件是表单域的 HTML 表示。每个表单域都被分配了一个合理的 Widget 类,但是我们可以很容易地覆盖这个设置。

表单状态

Django 中的表单可以是绑定状态,也可以是未绑定状态。什么是绑定和未绑定状态?

未绑定状态:如果表单没有关联的数据,则它处于未绑定状态。例如,首次向用户显示的空表单处于未绑定状态。

绑定状态:将数据赋予表单的行为称为绑定表单。如果表单有用户提交的数据(有效或无效),则表单处于绑定状态。

is_bound 属性和 is_valid()方法

我们可以使用is_bound属性来检查表单是否处于绑定状态。如果表单处于绑定状态,则is_bound属性返回True,否则返回False

同样,我们可以使用is_valid()方法检查表单数据是否有效。如果数据有效,则is_valid()返回True,否则返回False。需要注意的是,如果is_valid()返回True,那么is_bound属性必然会返回True

访问已清除的数据

当用户通过表单提交数据时,Django 会清除数据,然后对其进行验证。

用户通过表单提交的任何数据都将作为字符串传递给服务器。使用哪种类型的表单域来创建表单并不重要。最终,浏览器会以字符串形式发送所有内容。当 Django 清理数据时,它会自动将数据转换为适当的类型。例如,IntegerField数据将被转换为整数,CharField数据将被转换为字符串,BooleanField数据将被转换为布尔值(TrueFalse)等等。一旦数据被清理和验证,Django 通过cleaned_data字典使其可用。

cleaned_date['field_name']

千万不要直接使用self.field_name访问数据,因为可能不安全。

Django Shell 中的 Django 表单

在本节中,我们将学习如何使用 Django Shell 绑定数据和验证表单。在终端或命令提示符下键入./manage.py shell命令,启动 Django Shell。接下来,导入LanguageForm类并将其实例化:

$ ./manage.py shell
Python 3.5.2 (default, Nov 23 2017, 16:37:01) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>
>>> from djangobin.forms import LanguageForm
>>>
>>> f = LanguageForm()
>>>

此时,我们的表单对象f是未绑定的,因为表单中没有数据。我们可以用is_bound属性来验证这个事实。

>>>
>>> f.is_bound
False
>>>

不出所料is_bound属性返回False。虽然没有意义,但我们也可以通过调用is_valid()方法来检查表单是否有效。

>>>
>>> f.is_valid()
False
>>>

调用is_valid()方法会导致表单数据的清理和验证。在这个过程中,Django 创建了一个名为cleaned_data的属性,这是一个字典,只包含通过验证测试的字段中的干净数据。请注意,cleaned_data属性仅在您调用is_valid()方法后才可用。在调用is_valid()方法之前尝试访问cleaned_data会抛出AttributeError异常。

显然,现在的问题是“我们如何将数据绑定到表单上”?

要将数据绑定到表单,只需将包含表单数据的字典传递给表单构造函数:

>>>
>>> data = {
... 'name': 'ruby',
... 'lang_code': 'ruby',
... 'slug': 'ruby lang', 
... 'mime': 'text/plain',
... }
>>>
>>>
>>> f = LanguageForm(data)
>>>

我们的表单对象f现在有数据了,可以说是绑定了。让我们用is_bound属性来验证一下。

>>>
>>> f.is_bound
True
>>>

不出所料,我们的形式现在被绑定了。我们还可以通过传递一个空字典来获得一个绑定表单。

>>>
>>> f2 = LanguageForm({})
>>>
>>> f2.is_bound
True
>>>

好了,现在让我们在调用is_valid()方法之前尝试访问cleaned_data属性。

>>>
>>> f.cleaned_data
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'CategoryForm' object has no attribute 'cleaned_data'
>>>
>>>

不出所料,我们得到了一个AttributeError异常。现在我们将通过调用is_valid()方法来验证表单。

>>>
>>> f.is_valid()
False
>>>
>>> f.cleaned_data
{'lang_code': 'ruby', 'mime': 'text/plain', 'name': 'ruby'}
>>>

我们的验证失败了,但我们现在可以访问cleaned_data字典。请注意,cleaned_data字典中没有slug键,因为 Django 未能验证该字段。除此之外,表单验证也未能验证LanguageFormcreated_onupdated_on字段,因为我们没有向其提供任何数据。

始终记住cleaned_data属性将只包含经过清理和验证的数据,不包含其他内容。

为了访问错误,表单对象提供了一个errors属性,这是一个类型为ErrorDict的对象,但是在大多数情况下,您可以将其用作字典。

>>> 
>>> f.errors
{'created_on': ['This field is required.'],
 'slug': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."],
 'updated_on': ['This field is required.']}
>>>
>>>

请注意,有三个字段未通过验证过程。默认情况下,errors属性返回所有未通过验证测试的字段的错误消息。以下是如何获取特定字段的错误消息。

>>>
>>> f.errors['slug']
["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]
>>>
>>>
>>> f.errors['updated_on']
['This field is required.']
>>>
>>>
>>> f.errors['created_on']
['Enter a valid date/time.']
>>>
>>>

errors对象提供了两种以不同格式输出错误的方法:

方法 说明
as_data() 返回带有ValidationError对象的字典,而不是字符串。
as_json() 将错误作为 JSON 返回
>>>
>>> f.errors.as_data()
{'created_on': [ValidationError(['This field is required.'])],
 'slug': [ValidationError(["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."])],
 'updated_on': [ValidationError(['This field is required.'])]}
>>>
>>>

>>>
>>> f.errors.as_json()
('{"created_on": [{"message": "This field is required.", "code": "required"}], '
 '"slug": [{"message": "Enter a valid \'slug\' consisting of letters, numbers, '
 'underscores or hyphens.", "code": "invalid"}], "updated_on": [{"message": '
 '"This field is required.", "code": "required"}]}')
>>>
>>>

请注意,与cleaned_data属性不同的是,errors属性始终可供您使用,而无需首先调用is_valid()方法。但是有一个警告,在调用is_valid()方法之前尝试访问errors属性会导致表单数据的清理和验证,从而在过程中创建cleaned_data属性。换句话说,试图首先访问errors属性将导致隐式调用is_valid()方法。但是,在您的代码中,您应该总是显式调用is_valid()方法。

为了再次演示整个过程,让我们创建另一个表单对象,但是这一次我们将使用通过验证的数据绑定表单。

>>>
>>> from datetime import datetime
>>>
>>>
>>> data = {
...     'name': 'ruby',
...     'lang_code': 'ruby',
...     'slug': 'ruby', 
...     'mime': 'text/plain',
...     'created_on': datetime.now(),
...     'updated_on': datetime.now(),
... }
>>>
>>>
>>> f = LanguageForm(data)
>>>
>>>
>>> f.is_bound
True
>>>
>>>
>>> f.is_valid()
True
>>>
>>>
>>> f.cleaned_data
{'created_on': datetime.datetime(2018, 4, 5, 18, 9, 21, 244298, tzinfo=<UTC>),
 'lang_code': 'ruby',
 'mime': 'text/plain',
 'name': 'ruby',
 'slug': 'ruby',
 'updated_on': datetime.datetime(2018, 4, 5, 18, 9, 21, 244317, tzinfo=<UTC>)}
>>>
>>>
>>> f.errors
{}
>>>

深入挖掘表单验证

is_valid()方法被调用时,Django 在幕后做以下事情:

  1. 第一步是调用 Field 的clean()方法。每个表单域都有一个clean()方法,它执行以下两件事:

    1. 将字段数据转换为适当的 Python 类型(回想一下,数据是由浏览器作为字符串发送到服务器的)。例如,如果字段定义为IntegerField,则clean()方法会将数据转换为 Python int,如果未能转换,则会引发ValidationError异常。

    2. 验证从步骤 1 接收的数据。如果验证成功,数据将被插入cleaned_data属性。如果失败,将引发ValidationError。我们通常不会忽略菲尔德的clean()方法。

  2. 在第 2 步中,调用 Field 的clean_<fieldname>()方法为该字段提供一些额外的验证。注意<fieldname>是一个占位符,不是实际的 Python 方法。默认情况下,Django 不定义这些方法。这些方法通常由开发人员编写,为该领域提供一些额外的验证例程。它们不接受任何参数,但必须返回字段的新值。此方法仅在ValidationError未被 Field 的clean()方法提升时调用。这意味着,如果调用此方法,就可以保证该字段的数据得到清理和验证。因此,您必须始终使用cleaned_data['fieldname']在该方法中访问字段数据。此方法返回的值将替换cleaned_data字典中该字段的现有值。

Django 对所有表单域重复步骤 1 和 2。

最后调用 Form 的类clean()方法。如果要执行需要访问多个字段的验证,请在表单类中重写此方法。

注:这是对 Django 验证流程过于简化的看法。现实要复杂得多。

让我们举一个例子来理解当在LanguageForm实例上调用is_valid()方法时,Django 是如何执行清理和验证的。

第一步- 调用name字段的clean()方法来清理和验证数据。成功后,它会将干净且经过验证的数据放入cleaned_data字典。如果清洗或验证失败ValidationError将引发异常并跳过对clean_name()的调用。尽管如此,将调用以下字段的clean()方法。

第二步- 调用name字段的clean_name()方法(假设该方法是在表单类中定义的,并且ValidationError在第一步中没有出现)来执行一些额外的验证。

第三步- 调用lang_code字段的clean()方法来清理和验证数据。成功后,它会将干净且经过验证的数据放入cleaned_data字典。如果清洗或验证失败ValidationError将引发异常并跳过对clean_email()方法的调用。尽管如此,将调用以下字段的clean()方法。

第四步-lang_code字段的clean_lang_code()是一个被调用的方法(假设这个方法是在表单类中定义的并且ValidationError在第三步中没有出现)来执行一些额外的验证。此时,保证lang_code字段被清理和验证,因此下面的代码在clean_lang_code()方法中完全有效。

lang_code = self.cleaned_data['lang_code']; ## that's okay

但是不能保证来自其他字段的数据,例如clean_lang_code()方法内部的name字段。所以,你不应该试图这样访问name字段里面的clean_lang_code()方法:

name = self.cleaned_data['name']; # Not good, you may get an error for doing this

如果您想为name字段提供额外的验证,请在clean_name()方法中进行,因为它保证在那里可用。

这个过程对每个表单域重复进行。最后,调用窗体的clean()方法或其覆盖。关于表单的clean()方法,需要记住的一件重要的事情是,这里保证不存在任何字段。要访问字段数据,必须始终使用字典的对象get()方法,如下所示:

self.cleaned_data.get('name')

如果cleaned_data字典中没有name键,那么get()方法将返回None

实现自定义验证器

在本节中,我们将在我们的LanguageForm类中实现一些自定义验证器。以下是我们想要实现的目标。

  1. 防止用户创建名为"djangobin""DJANGOBIN"的语言。
  2. 只保存小写的鼻涕虫。在这一点上,没有什么能阻止我们保存大写的鼻涕虫。
  3. 鼻涕虫和哑剧的价值不应该相同。

注:当然,这些只是表面的验证。实现它们的全部目的是向您展示如何执行自定义验证。

打开forms.py,修改代码如下:

djangobin/django _ project/djangobin/forms . py

from django import forms
from django.core.exceptions import ValidationError

class LanguageForm(forms.Form):
    name = forms.CharField(max_length=100)
    lang_code = forms.CharField()
    slug = forms.SlugField()
    mime = forms.CharField()
    created_on = forms.DateTimeField()
    updated_on = forms.DateTimeField()

    def clean_name(self):
        name = self.cleaned_data['name']
        if name == 'djangobin' or name == 'DJANGOBIN':
            raise ValidationError("name can't be {}.".format(name))

        # Always return the data
        return name

    def clean_slug(self):
        return self.cleaned_data['slug'].lower()

    def clean(self):
        cleaned_data = super(LanguageForm, self).clean()
        slug = cleaned_data.get('slug')
        mime = cleaned_data.get('mime')

        if slug == mime:
            raise ValidationError("Slug and MIME shouldn't be same.")

        # Always return the data
        return cleaned_data

这里我们已经创建了三个自定义验证器:

clean_name()方法确保语言的名称不应该是djangobinDJANGOBIN。只有当name字段的clean()方法没有引发ValidationError异常时,才会调用该方法。

clean_slug()方法将鼻涕虫转换成小写。就像clean_name()法一样,只有当蛞蝓场的clean()法没有发出ValidationError时才会被调用。

最后,我们已经覆盖了表单的clean()方法。在调用该方法时,单个字段的clean()方法已经被执行。此方法引发的错误不会与特定字段相关联。他们将进入一个名为__all__的独立领域。这种误差称为非场误差。

让我们测试一下定制验证器。

重新启动 Django shell 以使更改生效,然后输入以下代码。

>>>
>>> from djangobin.forms import LanguageForm
>>>
>>> from datetime  import datetime
>>>
>>>
>>> data = {
...     'name': 'djangobin',
...     'lang_code': 'ruby',
...     'slug': 'RUBY', 
...     'mime': 'ruby',
...     'created_on': datetime.now(),
...     'updated_on': datetime.now(),
... }
>>>
>>>
>>> f = LanguageForm(data)
>>>
>>>
>>> f.is_bound
True
>>>
>>>
>>> f.is_valid()
False
>>>
>>>
>>> f.cleaned_data
{'created_on': datetime.datetime(2018, 4, 6, 6, 0, 57, 261639, tzinfo=<UTC>),
 'lang_code': 'ruby',
 'mime': 'ruby',
 'slug': 'ruby',
 'updated_on': datetime.datetime(2018, 4, 6, 6, 0, 57, 261658, tzinfo=<UTC>)}
>>>
>>>
>>> f.errors
{'__all__': ["Slug and MIME shouldn't be same."],
 'name': ["name can't be djangobin."]}
>>>
>>>

不出所料,表单验证失败,因为djangobin不是有效的语言名称,slug名称mime字段的值相同。请注意,cleaned_data字典包含小写的slug,这要感谢clean_slug()方法。

让我们再次尝试验证表单,这次我们将在每个字段中提供有效数据。

>>>
>>>
>>> data = {
...     'name': 'ruby',
...     'lang_code': 'ruby',
...     'slug': 'RUBY', 
...     'mime': 'text/plain',
...     'created_on': datetime.now(),
...     'updated_on': datetime.now(),
... }
>>>
>>> 
>>> f = LanguageForm(data)
>>> 
>>>
>>> f.is_bound
True
>>> 
>>> f.is_valid()
True
>>>
>>>
>>> f.cleaned_data
{'created_on': datetime.datetime(2018, 4, 6, 6, 13, 41, 437553, tzinfo=<UTC>),
 'lang_code': 'ruby',
 'mime': 'text/plain',
 'name': 'ruby',
 'slug': 'ruby',
 'updated_on': datetime.datetime(2018, 4, 6, 6, 13, 41, 437569, tzinfo=<UTC>)}
>>>
>>>
>>> f.errors
{}
>>>
>>>

这次验证成功,因为每个字段中的数据都是正确的。

通用表单域选项

在本节中,我们将讨论一些可用于所有类型表单字段的核心字段选项。

需要

默认情况下,您需要在每个表单字段中输入数据。如果您在提交表单时没有向字段提供任何数据,则该字段的clean()方法将引发ValidationError异常。

>>>
>>> from django import forms
>>>
>>> f = forms.CharField()
>>> 
>>> f.clean(100)   
'100'
>>> 
>>> f.clean("")    # An empty string ("") signifies no data.
...      
django.core.exceptions.ValidationError: ['This field is required.']
>>>
Traceback (most recent call last):

我们可以通过将required=False传递给字段构造器来使字段可选。

>>> 
>>> f = forms.CharField(required=False)
>>> 
>>> f.clean("")
''
>>>

标签

label为表单域指定了一个人性化的名称。这将出现在<label>标签内。如果不指定此选项,Django 将通过取字段名、大写、将下划线转换为空格并附加一个尾随冒号(:)来创建默认标签。

>>>
>>> class PostForm(forms.Form):
...     title = forms.CharField(label="Enter title")
...     content = forms.CharField()
... 
>>> 
>>> f = PostForm()
>>> 
>>> print(f)
<tr>
    <th>
        <label for="id_title">Enter title:</label>
    </th>
    <td>
        <input type="text" name="title" required id="id_title" />
    </td>
</tr>
<tr>
    <th>
        <label for="id_content">Content:</label>
    </th>
    <td>
    <input type="text" name="content" required id="id_content" />
    </td>
</tr>

最初的

initial参数设置渲染字段时要显示的初始数据。

>>> 
>>> class PostForm(forms.Form):
...     title = forms.CharField(initial="Enter title")
...     content = forms.CharField()
... 
>>> 
>>>

请注意,向字段提供初始数据并不会神奇地将表单置于绑定状态。

>>> 
>>> f = PostForm()
>>>
>>> f.is_bound
False
>>>

但是如果我们现在渲染表单,title字段将具有Enter title的值。

>>> 
>>> f
<PostForm bound=False, valid=Unknown, fields=(title;content)>
>>>
>>> print(f)
<tr><th><label for="id_title">Title:</label></th><td><input type="text" name="title" value="Enter title" required id="id_title" /></td></tr>
<tr><th><label for="id_content">Content:</label></th><td><input type="text" name="content" required id="id_content" /></td></tr>
>>> 
>>>

我们还可以在实例化表单时设置初始值。这允许我们一次为多个字段设置初始值。例如:

>>> 
>>> f = PostForm(initial={'title': 'Some title', 'content': 'Some content'})
>>> 
>>> print(f)
<tr><th><label for="id_title">Title:</label></th><td><input type="text" name="title" value="Some title" required id="id_title" /></td></tr>
<tr><th><label for="id_content">Content:</label></th><td><input type="text" name="content" value="Some content" required id="id_content" /></td></tr>
>>> 
>>>

这种提供初始值的方法非常灵活,因为它允许我们动态设置初始数据。我们将在接下来的课程中看到这样的例子。

小部件

此参数允许我们更改表单字段使用的默认小部件。

每个表单域都分配有一个默认小部件。这就是字段如何知道如何将自己渲染为 HTML。

从形式上来说,小部件是一个控制 HTML 中特定表单元素的渲染和从中提取 GET/POST 数据的类。就像模型和表单字段一样,Django 提供了表示常见表单元素的内置小部件类。下表列出了一些常见的 Widget 类及其表示的表单元素。

小部件类 超文本标记语言
TextInput <input type="text" ...>
PasswordInput <input type="password" ...>
EmailInput <input type="email" ...>
Textarea <textarea>...</textarea>
URLInput <input type="url" ...>
DateTimeInput <input type="text" ...>

为了便于快速参考,下表列出了常见的表单字段及其使用的默认小部件。

表单域 默认小部件类
CharField TextInput
EmailField EmailInput
SlugField TextInput
DateTimeField DateTimeInput

注意:您可以在这个网址查看默认字段-小部件配对的完整列表。

要更改默认小部件,只需在定义字段时为widget参数分配一个新的小部件类。例如:

>>>
>>> class PostForm(forms.Form):
...     title = forms.CharField()
...     content = forms.CharField(widget=forms.Textarea)
... 
>>>

默认情况下,CharField将自身渲染为TextInput(或<input type="text" ...>)。在前面的代码中,我们强制 Django 将content字段渲染为Textarea(即<textarea>...</textarea>)而不是TextInput

>>> 
>>> f = PostForm()
>>>
>>> print(f)
<tr><th><label for="id_title">Title:</label></th><td><input type="text" name="title" required id="id_title" /></td></tr>
<tr><th><label for="id_content">Content:</label></th><td><textarea name="content" required rows="10" cols="40" id="id_content">
</textarea></td></tr>
>>>

我们还可以使用attrs属性为表单元素设置附加属性:

>>> 
>>> class PostForm(forms.Form):
...     title = forms.CharField()
...     content = forms.CharField(widget=forms.Textarea(attrs={'class': 'content', 'row':'5', 'cols':'10'}))
... 
>>> 
>>> f = PostForm()
>>> 
>>> print(f)
<tr><th><label for="id_title">Title:</label></th><td><input type="text" name="title" required id="id_title" /></td></tr>
<tr><th><label for="id_content">Content:</label></th><td><textarea name="content" required row="5" cols="10" rows="10" id="id_content" class="content">
</textarea></td></tr>
>>> 
>>>

帮助 _ 文本

此参数用于指定有关字段的一些重要信息。

>>> 
>>> class PostForm(forms.Form):
...     title = forms.CharField()
...     content = forms.CharField(help_text="Good stuff")
... 
>>> 
>>> f = PostForm()
>>> 
>>> 
>>> print(f)
<tr><th><label for="id_title">Title:</label></th><td><input type="text" name="title" required id="id_title" /></td></tr>
<tr><th><label for="id_content">Content:</label></th><td><input type="text" name="content" required id="id_content" /><br /><span class="helptext">Good stuff</span></td></tr>
>>> 
>>>

将表单数据保存到数据库

那么,我们如何将通过表单收到的数据保存到数据库中呢?在本章的前面,我们已经讨论过,与模型字段不同,表单字段不知道如何在数据库中表示自己。此外,与models.Model类不同的是,forms.Form类不提供save()方法将表单数据保存到数据库中。

解决办法就是实行我们自己的save()方法。方法名没有限制,你可以随意调用。打开forms.py文件,在LanguageForm课快结束时添加save()方法:

djangobin/django _ project/djangobin/forms . py

from django import forms
from django.core.exceptions import ValidationError
from .models import Language

class LanguageForm(forms.Form):
    #...

    def clean(self):
        #...

    def save(self):
        new_lang = Language.objects.create(
            name = self.cleaned_data['name'],
            lang_code = self.cleaned_data['lang_code'],
            slug = self.cleaned_data['slug'],
            mime = self.cleaned_data['mime'],
            created_on = self.cleaned_data['created_on'],
            updated_on = self.cleaned_data['updated_on'],
        )
        return new_lang

这里没有什么新内容,在第 3 行,我们从 djangobin 应用导入模型。在第 12-21 行,我们定义了save()方法,该方法使用表单数据创建一个新的Language对象。请注意,在创建新的Language对象时,我们通过cleaned_data字典访问表单数据。

再次重启 Django shell,让我们尝试通过LanguageForm创建一个新的Language

>>>
>>> from djangobin.forms import LanguageForm
>>>
>>> from datetime import datetime
>>>
>>> data = {
...     'name': 'Go',
...     'lang_code': 'go',
...     'slug': 'go', 
...     'mime': 'text/x-gosrc',
...     'created_on': datetime.now(),
...     'updated_on': datetime.now(),
... }
>>>
>>> f = LanguageForm(data)
>>>
>>> f.is_bound
True
>>>
>>> f.is_valid()
True
>>>
>>> f.save()
<Language: Go>
>>> 
>>>
>>> from djangobin.models import Language
>>>
>>> l = Language.objects.get(name='Go')
>>> 
>>> l
<Language: Go>
>>> 
>>> l.pk
17
>>> 
>>>

果然,我们新创建的Language对象现在保存在数据库中。

我们的表单功能齐全。此时,我们可以继续为应用的其余部分创建表单类;但是有一个大问题。

这种方法的问题在于LanguageForm类中的字段映射到了Language模型中的字段。因此,在LanguageForm重新定义它们是多余的。如果我们在Language模型中添加或修改任何字段,那么我们必须相应地更新我们的LanguageForm类。

此外,您可能已经注意到,我们定义模型字段和表单字段的方式几乎没有什么不同。例如:

Language模型中的slug字段定义如下:

slug = models.SlugField(max_length=100, unique=True)

另一方面,LanguageForm中的同一个字段定义如下:

slug = forms.SlugField(max_length=100)

注意LanguageForm中的slug字段没有unique=True属性。这是因为unique=True只为模型字段定义,不为表单字段定义。解决这个问题的一个方法是通过实现如下的clean_slug()方法来创建一个自定义验证器:

#...
class LanguageForm(forms.Form):
    #...

    def clean_slug(self):
        slug = self.cleaned_data['slug'].lower()
        r = Language.objects.filter(slug=slug)
        if r.count:
            raise ValidationError("{0} already exists".format(slug))

        return slug.lower()

同样,表单字段不提供、auto_add_nowauto_now参数来自动填充日期和时间。我们可以通过覆盖表单的clean()方法来实现这些参数提供的功能。

如您所见,对于 Django 模型提供的每个功能,我们必须添加各种自定义验证器,覆盖表单的clean()方法等等。当然,这涉及到很多工作。我们可以通过使用ModelForm来避免所有这些问题。

使用模型表单消除冗余

大多数情况下,您会希望有一个与模型类紧密匹配的表单。这就是ModelForm班发挥作用的地方。ModelForm允许您构建基于模型类字段的表单。它还为您提供了自定义表单的选项。

要使用ModelForm,请执行以下操作:

  1. 将表单类的继承从forms.Form改为forms.ModelForm
  2. 使用Meta类的model属性通知forms.py中的表单类使用哪个模型。

经过这两个步骤,我们可以删除我们在LanguageForm类中定义的所有表单字段。此外,我们也可以去掉save()方法,因为ModelForm提供了这个方法。这里需要提到的是,我们在本章前面实现的save()方法只能创建对象,不能更新对象。另一方面,来自ModelFormsave()方法可以做到这两点。

这是修改后的代码。

djangobin/django _ project/djangobin/forms . py

from django import forms
from django.core.exceptions import ValidationError
from .models import Language

class LanguageForm(forms.ModelForm):
    class Meta:
        model = Language        

    def clean_name(self):
        name = self.cleaned_data['name']
        name_l = name.lower()
        if name == 'djangobin' or name == 'DJANGOBIN':
            raise ValidationError("name can't be {}.".format(name))
        return name

    def clean_email(self):
        return self.cleaned_data['email'].lower()

    def clean(self):
        cleaned_data = super(LanguageForm, self).clean()
        slug = cleaned_data.get('slug')
        mime = cleaned_data.get('mime')

        if slug == mime:
            raise ValidationError("Slug and MIME shouldn't be same.")

        # Always return the data
        return cleaned_data

接下来,我们要告诉LanguageForm类我们要在表单中显示哪些字段。为此,我们使用Meta类的fields属性。它接受您想要在表单中显示的字段名称列表或元组。如果要显示所有字段,只需使用"__all__"(这是双下划线)。

fields = '__all__'      # display all the fields in the form
fields = ['title', 'content']  # display only title and content field in the form

类似地,还有一个名为exclude的补充属性,它接受您不想在表单中显示的字段名列表。

exclude = ['slug', 'pub_date'] # show all the fields except slug and pub_date

让我们更新代码以使用fields属性。

djangobin/django _ project/djangobin/forms . py

from django import forms
from django.core.exceptions import ValidationError
from .models import Language

class LanguageForm(forms.Form):
    class Meta:
        model = Language
        fields = '__all__'

    def clean_name(self):
        name = self.cleaned_data['name']
        if name == 'djangobin' or name == 'DJANGOBIN':
            raise ValidationError("name can't be {}.".format(name))

        # Always return the data
        return name

    def clean_slug(self):
        return self.cleaned_data['slug'].lower()

    def clean(self):
        cleaned_data = super(LanguageForm, self).clean()
        slug = cleaned_data.get('slug')
        mime = cleaned_data.get('mime')

        if slug == mime:
            raise ValidationError("Slug and MIME shouldn't be same.")

        # Always return the data
        return cleaned_data

请注意,我们根本没有更改我们的自定义验证器,因为它们也与ModelForm一起工作。

模型验证

除了表单验证,ModelForm还执行模型验证。模型验证在数据库级别验证数据。

模型验证分三步进行。

  1. Model.clean_fields() -该方法验证模型中的所有字段

  2. Model.clean() -工作方式和 Form 的clean()方法一样。如果要在数据库级别执行一些需要访问多个字段的验证,请在模型类中重写此方法。默认情况下,此方法不执行任何操作。

  3. Model.validate_unique() -该方法检查施加在模型上的唯一性约束。

模型验证在调用表单的clean()方法后立即开始。

现在,要记住最重要的一句话:

默认情况下,Django 不使用模型验证。

注:之所以出现这种莫名其妙的行为,是因为向后兼容

这意味着 Django 允许你破坏数据库中的数据。我们可以通过将包含无效数据的对象保存到数据库中来验证这一事实。

>>>
>>> l = Language(
...     name='racket',
...     slug='this is an invalid slug',
...     mime='text/plain',
...     lang_code='racket',
...     file_extension='*.rkt'    
... )
>>> 
>>> 
>>> l.save()
>>> 
>>> l.pk
9
>>> 
>>> l
<Language: racket>
>>>
>>>

这里slug字段包含无效数据,但是 Django 仍然将对象保存到数据库中。

我们可以使用full_clean()方法手动触发模型验证,如下所示:

>>> 
>>> l = Language(
...     name='Haskell',
...     slug='Hask ell',
...     mime='text/x-haskell',
...     lang_code='hs',    
...     file_extension='*.hs'
... )
>>> 
>>> 
>>> l.full_clean()
Traceback (most recent call last):
  ...  
django.core.exceptions.ValidationError: {'slug': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]}
>>> 
>>>

调用full_clean()方法只触发模型验证,不保存对象到数据库。

由于ModelForm自动触发模型验证,我们不需要担心手动调用full_clean()方法。

覆盖模型表单中的默认字段

ModelForm的效用不仅仅局限于快速创建表单。它还提供了一些Meta属性来定制表单域。例如,widgets属性允许我们覆盖默认的小部件或向表单元素添加附加属性。

from django.forms import ModelForm, Textarea

class LanguageForm(forms.ModelForm):
    class Meta:
        model = Language
        fields = '__all__'
        widgets = {
            'file_extension': Textarea(attrs={'rows': 5, 'cols': 10}),
        }

同样,我们可以使用内部Meta类的labelshelp_texts属性添加标签和帮助文本。

from django.forms import ModelForm, Textarea

class LanguageForm(forms.ModelForm):
    class Meta:
        model = Language
        fields = '__all__'
        labels = {
            'mime': 'MIME Type',
            'lang_code': 'Language Code',
        },
        help_texts = {            
            'lang_code': 'Short name of the Pygment lexer to use',
            'file_extension': 'Specify extension like *.txt, *.md etc;'
        },

暂时就这些。这一章相当长。尽管如此,我们已经了解了很多关于 Django 表单的知识。在下一章中,我们将学习如何在模板中渲染表单。



在 Django 中展示表单

原文:https://overiq.com/django-1-11/displaying-forms-in-django/

最后更新于 2020 年 7 月 27 日


到目前为止,我们一直在使用 Django Shell 来演示表单是如何工作的。在本课中,我们将学习如何在模板中显示表单。

Django 提供了以下三种显示表单元素的方法:

>>>
>>> from djangobin.forms import LanguageForm
>>> f = LanguageForm()
>>>

  1. as_p()
  2. as_table()
  3. as_ul()

as_p()

该方法在表单域中显示一系列<p>标签:

>>>
>>> f.as_p()
<p><label for="id_name">Name:</label> <input type="text" name="name" maxlength="100" required id="id_name" /></p>
<p><label for="id_lang_code">Lang code:</label> <input type="text" name="lang_code" required id="id_lang_code" /></p>
<p><label for="id_slug">Slug:</label> <input type="text" name="slug" required id="id_slug" /></p>
<p><label for="id_mime">Mime:</label> <input type="text" name="mime" required id="id_mime" /></p>
<p><label for="id_created_on">Created on:</label> <input type="text" name="created_on" required id="id_created_on" /></p>
<p><label for="id_updated_on">Updated on:</label> <input type="text" name="updated_on" required id="id_updated_on" /></p>
>>>
>>>

as_table()

该方法在表单域中显示一系列<tr>标签:

>>>
>>> f.as_table()
<tr><th><label for="id_name">Name:</label></th><td><input type="text" name="name" maxlength="100" required id="id_name" /></td></tr>
<tr><th><label for="id_lang_code">Lang code:</label></th><td><input type="text" name="lang_code" required id="id_lang_code" /></td></tr>
<tr><th><label for="id_slug">Slug:</label></th><td><input type="text" name="slug" required id="id_slug" /></td></tr>
<tr><th><label for="id_mime">Mime:</label></th><td><input type="text" name="mime" required id="id_mime" /></td></tr>
<tr><th><label for="id_created_on">Created on:</label></th><td><input type="text" name="created_on" required id="id_created_on" /></td></tr>
<tr><th><label for="id_updated_on">Updated on:</label></th><td><input type="text" name="updated_on" required id="id_updated_on" /></td></tr> 
>>>
>>>

as_ul()

该方法在表单域中显示一系列<li>标签:

>>>
>>> f.as_ul()
<li><label for="id_name">Name:</label> <input type="text" name="name" maxlength="100" required id="id_name" /></li>
<li><label for="id_lang_code">Lang code:</label> <input type="text" name="lang_code" required id="id_lang_code" /></li>
<li><label for="id_slug">Slug:</label> <input type="text" name="slug" required id="id_slug" /></li>
<li><label for="id_mime">Mime:</label> <input type="text" name="mime" required id="id_mime" /></li>
<li><label for="id_created_on">Created on:</label> <input type="text" name="created_on" required id="id_created_on" /></li>
<li><label for="id_updated_on">Updated on:</label> <input type="text" name="updated_on" required id="id_updated_on" /></li>
>>>
>>>

请注意,渲染的 HTML 没有<form>标签和提交按钮。表单方法只输出表单字段。为了使表单充分发挥作用,我们必须手动添加<form>标签和提交按钮,如下所示:

<form action="/url-to-submit/" method="post">    
    {{ form.as_p }}
    <input type="submit" value="Submit" />
</form>

我们也可以在模板中输出表单字段,只需输入{{ f }},相当于{{ f.as_table }}

<form action="/url-to-submit/" method="post">    
    {{ f }}
    <input type="submit" value="Submit" />
</form>

在表单的绑定状态下,这些方法还会输出验证错误以及前一个请求中填充的数据。

>>>
>>> f2 = LanguageForm()
>>>
>>> f2.is_bound
False
>>>
>>> print(f2.as_p())
<p><label for="id_name">Name:</label> <input type="text" name="name" maxlength="100" required id="id_name" /></p>
<p><label for="id_lang_code">Lang code:</label> <input type="text" name="lang_code" required id="id_lang_code" /></p>
<p><label for="id_slug">Slug:</label> <input type="text" name="slug" required id="id_slug" /></p>
<p><label for="id_mime">Mime:</label> <input type="text" name="mime" required id="id_mime" /></p>
<p><label for="id_created_on">Created on:</label> <input type="text" name="created_on" required id="id_created_on" /></p>
<p><label for="id_updated_on">Updated on:</label> <input type="text" name="updated_on" required id="id_updated_on" /></p>
>>>

让我们用一些数据绑定表单f2

>>> 
>>> data = {
...     'name': 'sql',
...     'lang_code': 'sql',
...     'slug': 'sql###',               
... }
>>>
>>>
>>> f2 = LanguageForm(data)
>>>
>>> 
>>> f2.is_valid()
False
>>>
>>>
>>> print(f2.as_p())
<ul class="errorlist nonfield"><li>Slug and MIME shouldn&#39;t be same.</li></ul>
<p><label for="id_name">Name:</label> <input type="text" name="name" value="sql" maxlength="100" required id="id_name" /></p>
<p><label for="id_lang_code">Lang code:</label> <input type="text" name="lang_code" value="sql" required id="id_lang_code" /></p>
<ul class="errorlist"><li>Enter a valid &#39;slug&#39; consisting of letters, numbers, underscores or hyphens.</li></ul>
<p><label for="id_slug">Slug:</label> <input type="text" name="slug" value="sql###" required id="id_slug" /></p>
<ul class="errorlist"><li>This field is required.</li></ul>
<p><label for="id_mime">Mime:</label> <input type="text" name="mime" required id="id_mime" /></p>
<ul class="errorlist"><li>This field is required.</li></ul>
<p><label for="id_created_on">Created on:</label> <input type="text" name="created_on" required id="id_created_on" /></p>
<ul class="errorlist"><li>This field is required.</li></ul>
<p><label for="id_updated_on">Updated on:</label> <input type="text" name="updated_on" required id="id_updated_on" /></p>
>>>

我们已经在 Shell 中探索了足够多的形式,现在让我们创建一个真实的形式。

创建真实表单

从 djangobin app 打开views.py文件,在文件末尾添加add_lang()视图:

djangobin/django_project/djangobin/views.py

from django.shortcuts import HttpResponse, render, redirect
from .forms import LanguageForm

#...

def profile(request, username):
    return HttpResponse("<p>Profile page of #{}</p>".format(username))

def add_lang(request):
    if request.POST:
        f = LanguageForm(request.POST)
        if f.is_valid():
            lang = f.save()
            return redirect('djangobin:add_lang')

    else:
        f = LanguageForm()

    return render(request, 'djangobin/add_lang.html', {'form': f} )

以下是它的工作原理:

  1. 当一个 GET 请求到来时,我们创建一个未绑定的LanguageForm(第 18 行)并渲染一个空表单(第 20 行)。

  2. 如果请求是 POST(第 11 行),我们创建一个绑定表单(第 12 行),并使用is_valid()方法进行验证。如果表单有效,我们保存语言并将用户重定向到添加语言页面。另一方面,如果验证失败,控制从 if-else 语句中出来,我们返回一个包含表单数据和验证错误的新响应(第 20 行)。

接下来,打开urls.py并在文件末尾添加add_lang网址模式:

决哥/决哥 _ 项目/决哥/URL . py】

#...
urlpatterns = [
    #...
    url('^tag/(?P<tag>[\w-]+)/$', views.tag_list, name='tag_list'),
    url('^add-lang/$', views.add_lang, name='add_lang'),
]

最后,这里是add_lang.html模板的代码。

djangobin/django _ project/djangobin/templates/djangobin/add _ lang . html

{% extends "base.html" %}

{% block title %}
    Add Lanugage
{% endblock %}

{% block content %}

    <h2>Add a new Language</h2>

    <form action="" method="post">
        {% csrf_token %}
        <table>
            {{ form.as_table }}
            <tr>
                <td></td>
                <td><input type="submit"></td>
            </tr>
        </table>
    </form>

{% endblock %}

这里没有什么新东西,除了{% csrf_token %}标签。csrf_token是 Django 用来防止 CSRF(跨站点请求伪造)攻击的特殊标签。您不需要知道它在内部是如何工作的,只需将{% csrf_token %}放入您的表单模板中,Django 将处理其他一切。如果您有兴趣了解更多关于 CSRF 袭击的信息,请点击这里的。

默认情况下,Django 希望您在每个表单上添加csrf_token标签。如果您没有这样做,那么在提交表单时,您将会收到类似下面这样的 HTTP 403 禁止错误:

使用./manage.py runserver启动服务器,访问http://localhost:8000/add-lang/。您将看到如下添加语言:

在具有唯一约束的字段中输入一些重复数据。您将得到如下验证错误:

字段的验证错误显示在字段本身的上方。另外,请注意表单顶部的错误,上面写着“Slug 和 MIME 不应该相同。”。这是非字段错误,因此,它显示在所有字段上方的表单顶部。

请记住LanguageForm表单正在执行两个验证:表单验证和模型验证。非字段错误(“Slug 和 MIME 不应该相同”)来自表单验证,而关于重复数据的错误来自模型验证。

现在,输入数据库中尚不存在的新语言。这一次,表单会将数据保存到数据库中,然后您将被重定向到“添加语言”页面。

我们的数据已保存,但没有得到任何确认。我们可以使用 flash 消息轻松解决这个问题。

火急文电

网络应用是关于用户体验的。每次操作后,您必须通知用户操作的结果。这些通知通常也称为闪存消息。Django 提供了一个名为django.contrib.messages的内置框架来显示 flash 消息。django.contrib.messages框架已经预装了,所以您不必配置任何东西来使用它。

要显示闪光信息,我们必须首先从django.contrib包导入messages包。

from django.contrib import messages

messages包提供了一个名为add_message()的功能来设置 flash 消息。add_message()函数接受两个参数,request对象和您想要显示的消息。这里有一个例子:

from django.contrib import messages
messages.add_message(request, 'Email Sent!')

我们还可以将消息级别传递给add_message()功能。消息级别允许我们在模板中格式化 flash 消息。下表列出了可以从django.contrib.messages包导入的内置消息级别。

常量 描述
DEBUG 它用于显示开发相关消息。
INFO 它用于显示信息性消息。
SUCCESS 它用于显示与成功相关的消息。
WARNING 它用于显示与警告相关的消息。
ERROR 它用于显示与错误相关的消息。

以下是一些如何设置消息级别的示例:

from django.contrib import messages

messages.add_message(request, messages.DEBUG, '10 queries executed.')
messages.add_message(request, messages.INFO, 'Your are loggedin as staff member.')
messages.add_message(request, messages.SUCCESS, 'Email verification sent.')
messages.add_message(request, messages.WARNING, 'Change you password.')
messages.add_message(request, messages.ERROR, 'Failed to update the profile.')

要访问模板中的 flash 消息,我们使用messages变量,如下所示:

{% if messages %}
<ul class="messages">
    {% for message in messages %}
    <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
    {% endfor %}
</ul>
{% endif %}

就像request变量一样,messages是另一个特殊的变量,如果你使用render()函数,你可以在模板中访问它。

现在你知道如何使用 flash 消息了,让我们以添加语言的形式来使用它。

打开 Django 斌的views.py并修改为使用django.contrib.messages框架,如下所示:

djangobin/django_project/djangobin/views.py

from django.shortcuts import HttpResponse, render, reverse, redirect
from django.contrib import messages
from .forms import LanguageForm

#...

def add_lang(request):
    if request.POST:
        f = LanguageForm(request.POST)
        if f.is_valid():
            lang = f.save()
            messages.add_message(request, messages.INFO, 'Language saved.')
            return redirect('djangobin:add_lang')

    else:
        f = LanguageForm()

    return render(request, 'djangobin/add_lang.html', {'form': f} )

接下来,显示 flash 消息修改add_lang.html模板如下:

{% extends "base.html" %}

{% block title %}
    Add Lanugage
{% endblock %}

{% block content %}

    <h2>Add a new Language</h2>

    {% if messages %}
        <ul class="messages">
            {% for message in messages %}
                <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <form action="" method="post">
        {% csrf_token %}
        <table>
            {{ form.as_table }}
            <tr>
                <td></td>
                <td><input type="submit"></td>
            </tr>
        </table>
    </form>

{% endblock %}

http://127.0.0.1:8000/cadmin/post/add/重新访问添加帖子表单并添加语言。这一次你会得到这样的成功信息:

通过表单更新数据

我们到现在建立的表格,只能创造新的记录吗?更新那些记录怎么样?

在更新记录时,第一步是显示预先填充了数据库数据的表单。Django 为此任务提供了一个instance参数。下面的 shell 会话演示了如何使用它。

>>> 
>>> from djangobin.models import *
>>> from djangobin.forms import *
>>> 
>>> 
>>> l = Language.objects.get(name='SQL')
>>> 
>>> l
<Language: SQL>
>>> 
>>>
>>> f = LanguageForm(instance=l)
>>> 
>>> print(f.as_p())
<p><label for="id_name">Name:</label> <input type="text" name="name" value="SQL" maxlength="100" id="id_name" required /></p>
<p><label for="id_lang_code">Language Code:</label> <input type="text" name="lang_code" value="sql" maxlength="100" id="id_lang_code" required /></p>
<p><label for="id_slug">Slug:</label> <input type="text" name="slug" value="sql" maxlength="100" id="id_slug" required /></p>
<p><label for="id_mime">Mime:</label> <input type="text" name="mime" value="text/x-sql" maxlength="100" id="id_mime" required /> <span class="helptext">MIME to use when sending snippet as file</span></p>
<p><label for="id_file_extension">File extension:</label> <input type="text" name="file_extension" value=".sql" maxlength="10" id="id_file_extension" required /></p>
>>> 
>>>

请注意,print(f.as_p())的输出包含所有预填充了数据库数据的表单字段。

此时,你可能会想,“我们的表单有数据,所以它应该处于绑定状态”,对吗?答案是:没有,表单还处于未绑定状态。instance属性的使用仅限于显示数据。就这样。我们可以通过在表单对象上使用is_bound属性来验证这个事实。

>>> 
>>> f.is_bound
False
>>>

那么我们如何在更新对象时将数据绑定到表单呢?

要将数据绑定到表单,请传递包含数据的字典以及实例属性,如下所示:

>>> 
>>> f = LanguageForm({}, instance=l)
>>> 
>>> f.is_bound
True
>>>

虽然字典是空的,但我们的形式仍然处于绑定状态。

在现实世界中,我们会传递request.POST而不是空字典({})。

f = CategoryForm(request.POST, instance=c)

另一件需要记住的重要事情是,在保存数据时,save()方法将使用来自request.POST的数据,而不是来自instance=c的数据。

让我们利用这些知识来创建 Change 语言形式。

打开 Django 斌的views.py,在add_lang()视图下方增加update_lang()视图功能,如下图:

djangobin/django_project/djangobin/views.py

from django.shortcuts import HttpResponse, render, redirect, get_object_or_404
from django.contrib import messages
from .forms import LanguageForm
from .models import Language

#...

def add_lang(request):
    #...

def update_lang(request, lang_slug):
    l = get_object_or_404(Language, slug__iexact=lang_slug)
    if request.POST:
        f = LanguageForm(request.POST, instance=l)
        if f.is_valid():
            lang = f.save()
            messages.add_message(request, messages.INFO, 'Language Updated.')
            return redirect('djangobin:update_lang', lang.slug)

    else:
        f = LanguageForm(instance=l)

    return render(request, 'djangobin/update_lang.html', {'form': f} )

urls.py文件中,添加update_lang网址模式如下:

决哥/决哥 _ 项目/决哥/URL . py】

#...

urlpatterns = [
    #...
    url('^add-lang/$', views.add_lang, name='add_lang'),
    url('^update-lang/(?P<lang_slug>[\w-]+)/$', views.update_lang, name='update_lang'),
]

这里是update_lang.html模板的代码:

djangobin/django _ project/djangobin/templates/djangobin/update _ lang . html

{% extends "base.html" %}

{% block title %}
    Change Lanugage
{% endblock %}

{% block content %}

    <h2>Change Language</h2>

    {% if messages %}
        <ul class="messages">
            {% for message in messages %}
                <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <form action="" method="post">
        {% csrf_token %}
        <table>
            {{ form.as_table }}
            <tr>
                <td></td>
                <td><input type="submit"></td>
            </tr>
        </table>
    </form>

{% endblock %}

访问http://localhost:8000/update-lang/sql/会看到如下的更改语言页面:

就像“添加语言”页面一样,“更改语言”页面显示验证错误,并预先填充来自上一个请求的数据。更新完语言后,点击“提交”按钮将更改保存到数据库。



Django 手动渲染表单字段

原文:https://overiq.com/django-1-11/django-rendering-form-fields-manually/

最后更新于 2020 年 7 月 27 日


上一课中,我们已经看到了几种显示表单的方法。毫无疑问,这些方法让我们能够快速创建表单,但它们也对表单的渲染方式提供了最少的控制。如果你想对你的表单有一个完美的像素控制,请继续阅读。

目前,我们的add_lang.html模板是这样的:

djangobin/django _ project/djangobin/templates/djangobin/add _ lang . html

{% extends "base.html" %}

{% block title %}
    Add Lanugage
{% endblock %}

{% block content %}

    <h2>Add a new Language</h2>

    {% if messages %}
        <ul class="messages">
            {% for message in messages %}
                <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <form action="" method="post">
        {% csrf_token %}
        <table>
            {{ form.as_table }}
            <tr>
                <td></td>
                <td><input type="submit"></td>
            </tr>
        </table>
    </form>

{% endblock %}

在手动渲染每个字段之前,用以下代码替换<form>标记:

<form action="" method="post">
        {% csrf_token %}
        <table>            
            <tr>
                <th>
                    <label for="id_name">Name:</label>
                </th>
                <td>
                    <input type="text" name="name" maxlength="100" id="id_name" />
                </td>
            </tr>
            <tr>
                <th>
                    <label for="id_lang_code">Language Code:</label>
                </th>
                <td>
                    <input type="text" name="lang_code" maxlength="100" id="id_lang_code" />
                </td>
            </tr>
            <tr>
                <th>
                    <label for="id_slug">Slug:</label>
                </th>
                <td>
                    <input type="text" name="slug" maxlength="100" id="id_slug" />
                </td>
            </tr>
            <tr>
                <th>
                    <label for="id_mime">Mime:</label>
                </th>
                <td>
                    <input type="text" name="mime" maxlength="100" id="id_mime" />
                </td>
            </tr>
            <tr>
                <td></td>
                <td><input type="submit"></td>
            </tr>
        </table>
    </form>

将浏览器指向http://127.0.0.1:8000/add-lang/。在namelang_code字段中输入一些数据(数据是否有效无关紧要,只需填写即可),将后面两个字段留空(即 Slug 和 Mime)。最后点击提交按钮。您将再次显示一个空表单。

等一下!这是怎么回事?我们所有的表单域都是必需的,但是为什么现在会显示任何错误呢?

问题是我们在硬编码我们的 HTML,我们没有使用像as_p()as_table()这样的表单方法来显示表单字段。因此,我们无法向用户显示表单的绑定状态。

如果我们使用类似as_p()as_table()的方法,而不是硬编码单个表单字段,我们会得到如下验证错误:

除了验证错误之外,表单没有预填充我们在上次请求中提交表单时在namelang_code字段中输入的数据(有效或无效)。

在下一节中,我们将学习如何纠正所有这些问题。

显示特定于字段的错误

要显示与特定字段相关的错误,请使用form.field_name.errors变量。例如,这就是我们如何显示与name字段相关的错误。

{# check whether there are any errors or not #}

{% if form.name.errors %}
    <ul>
    {% for error in form.name.errors %}
        <li>{{ error }}</li>
    {% endfor %}
{% endif %}

打开add_lang.html,修改文件如下:

djangobin/django _ project/djangobin/templates/djangobin/add _ lang . html

{# ... #}
<table>
    <tr>
        <td></td>            
        {% if form.name.errors %}
            {% for error in form.name.errors %}
                <td>{{ error }}</td>
            {% endfor %}
        {% endif %}
    </tr>
    <tr>
        <th>
            <label for="id_name">Name</label>
        </th>
        <td>
            <input type="text" id="id_name" name="name">
        </td>
    </tr>  
    <tr>
        <th>
            <label for="id_email">Email</label>
        </th>
        <td>
            <input type="email" id="id_email" name="email">
        </td>
    </tr>
{# ... #}
</table>
...

访问http://127.0.0.1:8000/add-lang/点击提交按钮,无需在任何字段输入数据。您应该会在name字段上方看到如下的This field is required.错误消息:

显示非字段错误

回想一下,我们可以覆盖表单的clean()方法来添加需要访问多个字段的验证。clean()方法提出的错误并不是针对某一个领域的,事实上,错误属于整个形式。在 Django 术语中,我们称这种错误为非现场错误。要访问模板内的非字段错误,我们使用form.non_field_errors变量。例如,我们可以使用以下代码在反馈表单中显示非字段错误。

{% if form.non_field_errors %}
<ul>
    {% for error in form.non_field_errors %}
        <li>{{ error }}</li>
    {% endfor %}
</ul>
{% endif %}

打开add_lang.html,修改如下:

djangobin/django _ project/djangobin/templates/djangobin/add _ lang . html

{# ... #}
    {% if messages %}
        <ul class="messages">
            {% for message in messages %}
                <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    {% if form.non_field_errors %}
        <ul>
            {% for error in form.non_field_errors %}
                <li>{{ error }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <form action="" method="post">
        {% csrf_token %}
        <table>
{# ... #}

访问添加语言页面,在slugmime字段输入相同的数据,然后点击提交。您将在表单顶部看到一个非字段错误,如下所示:

使用快捷方式

Django 提供了显示字段错误和非字段错误的快捷方式。我们还可以如下显示与name字段相关的错误:

{{ form.name.errors }}

上述代码相当于:

{% if form.name.errors %}
    <ul class="errorlist">
    {% for error in form.name.errors %}
        <li>{{ error }}</li>
    {% endfor %}
    </ul>
{% endif %}

同样,我们可以使用以下快捷方式显示非字段错误:

{{ form.non_field_errors }}

上述代码相当于:

{% if form.non_field_errors %}
    <ul class="errorlist">
        {% for error in form.non_field_errors %}
            <li>{{ error }}</li>
        {% endfor %}
    </ul>
{% endif %}

打开add_lang.html模板,修改文件使用这些快捷方式,如下所示:

djangobin/django _ project/djangobin/templates/djangobin/add _ lang . html

{# ... #}
    {% if messages %}
        <ul class="messages">
            {% for message in messages %}
                <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    {{ form.non_field_errors }}

    <form action="" method="post">
        {% csrf_token %}
        <table>
            <tr>
                <th><label for="id_name">Name:</label></th>
                <td>
                    {{ form.name.errors }}
                    <input type="text" name="name" required maxlength="100" id="id_name" />
                </td>
            </tr>
            <tr>
                <th><label for="id_lang_code">Language Code:</label></th>
                <td>
                    {{ form.lang_code.errors }}
                    <input type="text" name="lang_code" required maxlength="100" id="id_lang_code" />
                </td>
            </tr>
            <tr>
                <th><label for="id_slug">Slug:</label></th>
                <td>
                    {{ form.slug.errors }}
                    <input type="text" name="slug" required maxlength="100" id="id_slug" />
                </td>
            </tr>
            <tr>
                <th><label for="id_mime">Mime:</label></th>
                <td>
                    {{ form.mime.errors }}
                    <input type="text" name="mime" required maxlength="100" id="id_mime" />
                </td>
            </tr>
            <tr>
                <td></td>
                <td><input type="submit"></td>
            </tr>
        </table>
    </form>
{# ... #}

访问http://127.0.0.1:8000/add-lang/并点击提交按钮,无需输入任何内容,此时您应该会在每个字段上方看到This field is required.错误信息。

填充字段值

我们表单中最大的可用性问题是,在绑定状态下,它不显示用户在之前的请求中提交的任何数据。例如,在name字段输入django,点击提交。您应该会看到这样一个表单:

注意name字段中没有数据。Django 在变量form.field_name.value中提供了绑定字段值。因此要在name字段中显示数据,请将相应<input>元素的value属性设置为{{ form.name.value }}

但是有一个小问题。如果表单处于未绑定状态,{{ form.name.value }}将打印None。因此,在显示任何内容之前,您必须始终使用{% if %}标记首先检查form.name.value变量中值的存在。例如:

{% if form.name.value %}
    <input type="text" id="id_name" name="name" value="{{ form.name.value }}">
{% else %}
    <input type="text" id="id_name" name="name">
{% endif %}

打开add-lang.html,修改表单如下:

djangobin/django _ project/djangobin/templates/djangobin/add _ lang . html

{# ... #}
<form action="" method="post">
    {% csrf_token %}
    <table>
        <tr>
            <td><label for="id_name">Name</label></td>
            <td>
                {{ form.name.errors }}
                {% if form.name.value %}
                    <input type="text" id="id_name" name="name" value="{{ form.name.value }}">
                {% else %}
                    <input type="text" id="id_name" name="name">
                {% endif %}
            </td>
        </tr>
{# ... #}

再次访问http://127.0.0.1:8000/add-lang/,在name字段输入"django",点击提交。

不出所料,我们的绑定表单在name字段中预填充了数据。

Django 还提供了{{ form.field_name }}快捷方式,可以在绑定和未绑定状态下输出整个表单域。换句话说,{{ form.name }}和:

{% if form.name.value %}
    <input type="text" id="id_name" name="name" value="{{ form.name.value }}">
{% else %}
    <input type="text" id="id_name" name="name">
{% endif %}

打开add_lang.html并修改文件以使用该新快捷方式,如下所示:

djangobin/django _ project/djangobin/templates/djangobin/add _ lang . html

{# ... #}
    {{ form.non_field_errors }}

    <form action="" method="post">
        {% csrf_token %}
        <table>
            <tr>
                <th>
                    <label for="id_name">Name:</label>
                </th>
                <td>
                    {{ form.name.errors }}
                    {{ form.name }}
                </td>
            </tr>
            <tr>
                <th>
                    <label for="id_lang_code">Language Code:</label>
                </th>
                <td>
                    {{ form.lang_code.errors }}
                    {{ form.lang_code }}
                </td>
            </tr>
            <tr>
                <th>
                    <label for="id_slug">Slug:</label>
                </th>
                <td>
                    {{ form.slug.errors }}
                    {{ form.slug }}
                </td>
            </tr>
            <tr>
                <th>
                    <label for="id_mime">Mime:</label>
                </th>
                <td>
                    {{ form.mime.errors }}
                    {{ form.mime }}
                </td>
            </tr>
            <tr>
                <td></td>
                <td><input type="submit"></td>
            </tr>
        </table>
    </form>
{# ... #}

这个快捷方式需要注意的一点是,它会生成id_fieldname形式的id值,所以如果字段的名称是name,那么id属性的值就是id_name

我们的反馈表正在按预期运行。它可以显示验证错误,也可以预先填充来自上一个请求的数据。

显示标签

Django 提供了以下两个变量来分别生成标签 id 和标签名称。

  1. form.field_name.id_for_label
  2. form.field_name.label

下面是我们如何在<label>标签中使用它们。

<label for="{{ form.name.id_for_label }}">{{ form.name.label }}</label>

这将输出:

<label for="id_name">Name</label>

就像其他字段一样,Django 提供了使用{{ form.field_name.label_tag}}变量生成完整<label>标签的快捷方式。

所以,{{ form.name.label_tag }}

相当于:

<label for="{{ form.name.id_for_label }}">{{ form.name.label }}</label>

打开add_lang.html并修改文件以使用form.field_name.label_tag变量,如下所示:

djangobin/django _ project/djangobin/templates/djangobin/add _ lang . html

{# ... #}
    <form action="" method="post">
        {% csrf_token %}
        <table>
            <tr>
                <th>{{ form.name.label_tag }}</th>
                <td>
                    {{ form.name.errors }}
                    {{ form.name }}
                </td>
            </tr>
            <tr>
                <th>{{ form.lang_code.label_tag }}</th>
                <td>
                    {{ form.lang_code.errors }}
                    {{ form.lang_code }}
                </td>
            </tr>
            <tr>
                <th>{{ form.slug.label_tag }}</th>
                <td>
                    {{ form.slug.errors }}
                    {{ form.slug }}
                </td>
            </tr>
            <tr>
                <th>{{ form.mime.label_tag }}</th>
                <td>
                    {{ form.mime.errors }}
                    {{ form.mime }}
                </td>
            </tr>
            <tr>
                <td></td>
                <td><input type="submit"></td>
            </tr>
        </table>
    </form>
{# ... #}

打印帮助 _ 文本

如果help_text属性是为模型或表单类中的字段定义的,我们也可以打印它的值。要打印help_text我们使用{{ form.field_name.help_text }}变量。

回想一下,我们已经为Language模型中的mime字段定义了help_text属性,如下所示:

djangobin/django _ project/djangobin/models . py

#...
class Language(models.Model):
    name = models.CharField(max_length=100)
    lang_code = models.CharField(max_length=100, unique=True, verbose_name='Language Code')
    slug = models.SlugField(max_length=100, unique=True)
    mime = models.CharField(max_length=100, help_text='MIME to use when sending snippet as file.')
    #...

再次打开add_lang.html并在{{ form.name }}正下方添加{{ form.name.help_text }},如下所示:

djangobin/django _ project/djangobin/templates/djangobin/add _ lang . html

...
<form action="" method="post">
    {% csrf_token %}
    <table>
        ...
        <tr>
            <th>{{ form.mime.label_tag }}</th>
                <td>
                    {{ form.mime.errors }}
                    {{ form.mime }}
                    {{ form.mime.help_text }}
                </td>
            </tr>
            <tr>
                <td></td>
                <td><input type="submit"></td>
            </tr>
    </table>  
</form>
...

参观http://127.0.0.1:8000/add-lang/你会看到mime场旁边的help_text如下。

在表单域上循环

Django 还有一个锦囊妙计,可以让你进一步缩短代码。我们可以使用下面的代码简单地循环表单字段,而不是手动键入单个字段。

{# ... #}
<form action="" method="post">
            {% csrf_token %}
            <table>
                {% for field in form %}
                    <tr>
                        <td>{{ field.label_tag }}</td>
                        <td>
                            {{ field.errors }}
                            {{ field }}                            
                        </td>
                        <td>
                            {% if field.help_text %}
                            {{ field.help_text }}
                            {% endif %}
                        </td>
                    </tr>
                {% endfor %}
                    <tr>
                        <td></td>
                        <td><input type="submit" value="Submit"></td>
                    </tr>
            </table>
        </form>    
{# ... #}

像往常一样,这个方法不输出<form>标签和提交按钮(<input type="submit">),你必须在代码中手动添加它们。

如您所见,在您键入的代码量和您得到的控制之间有一个权衡。代码越少,我们的控制力就越弱。在为 djangobin 应用创建表单时,我们将使用我们在这里学到的一些变量。



在 Django 中处理静态内容

原文:https://overiq.com/django-1-11/handling-static-content-in-django/

最后更新于 2020 年 7 月 27 日


在这个阶段,我们的网站看起来非常简单,因为我们还没有向其中添加任何图像、CSS 和 JavaScript。在 Django,我们称这些文件为静态文件,因为它们不经常变化。在本课中,我们将学习如何在 Django 中使用静态文件。

静态文件配置

  1. Django 提供了一个名为staticfiles的内置应用来管理静态文件。第一步是确定你已经把'django.contrib.staticfiles'列入了INSTALLED_APPS名单。如果'django.contrib.staticfiles'没有列出,现在就添加到INSTALLED_APPS列表中。此时,INSTALLED_APPS的设置应该是这样的:

    djangobin/django _ project/django _ project/settings . py

    #...
    
    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'djangobin',        
    ]
    
    #...
    
    
  2. 就像模板一样,Django 会自动在每个安装的应用的static目录中搜索静态文件。在 djangobin 应用中创建一个名为static的新目录。在这个static目录中创建另一个名为djangobin的目录。回想一下,为了解决名称冲突,我们遵循了与创建templates目录时相同的惯例

    为了方便管理文件,在static/djangobin目录中创建三个名为cssjsimages的新目录。这是我们存储静态资产的地方。

    您也可以使用STATICFILES_DIR设置指定其他目录来搜索静态文件。例如:

    STATICFILES_DIR  = [
        os.path.join(BASE_DIR, 'static-dir1'),
        '/opt/dir2',
    ]
    
    

    但是,我们的项目不够大,无法将静态文件存储在多个目录中,因此我们不打算定义此设置。

  3. STATIC_URL设置定义访问静态文件的基本 URL。默认设置为/static/。(斜线很重要)。这意味着存储在djangobin/static/djangobimg/中的图像文件logo.png可以通过网址http://127.0.0.1:8000/static/djangobimg/logo.png访问。

    如果我们将STATIC_URL设置为'static-assets',那么logo.jpg将在http://127.0.0.1:8000/static-assets/djangobimg/logo.png可用。

下载静态文件

有了我们的目录,我们就可以提供静态内容了。下载这个 zip 文件,将其解压缩,并将所有cssjs文件放入适当的目录,如下所示:

djangobin/  <----- djangobin app directory
├── static
│   └── djangobin
│       ├── css
│       │   ├── all.css
│       │   ├── bootstrap.min.css
│       │   ├── bootstrap-select.min.css
│       │   ├── default.css
│       │   └── main.css
│       ├── images
│       └── js
│           ├── bootstrap.min.js
│           ├── bootstrap-select.min.js
│           └── jquery.js
│

加载静态文件

我们使用静态标签{% static 'path/to/file' %},来加载静态文件,但是在我们使用这个标签之前,我们必须在我们的模板中使用下面的代码来加载它。

{% load static %}

从全网站templates目录(即djangobin/templates/)打开base.html,修改如下:

决哥/决哥 _ project/决哥/样板/决哥/base.html

<!DOCTYPE html>
{% load static %}
<html>
<head>
    <title>{% block title %}Default Title{% endblock %}</title>
    <link rel="stylesheet" href="{% static 'djangobin/css/bootstrap.min.css' %}">
    <script src="{% static 'djangobin/js/jquery.js' %}"></script>
</head>
<body>
{# ... #}

保存文件并访问http://localhost:8000/add-lang/查看更改。

要检查网站是否正确加载了所有文件,请在谷歌浏览器中点击 Ctrl+Shift+J 打开开发者工具。如果控制台屏幕是空的,那么这意味着所有文件都加载良好。另一方面,如果有问题,您将在控制台中得到如下 404 错误:

如果是这种情况,请确保您已经下载了所有的资产,并按照指示将它们放在正确的目录中。另外,请仔细检查base.html文件中静态标签中指定的路径。

设置 STATIC_ROOT

目前,我们正在使用 Django 开发服务器提供静态文件。出于性能和安全原因,在生产中,我们将使用 Nginx 来服务静态文件。

为了实现这一点,我们需要设置另一个名为STATIC_ROOT的设置。打开settings.py文件后添加STATIC_ROOT设置如下:

djangobin/django _ project/django _ project/settings . py

#...
STATIC_URL = '/static/'

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

接下来,在项目根目录中创建一个名为staticfiles的目录。

STATIC_ROOT指定存储项目所有静态文件副本的位置。

要将所有文件收集到STATIC_ROOT中,我们使用./manage.py collectstatic命令。当我们准备部署我们的应用时,我们执行这个命令。由于我们仍处于开发阶段,我们将把它的执行推迟到第章在 Django的部署。



在 Django 中处理媒体文件

原文:https://overiq.com/django-1-11/handling-media-files-in-django/

最后更新于 2020 年 7 月 27 日


在 Django,用户上传的文件被称为媒体文件。以下是媒体文件的一些示例:

  • 一个用户上传了图片,pdf,ppt 等。
  • 电子商务网站上的产品图片。
  • 个人资料图像。等等

就像静态文件一样,为了提供媒体文件,我们必须在settings.py文件中添加一些配置。

媒体文件配置

媒体文件取决于两种配置:

  1. MEDIA_ROOT
  2. MEDIA_URL

这些变量都不是默认设置的。

媒体根设置

它包含将上传媒体文件的文件系统的绝对路径。它接受字符串,而不是列表或元组。

在 Django 项目根目录(djangobin/django_project)内新建一个名为media的目录,与manage.py所在的位置相同。

django_project/
├── db.sqlite3
├── djangobin
├── django_project
├── manage.py
├── media           <------------
├── staticfiles
└── templates

5 directories, 2 files

打开settings.py文件,在文件末尾添加以下代码,就在我们之前设置的STATIC_ROOT设置下面。

djangobin/django _ project/django _ project/settings . py

#...
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfies')

MEDIA_ROOT  = os.path.join(BASE_DIR, 'media')

媒体 _ 网址设置

该设置的工作原理类似于STATIC_URL,用于访问媒体文件。就在MEDIA_ROOT变量下面,定义MEDIA_ROOT变量如下:

djangobin/django _ project/django _ project/settings . py

#...
MEDIA_ROOT  = os.path.join(BASE_DIR, 'media')

MEDIA_URL = '/media/'

下载这个的图片,保存为media目录下的python.png。要访问文件,请访问http://127.0.0.1:8000/media/python.png。您将得到一个 HTTP 404 错误,如下所示:

但是为什么呢?

问题是 Django 开发服务器默认不提供媒体文件。为了让 Django 开发服务器提供静态服务,我们必须在 sitewide urls.py文件中添加一个 URL 模式。

在 Django 项目配置目录(djangobin/django_project/django_project)中打开urls.py,更新如下:

djangobin/django _ project/django _ project/URLs . py

from django.conf.urls import url, include
from django.contrib import admin
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    url(r'^', include('djangobin.urls', namespace='djangobin')),
    url(r'^admin/', admin.site.urls),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

现在再次访问http://127.0.0.1:8000/media/python.png,这次你应该可以看到图像了。

就像静态文件一样,在生产中,您应该始终使用真实的 web 服务器来提供媒体文件。不幸的是,我们的项目不依赖于媒体文件。然而,随着目录结构的布局,实现需要媒体文件的功能并不需要太多时间。作为一个挑战,在完成这个系列之后,尝试自己实现这样的特性。



构建 djangobin——第一步

原文:https://overiq.com/django-1-11/building-djangobin-the-first-steps/

最后更新于 2020 年 7 月 27 日


在前几章中,我们已经了解了很多关于 Django 的知识。在本章中,我们将开始构建 djangobin 应用。

创建片段

让我们从构建一个允许用户提交新代码片段的表单开始。

打开 djangobin 应用的forms.py文件。此时,它应该是这样的:

djangobin/django _ project/djangobin/forms . py

from django import forms
from django.core.exceptions import ValidationError
from .models import Language

class LanguageForm(forms.ModelForm):
    class Meta:
        model = Language
        fields = '__all__'

    def clean_name(self):
        name = self.cleaned_data['name']
        if name == 'djangobin' or name == 'DJANGOBIN':
            raise ValidationError("name can't be {}.".format(name))

        # Always return the data
        return name

    def clean_slug(self):
        return self.cleaned_data['slug'].lower()

    def clean(self):
        cleaned_data = super(LanguageForm, self).clean()
        slug = cleaned_data.get('slug')
        mime = cleaned_data.get('mime')

        if slug == mime:
            raise ValidationError("Slug and MIME shouldn't be same.")

        # Always return the data
        return cleaned_data

删除所有内容并输入以下代码:

djangobin/django _ project/djangobin/forms . py

from django import forms
from django.core.exceptions import ValidationError
from .models import Snippet, Language, Author
from .utils import Preference, get_current_user

class SnippetForm(forms.ModelForm):

    class Meta:
        model = Snippet
        fields = ('original_code', 'language', 'expiration', 'exposure', 'title', 'tags')
        widgets = {
            'original_code': forms.Textarea(attrs={'class': 'form-control', 'rows': '10',
                                                        'spellcheck': 'false'}),
            'language': forms.Select(attrs={'class': 'selectpicker foo form-control',
                                            'data-live-search': 'true',
                                            'data-size': '5'}),
            'expiration': forms.Select(attrs={'class': 'selectpicker form-control'}),
            'exposure': forms.Select(attrs={'class': 'selectpicker form-control'}),
            'title': forms.TextInput(attrs={'class': 'selectpicker form-control',
                                            'placeholder': 'Enter Title (optional)'}),            
        }

    def save(self, request):
        # get the Snippet object, without saving it into the database
        snippet = super(SnippetForm, self).save(commit=False)
        snippet.user = get_current_user(request)
        snippet.save()
        return snippet

这里我们定义了一个继承自forms.ModelFormSnippetForm类。Meta类的model属性将SnippetForm连接到Snippet模型,fields属性指定了您想要在表单中显示的模型字段列表。

在第 12 行,我们使用内部Meta类的widgets属性向模型字段添加引导 CSS 类和其他属性。

在第 24-29 行,我们覆盖了ModelForm类的save()方法。save()方法以request为参数,这样登录的用户就可以在方法内部访问。在第 26 行,我们用commit=True调用父类的save()方法。默认情况下,ModelFormsave()方法创建表单连接到的模型实例,将其保存到数据库并返回。如果用commit=True调用save()方法,那么它只创建并返回模型实例,而不将其保存到数据库中。当我们想要在保存或设置一些附加数据之前修改对象时,我们通常会这样做,这是我们接下来要做的。

在第 27 行,我们用request参数调用get_current_user()函数。get_current_user()是 djangobin 应用的utils.py文件中定义的实用功能,如下所示:

决哥/决哥 _ 项目/决哥/utils.py】

from django.contrib.auth.models import User

class Preference:
    #...

def get_current_user(request):
    if request.user.is_authenticated:
        return request.user
    else:
       return User.objects.filter(username='guest')[0]

一个Snippet模型与User模型有一对多的关系。因此,Snippet对象必须与User对象相关联。如果用户在登录后创建代码片段,那么我们希望将代码片段分配给该用户。否则,我们希望将代码片段分配给来宾用户。这本质上就是get_current_user()的功能。如果用户已登录,则get_current_user()返回该用户的实例。否则,它返回一个来宾用户的实例,该实例只是一个User对象,其usernameguest

一旦设置了用户,我们保存Snippet对象并返回。

启动 Django shell 并创建一个新的guest用户,如下所示:

>>>
>>> from django.contrib.auth.models import User
>>> 
>>> User.objects.create_user(
...  username='guest',
...  email='guest@overiq.com',
...  password='password'
... )
<User: guest>
>>>

接下来,打开views.py并在文件顶部添加修改index()查看功能,如下所示:

djangobin/django_project/djangobin/views.py

from django.shortcuts import HttpResponse, render, redirect, get_object_or_404, reverse
from django.contrib import messages
from .forms import SnippetForm
from .models import Language

def index(request):        
    if request.method ==  'POST':
        f = SnippetForm(request.POST)

        if f.is_valid():
            snippet = f.save(request)            
            return redirect(reverse('djangobin:snippet_detail', args=[snippet.slug]))

    else:
        f = SnippetForm()
    return render(request, 'djangobin/index.html', {'form': f})

def snippet_detail(request, snippet_slug):
    #...

该视图功能显示SnippetForm表单,并将提交的片段保存到数据库。

在此之前,我们进入下一部分,删除add_langupdate_lang网址模式,并查看与之相关的功能。

Django 宾的基本模板

接下来,让我们为 djangobin 应用设置一个基础模板。在 djangobin 应用的templates/目录中创建一个名为base.html的模板,代码如下:

决哥/决哥 _ project/决哥/样板/决哥/base.html

<!DOCTYPE html>
<html lang="en">
<head>

    {% load static %}

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block title %}Djangobin{% endblock %}</title>

    <!-- Bootstrap -->
    <link rel="stylesheet" href="{% static 'djangobin/css/bootstrap.min.css' %}" >

    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="{% static 'djangobin/css/bootstrap-select.min.css' %}" >

    <link href="https://use.fontawesome.com/releases/v5.0.7/css/all.css" rel="stylesheet">

    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
    <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
    <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->

    <link rel="stylesheet" href="{% static 'djangobin/css/main.css' %}">
    <link rel="stylesheet" href="{% static 'djangobin/css/default.css' %}">
</head>
<body>

<nav class="navbar navbar-default navbar-inverse navbar-fixed-top">
    <div class="container">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed"
                    data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"
                    aria-expanded="false">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="{% url 'djangobin:index' %}">DjangoBin</a>
        </div>

        <!-- Collect the nav links, forms, and other content for toggling -->
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
            <ul class="nav navbar-nav">
                <li {% if request.path == '/' %}class='active'{% endif %} >
                    <a href="{% url 'djangobin:index' %}">Add new</a>
                </li>
                <li {% if request.path == '/trending/' %}class='active'{% endif %}>
                    <a href="">Trending<span class="sr-only">(current)</span></a>
                </li>
                <li {% if request.path == '/about/' %}class='active'{% endif %}>
                    <a href="">About</a>
                </li>
                <li {% if request.path == '/contact/' %}class='active'{% endif %}>
                    <a href="">Contact</a>
                </li>
            </ul>

            <form action="" class="navbar-form navbar-left" method="get">
                <div class="form-group">
                    <input type="text" name="query" class="form-control" placeholder="Search" value="">
                </div>
            </form>
            <ul class="nav navbar-nav navbar-right">
                <li class="dropdown">
                    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
                       aria-haspopup="true" aria-expanded="false">
                        {% if request.user.is_authenticated %}
                            {{ request.user.username|upper }}
                        {% else %}
                            GUEST
                        {% endif %}
                        <span class="caret"></span>
                    </a>
                    {% if request.user.is_authenticated %}
                        <ul class="dropdown-menu">
                            <li><a href="">My Pastes</a></li>
                            <li><a href="">Account Details</a></li>
                            <li><a href="">Settings</a></li>
                            <li role="separator" class="divider"></li>
                            <li><a href="">Logout</a></li>
                        </ul>
                    {% else %}
                        <ul class="dropdown-menu">
                            <li><a href="">Sign Up</a></li>
                            <li><a href="">Login</a></li>
                        </ul>
                    {% endif %}
                </li>
            </ul>
        </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
</nav>

<div class="container">

    <div class="row">

        <div class="col-lg-9 col-md-9">

            {% if not request.user.is_authenticated and not request.path == '/login/'  %}
                <p class="alert alert-info">
                    <a href="" class="alert-link">Login</a> to access other cool features.
                </p>
            {% endif %}

            {% block main %}
                {#  override this block in the child template  #}
            {% endblock %}

        </div>

        <div class="col-lg-3 col-md-3 text-center hidden-sm hidden-xs">
            <p>Recent Snippets</p>

            <div class="list-group">
                <a href="#" class="list-group-item">
                    <h5 class="list-group-item-heading"><span class="fas fa-globe"
                        "></span> Untitled (css)</h5>
                    <p class="list-group-item-text">21 sec ago.</p>
                </a>

                <a href="#" class="list-group-item">
                    <h5 class="list-group-item-heading"><span class="fas fa-globe"
                        "></span> Untitled (c++)</h5>
                    <p class="list-group-item-text">21 sec ago.</p>
                </a>
                <a href="#" class="list-group-item">
                    <h5 class="list-group-item-heading"><span class="fas fa-globe"
                        "></span> Login View in Django</h5>
                    <p class="list-group-item-text">21 sec ago.</p>
                </a>
                <a href="#" class="list-group-item">
                    <h5 class="list-group-item-heading"><span class="fas fa-globe"
                        "></span> CBVs In Django...</h5>
                    <p class="list-group-item-text">21 sec ago.</p>
                </a>
                <a href="#" class="list-group-item">
                    <h5 class="list-group-item-heading"><span class="fas fa-globe"
                        "></span> Untitled</h5>
                    <p class="list-group-item-text">21 sec ago.</p>
                </a>
                <a href="#" class="list-group-item">
                    <h5 class="list-group-item-heading"><span class="fas fa-globe"
                        "></span> Untitled</h5>
                    <p class="list-group-item-text">21 sec ago.</p>
                </a>
                <a href="#" class="list-group-item">
                    <h5 class="list-group-item-heading"><span class="fas fa-globe"
                        "></span> Untitled</h5>
                    <p class="list-group-item-text">21 sec ago.</p>
                </a>
                <a href="#" class="list-group-item">
                    <h5 class="list-group-item-heading"><span class="fas fa-globe"
                        "></span> Untitled</h5>
                    <p class="list-group-item-text">21 sec ago.</p>
                </a>
            </div>

        </div>
    </div>
</div>

<hr>

<footer>
    <div class="social-icons">
        <div class="container text-center">
            <ul class="list-inline">
                <li class="list-inline-item social-github">
                    <a href="https://github.com/">
                        <i class="fab fa-github"></i>
                    </a>
                </li>
                <li class="list-inline-item social-twitter">
                    <a href="https://twitter.com/">
                        <i class="fab fa-twitter-square"></i>
                    </a>
                </li>
                <li class="list-inline-item social-facebook">
                    <a href="https://www.facebook.com/">
                        <i class="fab fa-facebook-square"></i>
                    </a>
                </li>
                <li class="list-inline-item social-google-plus">
                    <a href="https://plus.google.com/">
                        <i class="fab fa-google-plus-g"></i>
                    </a>
                </li>
            </ul>
        </div>
    </div>

    <div class="main-footer">
        <div class="container text-center">
            <ul>
                <li><a href="#">Source Code</a></li>
                <li><a href="#">OverIQ</a></li>
                <li><a href="#">Contact</a></li>
                <li><a href="#">Other Tutorials</a></li>
            </ul>
        </div>
    </div>
</footer>

<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src={% static "djangobin/js/jquery.js" %}></script>

<!-- Include all compiled plugins (below), or include individual files as needed -->
<!-- Latest compiled and minified JavaScript -->
<script src={% static "djangobin/js/bootstrap.min.js" %}></script>

<!-- Latest compiled and minified JavaScript -->
<script src={% static "djangobin/js/bootstrap-select.min.js" %}></script>

<script>
    $(function () {
        $('[data-toggle="tooltip"]').tooltip()
    })

</script>

</body>
</html>

大部分代码应该是直接的。但我们仍然会仔细检查,只是为了确保你明白一切。

回想一下,在模板内部,您总是可以访问request变量。我们使用这个变量来访问当前 web 请求和登录用户的详细信息。

在第 5 行,我们使用{% load %}标签加载{% static %}标签。

在第 10 行,我们定义了一个名为title的块。从该模板继承的模板可以用内容填充块。

在第 27-28 行,我们使用{% static %}标签为 CSS 文件构建 URL。

在第 50-61 行中,我们使用几个{% if %}标签将一类active添加到相应的<li>元素中,以突出显示菜单中的当前选项。

在第 73-77 行,我们测试用户是否登录。如果是,我们在应用upper过滤器后显示登录的用户名。如果用户没有登录,我们显示GUEST

在第 80-93 行,我们再次测试用户是否登录。如果是这样,我们会显示一些与个人资料相关的链接,如我的贴、设置、注销等。否则,我们会显示登录和注册页面的链接。

在第 107-111 行,我们显示了一个登录页面的链接,只有当用户没有登录并且request.path不等于/login/时。

在第 114 行,我们定义了一个名为main的块。该块的内容将由子模板提供。

在第 214-221 行,我们再次使用{% static %}标签来构建到 JavaScript 文件的链接。

你可能已经注意到了,大部分<a>元素的href属性是空的。我们将继续更新它们。

现在,我们在templates目录
中创建一个名为index.html的子模板,代码如下:

djangobin/django _ project/djangobin/templates/djangobin/index . html

{% extends 'djangobin/base.html' %}

{% load static %}

{% block main %}

    <form action="" class="form-horizontal" method="post">

        {% csrf_token %}

        {{ form.original_code.errors }}

        {{ form.original_code }}

        <hr>

        <div class="form-group">
            <label for="{{ form.language.id_for_label }}" class="col-sm-2 control-label">Language</label>
            <div class="col-lg-10 col-md-10 col-sm-10">
                {{ form.language }}
            </div>
        </div>

        {{ form.expiration.errors }}

        <div class="form-group">
            <label for="{{ form.expiration.id_for_label }}" class="col-sm-2  control-label">Expiration</label>
            <div class="col-lg-10 col-md-10 col-sm-10">
                {{ form.expiration }}
            </div>
        </div>

        {{ form.exposure.errors }}

        <div class="form-group">
            <label for="{{ form.exposure.id_for_label }}" class="col-sm-2  control-label">Exposure</label>
            <div class="col-lg-10 col-md-10 col-sm-10">
                {{ form.exposure }}
            </div>
        </div>

        <div class="form-group">
            <label for="{{ form.title.id_for_label }}" class="col-sm-2  control-label">Title</label>
            <div class="col-lg-10 col-md-10 col-sm-10">
                {{ form.title }}
            </div>
        </div>

        <div class="form-group">
            <label for="{{ form.tags.id_for_label }}" class="col-sm-2  control-label">Tags</label>
            <div class="col-lg-10 col-md-10 col-sm-10">
                {{ form.tags }}
            </div>
        </div>

        <div class="form-group">
            <label for="inputEmail3" class="col-sm-2 control-label"></label>
            <div class="col-lg-10 col-md-10 col-sm-10">
                <button type="submit" class="btn btn-primary">Create</button>
            </div>
        </div>

    </form>

{% endblock %}

这个模板中没有值得解释的新内容。我们只是在利用我们到目前为止学到的东西。如果您需要复习 Django 模板章节的基础知识。

如果你现在访问http://localhost:8000/。您将看到这样的页面:

页面显示正确,但有一个问题。

仔细看看Tags场是如何渲染的:

由于Snippet模型与Tag模型具有多对多的关系,因此使用多重选择<select>框显示标签。这意味着您只能选择数据库中已经存在的标签。

不可能预料到用户将用来创建片段的所有标签。因此,更好的方法是让用户在创建代码片段时指定一个逗号分隔的标签列表。为此,我们必须在SnippetForm类中定义一个新的表单字段。

forms.py中,定义Meta类上方的snippet_tag字段,如下所示:

djangobin/django _ project/djangobin/forms . py

#...
class SnippetForm(forms.ModelForm):

    snippet_tags = forms.CharField(required=False,
                           widget=forms.TextInput(attrs={
                               'class': 'selectpicker form-control',
                               'placeholder': 'Enter tags (optional)'
                            }))

    class Meta:
        model = Snippet
#...

另外,从Meta类的fields属性中移除tags字段。

djangobin/django _ project/djangobin/forms . py

#...
    class Meta:
        model = Snippet
        fields = ('original_code', 'language', 'expiration', 'exposure', 'title',)
        widgets = {
#...

为了融入这些变化,我们现在必须更新SnippetForm类的save()方法。

djangobin/django _ project/djangobin/forms . py

from django import forms
from django.core.exceptions import ValidationError
from .models import Snippet, Language, Author, Tag
from .utils import Preference, get_current_user

class SnippetForm(forms.ModelForm):
    #...

    def save(self, request):
        snippet = super(SnippetForm, self).save(commit=False)
        snippet.user = get_current_user(request)
        snippet.save()
        tag_list = [tag.strip().lower() 
                   for tag in self.cleaned_data['snippet_tags'].split(',') if tag ]
        if len(tag_list) > 0:
            for tag in tag_list:
                t = Tag.objects.get_or_create(name=tag)
                snippet.tags.add(t[0])
        return snippet

在第 14 行,我们使用列表理解和表单对象的cleaned_data属性来创建提交标签的列表。

如果tag_list不为空,我们使用 for 循环对其进行循环。在 for 循环中,我们创建Tag对象(仅当它不存在时)并将其与Snippet对象相关联。

需要注意的是,SnippetFormsnippet_tags场是一个全新的场,与Snippet模型的tags场没有任何关系。

最后,更新index.html使用snippets_tags字段,如下所示:

djangobin/django _ project/djangobin/templates/djangobin/index . html

{# ... #}
        <div class="form-group">
            <label for="{{ form.title.id_for_label }}" class="col-sm-2  control-label">Title</label>
            <div class="col-lg-10 col-md-10 col-sm-10">
                {{ form.title }}
            </div>
        </div>

        <div class="form-group">
            <label for="{{ form.snippet_tags.id_for_label }}" class="col-sm-2  control-label">Tags</label>
            <div class="col-lg-10 col-md-10 col-sm-10">
                {{ form.snippet_tags }}
            </div>
        </div>

        <div class="form-group">
            <label for="inputEmail3" class="col-sm-2 control-label"></label>
            <div class="col-lg-10 col-md-10 col-sm-10">
                <button type="submit" class="btn btn-primary">Create</button>
            </div>
        </div>
{# ... #}

我们现在可以在创建片段时指定逗号分隔的标签列表。
在下一节中,我们将创建页面来显示高亮显示的片段。

显示代码片段

views.py文件中,修改snippet_detail()视图,就在index()视图的下方功能如下:

djangobin/django_project/djangobin/views.py

#...
from .models import Language, Snippet

#...

def snippet_detail(request, snippet_slug):
    snippet = get_object_or_404(Snippet, slug=snippet_slug)
    snippet.hits += 1
    snippet.save()
    return render(request, 'djangobin/snippet_detail.html', {'snippet': snippet})

该视图函数显示高亮显示的代码片段,并将点击次数增加 1。

templates目录中创建新模板snippet_detail.html,代码如下:

djangobin/django _ project/djangobin/templates/djangobin/snippet _ detail . html

{% extends 'djangobin/base.html' %}

{% load static %}

{% block main %}

    <div class="media post-meta">
        <div class="media-left">
            <a href="#">
                <img alt="64x64" class="media-object" data-src="holder.js/64x64" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+PCEtLQpTb3VyY2UgVVJMOiBob2xkZXIuanMvNjR4NjQKQ3JlYXRlZCB3aXRoIEhvbGRlci5qcyAyLjYuMC4KTGVhcm4gbW9yZSBhdCBodHRwOi8vaG9sZGVyanMuY29tCihjKSAyMDEyLTIwMTUgSXZhbiBNYWxvcGluc2t5IC0gaHR0cDovL2ltc2t5LmNvCi0tPjxkZWZzPjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+PCFbQ0RBVEFbI2hvbGRlcl8xNjFkYzE3YWQ0ZiB0ZXh0IHsgZmlsbDojQUFBQUFBO2ZvbnQtd2VpZ2h0OmJvbGQ7Zm9udC1mYW1pbHk6QXJpYWwsIEhlbHZldGljYSwgT3BlbiBTYW5zLCBzYW5zLXNlcmlmLCBtb25vc3BhY2U7Zm9udC1zaXplOjEwcHQgfSBdXT48L3N0eWxlPjwvZGVmcz48ZyBpZD0iaG9sZGVyXzE2MWRjMTdhZDRmIj48cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIGZpbGw9IiNFRUVFRUUiLz48Zz48dGV4dCB4PSIxMi4xNzk2ODc1IiB5PSIzNi41Ij42NHg2NDwvdGV4dD48L2c+PC9nPjwvc3ZnPg==" data-holder-rendered="true" style="width: 64px; height: 64px;">
            </a>
        </div>
        <div class="media-body">
            <h4 class="media-heading">{{ snippet.title|default:"Untitled" }}</h4>
            <p>
                <i class="fas fa-user" data-toggle="tooltip" title="" data-original-title="Paste creator"></i> by
                {{ snippet.user.username|capfirst }} &nbsp;
                <i class="fas fa-calendar-alt" data-toggle="tooltip" title="" data-original-title="Creation Date" ></i>
                <time title="{{ snippet.created_on }}">{{ snippet.created_on|date:"M jS,  Y" }}</time> &nbsp;</span>
                <i class="fas fa-eye"  data-toggle="tooltip" title="" data-original-title="Visits to this paste" ></i>
                {{ snippet.hits }} &nbsp;&nbsp;
                <i class="fas fa-stopwatch" data-toggle="tooltip" title="" data-original-title="Expiration time"></i>
                {{ snippet.expiration }}  &nbsp;
                {% if snippet.tags.all %}
                    <i class="fas fa-tags" data-toggle="tooltip" title="" data-original-title="Tags"></i>
                    {% for tag in snippet.tags.all %}
                        <a href="">{{ tag }}</a>{% if not forloop.last %},{% endif %}
                    {% endfor %}
                {% endif %}
            </p>
        </div>

    </div>

    <div class="codeblock">
        <div class="toolbar clearfix">
            <span class="at-left"><a href="">{{ snippet.language }}</a></span>
            <span class="at-right">
                <a onclick="return confirm('Sure you want to delete this paste? ')" href="">delete</a>
                <a href="">raw</a>
                <a href="">download</a>
            </span>
        </div>
        <div class="code-wrapper">{{ snippet.highlighted_code|safe }}</div>
    </div>

{% endblock %}

在第 13-31 行,我们显示了代码片段的元数据,在第 44 行,我们显示了高亮显示的代码。

现在访问http://localhost:8000/并尝试创建一个片段。您将获得如下代码片段详细信息页面:

下载代码片段

就在snippet_detail()视图下方,定义download_snippet()视图功能如下:

#...

def download_snippet(request, snippet_slug):
    snippet = get_object_or_404(Snippet, slug=snippet_slug)
    file_extension = snippet.language.file_extension
    filename = snippet.slug + file_extension
    res = HttpResponse(snippet.original_code)
    res['content-disposition'] = 'attachment; filename=' + filename + ";"
    return res

这个视图函数从数据库中检索代码片段,并将其发送给客户端。

请注意,我们正在创建HttpResponse实例之后设置一个名为Content-Disposition的附加标题。Content-Disposition标题告诉浏览器将响应保存为附件,而不是显示。

接下来,打开 djangobin 应用的urls.py并添加一个名为download_snippet的新 URL 模式,如下所示:

决哥/决哥 _ 项目/决哥/URL . py】

#...

urlpatterns = [
    #...
    url('^trending/$', views.trending_snippets, name='trending_snippets'),
    url('^trending/(?P<language_slug>[\w]+)/$', views.trending_snippets, name='trending_snippets'),
    url('^(?P<snippet_slug>[\d]+)/$', views.snippet_detail, name='snippet_detail'),
    url('^tag/(?P<tag>[\w-]+)/$', views.tag_list, name='tag_list'),
    url('^download/(?P<snippet_slug>[\d]+)/$', views.download_snippet, name='download_snippet'),
]

现在,为了让用户下载代码,我们必须在代码片段详细信息页面中添加一个链接。打开snippet_detail.html并用class="codeblock"修改<div>标签,如下所示:

djangobin/django _ project/djangobin/templates/djangobin/snippet _ detail . html

{#  ...  #}
    <div class="codeblock">
        <div class="toolbar clearfix">
            <span class="at-left"><a href="">{{ snippet.language }}</a></span>
            <span class="at-right">
                <a onclick="return confirm('Sure you want to delete this paste? ')" href="">delete</a>
                <a href="">raw</a>
                <a href="{% url 'djangobin:download_snippet' snippet.slug %}">download</a>
            </span>
        </div>
        <div class="code-wrapper">{{ snippet.highlighted_code|safe }}</div>
    </div>
{#  ...  #}

访问http://localhost:8000/并创建一个新的片段。在代码片段详细信息页面中,单击“下载”链接下载代码片段。

显示原始片段

views.py中,在download_snippet()视图下方增加raw_snippet()视图功能,如下:

djangobin/django_project/djangobin/views.py

#...
def raw_snippet(request, snippet_slug):
    snippet = get_object_or_404(Snippet, slug=snippet_slug)
    return HttpResponse(snippet.original_code, content_type=snippet.language.mime)

该视图将以原始格式显示代码片段。

将名为raw_snippet的新网址模式添加到urls.py中,如下所示:

决哥/决哥 _ 项目/决哥/utils.py】

#...
urlpatterns = [
    #...
    url('^(?P<snippet_slug>[\d]+)/$', views.snippet_detail, name='snippet_detail'),
    url('^tag/(?P<tag>[\w-]+)/$', views.tag_list, name='tag_list'),
    url('^download/(?P<snippet_slug>[\d]+)/$', views.download_snippet, name='download_snippet'),
    url('^raw/(?P<snippet_slug>[\d]+)/$', views.raw_snippet, name='raw_snippet'), 
]

最后,在snippet_detail.html模板中添加一个到原始片段的链接,如下所示:

{# ... #}
<div class="codeblock">
    <div class="toolbar clearfix">
            <span class="at-left"><a href="">{{ snippet.language }}</a></span>
            <span class="at-right">
                <a onclick="return confirm('Sure you want to delete this paste? ')" href="">delete</a>
                <a href="{% url 'djangobin:raw_snippet' snippet.slug %}">raw</a>
                <a href="{% url 'djangobin:download_snippet' snippet.slug %}">download</a>
            </span>
        </div>
    <div class="code-wrapper">{{ snippet.highlighted_code|safe }}</div>
</div>
{# ... #}

现在,您可以通过单击片段详细信息页面中的“原始”链接来查看原始片段。

使用上下文处理器显示最近的片段

我们希望在 Djangobin 应用的每个页面上显示最近的公开片段。目前,最近的 Snippet 列表只是一系列硬编码的<a>标签。

首先,您可能认为我们可以通过执行以下操作来轻松显示最近的片段:

djangobin/django_project/djangobin/views.py

def snippet_detail(request, snippet_slug):
    recent_snippet = Snippet.objects.filter(exposure='public').order_by("-created_on")[:8]
    snippet = get_object_or_404(Snippet, slug=snippet_slug)
    snippet.hits += 1
    snippet.save()
    return render(request, 'djangobin/snippet_detail.html', {'snippet': snippet, 
                                                             'recent_snippet': recent_snippet})

这种方法没有错,唯一的问题是,如果我们选择走这条路线,那么我们将不得不重复获取最近的代码片段列表,并将其传递给每个视图函数中的相应模板——上下文处理器来拯救。

在课程加载模板中,我们了解到render()功能会自动使所有模板中的某些变量可用。两个这样的变量是requestmessage。包含当前请求数据的request变量和包含闪光信息的messages变量。render()功能使用名为RequestContext的东西来实现这一点。

RequestContext只是Context的一个特殊子类。RequestContextContext的不同之处在于:

  1. 它接受request作为它的第一个参数。
  2. 它基于context_processors选项自动填充模板上下文。

context_processors只是一个可调用的列表,叫做上下文处理器。每个上下文处理器都是一个接受HttpRequest对象并返回要合并到模板上下文中的项目字典的函数。默认情况下context_processors列表如下:

djangobin/django _ project/django _ project/settings . py

#...
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(BASE_DIR, 'templates'),
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
#...

requestmessage可调用的来源如下:

def request(request):
    return {'request': request}

def messages(request):
    """
    Returns a lazy 'messages' context variable.
    """
    return {
        'messages': get_messages(request),
        'DEFAULT_MESSAGE_LEVELS': DEFAULT_LEVELS,
    }

现在你知道requestmessages变量来自哪里了。

要使变量对所有模板都是全局可访问的,您必须定义一个自定义上下文处理器。

djangobin应用目录中创建一个名为context_processors.py的新文件,并向其中添加以下代码:

djangobin/django _ project/djangobin/context _ processors . py

from .models import Snippet

def recent_snippets(request):
    return dict(recent_snippets=Snippet.objects.filter(exposure='public').order_by("-id")[:8])

将此上下文处理器添加到settings.py文件中的context_processors选项,如下所示:

djangobin/django _ project/django _ project/settings . py

#...
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(BASE_DIR, 'templates'),
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'djangobin.context_processors.recent_snippets',
            ],
        },
    },
]
#...

现在,我们所有的模板都可以访问recent_snippets变量。

为了显示最近的片段,我们必须在 djangobin 应用的base.html文件中进行一些更改。打开base.html,修改如下:

决哥/决哥 _ project/决哥/样板/决哥/base.html

{# ... #}
<div class="col-lg-3 col-md-3 text-center hidden-sm hidden-xs">
            <p>Recent Snippets</p>
            <div class="list-group">

                {% for recent_snippet in recent_snippets %}
                    <a href="{{ recent_snippet.get_absolute_url }}" class="list-group-item">
                        <h5 class="list-group-item-heading"><span class="fa fa-globe
"></span> {{ recent_snippet.title }}</h5>
                        <p class="list-group-item-text">{{ recent_snippet.created_on }}</p>
                    </a>
                {% endfor %}

            </div>        

        </div>
{# ... #}

访问主页或片段详细信息页面,您将看到如下所示的最近片段列表:

人性化时间

目前,最近摘录列表以下列格式显示日期和时间:

"April 12, 2018, 7:09 a.m"

更方便用户的方法是这样显示日期和时间:

  • 两周前
  • 23 小时前
  • 10 秒前等等。

Django 自带一个名为 humanize 的内置应用,它提供模板过滤器来格式化数字、日期和时间。

默认情况下不安装人性化应用。要安装它,请将

django.contrib.humanize添加到settings.py文件中的INSTALLED_APPS列表,如下所示:

djangobin/django _ project/django _ project/settings . py

#...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.humanize',
    'djangobin',
]
#...

现在打开base.html模板,并在加载静态模板标签的线的正下方添加{% load humanize %}:

决哥/决哥 _ project/决哥/样板/决哥/base.html

<!DOCTYPE html>
<html lang="en">
<head>

    {% load static %}
    {% load humanize %}

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
{# ... #}

最后将naturaltime滤镜添加到base.html中,如下所示:

决哥/决哥 _ project/决哥/样板/决哥/base.html

{# ... #}
<div class="col-lg-3 col-md-3 text-center hidden-sm hidden-xs">
            <p>Recent Snippets</p>

            <div class="list-group">

                {% for recent_snippet in recent_snippets %}
                    <a href="{{ recent_snippet.get_absolute_url }}" class="list-group-item">
                        <h5 class="list-group-item-heading"><span class="fa fa-globe
"></span> {{ recent_snippet.title }}</h5>
                        <p class="list-group-item-text">{{ recent_snippet.created_on|naturaltime }}</p>
                    </a>
                {% endfor %}

            </div>

        </div>
{# ... #}

访问主页或代码片段详细信息页面,您将看到更新的日期和时间格式如下:



创建趋势片段页面

原文:https://overiq.com/django-1-11/creating-trending-snippet-page/

最后更新于 2020 年 7 月 27 日


我们的下一个任务是根据代码片段的点击量显示代码片段列表页面。我们希望在 URL 路径/trending/显示所有语言的趋势片段列表,在 URL 路径/trending/<language_slug>/显示特定语言的片段列表。

让我们从修改views.py文件中的trending_snippets视图功能开始,如下所示:

djangobin/django_project/djangobin/views.py

#...
def raw_snippet(request, snippet_slug):
    #...

def trending_snippets(request, language_slug=''):
    lang = None
    snippets = Snippet.objects
    if language_slug:
        snippets = snippets.filter(language__slug=language_slug)
        lang = get_object_or_404(Language, slug=language_slug)
    snippets = snippets.all()
    return render(request, 'djangobin/trending.html', {'snippets': snippets, 'lang': lang})

def tag_list(request, tag):
    #...

在 djangobin 应用的templates目录中创建trending.html模板,代码如下:

djangobin/django _ project/djangobin/templates/djangobin/trending . html

{% extends 'djangobin/base.html' %}

{% load static %}
{% load humanize %}

{% block title %}
    Trending {{ lang.name }} Snippets - {{ block.super }}
{% endblock %}

{% block main %}

    <h5><i class="fas fa-chart-line"></i> Trending {{ lang.name }} Snippets</h5>
    <hr>

    <table class="table">
        <thead>
        <tr>
            <th>Title</th>
            <th>Date</th>
            <th>Hits</th>
            <th>Language</th>
            <th>User</th>
        </tr>
        </thead>
        <tbody>

        {% for snippet in snippets %}
            <tr>
                <td><i class="fas fa-globe"></i>
                    <a href="{{ snippet.get_absolute_url }}">{{ snippet.title }}</a>
                </td>
                <td title="{{ snippet.created_on }}">{{ snippet.created_on|naturaltime }}</td>
                <td>{{ snippet.hits }}</td>
                <td><a href="{% url 'djangobin:trending_snippets' snippet.language.slug  %}">{{ snippet.language }}</a></td>
                {% if not snippet.user.profile.private %}
                    <td><a href="{{ snippet.user.profile.get_absolute_url }}">{{ snippet.user.username|title }}</a></td>
                {% else %}
                    <td>-</td>
                {% endif %}

            </tr>
        {% empty %}
            <tr class="text-center">
                <td colspan="4">There are no snippets.</td>
            </tr>
        {% endfor %}

        </tbody>
    </table>

{% endblock %}

这里有几件事需要注意:

在第 27-46 行,我们使用一个{% for %}标签来循环代码片段列表。

在第 29-34 行,我们显示片段标题、创建时间、点击量和语言名称。

在第 35-39 行,我们检查代码片段创建者概要文件是否是私有的。如果配置文件不是私有的,我们会显示用户名和用户配置文件页面的链接。另一方面,如果配置文件是私有的,我们在用户列中显示一个-(破折号)。

接下来,更新 djangobin 应用的base.html模板,以包含一个指向趋势片段页面的链接:

决哥/决哥 _ project/决哥/样板/决哥/base.html

{# ... #}
            <ul class="nav navbar-nav">
                <li {% if request.path == '/' %}class='active'{% endif %} >
                    <a href="{% url 'djangobin:index' %}">Add new</a>
                </li>
                <li {% if request.path == '/trending/' %}class='active'{% endif %}>
                    <a href="{% url 'djangobin:trending_snippets' %}">Trending<span class="sr-only">(current)</span></a>
                </li>
                <li {% if request.path == '/about/' %}class='active'{% endif %}>
                    <a href="">About</a>
                </li>
                <li {% if request.path == '/contact/' %}class='active'{% endif %}>
                    <a href="">Contact</a>
                </li>
            </ul>
{# ... #}

同样,要显示特定语言的趋势片段,请添加到snippet_detail.html模板的链接,如下所示:

djangobin/django _ project/djangobin/templates/djangobin/snippet _ detail . html

{# ... #}
    <div class="codeblock">

        <div class="toolbar clearfix">
            <span class="at-left"><a href="{% url 'djangobin:trending_snippets' snippet.language.slug %}">{{ snippet.language }}</a></span>
            <span class="at-right">
                <a onclick="return confirm('Sure you want to delete this paste? ')" href="">delete</a>
                <a href="{% url 'djangobin:raw_snippet' snippet.slug %}">raw</a>
                <a href="{% url 'djangobin:download_snippet' snippet.slug %}">download</a>
            </span>
        </div>
        <div class="code-wrapper">{{ snippet.highlighted_code|safe }}</div>
    </div>

{% endblock %}   
{# ... #}

将浏览器导航至http://127.0.0.1:8000/trending/。您将看到所有语言的趋势片段列表。

要查看特定语言的趋势片段,请单击“语言”列中的语言名称。

查看特定语言的趋势片段的另一种方法是单击片段详细信息页面中的语言名称。

无论哪种方式,您最终都会得到一个特定语言的片段列表,如下所示:

分页基础

让我们假设我们的网站变得非常受欢迎,并开始每天接受数百万次访问。

突然我们就有问题了!

就目前情况而言,我们正在趋势片段页面中同时显示所有片段。这不仅会增加页面大小,还会降低用户体验。用户将不得不费力地浏览代码片段列表,以找出他们在寻找什么。

解决方案是使用分页,将一个长列表分成多个页面。

Django 提供了一个名为Paginator的类,它允许我们创建分页记录。使用时,从django.core.paginator模块导入。

from django.core.paginator import Paginator

要使用分页,我们必须首先创建一个Paginator对象。Paginator构造函数有以下语法:

paginator_object = Paginator(object_list, records_per_page)

object_list可以是元组、列表、查询集等。records_per_page是你想在每页上显示的记录数。

下面是创建一个Paginator对象以每页显示 5 个片段的代码:

paginator = Paginator(Snippet.objects.all(), 5)

Paginator对象有以下属性和方法,在创建分页记录时经常使用。

属性/方法 描述
count count 属性返回所有页面中的记录总数。
num_pages num_pages属性返回总页数。
page(number) page()方法接受一个页码,并返回该页的一个Page实例。我们使用Page实例来访问页面中的记录。如果参数不是数字,则抛出PageNotAnInteger异常。如果指定的页面不存在,它会抛出一个EmptyPage异常。

Page对象也提供了一些有用的属性和方法。

属性/方法 描述
object_list 此页面中的对象列表。
number 1基于当前页面的数字计数。
has_next() 如果有下一页,返回True。否则False
has_previous() 如果有上一页,返回True。否则False
next_page_number() 返回下一个页码。如果下一页不存在,它抛出一个django.core.paginator.InvalidPage异常
previous_page_number() 返回上一页的页码。如果上一页不存在,它抛出一个django.core.paginator.InvalidPage异常
paginator 该属性返回附着在Page对象上的Paginated对象。

打开 Django shell,让我们尝试一下到目前为止学到的一些东西:

>>>
>>> from django.core.paginator import Paginator
>>> from djangobin.models import *
>>>
>>> p = Paginator(Snippet.objects.all(), 3)   # Creating a Paginator object to show 3 snippet per page
>>>
>>> type(p)
<class 'django.core.paginator.Paginator'>
>>>
>>> p.count      # total number of records across all the pages
10
>>>
>>> p.num_pages    # total number of pages
4
>>>

获取第一页的记录:

>>>
>>> page1 = p.page(1)   # creating Page object for the first page.
>>>>
>>> page1
<Page 1 of 4>
>>>
>>> type(page1)
<class 'django.core.paginator.Page'>
>>>
>>> page1.object_list   # get a list of posts for the first page.
<QuerySet [<Snippet: Untitled - Python>, <Snippet: Untitled - Python>, <Snippet: Untitled - Bash>]>
>>>
>>> page1.number
1
>>>
>>> page1.has_previous()
False
>>>
>>> page1.has_next()
True
>>>
>>> page1.next_page_number()
2
>>>
>>> page1.previous_page_number()
...    
django.core.paginator.EmptyPage: That page number is less than 1
>>>
Traceback (most recent call last):  

要访问Paginator对象,请使用paginator属性:

>>>
>>> page1.paginator
<django.core.paginator.Paginator object at 0x0000000004331550>
>>>

page1.paginatorp指向同一个对象:

>>>
>>> page1.paginator == p
True
>>>

一旦我们访问了Paginator对象,我们也可以访问它的属性和方法。

>>>
>>> page1.paginator.num_pages
4
>>>

同样,我们可以访问第二页的记录:

>>>
>>> page2 = p.page('2')      # creating Page object for the second page.
>>>                          # notice that we are passing a string with integer value
>>>
>>> page2.object_list
<QuerySet [<Snippet: Untitled - Python>, <Snippet: Untitled - Bash>, <Snippet: Untitled - C>]>
>>>
>>> page2.number
2
>>>
>>> page2.has_previous()
True
>>>
>>> page2.has_next()
True
>>>
>>> page2.previous_page_number()
1
>>>
>>> page2.next_page_number()
3
>>>
>>> pagea = p.page('a')     # Here we are passing string with a non integer value.
....    
django.core.paginator.PageNotAnInteger: That page number is not an integer
>>>
>>>
>>> page100 = p.page(100)    # Trying to access records for 100th page
...   
django.core.paginator.EmptyPage: That page contains no results
>>>

我希望你明白这里的意思。

一旦我们访问了Page对象,我们就可以在我们的模板中使用它来循环页面中的每个项目。考虑以下代码:

>>>
>>>
>>> p = Paginator(Snippet.objects.all(), 3)
>>>
>>> page1 = p.page(1)
>>>
>>> from django import template
>>>
>>> t = template.Template("{% for s in snippets %}<p>{{s}}</p>{% endfor %}")
>>>
>>> c = template.Context({'snippets': page1 })
>>>
>>> t.render(c)
'<p>Untitled - Python</p><p>Untitled - Python</p><p>Untitled - Bash</p>'
>>>

所以我们使用分页所需要做的就是将Page对象作为上下文变量传递给模板。就这样。我们不需要修改我们的{% for %}标签来使用Page对象。

将分页添加到趋势分析代码段页面

我们的目标是使用以下网址模式对代码片段列表进行分页:

/trending/?page=<page_numer>

同样,为了给特定语言的代码片段列表分页,我们将使用以下 URL 模式。

/trending/<language_slug>/?page=<page_numer>

打开views.py并修改trending_snippets()查看功能如下:

djangobin/django_project/djangobin/views.py

#...
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
from .forms import SnippetForm
from .models import Language, Snippet

#...
def trending_snippets(request, language_slug=''):
    lang = None
    snippets = Snippet.objects
    if language_slug:
        snippets = snippets.filter(language__slug=language_slug)
        lang = get_object_or_404(Language, slug=language_slug)
    snippets = snippets.all()

    paginator = Paginator(snippets, 5)

    # get the page parameter from the query string
    # if page parameter is available get() method will return empty string ''
    page = request.GET.get('page')

    try:
        # create Page object for the given page
        posts = paginator.page(page)
    except PageNotAnInteger:
        # if page parameter in the query string is not available, return the first page
        snippets = paginator.page(1)
    except EmptyPage:
        # if the value of the page parameter exceeds num_pages then return the last page
        snippets = paginator.page(paginator.num_pages)

    return render(request, 'djangobin/trending.html', {'snippets': snippets, 'lang': lang})

创建分页记录的代码(从第 16 行到第 30 行)在所有视图中都是相同的。我们可以将任务分配给一个函数,而不是一遍又一遍地复制和粘贴相同的代码。

utils.py文件末尾添加paginate_records()功能如下:

决哥/决哥 _ 项目/决哥/utils.py】

from django.contrib.auth.models import User
from django.core.paginator import Paginator, InvalidPage, EmptyPage, PageNotAnInteger

def get_current_user(request):
    #...

def paginate_result(request, object_list, item_per_page):
    paginator = Paginator(object_list, item_per_page)

    page = request.GET.get('page')

    try:
        results = paginator.page(page)
    except PageNotAnInteger:
        # If page is not an integer, deliver first page.
        results = paginator.page(1)
    except EmptyPage:
        # If page is out of range (e.g. 9999), deliver last page of results.
        results = paginator.page(paginator.num_pages)

    return results

然后,更新views.py中的trending_snippets()视图,使用paginate_result()功能,如下所示:

djangobin/django_project/djangobin/views.py

from django.shortcuts import (HttpResponse, render, redirect,
                              get_object_or_404, reverse, get_list_or_404)
#...
from .forms import SnippetForm
from .utils import paginate_result

#...

def trending_snippets(request, language_slug=''):
    lang = None
    snippets = Snippet.objects
    if language_slug:
        snippets = snippets.filter(language__slug=language_slug)
        lang = get_object_or_404(Language, slug=language_slug)

    snippet_list = get_list_or_404(snippets.filter(exposure='public').order_by('-hits'))
    snippets = paginate_result(request, snippet_list, 5)

    return render(request, 'djangobin/trending.html', {'snippets': snippets, 'lang': lang})

接下来,通过在结束的<table>标签后添加以下代码,添加到trending_snippets.html的分页链接。

djangobin/django _ project/djangobin/templates/djangobin/trending . html

{# ... #}
            </tr>
        {% empty %}
            <tr class="text-center">
                <td colspan="4">There are no snippets.</td>
            </tr>
        {% endfor %}

        </tbody>
    </table>

    {% if snippets.paginator.num_pages > 1 %}
        <nav aria-label="...">
            <ul class="pager">

                <li>Page {{ snippets.number }} of {{ snippets.paginator.num_pages }}</li>

                {% if snippets.has_previous %}
                    <li><a href="?page={{ snippets.previous_page_number }}">Previous</a></li>
                {% endif %}

                {% if snippets.has_next %}
                    <li><a href="?page={{ snippets.next_page_number }}">Next</a></li>
                {% endif %}
            </ul>
        </nav>
    {% endif %}

{% endblock %}

打开浏览器,访问http://127.0.0.1:8000/trending/。您将在表格下方看到分页链接,如下所示:

创建标签页

本节的目标是创建一个标签页面,允许用户浏览特定标签的片段。

打开views.py并修改tag_list视图,如下所示:

djangobin/django_project/djangobin/views.py

#...
from .forms import SnippetForm
from .models import Language, Snippet, Tag
from .utils import paginate_result

#...
def trending_snippets(request, language_slug=''):
    #...

def tag_list(request, tag):
    t = get_object_or_404(Tag, name=tag)
    snippet_list = get_list_or_404(t.snippet_set)
    snippets = paginate_result(request, snippet_list, 5)
    return render(request, 'djangobin/tag_list.html', {'snippets': snippets, 'tag': t})

在 djangobin 应用中创建一个名为tag_list.html的模板,并向其中添加以下代码:

决哥/决哥 _ project/决哥/样板/决哥/标记 _list.html】

{% extends 'djangobin/base.html' %}

{% load static %}
{% load humanize %}

{% block title %}
    Snippets tagged "{{ tag.name }}" - {{ block.super }}
{% endblock %}

{% block main %}

    <h5><i class="fas fa-tag"></i> Snippets tagged: {{ tag.name }}</h5>
    <hr>

    <table class="table">
        <thead>
        <tr>
            <th>Title</th>
            <th>Date</th>
            <th>Hits</th>
            <th>Language</th>
            <th>User</th>
        </tr>
        </thead>
        <tbody>

        {% for snippet in snippets %}
            <tr>
                <td><i class="fas fa-globe"></i>
                    <a href="{{ snippet.get_absolute_url }}">{{ snippet.title|default:"Untitled" }}</a>
                </td>
                <td>{{ snippet.created_on|naturaltime }}</td>
                <td>{{ snippet.hits }}</td>
                <td><a href="{% url 'djangobin:trending_snippets' snippet.language.slug  %}">{{ snippet.language }}</a></td>
                {% if not snippet.user.profile.private %}
                    <td><a href="{{ snippet.user.profile.get_absolute_url }}">{{ snippet.user.username|title }}</a></td>
                {% else %}
                    <td>-</td>
                {% endif %}

            </tr>
        {% endfor %}

        </tbody>
    </table>

    {% if snippets.paginator.num_pages > 1 %}
        <nav aria-label="...">
            <ul class="pager">

                <li>Page {{ snippets.number }} of {{ snippets.paginator.num_pages }}</li>

                {% if snippets.has_previous %}
                    <li><a href="?page={{ snippets.previous_page_number }}">Previous</a></li>
                {% endif %}

                {% if snippets.has_next %}
                    <li><a href="?page={{ snippets.next_page_number }}">Next</a></li>
                {% endif %}
            </ul>
        </nav>
    {% endif %}

{% endblock %}

这里没有什么新的,这个模板几乎类似于趋势片段页面。

更新snippet_detail.html模板,显示标签列表页面的链接,如下所示:

djangobin/django _ project/djangobin/templates/djangobin/snippet _ detail . html

{# ... #}
        <div class="media-body">
            <h4 class="media-heading">{{ snippet.title|default:"Untitled" }}</h4>
            <p>
                <i class="fas fa-user" data-toggle="tooltip" title="" data-original-title="Paste creator"></i> by
                {{ snippet.user.username|capfirst }} &nbsp;
                <i class="fas fa-calendar-alt" data-toggle="tooltip" title="" data-original-title="Creation Date" ></i>
                <time title="{{ snippet.created_on }}">{{ snippet.created_on|date:"M jS,  Y" }}</time> &nbsp;</span>
                <i class="fas fa-eye"  data-toggle="tooltip" title="" data-original-title="Visits to this paste" ></i>
                {{ snippet.hits }} &nbsp;&nbsp;
                <i class="fas fa-stopwatch" data-toggle="tooltip" title="" data-original-title="Expiration time"></i>
                {{ snippet.expiration }}  &nbsp;
                {% if snippet.tags.all %}
                    <i class="fas fa-tags" data-toggle="tooltip" title="" data-original-title="Tags"></i>
                    {% for tag in snippet.tags.all %}
                        <a href="{{ tag.get_absolute_url }}">{{ tag }}</a>{% if not forloop.last %},{% endif %}
                    {% endfor %}
                {% endif %}
            </p>
        </div>
{# ... #}

打开浏览器,点击代码片段详细信息页面中的任何标签。您应该会看到如下标签列表页面:



构建【联系我们】表单

原文:https://overiq.com/django-1-11/building-contact-us-form/

最后更新于 2020 年 7 月 27 日


在本课中,我们将创建一个联系人表单。这将允许我们的访问者直接向管理电子邮件地址发送反馈。

我们在最后几章中构建的表单与 models 类密切相关。然而,也有可能(有时更可行)创建独立的表单,而与模型没有任何关系。举一个完整过程的例子,我们将在本章中构建的 Contact 表单将继承自forms.Form类,而不是forms.ModelForm。但在此之前,我们必须学会如何与 Django 发送电子邮件。

与 Django 一起发送电子邮件

要发送电子邮件,Django 要求您添加一些配置。以下是 Django 提供的一些常见配置选项的列表:

  • SERVER_EMAIL:指定 Django 发送错误信息给ADMINSMANAGERS的邮件地址。

  • EMAIL_BACKEND:指定用于发送邮件的后端的名称。django.core.mail.backends.smtp.EmailBackend表示 Django 将使用 SMTP 服务器发送电子邮件。Django 还有很多其他后台。以下是另外两个常用的后端:

    • django.core.mail.backends.filebased.EmailBackend
    • django.core.mail.backends.console.EmailBackend

    前者允许我们将电子邮件写入文件,而不是将其转发到 SMTP 服务器。后者将电子邮件直接打印到控制台。

  • EMAIL_HOST:指定邮件服务器或 SMTP 服务器的地址。

  • EMAIL_HOST_USER:指定 SMTP 服务器的用户名。

  • EMAIL_HOST_PASSWORD:SMTP 服务器的密码。

  • EMAIL_PORT:用于连接到 SMTP 服务器的端口。

  • EMAIL_USE_TLS:指定是否使用 TLS 安全。

  • DEFAULT_FROM_EMAIL:指定站点管理员普通通信使用的默认电子邮件地址。

  • ADMINS:指定发送错误通知的人员列表。当站点处于生产状态(即DEBUG = False)并且任何视图引发异常时,Django 将向ADMINS列表中指定的所有人员发送电子邮件。ADMINS列表中的每一项都是一个元组。例如:

    ADMINS = [    
        ('name1', 'name1@email.com'),
        ('name2', 'name2@email.com'),
    ]
    
    
  • MANAGERS:指定 404 个未发现错误发送断链邮件的人员列表。它接受与ADMINS相同格式的电子邮件。

    MANAGERS = [    
        ('name1', 'name1@email.com'),
        ('name2', 'name2@email.com'),
    ]
    
    

    要启用此功能,您必须在settings.py文件的MIDDLEWARE设置中添加django.middleware.common.BrokenLinkEmailsMiddleware中间件。

下面的列表显示了通过 Gmail SMTP 服务器发送电子邮件所需的配置。

SERVER_EMAIL = 'infooveriq@gmail.com'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_PASSWORD = 'password'
EMAIL_HOST_USER = SERVER_EMAIL
EMAIL_PORT = 587
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

我们正处于开发阶段,只想发送测试电子邮件。因此,我们将使用控制台后端。打开settings.py并在文件末尾添加以下选项:

djangobin/django _ project/django _ project/settings . py

#...
MEDIA_ROOT  = os.path.join(BASE_DIR, 'media')

MEDIA_URL = '/media/'

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

SERVER_EMAIL = 'infooveriq@gmail.com'
DEFAULT_FROM_EMAIL = SERVER_EMAIL

ADMINS = (
    ('OverIQ', 'admin@overiq.com'),
)

MANAGERS = (
    ('OverIQ', 'manager@overiq.com'),
)

使用 sendtestemail 测试电子邮件

Django 提供sendtestemail命令,该命令向指定的电子邮件 id 发送测试电子邮件。它使用DEFAULT_FROM_EMAIL设置中指定的电子邮件 id 发送电子邮件。

在终端中输入以下命令:

$ ./manage.py sendtestemail test@example.com
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Test email from pc on 2018-04-14 08:02:32.394195+00:00
From: infooveriq@gmail.com
To: test@example.com
Date: Sat, 14 Apr 2018 08:02:32 -0000
Message-ID: <20180414080232.16065.81775@pc>

If you're reading this, it was successful.
-------------------------------------------------------------------------------

如果您得到与上面相同的输出,那么这意味着一切都在按预期工作。

除了向指定的电子邮件 id 发送电子邮件,您还可以使用--admins--managers选项让sendtestemailADMINSMANAGERS发送电子邮件:

$ ./manage.py sendtestemail --managers
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: [Django] Test email from pc on 2018-04-14 08:05:12.545568+00:00
From: infooveriq@gmail.com
To: manager@overiq.com
Date: Sat, 14 Apr 2018 08:05:12 -0000
Message-ID: <20180414080512.16961.91759@pc>

This email was sent to the site managers.
-------------------------------------------------------------------------------

$ ./manage.py sendtestemail --admins
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: [Django] Test email from pc on 2018-04-14 08:08:31.878881+00:00
From: infooveriq@gmail.com
To: admin@overiq.com
Date: Sat, 14 Apr 2018 08:08:31 -0000
Message-ID: <20180414080831.17122.89062@pc>

This email was sent to the site admins.
-------------------------------------------------------------------------------

send_mail()函数

send_mail()是在 Django 发邮件最简单的方式。它的语法是:

send_mail(subject, message, from_email, recipient_list)

Subject:邮件主题。

message:消息。

from_email:发件人的邮件

recipient_list:收件人列表

成功后send_mail()返回1,否则0返回。

mail_admins()函数

main_admins()功能向ADMINS设置中指定的管理员发送电子邮件。它的语法是:

mail_admins(subject, message, fail_silently=False)

Subject:邮件的主题。

message:要发送的消息。

它使用在SERVER_EMAIL设置中指定的电子邮件作为发送者的电子邮件。

由于我们有兴趣向管理员发送电子邮件,这是我们在构建“联系我们”页面时要使用的功能。

创建联系我们页面

打开forms.py文件,将ContactForm类添加到文件末尾,如下所示:

djangobin/django _ project/djangobin/forms . py

#...

class ContactForm(forms.Form):
    BUG = 'b'
    FEEDBACK = 'fb'
    NEW_FEATURE = 'nf'
    OTHER = 'o'
    purpose_choices = (
        (FEEDBACK, 'Feedback'),
        (NEW_FEATURE, 'Feature Request'),
        (BUG, 'Bug'),
        (OTHER, 'Other'),
    )

    name = forms.CharField()
    email = forms.EmailField()
    purpose = forms.ChoiceField(choices=purpose_choices)
    message = forms.CharField(widget=forms.Textarea(attrs={'cols': 40, 'rows': 5}))

views.py文件中,添加名为contact的视图,如下所示:

djangobin/django_project/djangobin/views.py

#...
from django.core.mail import mail_admins
import datetime
from .forms import SnippetForm, ContactForm
from .models import Language, Snippet, Tag
from .utils import paginate_result
#...

def trending_snippets(request, language_slug=''):
    #...

def contact(request):
    if request.method == 'POST':
        f = ContactForm(request.POST)
        if f.is_valid():

            name = f.cleaned_data['name']
            subject = "You have a new Feedback from {}:<{}>".format(name, f.cleaned_data['email'])

            message = "Purpose: {}\n\nDate: {}\n\nMessage:\n\n {}".format(
                dict(f.purpose_choices).get(f.cleaned_data['purpose']),
                datetime.datetime.now(),
                f.cleaned_data['message']
            )

            mail_admins(subject, message)
            messages.add_message(request, messages.INFO, 'Thanks for submitting your feedback.')

            return redirect('djangobin:contact')

    else:
        f = ContactForm()

    return render(request, 'djangobin/contact.html', {'form': f})

此视图显示联系人表单,并将提交的响应发送给所有管理员。

请注意我们如何在电子邮件正文中设置目的。purpose字段的数据将是purpose_choices中元组的第一个元素,即bfbnfo。这些信不是很有帮助。这就是为什么我们首先将元组转换成字典,然后使用存储在cleaned_data中的密钥来访问更可读的值。

接下来,在templates目录中创建一个名为contact.html的模板,代码如下:

djangobin/django _ project/djangobin/templates/djangobin/contact . html

{% extends 'djangobin/base.html' %}

{% block title %}
    Contact Us - {{ block.super }}
{% endblock %}

{% block main %}

    <h4>Contact </h4>
    <hr>

    {% if messages %}
        {% for message in messages %}
            <p class="alert alert-info">
                {{ message }}
            </p>
        {% endfor %}
    {% endif %}

    <form method="post">

        {% csrf_token %}

        <div class="form-group row">
            <div class="col-lg-5">
                {{ form.name.errors }}
                {{ form.name.label_tag }}
                {{ form.name }}
            </div>
        </div>

        <div class="form-group row">
            <div class="col-lg-5">
                {{ form.email.errors }}
                {{ form.email.label_tag }}
                {{ form.email }}
            </div>
        </div>

        <div class="form-group row">
            <div class="col-lg-5">
                {{ form.purpose.errors }}
                {{ form.purpose.label_tag }}
                {{ form.purpose }}
            </div>
        </div>

        <div class="form-group row">
            <div class="col-lg-5">
                {{ form.message.errors }}
                {{ form.message.label_tag }}
                {{ form.message }}
            </div>
        </div>

        <button type="submit" class="btn btn-primary">Submit</button>
    </form>

{% endblock %}

urls.py中添加新的网址模式,如下所示:

决哥/决哥 _ 项目/决哥/URL . py】

#...
urlpatterns = [
    #...
    url('^tag/(?P<tag>[\w-]+)/$', views.tag_list, name='tag_list'),
    url('^download/(?P<snippet_slug>[\d]+)/$', views.download_snippet, name='download_snippet'),
    url('^raw/(?P<snippet_slug>[\d]+)/$', views.raw_snippet, name='raw_snippet'),    
    url('^contact/$', views.contact, name='contact')    
]

接下来,在base.html中添加联系我们表单的链接,如下所示:

决哥/决哥 _ project/决哥/样板/决哥/base.html

{# ... #}
        <!-- Collect the nav links, forms, and other content for toggling -->
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
            <ul class="nav navbar-nav">
                <li {% if request.path == '/' %}class='active'{% endif %} >
                    <a href="{% url 'djangobin:index' %}">Add new</a>
                </li>
                <li {% if request.path == '/trending/' %}class='active'{% endif %}>
                    <a href="{% url 'djangobin:trending_snippets' %}">Trending<span class="sr-only">(current)</span></a>
                </li>
                <li {% if request.path == '/about/' %}class='active'{% endif %}>
                    <a href="">About</a>
                </li>
                <li {% if request.path == '/contact/' %}class='active'{% endif %}>
                    <a href="{% url 'djangobin:contact' %}">Contact</a>
                </li>
            </ul>
{# ... #}

要查看我们的创作,请访问http://127.0.0.1:8000/contact/并尝试提交一两个反馈。

提交后,您将获得如下成功消息:

在运行服务器的 shell 中,您应该获得如下输出:

[10/May/2018 14:19:05] "GET /contact/ HTTP/1.1" 200 9875
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: [Django] You have a new Feedback from spike:<spike@mail.com>
From: infooveriq@gmail.com
To: admin@overiq.com
Date: Thu, 10 May 2018 14:19:30 -0000
Message-ID: <20180510141930.6825.79585@pc>

Purpose: Feedback

Date: 2018-05-10 14:19:30.825265

Message:

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet, animi assumenda eos exercitationem incidunt molestias nihil non quas soluta voluptatibus! Architecto blanditiis eos nulla quas! A odit optio quibusdam voluptatibus!
-------------------------------------------------------------------------------



Django 用户登录和注销

原文:https://overiq.com/django-1-11/django-logging-users-in-and-out/

最后更新于 2020 年 7 月 27 日


Django 为登录和注销用户提供了内置的 URL 模式和视图功能。但是在我们将它们添加到我们的项目之前,我们将使用 Django 身份验证框架提供的一些实用功能自行创建登录和注销系统。

身份验证()和登录()功能

Django 认证框架(django.contrib.auth)提供authenticate()login()功能,其工作分别是认证和登录用户。

authenticate()函数接受两个关键字参数usernamepassword,如果usernamepassword有效,则返回一个类型为User的对象。否则返回None

>>>
>>> from django.contrib import auth
>>>
>>> user = auth.authenticate(username='admin', password='passwordd')
>>>
>>> user
<User: admin>
>>>
>>> if user is not None:
...     print("Credentials are valid")
... else:
...     print("Invalid Credentials")
...
Credentials are valid
>>>
>>>

authenticate()功能只验证提供的凭证是否有效。它不会登录用户。

要登录用户,我们使用login()功能。它需要两个参数,request对象(HttpRequest)和一个User对象。它的工作原理是使用Django 会话框架在会话中保存用户标识。

用户一旦登录,应该可以注销,这是logout()功能的职责。

注销()功能

要注销用户,我们使用logout()功能。它接受请求(HttpRequest)对象并返回None。调用logout()功能会完全删除与登录用户相关的会话数据和 cookie。

需要注意的是,如果用户还没有登录,调用logout()函数不会抛出任何错误。

现在我们有足够的知识来推出我们自己的登录系统。

创建登录系统

在 djangobin app 的views.py文件中,在文件末尾添加loginlogoutuser_details视图,如下所示:

djangobin/django_project/djangobin/views.py

#...
from django.core.mail import mail_admins
from django.contrib.auth.models import User
from django.contrib import auth
import datetime
from .forms import SnippetForm, ContactForm
from .models import Language, Snippet, Tag
from .utils import paginate_result

#...

def profile(request):
    #...

def login(request):
    if request.user.is_authenticated():
        return redirect('djangobin:admin')

    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        user = auth.authenticate(username=username, password=password)

        if user is not None:
            # correct username and password login the user
            auth.login(request, user)
            return redirect('djangobin:user_details')

        else:
            messages.error(request, 'Error wrong username/password')

    return render(request, 'djangobin/login.html')

def logout(request):
    auth.logout(request)
    return render(request,'djangobin/logout.html')

def user_details(request):    
    user = get_object_or_404(User, id=request.user.id)    
    return render(request, 'djangobin/user_details.html', {'user': user})

然后用下面的代码创建三个模板login.htmllogout.htmluser_details.html:

决哥/决哥 _ 项目/决哥/样板/决哥/登录. html

{% extends "djangobin/base.html"  %}

{% block title %}
    Login - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="row">
        <div class="col-lg-6 col-md-6 col-sm-6">

            <h4>Login</h4>
            <hr>

            {% if messages %}
                {% for message in messages %}
                    <p class="alert alert-info">{{ message }}</p>
                {% endfor %}
            {% endif %}

                <form method="post">

                    {% csrf_token %}

                    <table class="table">
                        <tr>
                            <th><label for="id_username">Username:</label></th>
                            <td><input type="text" name="username" id="id_username" required /></td>
                        </tr>
                        <tr>
                            <th><label for="id_password">Password:</label></th>
                            <td><input type="password" name="password" id="id_password" required /></td>
                        </tr>
                        <tr>
                            <td><input type="hidden" name="next" value=""></td>
                            <td><button type="submit" class="btn btn-primary">Submit</button></td>
                        </tr>
                    </table>

                </form>            
        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="/password-reset/">Forgot Password?</a> <br>
                <a href="/register/">Create new account.</a> <br>
                <a href="/contact/">Feedback</a>
            </p>
        </div>

    </div>

{% endblock %}

djangobin/django _ project/djangobin/templates/djangobin/logout . html

{% extends "djangobin/base.html"  %}

{% block title %}
    Logout - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="container">

        <p>You are logged out. <a href="{% url 'djangobin:login' %}">Click here</a> to login again.</p>

    </div>

{% endblock %}

djangobin/django _ project/djangobin/templates/djangobin/user _ details . html

{% extends 'djangobin/base.html' %}

{% block title %}
    User Details - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="row">

        <div class="col-lg-6 col-md-6 col-sm-6">

            <h4>Account Details</h4>

            <hr>

            <dl class="dl-horizontal">
                <dt>Username</dt>
                <dd>{{ request.user.username }}</dd>

                <dt>Email</dt>
                <dd>{{ request.user.email }}</dd>

                <dt>Date Joined</dt>
                <dd>{{ request.user.date_joined }}</dd>

                <dt>Last Login</dt>
                <dd>{{ request.user.last_login }}</dd>

                <dt>Snippet created</dt>
                <dd>{{ request.user.profile.get_snippet_count }}</dd>
            </dl>

        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="">My Pastes</a> <br>
                <a href="">Settings</a> <br>
                <a href="">Change Password.</a> <br>
                <a href="{% url 'djangobin:logout' %}">Logout.</a> <br>
            </p>
        </div>

    </div>

{% endblock %}

这里没有什么特别的,我们只是使用我们在Django 认证框架基础知识一章中学到的一些属性来获取一些关于登录用户的信息。

base.html文件中添加登录和注销页面的链接,如下所示:

决哥/决哥 _ project/决哥/样板/决哥/base.html

{# ... #}
                    {% if request.user.is_authenticated %}
                        <ul class="dropdown-menu">
                            <li><a href="">My Pastes</a></li>
                            <li><a href="">Account Details</a></li>
                            <li><a href="">Settings</a></li>
                            <li role="separator" class="divider"></li>
                            <li><a href="{% url 'djangobin:logout' %}">Logout</a></li>
                        </ul>
                    {% else %}
                        <ul class="dropdown-menu">
                            <li><a href="">Sign Up</a></li>
                            <li><a href="{% url 'djangobin:login' %}">Login</a></li>
                        </ul>
                    {% endif %}
                </li>
            </ul>
        </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
</nav>

<div class="container">

    <div class="row">

        <div class="col-lg-9 col-md-9">

            {% if not request.user.is_authenticated and not request.path == '/login/'  %}
                <p class="alert alert-info">
                    <a href="{% url 'djangobin:login' %}" class="alert-link">Login</a> to access other cool features.
                </p>
            {% endif %}

            {% block main %}
                {#  override this block in the child template  #}
            {% endblock %}

        </div>
{# ... #}

最后,在 djangobin 应用的urls.py文件中添加以下三种 URL 模式:

决哥/决哥 _ 项目/决哥/URL . py】

#...
urlpatterns = [
    #...
    url('^contact/$', views.contact, name='contact'),
    url(r'^login/$', views.login, name='login'),
    url(r'^logout/$', views.logout, name='logout'),
    url(r'^userdetails/$', views.user_details, name='user_details'),
]

启动开发服务器,访问http://127.0.0.1:8000/login/。你应该得到这样一页:

输入虚假的用户名和密码,您会得到如下错误:

现在输入正确的用户名和密码,您将被重定向到用户详细信息页面:

要注销,请单击页面右侧的注销链接。您应该会看到这样的注销页面:

使用内置的登录()和注销()视图

Django 提供了两个内置视图django.contrib.auth.login()django.contrib.auth.logout(),分别用于登录和注销用户。

要使用这些视图,请从django.contrib.auth包导入它们,并更新urls.py文件中的loginlogout网址模式,如下所示:

决哥/决哥 _ 项目/决哥/URL . py】

#...
from django.contrib.auth import views as auth_views
from . import views

# app_name = 'djangobin'

urlpatterns = [
    #...
    url(r'^login/$', auth_views.login, name='login'),
    url(r'^logout/$', auth_views.logout, name='logout'),
    url(r'^userdetails/$', views.user_details, name='user_details'),
]

保存urls.py文件,访问http://127.0.0.1:8000/login/。您将获得如下TemplateDoesNotExist例外:

问题在于,默认情况下,django.contrib.auth.login()视图会寻找一个名为registration/login.html的模板。然而,Django 没有提供这个模板,这就是为什么会引发TemplateDoesNotExist异常。

另外,请注意模板加载器死后部分。它告诉你 Django 试图找到模板的确切顺序。

我们可以使用template_name关键字参数将不同的模板传递给django.contrib.auth.login()视图,如下所示:

url(r'^login/$',
    auth_views.login, 
    {'template_name': 'djangobin/login.html'}, 
    name='login'
)

同样,默认情况下,django.contrib.auth.logout()视图使用管理应用(django.contrib.admin)中的registration/logged_out.html模板。如果你从 Django 管理网站注销,你会看到同样的模板。

参观http://127.0.0.1:8000/logout/自己看看。

就像django.contrib.auth.login()视图一样,我们可以通过将template_name关键字参数传递给django.contrib.auth.logout()视图来使用不同的模板,如下所示:

url(r'^logout/$', 
    auth_views.logout, 
    {'template_name': 'djangobin/logout.html'}, 
    name='logout'
)

修改登录和注销网址模式以使用自定义模板,如下所示:

决哥/决哥 _ 项目/决哥/URL . py】

#...

urlpatterns = [
    #...
    url(r'^login/$', auth_views.login, {'template_name': 'djangobin/login.html'}, name='login'),
    url(r'^logout/$', auth_views.logout, {'template_name': 'djangobin/logout.html'}, name='logout'),
    url(r'^userdetails/$', views.user_details, name='user_details'),    
]

接下来,更新login.html模板,使用django.contrib.auth.login()提供的form模板变量,视图如下:

决哥/决哥 _ 项目/决哥/样板/决哥/登录. html

{% extends "djangobin/base.html"  %}

{% block title %}
    Login - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="row">
        <div class="col-lg-6 col-md-6 col-sm-6">

            <h4>Login</h4>
            <hr>

            {% if messages %}
                {% for message in messages %}
                    <p class="alert alert-info">{{ message }}</p>
                {% endfor %}
            {% endif %}

            <form method="post">

                {% csrf_token %}

                <table class="table">
                    {{ form.as_table }}
                    <tr>
                        <td>&nbsp;</td>
                        <td><button type="submit" class="btn btn-primary">Submit</button></td>
                    </tr>
                </table>

            </form>
        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="/password-reset/">Forgot Password?</a> <br>
                <a href="/register/">Create new account.</a> <br>
                <a href="#">Feedback</a>
            </p>
        </div>

    </div>

{% endblock %}

我们的登录视图几乎准备好了。访问http://127.0.0.1:8000/login/并尝试使用错误的用户名和密码登录。你会遇到这样的错误:

请尝试使用正确的用户名和密码再次登录。成功后,您将重定向至/accounts/profile/网址。这是django.contrib.auth.login()视图的另一个默认行为。

我们在 djangobin 的urls.py中没有任何 URL 模式来匹配/accounts/profile/的 URL 路径,这就是服务器返回 HTTP 404 错误的原因。

我们可以使用LOGIN_REDIRECT_URL设置轻松覆盖这种行为。打开settings.py文件,在文件末尾添加LOGIN_REDIRECT_URL,如下所示:

djangobin/django _ project/django _ project/settings . py

#...
MANAGERS = (
    ('OverIQ', 'manager@overiq.com'),
)

LOGIN_REDIRECT_URL = 'djangobin:index'

这将把重定向网址从/accounts/profile/改为/

我们也可以直接传递网址路径,而不是传递网址模式的名称。

从现在开始,成功登录后,django.contrib.auth.login()视图会将用户重定向到/ URL 路径,而不是/accounts/profile/

但这仍然有一些局限性。例如,假设您正在浏览趋势片段,然后决定登录。登录后,再次重定向到趋势页面而不是/ URL 更有意义。

要做到这一点,我们可以嵌入一个名为next的隐藏字段,其中包含登录后要重定向到的 URL。

django.contrib.auth.login()视图接收到next作为开机自检数据时,它会重定向到隐藏的next字段中指定的网址。

django.contrib.auth.login()视图还提供了一个名为next的上下文变量,其中包含用户登录后将被重定向的网址。next变量的值可以是/accounts/profile/LOGIN_REDIRECT_URL变量中指定的网址。

我们使用如下查询字符串指定next字段的值:

http://127.0.0.1:8000/login/?next=/trending/

打开login.html并添加名为next的隐藏字段,如下所示:

决哥/决哥 _ 项目/决哥/样板/决哥/登录. html

{# ... #}
            <form method="post">

                {% csrf_token %}

                <table class="table">
                    {{ form.as_table }}
                    <tr>
                        <td><input type="hidden" name="next" value="{{ next }}"></td>
                        <td><button type="submit" class="btn btn-primary">Submit</button></td>
                    </tr>
                </table>

            </form>
{# ... #}

以上代码是这样工作的:

如果我们使用http://localhost:8000/login/网址访问登录页面,那么登录django.contrib.auth.login()后会将用户重定向到/网址。另一方面,如果我们访问登录页面,使用http://127.0.0.1:8000/login/?next=/trending/网址,那么django.contrib.auth.login()视图会将用户重定向到/trending/网址。

接下来,修改base.htmlnext查询参数提供一个值,如下所示:

决哥/决哥 _ project/决哥/样板/决哥/base.html

{# ... #}
<div class="container">

    <div class="row">

        <div class="col-lg-9 col-md-9">

            {% if not request.user.is_authenticated and not request.path == '/login/'  %}
                <p class="alert alert-info">
                    <a href="{% url 'djangobin:login' %}?next={{ request.path }}" class="alert-link">Login</a> to access other cool features.
                </p>
            {% endif %}
{# ... #}

让我们测试一下是否一切正常。

如果您已经登录,请先通过直接访问http://localhost:8000/logout/网址或点击页面右上角的注销链接注销。

然后,导航至登录页面(http://localhost:8000/login/),输入正确的用户名和密码。成功后,您将被重定向到 djangobin 的索引页面:

再次注销,并通过单击趋势分析片段页面中的“登录”链接再次导航到登录页面。这次登录后,您将被重定向到/trending/而不是/网址。

使用电子邮件和密码登录

如您所见,默认情况下,Django 要求您输入用户名和密码才能登录应用。如果你故意想要这种行为,没关系。然而,为了向您展示如何选择替代路线,我们的 djangobin 应用将使用电子邮件和密码来验证用户。为了完成这项任务,我们将创建一个自定义表单和视图函数。

打开forms.py并添加LoginForm类,如下所示:

djangobin/django _ project/djangobin/forms . py

#...
class ContactForm(forms.Form):
    #...

class LoginForm(forms.Form):
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)

接下来,修改login()视图功能,使用LoginForm如下:

djangobin/django_project/djangobin/views.py

#...
from .forms import SnippetForm, ContactForm, LoginForm
#...

def login(request):
    if request.method == 'POST':

        f = LoginForm(request.POST)
        if f.is_valid():

            user = User.objects.filter(email=f.cleaned_data['email'])

            if user:
                user = auth.authenticate(
                    username=user[0].username,
                    password=f.cleaned_data['password'],
                )

                if user:
                    auth.login(request, user)
                    return redirect( request.GET.get('next') or 'djangobin:index' )

            messages.add_message(request, messages.INFO, 'Invalid email/password.')
            return redirect('djangobin:login')

    else:
        f = LoginForm()

    return render(request, 'djangobin/login.html', {'form': f})

在第 12 行,我们正在检查与提交的电子邮件相关联的任何用户是否存在。

如果用户存在,在第 15 行,我们使用authenticate()功能对其进行认证。请注意,传递给authenticate()函数的参数仍然是用户名和密码。

如果认证成功,我们使用login()功能登录用户并重定向。

更新urls.py文件中的loginlogout网址模式,使用views.py文件的login()logout功能,如下所示:

决哥/决哥 _ 项目/决哥/URL . py】

#...
urlpatterns = [
    #...
    url(r'^login/$', views.login, name='login'),
    url(r'^logout/$', views.logout, name='logout'),
    url(r'^userdetails/$', views.user_details, name='user_details'),
]

访问登录页面,输入不正确的电子邮件和密码。您将得到如下错误:

现在,输入正确的电子邮件和密码,您将被重定向到索引页面。

我们的登录和注销系统工作正常,但是从可用性的角度来看,还是有一个问题。

问题是登录表单对登录用户仍然可见。

向登录用户显示登录表单是毫无意义的。要解决该问题,只需在login()查看功能开始时检查用户是否登录,如下所示:

djangobin/django_project/djangobin/views.py

#...

def login(request):

    if request.user.is_authenticated:
        return redirect('djangobin:profile', username=request.user.username)

    if request.method == 'POST':

        f = LoginForm(request.POST)
        if f.is_valid():

如果您现在访问http://localhost:8000/login/,登录后,您将被重定向到用户配置文件页面。

目前,用户配置文件页面只显示用户的姓名。我们将对其进行更新,以显示即将到来的课程中的片段列表。

限制访问

实现登录系统的关键是防止对管理页面的未授权访问。

限制访问页面的一个简单方法是首先使用is_authenticated()方法检查用户是否通过身份验证,然后相应地重定向用户。例如:

def our_view(request):
    if not request.user.is_authenticated():
        return redirect("login")

    return render(request, 'app/view.html')

我们可以在每个管理视图功能开始时复制并粘贴这个条件。这是可行的,但 Django 提供了一个更好的方法。

限制页面访问的首选方式是使用login_required装饰器。要使用login_required装饰器,您必须从django.contrib.auth.decorators模块导入它。

让我们更新user_detailslogout视图以使用login_required装饰器,如下所示:

djangobin/django_project/djangobin/views.py

#...
from django.contrib import auth
from django.contrib.auth.decorators import login_required
import datetime
from .forms import SnippetForm, ContactForm, LoginForm
#...

#...

@login_required
def logout(request):
    auth.logout(request)
    return render(request,'djangobin/logout.html')

@login_required
def user_details(request):
    user = get_object_or_404(User, id=request.user.id)
    return render(request, 'djangobin/user_details.html', {'user': user})

以下是login_required装饰器的工作原理:

如果用户没有登录,那么它会将用户重定向到/accounts/login/(默认登录网址),将当前绝对网址作为一个值传递给next查询参数。另一方面,如果用户登录了,那么login_required将什么也不做。

要更改默认登录网址,我们使用LOGIN_URL设置。LOGIN_URL接受网址路径或网址模式的名称。打开settings.py文件,在文件末尾添加以下变量。

djangobin/django _ project/django _ project/settings . py

#...

LOGIN_REDIRECT_URL = 'djangobin:index'

LOGIN_URL = 'djangobin:login'

这将默认登录从/accounts/login/更改为/login/。如果您尝试访问应用了login_required装饰器的视图,您将被重定向到/login/网址,而不是/accounts/login/

要验证更改,请访问http://localhost:8000/userdetails/网址,您将被重定向到http://localhost:8000/login/?next=/userdetails/



Django 的用户注册

原文:https://overiq.com/django-1-11/user-registration-in-django/

最后更新于 2020 年 7 月 27 日


Django 认证框架(django.contrib.auth)提供了一个名为UserCreationForm(继承自ModelForm类)的表单来处理新用户的创建。它有三个字段,即usernamepassword1password2(用于密码确认)。要使用UserCreationForm,必须首先从django.contrib.auth.forms导入,如下所示:

from django.contrib.auth.forms import UserCreationForm

不幸的是,Django 没有提供任何视图来处理用户的创建,所以我们必须创建自己的视图。

在 djangobin 应用的urls.py文件中,在urlpatterns列表的末尾添加一个名为signup的 URL 模式。

决哥/决哥 _ 项目/决哥/URL . py】

#...
urlpatterns = [
    #...
    url(r'^userdetails/$', views.user_details, name='user_details'),
    url(r'^signup/$', views.signup, name='signup'),
]

在 Django 斌的views.py中创建一个名为signup()的视图函数,如下所示:

djangobin/django_project/djangobin/views.py

#...
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import UserCreationForm
import datetime
from .forms import SnippetForm, ContactForm, LoginForm
#...

def login(request, **kwargs):
    #...

def signup(request):

    if request.user.is_authenticated:
        return redirect('djangobin:profile', username=request.user.username)

    if request.method == 'POST':
        f = UserCreationForm(request.POST)
        if f.is_valid():
            f.save()
            messages.success(request, 'Account created successfully')
            return redirect('signup')

    else:
        f = UserCreationForm()

    return render(request, 'djangobin/signup.html', {'form': f})

接下来,在templates目录中创建新模板signup.html,代码如下:

决哥/决哥 _ project/决哥/样板/决哥/signup.html】

{% extends "djangobin/base.html"  %}

{% block title %}
    Sign Up - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="row">
        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Sign Up</h4>
            <hr>
            {% if messages %}
                {% for message in messages %}
                    <p class="alert alert-info">{{ message }}</p>
                {% endfor %}
            {% endif %}

            <form method="post">

                {% csrf_token %}

                <table class="table">
                    {{ form.as_table }}
                    <tr>
                        <td><input type="hidden" name="next" value="{{ next }}"></td>
                        <td><button type="submit" class="btn btn-primary">Submit</button></td>
                    </tr>
                </table>

            </form>
        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="/reset-password/">Forgot Password?</a> <br>
                <a href="{% url 'djangobin:login' %}">Login.</a> <br>
            </p>
        </div>

    </div>

{% endblock %}

base.html模板中添加注册页面链接,如下所示:

决哥/决哥 _ project/决哥/样板/决哥/base.html

{# ... #}
                    {% if request.user.is_authenticated %}
                        <ul class="dropdown-menu">
                            <li><a href="">My Pastes</a></li>
                            <li><a href="">Account Details</a></li>
                            <li><a href="">Settings</a></li>
                            <li role="separator" class="divider"></li>
                            <li><a href="{% url 'djangobin:logout' %}">Logout</a></li>
                        </ul>
                    {% else %}
                        <ul class="dropdown-menu">
                            <li><a href="{% url 'djangobin:signup' %}">Sign Up</a></li>
                            <li><a href="{% url 'djangobin:login' %}?next={{ request.path }}">Login</a></li>
                        </ul>
                    {% endif %}
{# ... #}

打开浏览器,导航至http://127.0.0.1:8000/signup/。你应该看到这样的注册页面。

通过输入用户名和密码创建新用户。成功后,您将收到“帐户创建成功”的消息。

如果输入的用户名已经存在或密码不匹配,则表单将显示如下错误:

使用UserCreationForm创建的用户将is_superuseris_staff属性设置为False,但is_active设置为True,这意味着这些用户无法登录 Django 管理网站。

另一点要记住的是,每次我们创建一个新的User对象时,一个相关的Author实例将被自动创建。这是因为来自User模型的post_save信号触发了在models.py文件中定义的create_author()功能的执行。

UserCreationForm唯一的缺点就是没有email场。因此,我们无法使用它发送电子邮件验证来验证帐户。

大多数情况下,用户注册包括以下步骤:

  1. 用户填写登记表并点击提交。
  2. 该网站向提交的电子邮件发送电子邮件验证链接。
  3. 用户点击激活链接验证帐户。

此时,我们有两个选择:

  1. 扩展UserCreationForm以包括电子邮件字段和电子邮件验证功能。
  2. 从头开始创建全新的用户注册表单。

在 Django 1.10 系列中,我们已经看到了如何从头开始创建登记表。因此,在本系列中,我们将采用另一种方法。

扩展用户创建表单

打开 djangobin 的forms.py并在文件末尾添加CreateUserForm,如下所示:

djangobin/django _ project/djangobin/forms . py

#...
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.tokens import default_token_generator
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.conf import settings
from django.template.loader import render_to_string
from django.core.mail import send_mail
from .models import Snippet, Language, Author, Tag
from .utils import Preference, get_current_user

#...

class CreateUserForm(UserCreationForm):
    class Meta:
        model = User
        fields = ('username', 'email', 'password1', 'password2')

    def clean_email(self):
        email = self.cleaned_data['email']
        if not email:
            raise ValidationError("This field is required.")
        if User.objects.filter(email=self.cleaned_data['email']).count():
            raise ValidationError("Email is taken.")
        return self.cleaned_data['email']

    def save(self, request):

        user = super(CreateUserForm, self).save(commit=False)
        user.is_active = False
        user.save()

        context = {
            # 'from_email': settings.DEFAULT_FROM_EMAIL,
            'request': request,
            'protocol': request.scheme,
            'username': self.cleaned_data.get('username'),
            'domain': request.META['HTTP_HOST'],
            'uid': urlsafe_base64_encode(force_bytes(user.pk)),
            'token': default_token_generator.make_token(user),
        }

        subject = render_to_string('djangobin/email/activation_subject.txt', context)
        email = render_to_string('djangobin/email/activation_email.txt', context)

        send_mail(subject, email, settings.DEFAULT_FROM_EMAIL, [user.email])

        return user

创建自定义登记表的第一步是创建一个继承自UserCreationForm类的类。

在第 19 行,我们指定要在表单中显示的模型字段。

在第 21-27 行中,我们为电子邮件字段定义了一种清理方法。此方法确保用户必须提供唯一的电子邮件 id。

最后,在第 29-50 行,我们覆盖了UserCreationFormsave()方法。save()方法做两件事:将is_active属性设置为False后,将用户保存到数据库中(这样账户就不能用来登录),并向用户邮箱发送邮件验证。

要发送电子邮件,我们使用内置的send_mail()功能。

电子邮件正文和主题的代码存储在 djangobin 的templates/djangobin目录下的email子目录中。

djangobin/django _ project/djangobin/templates/djangobin/email/activation _ subject . txt

Action Required to Complete the Account Creation - djangobin

djangobin/django _ project/djangobin/templates/djangobin/email/activation _ email . txt

Hello {{ username }}!

To confirm your registration, visit the following link:

{{ protocol }}://{{ domain }}{% url 'djangobin:activate' uid token %}

Welcome to Djanagobin!

在第 45-46 行,我们使用了一个名为render_to_string()的新函数。render_to_string()函数加载模板,渲染它并返回结果字符串。

我们在第 35-43 行定义的context变量包含我们的模板将使用的数据。

这里有两个重要的信息:

  1. 用户界面设计(User Interface Design 的缩写)
  2. 代币

我们使用这两条信息来创建激活链接,该链接将被发送到用户的电子邮件中。

uid是编码在 base 64 中的用户主键,令牌是使用用户相关数据和当前时间戳创建的哈希值。令牌用于检查激活方式是否有效。默认情况下,令牌仅在 3 天内有效。

我们在激活链接中包含了用户主键的编码版本,这样激活功能就可以确定它需要激活的用户。

为了从用户的主键创建基本 64 值,我们使用urlsafe_base64_encode()函数。它接受字节串并返回一个以 64 为基数的值。要将整数转换为字节串,我们使用force_bytes()函数(第 41 行)。

为了生成令牌,Django 提供了一个名为PasswordResetTokenGenerator的类。

这个类有两种方法:

  1. make_token(user)
  2. check_token(user, token)

make_token()接受用户,并基于用户相关数据返回令牌(第 42 行)。令牌是这样的:

4vf-8544d0407636ec564e5b

完整的验证链接如下所示:

http://example.com/MQ/4vf-8544d0407636ec564e5b/

其中MQ指的是编码在 base 64 中的用户主键。

现在你知道激活是如何产生的了。让我们看看当用户点击链接时会发生什么。

激活账户的第一步是在 base 64 中解码用户的主键(在上面的 URL 中为MQ)。为此,我们使用urlsafe_base64_decode()功能。该函数返回字节串,因此我们使用force_bytes()函数将字节串转换为字符串。

一旦我们有了主键,我们就从数据库中获取关联的对象,并调用check_token()来验证令牌是否有效。

如果令牌有效,我们将is_active属性设置为True,并将用户重定向到登录页面。另一方面,如果令牌无效,我们会向用户显示一条无效令牌错误消息。

打开views.py并修改signup()查看功能,使用CreateUserForm如下:

djangobin/django_project/djangobin/views.py

#...
import datetime
from .forms import SnippetForm, ContactForm, LoginForm, CreateUserForm
from .models import Language, Snippet, Tag
from .utils import paginate_result
#...

def signup(request):
    if request.method == 'POST':
        f = CreateUserForm(request.POST)
        if f.is_valid():
            f.save(request)
            messages.success(request, 'Account created successfully. Check email to verify the account.')
            return redirect('djangobin:signup')

    else:
        f = CreateUserForm()

    return render(request, 'djangobin/signup.html', {'form': f})

激活用户的查看功能称为activate_account(),在signup()查看功能的正下方定义:

djangobin/django_project/djangobin/views.py

#...
import datetime
from django.contrib.auth.tokens import default_token_generator
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from .forms import SnippetForm, ContactForm, LoginForm, CreateUserForm
#...

#...

def signup(request):
    #...

def activate_account(request, uidb64, token):
    try:
        uid = force_text(urlsafe_base64_decode(uidb64))
        user = User.objects.get(pk=uid)
    except (TypeError, ValueError, OverflowError, User.DoesNotExist):
        user = None
    if (user is not None and default_token_generator.check_token(user, token)):
        user.is_active = True
        user.save()
        messages.add_message(request, messages.INFO, 'Account activated. Please login.')
    else:
        messages.add_message(request, messages.INFO, 'Link Expired. Contact admin to activate your account.')

    return redirect('djangobin:login')

在 djangobin 的urls.py文件中,添加一个新的网址模式来激活帐户,如下所示:

决哥/决哥 _ 项目/决哥/URL . py】

#...
urlpatterns = [
    #...
    url(r'^userdetails/$', views.user_details, name='user_details'),
    url(r'^signup/$', views.signup, name='signup'),
    url(r'^activate/'
        r'(?P<uidb64>[0-9A-Za-z_\-]+)/'
        r'(?P<token>[0-9A-Za-z]{1,13}'
        r'-[0-9A-Za-z]{1,20})/$',
        views.activate_account, name='activate'),
]

现在,打开浏览器,导航至http://localhost:8000/signup/。在所有字段中输入数据,然后点击提交。

在 shell 中,运行服务器时,您会收到一封如下所示的验证电子邮件:

[11/May/2018 11:27:31] "GET /signup/ HTTP/1.1" 200 10594
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Action Required to Complete the Account Creation - djangobin
From: infooveriq@gmail.com
To: django@example.com
Date: Fri, 11 May 2018 11:27:53 -0000
Message-ID: <20180511112753.25336.4090@pc>

Hello django!

To confirm your registration, visit the following link:

http://localhost:8000/activate/MTM/4w3-81e3511bd45dc04e8ba9/

Welcome to Djanagobin!
-------------------------------------------------------------------------------

要激活帐户,请复制验证链接并将其粘贴到浏览器地址栏中。成功后,您将被重定向到登录页面:

如果链接过期或被篡改,那么会看到以下错误。

重置密码

让我们承认,偶尔,每个人都会忘记自己的密码。重置密码涉及使用以下四个内置视图功能:

  • password_reset()
  • password_reset_done()
  • password_reset_confirm()
  • password_reset_complete()

以下是重置密码的工作流程:

  1. password_reset()视图显示允许用户输入电子邮件的表单,并将密码重置链接发送到用户的电子邮件。

  2. password_reset_done()视图显示一个页面,其中包含如何重置密码的说明,如打开电子邮件、单击链接、在垃圾邮件中查找电子邮件等。该视图在password_reset()视图工作完成后立即调用。请注意,无论在步骤 1 中输入的电子邮件是否存在于数据库中,该视图都将始终被调用。这可以防止潜在的攻击者知道数据库中是否存在电子邮件。

  3. 当用户访问发送到电子邮件的密码重置链接时,会调用password_reset_confirm()。它显示了一个输入新密码的表单。

  4. 最后调用password_reset_complete()视图通知用户密码已经更改。

下表列出了这些视图函数使用的默认模板:

视角 模板
password_reset() 注册/密码重置 _form.html,注册/密码重置 _email.html,注册/密码重置 _subject.txt
password_reset_done() 注册/密码重置 _ 完成. html
password_reset_confirm() 注册/密码重置确认. html
password_reset_complete() 注册/密码重置确认. html

这些模板存储在django.contrib.admin应用的templates目录中。

所有这些模板的外观和感觉都非常类似于 Django 管理网站。因此,它们不适合我们的项目。

那么我们如何覆盖默认模板呢?

如果你在django/contrib/auth/views.py中检查视图函数的签名,你会发现它们每个都接受一个名为template_name的参数。

django/contib/auth/views . py

def password_reset(request,
                   template_name='registration/password_reset_form.html',
                   email_template_name='registration/password_reset_email.html',
                   subject_template_name='registration/password_reset_subject.txt',
                   password_reset_form=PasswordResetForm,
                   #...):

def password_reset_done(request,
                        template_name='registration/password_reset_done.html',
                        extra_context=None):                   

def password_reset_confirm(request, uidb64=None, token=None,
                           template_name='registration/password_reset_confirm.html',
                           token_generator=default_token_generator,
                           #...):

def password_reset_complete(request,
                            template_name='registration/password_reset_complete.html',
                            extra_context=None):

我们还将覆盖用于创建电子邮件的默认模板。如果你密切关注password_reset()签名,你会发现感兴趣的论点是:email_template_namesubject_template_name

打开urls.py文件,在列表末尾添加以下 4 个网址模式:

决哥/决哥 _ 项目/决哥/URL . py】

#...
urlpatterns = [
    #...
    url(r'^activate/'
        r'(?P<uidb64>[0-9A-Za-z_\-]+)/'
        r'(?P<token>[0-9A-Za-z]{1,13}'
        r'-[0-9A-Za-z]{1,20})/$',
        views.activate_account, name='activate'),

    # password reset URLs

    url('^password-reset/$', auth_views.password_reset,
        {'template_name': 'djangobin/password_reset.html',
         'email_template_name': 'djangobin/email/password_reset_email.txt',
         'subject_template_name': 'djangobin/email/password_reset_subject.txt',
         'post_reset_redirect': 'djangobin:password_reset_done',
        },
        name='password_reset'),

    url('^password-reset-done/$', auth_views.password_reset_done,
        {'template_name': 'djangobin/password_reset_done.html',},
        name='password_reset_done'),

    url(r'^password-confirm/'
        r'(?P<uidb64>[0-9A-Za-z_\-]+)/'
        r'(?P<token>[0-9A-Za-z]{1,13}'
        r'-[0-9A-Za-z]{1,20})/$',
        auth_views.password_reset_confirm,
        {'template_name': 'djangobin/password_reset_confirm.html',
         'post_reset_redirect': 'djangobin:password_reset_complete'},
        name='password_reset_confirm'),

    url(r'password-reset-complete/$',
        auth_views.password_reset_complete,
        {'template_name':
             'djangobin/password_reset_complete.html'},
        name='password_reset_complete'),
    ]

这里有几件事需要注意:

  • password_resetpassword_reset_confirm网址模式中的post_reset_redirect参数指定了在视图完成工作后要重定向的网址。

  • 就像activate_account()视图一样password_reset_confirm()也以uidb64(在 base 64 中编码的用户 id)和token作为参数。像往常一样,password_reset_confirm()视图解码uidb64中的值,以知道它正在为用户重置密码。令牌用于检查密码重置链接是否有效。

用于重置密码的模板代码如下:

djangobin/django _ project/djangobin/templates/djangobin/password _ reset . html

{% extends "djangobin/base.html"  %}

{% block title %}
    Password Reset - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="row">
        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Password Reset</h4>
            <hr>
            {% if messages %}
                {% for message in messages %}
                    <p class="alert alert-info">{{ message }}</p>
                {% endfor %}
            {% endif %}

            <form method="post">

                {% csrf_token %}

                <table class="table">
                    {{ form.as_table }}
                    <tr>
                        <td><input type="hidden" name="next" value="{{ next }}"></td>
                        <td><button type="submit" class="btn btn-primary">Submit</button></td>
                    </tr>
                </table>

            </form>
        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="{% url  'djangobin:signup' %}">Sign Up</a> <br>
                <a href="{% url  'djangobin:login' %}">Login.</a> <br>
            </p>
        </div>

    </div>

{% endblock %}

djangobin/django _ project/djangobin/templates/djangobin/email/password _ reset _ email . txt

Hello {{ user.email }}!

We've received a request to reset {{ user.get_username }}'s password.

If you would like to reset the password, visit the following link:

{{ protocol }}://{{ domain }}{% url 'djangobin:password_reset_confirm' uid token %}

If you did not request a password reset, please disregard this mail.

~ Djangobin

djangobin/django _ project/djangobin/templates/djangobin/email/password _ reset _ subject . txt

Password Reset Request - Djangobin

djangobin/django _ project/djangobin/templates/djangobin/password _ reset _ done . html

{% extends 'djangobin/base.html' %}

{% load static %}
{% load humanize %}

{% block main %}

    <div class="row">

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Email Sent</h4>
            <hr>
            <p>Password reset link is sent to your email.</p>
        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="">Forgot Password?</a> <br>
                <a href="{% url  'djangobin:signup' %}">Sign Up.</a> <br>
                <a href="#">Feedback</a>
            </p>
        </div>

    </div>

{% endblock %}

djangobin/django _ project/djangobin/templates/djangobin/password _ reset _ confirm . html

{% extends 'djangobin/base.html' %}

{% load static %}
{% load humanize %}

{% block main %}

    <div class="row">

        <div class="col-lg-6 col-md-6 col-sm-6">

            <h4>Enter your new password.</h4>
            <hr>
            {% if validlink %}
                <form action="" method="post">
                    <table>
                        {% csrf_token %}
                        {{ form.as_table }}
                        <tr>
                            <td></td>
                            <td><button type="submit" class="btn btn-primary">Reset Password</button></td>
                        </tr>
                    </table>
                </form>
            {% else %}
                <p>This Link is no longer valid. Click <a href="{% url 'djangobin:password_reset' %}">here</a> to create a new one.</p>
            {% endif %}

        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="">Forgot Password?</a> <br>
                <a href="{% url  'djangobin:signup' %}">Sign Up.</a> <br>
                <a href="#">Feedback</a>
            </p>
        </div>

    </div>

{% endblock %}

djangobin/django _ project/djangobin/templates/djangobin/password _ reset _ complete . html

{% extends 'djangobin/base.html' %}

{% load static %}
{% load humanize %}

{% block main %}

    <div class="row">

        <div class="col-lg-6 col-md-6 col-sm-6">

            <h4>Password Reset Success</h4>

            <hr>

            <div class="db-form">

                <p>Your password has been reset. Click <a href="{% url  'djangobin:login' %}">here</a> to login.</p>

            </div>
        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="">Forgot Password?</a> <br>
                <a href="{% url  'djangobin:signup' %}">Sign Up.</a>
                <a href="#">Feedback</a>
            </p>
        </div>

    </div>

{% endblock %}

打开浏览器,导航至http://localhost:8000/password-reset/。您将看到一个密码重置页面,要求您发送如下电子邮件:

输入有效的电子邮件并点击提交。您将被带到http://localhost:8000/password-reset-done/网址,如下所示:

如果输入的电子邮件存在于数据库中,那么您应该会在 shell 中看到以下输出。

[11/May/2018 13:07:33] "GET /password-reset/ HTTP/1.1" 200 9524
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Password Reset Request - Djangobin
From: infooveriq@gmail.com
To: noisyboy@mail.com
Date: Fri, 11 May 2018 13:14:49 -0000
Message-ID: <20180511131449.29374.83844@pc>

Hello noisyboy@mail.com!

We've received a request to reset noisyboy's password.

If you would like to reset the password, visit the following link:

http://localhost:8000/password-confirm/Mg/4w3-32c4a6bf60ae7b9dee77/

If you did not request a password reset, please disregard this mail.

~ Djangobin
-------------------------------------------------------------------------------

复制链接并将其粘贴到浏览器地址栏中。您将收到一份输入新密码的表格。

输入新密码,然后按回车键。最后,你会得到这样的成功信息:

需要注意的是,密码重置链接仅在 3 天内有效。要延长链接的有效性,请使用PASSWORD_RESET_TIMEOUT_DAYS设置。

此外,密码重置链接在密码重置成功后会自动过期。尝试访问过期、被篡改或已被使用的密码重置链接将导致以下错误:

密码更改器

在本节中,我们将创建一个表单,允许登录用户更改他们的密码。

就像密码重置一样,Django 提供了以下两个内置视图来处理密码更改:

  1. password_change
  2. password_change_done

password_change视图显示一个表单,允许登录用户在输入旧密码后更改用户密码。

用户更改密码后,password_change_done视图显示成功消息。

这两个视图都有密码保护,因此您必须先登录才能访问它们。

默认情况下,password_change()password_change_done()分别使用 Django 管理应用(django.contrib.admin)中的password_change_form.htmlpassword_change_done.html模板。我们可以通过使用template_name关键字参数来覆盖这个行为。

打开urls.py文件,在列表末尾添加以下两个网址模式:

决哥/决哥 _ 项目/决哥/URL . py】

#...
urlpatterns = [
    #...

    # password change URLs

    url(r'^password-change/$', auth_views.password_change,
        {'template_name': 'djangobin/password_change.html',
        'post_change_redirect': 'djangobin:password_change_done'},
        name='password_change'
        ),

    url(r'^password-change-done/$', auth_views.password_change_done,
        {'template_name': 'djangobin/password_change_done.html'},
        name='password_change_done'
        ),
    ]

templates目录中创建新模板password_change.htmlpassword_change_done.html,代码如下:

djangobin/django _ project/djangobin/templates/djangobin/password _ change . html

{% extends 'djangobin/base.html' %}

{% block title %}
    Password Change - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="row">

        <div class="col-lg-6 col-md-6 col-sm-6">

            <h4>Password change</h4>
            <hr>

            <form action="" method="post" class="form-horizontal" >
                {% csrf_token %}

                 <table class="table">
                    {{ form.as_table }}
                    <tr>
                        <td>&nbsp;</td>
                        <td><button type="submit" class="btn btn-primary">Submit</button></td>
                    </tr>
                </table>

            </form>

        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="{% url 'djangobin:profile' request.user.username %}">My Pastes</a> <br>
                <a href="{% url 'djangobin:user_details' %}">Account Details</a> <br>
                <a href="">Settings</a> <br>
                <a href="{% url 'djangobin:contact' %}">Feedback</a>
            </p>
        </div>

    </div>

{% endblock %}

djangobin/django _ project/djangobin/templates/djangobin/password _ change _ done . html

{% extends 'djangobin/base.html' %}

{% block title %}
    Password Change Done - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="row">

        <div class="col-lg-6 col-md-6 col-sm-6">

            <h4>Password change successful</h4>
            <hr>

            <p>Your password was changed.</p>

        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="{% url 'djangobin:profile' request.user.username %}">My Pastes</a> <br>
                <a href="{% url 'djangobin:user_details' %}">Account Details</a> <br>
                <a href="">Settings</a> <br>
                <a href="{% url 'djangobin:contact' %}">Feedback</a>
            </p>
        </div>

    </div>

{% endblock %}

现在,打开浏览器,导航至http://localhost:8000/password-change/。您应该会看到这样的密码更改页面:

更改您的密码,成功后,您将获得如下页面:



为 Djangobin 构建配置文件页面

原文:https://overiq.com/django-1-11/building-profile-pages-for-djangobin/

最后更新于 2020 年 7 月 27 日


创建设置视图

设置视图将允许登录用户更改首选项,如默认语言、过期和暴露。它还允许用户更改配置文件可见性选项。

概要视图(我们接下来将创建)以相反的时间顺序(即最新的第一个)显示了用户创建的代码片段的分页列表。默认情况下,配置文件页面是公共的。

所有这些设置都是由Author模型定义的。以下是它的外观,供您参考:

djangobin/django _ project/djangobin/models . py

#...
class Author(models.Model):
    user = models.OneToOneField(User, related_name='profile')
    default_language = models.ForeignKey(Language, on_delete=models.CASCADE,
                                         default=get_default_language)
    default_exposure = models.CharField(max_length=10, choices=Pref.exposure_choices,
                                        default=Pref.SNIPPET_EXPOSURE_PUBLIC)
    default_expiration = models.CharField(max_length=10, choices=Pref.expiration_choices,
                                        default=Pref.SNIPPET_EXPIRE_NEVER)
    private = models.BooleanField(default=False)
    views = models.IntegerField(default=0)
    #...

让我们从在forms.py中添加名为SettingForm的类开始,如下所示:

djangobin/django _ project/djangobin/forms . py

#...
class SettingForm(forms.ModelForm):    

    class Meta:
        model = Author
        fields = ('default_language', 'default_expiration' , 'default_exposure' , 'private')
        widgets = {
            'default_language': forms.Select(attrs={'class': 'selectpicker foo form-control',
                                                    'data-live-search': 'true',
                                                    'data-size': '5'}),
            'default_expiration': forms.Select(attrs={'class': 'selectpicker form-control'}),
            'default_exposure': forms.Select(attrs={'class': 'selectpicker form-control'})

        }

views.py中,在activate_account()视图的正下方添加settings()视图,如下所示:

djangobin/django_project/djangobin/views.py

#...
from .forms import SnippetForm, ContactForm, LoginForm, CreateUserForm, SettingForm
#...

@login_required
def settings(request):
    user = get_object_or_404(User, id=request.user.id)
    if request.method == 'POST':
        f = SettingForm(request.POST, instance=user.profile)
        if f.is_valid():
            f.save()
            messages.add_message(request, messages.INFO, 'Settings Saved.')
            return redirect(reverse('djangobin:settings'))

    else:
        f = SettingForm(instance=user.profile)

    return render(request, 'djangobin/settings.html', {'form': f})

settings.html模板的代码如下:

djangobin/django _ project/djangobin/templates/djangobin/settings . html

{% extends 'djangobin/base.html' %}

{% load static %}
{% load humanize %}

{% block title %}
    Account Preferences - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="row">

        <div class="col-lg-6 col-md-6 col-sm-6">

            <h4>Change Account Preferences</h4>

            <hr>

            {% if messages %}
                {% for message in messages %}
                    <p class="alert alert-info">{{ message }}</p>
                {% endfor %}
            {% endif %}

            <form action="" method="post" class="form-horizontal" >
                {% csrf_token %}

                <table class="table">
                    {{ form.as_table }}
                    <tr>
                        <td>&nbsp;</td>
                        <td><button type="submit" class="btn btn-primary">Submit</button></td>
                    </tr>
                </table>

            </form>

        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="">My Pastes</a> <br>
                <a href="">Account Details</a> <br>
                <a href="/password-change/">Change Password.</a> <br>
            </p>
        </div>

    </div>

{% endblock %}

将名为settings的新网址模式添加到urls.py中,如下所示:

决哥/决哥 _ 项目/决哥/URL . py】

#...
urlpatterns = [
    #...
    url(r'password-reset-complete/$',
        auth_views.password_reset_complete,
        {'template_name':'djangobin/password_reset_complete.html'},
        name='password_reset_complete'),

    url('^settings/$', views.settings, name='settings'),
]

最后,将设置页面的链接添加到base.html,如下所示:

决哥/决哥 _ project/决哥/样板/决哥/base.html

{# ... #}
                    {% if request.user.is_authenticated %}
                        <ul class="dropdown-menu">
                            <li><a href="">My Pastes</a></li>
                            <li><a href="">Account Details</a></li>
                            <li><a href="{% url 'djangobin:settings' %}">Settings</a></li>
                            <li role="separator" class="divider"></li>
                            <li><a href="{% url 'djangobin:logout' %}">Logout</a></li>
                        </ul>
                    {% else %}
                        <ul class="dropdown-menu">
                            <li><a href="{% url 'djangobin:signup' %}">Sign Up</a></li>
                            <li><a href="{% url 'djangobin:login' %}?next={{ request.path }}">Login</a></li>
                        </ul>
                    {% endif %}
{# ... #}

登录 Djangobin,访问http://localhost:8000/settings/。您应该会看到“帐户首选项”页面,如下所示:

选择所需的首选项,然后点击提交。您应该会看到如下消息:

创建纵断面图

概要视图以相反的时间顺序(即最新的第一个)显示用户创建的代码片段的分页列表。如果用户的配置文件是私有的,那么它的用户名将不会出现在趋势代码片段页面和标签列表页面中。

打开views.py并更新profile()查看功能如下:

djangobin/django_project/djangobin/views.py

from django.shortcuts import (HttpResponse, render, redirect,
                        get_object_or_404, reverse, get_list_or_404, Http404)
#...

def profile(request, username):
    user = get_object_or_404(User, username=username)

    # if the profile is private and logged in user is not same as the user being viewed,
    # show 404 error
    if user.profile.private and request.user.username != user.username:
        raise Http404

    # if the profile is not private and logged in user is not same as the user being viewed,
    # then only show public snippets of the user
    elif not user.profile.private and request.user.username != user.username:
        snippet_list = user.snippet_set.filter(exposure='public')
        user.profile.views += 1
        user.profile.save()

    # logged in user is same as the user being viewed
    # show everything
    else:
        snippet_list = user.snippet_set.all()

    snippets = paginate_result(request, snippet_list, 5)

    return render(request, 'djangobin/profile.html',
                  {'user' : user, 'snippets' : snippets } )

视图功能是这样工作的:

  1. 该视图接受一个名为username的额外参数,该参数将来自网址。

  2. 在第 7 行,我们使用get_object_or_404()获取相关的User对象。如果没有找到匹配的对象,get_object_or_404()将返回一个 HTTP 404 错误。

  3. 在第 11 行,我们正在检查配置文件是否是私有的,并且登录的用户是否与正在查看的用户相同。如果条件为真,我们显示一个 HTTP 404 错误;否则,程序控制转移到第 16 行的 elif 语句。

  4. 在第 16 行,我们正在检查配置文件是否不是私有的,并且登录的用户是否与正在查看的用户不同。如果条件为真,我们检索用户的公共片段,并将概要视图计数增加 1;否则,程序控制转移到第 23 行的 else 语句。

  5. 如果程序控制进入 else 语句,那么这意味着登录的用户与正在查看的用户相同。结果,我们获取了用户的所有片段。

  6. 在第 26 行,我们调用paginate_result()来获取分页结果。最后,在第 28 行,我们通过调用render()函数来渲染带有上下文数据的模板。

profile.html的代码如下:

djangobin/django _ project/djangobin/templates/djangobin/profile . html

{% extends 'djangobin/base.html' %}

{% load static %}
{% load humanize %}

{% block main %}

    <div class="media post-meta">
        <div class="media-left">
            <a href="#">
                <img alt="64x64" class="media-object" data-src="holder.js/64x64" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+PCEtLQpTb3VyY2UgVVJMOiBob2xkZXIuanMvNjR4NjQKQ3JlYXRlZCB3aXRoIEhvbGRlci5qcyAyLjYuMC4KTGVhcm4gbW9yZSBhdCBodHRwOi8vaG9sZGVyanMuY29tCihjKSAyMDEyLTIwMTUgSXZhbiBNYWxvcGluc2t5IC0gaHR0cDovL2ltc2t5LmNvCi0tPjxkZWZzPjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+PCFbQ0RBVEFbI2hvbGRlcl8xNjFkYzE3YWQ0ZiB0ZXh0IHsgZmlsbDojQUFBQUFBO2ZvbnQtd2VpZ2h0OmJvbGQ7Zm9udC1mYW1pbHk6QXJpYWwsIEhlbHZldGljYSwgT3BlbiBTYW5zLCBzYW5zLXNlcmlmLCBtb25vc3BhY2U7Zm9udC1zaXplOjEwcHQgfSBdXT48L3N0eWxlPjwvZGVmcz48ZyBpZD0iaG9sZGVyXzE2MWRjMTdhZDRmIj48cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIGZpbGw9IiNFRUVFRUUiLz48Zz48dGV4dCB4PSIxMi4xNzk2ODc1IiB5PSIzNi41Ij42NHg2NDwvdGV4dD48L2c+PC9nPjwvc3ZnPg==" data-holder-rendered="true" style="width: 64px; height: 64px;">
            </a>
        </div>
        <div class="media-body">
            <h4 class="media-heading">{{ user.username|capfirst }}'s Pastes</h4>
            <p>
                <i class="fas fa-calendar-alt" data-toggle="tooltip" title="" data-original-title="Account creation date" ></i> {{ user.date_joined }} &nbsp;
                <i class="fas fa-eye" data-toggle="tooltip" title="" data-original-title="Visits to this page"></i> {{ user.profile.views }} &nbsp;
            </p>
        </div>
    </div>

    <table class="table">
        <thead>
        <tr>
            <th>Title</th>
            <th>Date</th>
            <th>Hits</th>
            <th class="hidden-md">Expires</th>
            <th class="hidden-md">Exposure</th>
            <th class="hidden-md">Language</th>
        </tr>
        </thead>
        <tbody>
        {% for snippet in snippets %}
            <tr>
                <td><i class="fas fa-globe"></i>
                    <a href="{{ snippet.get_absolute_url }}">{{ snippet.title|default:"Untitled" }}</a>
                </td>
                <td title="{{ snippet.created_on }}">{{ snippet.created_on|naturaltime }}</td>
                <td>{{ snippet.hits }}</td>
                <td>{{ snippet.expiration|capfirst }}</td>
                <td>{{ snippet.exposure|capfirst }}</td>
                <td><a href="{{ snippet.language.get_absolute_url }}">{{ snippet.language }}</a></td>
            </tr>
        {% endfor %}
        </tbody>
    </table>

    {#  display pagination links  #}
    {% if snippets.paginator.num_pages > 1 %}
        <nav aria-label="...">
            <ul class="pager">

                <li>Page {{ snippets.number }} of {{ snippets.paginator.num_pages }}</li>

                {% if snippets.has_previous %}
                    <li><a href="?page={{ snippets.previous_page_number }}">Previous</a></li>
                {% endif %}

                {% if snippets.has_next %}
                    <li><a href="?page={{ snippets.next_page_number }}">Next</a></li>
                {% endif %}
            </ul>
        </nav>
    {% endif %}

{% endblock %}

接下来,在base.html模板中添加到纵断面图的链接,如下所示:

决哥/决哥 _ project/决哥/样板/决哥/base.html

{# ... #}
            <ul class="nav navbar-nav navbar-right">
                <li class="dropdown">
                    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
                       aria-haspopup="true" aria-expanded="false">
                        {% if request.user.is_authenticated %}
                            {{ request.user.username|upper }}
                        {% else %}
                            GUEST
                        {% endif %}
                        <span class="caret"></span>
                    </a>
                    {% if request.user.is_authenticated %}
                        <ul class="dropdown-menu">
                            <li><a href="{% url 'djangobin:profile' request.user %}">My Pastes</a></li>
                            <li><a href="{% url 'djangobin:user_details' %}">Account Details</a></li>
                            <li><a href="{% url 'djangobin:settings' %}">Settings</a></li>
                            <li role="separator" class="divider"></li>
                            <li><a href="{% url 'djangobin:logout' %}">Logout</a></li>
                        </ul>
                    {% else %}
                        <ul class="dropdown-menu">
                            <li><a href="">Sign Up</a></li>
                            <li><a href="{% url 'djangobin:login' %}?next={{ request.path }}">Login</a></li>
                        </ul>
                    {% endif %}
                </li>
            </ul>
{# ... #}

要查看登录到 Djangobin 的配置文件页面,请单击屏幕右上角的下拉列表,然后选择我的粘贴。

您应该会看到登录用户的配置文件页面,如下所示:

要查看其他用户的配置文件页面,请访问趋势片段页面或标签列表页面,然后单击“用户”列中列出的用户名。

请注意,趋势片段和标签列表页面中的配置文件链接仅在用户配置文件非私有时才会出现。如果配置文件是私有的,那么你会看到一个破折号(-)代替用户名。

为视图创建装饰器

一切看起来都很好,但实际上,我们的应用受到了关于私人片段的主要隐私问题的困扰。

我们希望私有代码片段只能由创建它们的用户访问(在他们登录之后)。然而,就目前的情况来看,如果你知道私有代码片段的网址,那么无论你是否创建了它,你都可以访问它们。换句话说,私有代码片段被视为未列出的代码片段(请记住,未列出的代码片段是不出现在网站任何页面上的代码片段。只有链接到代码片段的用户才能访问它)。

您可以通过登录到 Djangobin 并创建一个私有片段来验证这一点。复制代码片段的网址,在“私人”或“匿名”模式下打开一个新的浏览器窗口,粘贴网址,你应该可以查看代码片段。

这个问题不仅仅局限于查看片段。用户还可以查看原始片段并下载它们。

起初你可能会想,这样做可以解决问题:

def snippet_detail(request, snippet_slug):
    snippet = get_object_or_404(Snippet, slug=snippet_slug)

    # if the snippet is private and snippet's creator is not the same as logged in user, 
    # show HTTP 404 error
    if snippet.exposure == 'private' and snippet.user != request.user:
        raise Http404

    snippet.hits += 1
    snippet.save()
    return render(request, 'djangobin/snippet_detail.html', {'snippet': snippet,})

当然,这是解决问题的可能方法之一。但是如果你走这条路,你将不得不在所有其他视图中一遍又一遍地复制和粘贴相同的代码。我们可以做得更好——装修工人来救援。

装饰器是特殊的函数,它以另一个函数作为参数,并扩展原始函数而不显式修改它。您可以将装饰器视为您的函数的守卫,在调用原始函数之前,装饰器运行,这使得它们适合于执行需要在调用原始函数之前完成的任务(或测试)。

我们使用login_required装饰机已经有一段时间了,没有太在意。

@login_required
def my_view(request):
    #...
    pass

以下是当您访问与my_view功能相关联的网址时会发生的情况。

调用login_required()装饰器检查用户是否登录。如果用户未登录,则将用户重定向至/account/login/(或LOGIN_URL设置指定的网址);否则,将照常调用my_view()函数。

用装修师的基本概念下外带。我们现在将创建我们自己的定制装饰器。

models.py旁边新建一个名为decorators.py的文件,并添加以下代码:

djangobin/django _ project/djangobin/decorators . py

from functools import wraps
from django.shortcuts import Http404, get_object_or_404
from django.contrib.auth import get_user_model
from .models import Language, Snippet

def private_snippet(func):
    def wrapper(request, *args, **kwargs):        
        snippet = Snippet.objects.get(slug=kwargs.get('snippet_slug'))
        if snippet.exposure == 'private' and request.user != snippet.user:
            raise Http404
        return func(request, *args, **kwargs)
    return wrapper

这里我们定义了一个名为private_snippet的装饰器。它检查代码片段暴露是否是私有的,并且登录的用户是否与代码片段创建者相同。如果是,则调用 view 函数;否则,它会显示一个 HTTP 404 错误。

现在修改,views.py使用private_snippet装饰器,如下所示:

djangobin/django_project/djangobin/views.py

#...
from .forms import SnippetForm, ContactForm, LoginForm, CreateUserForm, SettingForm
from .models import Language, Snippet, Tag
from .utils import paginate_results
from .decorators import private_snippet

#...

@private_snippet
def snippet_detail(request, snippet_slug):
    snippet = get_object_or_404(Snippet, slug=snippet_slug)
    snippet.hits += 1
    snippet.save()
    return render(request, 'djangobin/snippet_detail.html', {'snippet': snippet,})

@private_snippet
def download_snippet(request, snippet_slug):
    snippet = get_object_or_404(Snippet, slug=snippet_slug)
    file_extension = snippet.language.file_extension
    filename = snippet.slug + file_extension
    res = HttpResponse(snippet.original_code)
    res['content-disposition'] = 'attachment; filename=' + filename + ";"
    return res

@private_snippet
def raw_snippet(request, snippet_slug):
    snippet = get_object_or_404(Snippet, slug=snippet_slug)
    return HttpResponse(snippet.original_code, content_type=snippet.language.mime)

#...

如果您现在试图查看或下载另一个用户的私人代码片段,您将会得到一个 HTTP 404 错误。

使用首选项初始化 SnippetForm

设置视图就位后,用户现在可以设置他们的首选项。但是就目前的情况来看,每次你创建一个新的代码片段,你必须从languageexpirationexposure字段中选择值。如果大多数时候你创建了永不过期的公共片段,那么自动将expirationexposure字段分别初始化为NeverPublic是有意义的。

我想提醒你注意的另一点是Exposure字段的选项。目前Exposure下拉菜单显示三个选项:PublicUnlistedPrivate。无论用户是否登录,这些选项始终显示。除非用户登录,否则他不能创建私人片段,因此向访客用户显示Private选项是完全没有意义的。此外,作为访客创建私有片段没有意义,因为您将无法访问它们。更好的方法是仅在用户登录时显示Private选项。这样,我们就能够将私有代码片段与登录的用户相关联。

要进行这些更改,我们必须更改表单的初始化方式。打开form.py文件并覆盖SnippetForm__init__()方法,如下所示:

djangobin/django _ project/djangobin/forms . py

#...
from .utils import Preference as Pref, get_current_user
#...

class SnippetForm(forms.ModelForm):
    #...

    class Meta:
        #...

    # override default __init__ so we can set the user preferences
    def __init__(self, request, *args, **kwargs):
        super(SnippetForm, self).__init__(*args, **kwargs)

        if request.user.is_authenticated:
            self.fields['exposure'].choices = Pref.exposure_choices
            self.initial = request.user.profile.get_preferences()
        else:
            self.fields['exposure'].choices = \
                [ (k, v) for k, v in Pref.exposure_choices if k != 'private' ]

            l = Language.objects.get(name='Plain Text')
            self.initial = {'language': l.id, 'exposure': 'public', 'expiration': 'never'}

request参数外,我们指定__init__()接受*args
T3。这只是皮德尼肯的一种说法,即__init__()可以接受任意数量的位置和关键字参数。

在第 14 行,我们正在调用父类的__init__()方法,这对于保留基础__init__()方法提供的功能是必要的。

在第 16 行,我们使用User对象的is_authenticated属性检查用户是否登录。如果用户已登录,我们会将所有曝光选项分配给exposure字段。

在下一行中,我们使用Author模型的get_preferences方法为languageexpirationexposure字段提供初始值。get_preferences()方法在Author模型中定义如下:

djangobin/django _ project/djangobin/models . py

class Author(models.Model):
    #...

    def get_snippet_count(self):
        return self.user.snippet_set.count()

    def get_preferences(self):
        return {'language': self.default_language.id, 'exposure': self.default_exposure,
                'expiration': self.default_expiration}

另一方面,如果用户没有登录。那么我们只给exposure字段提供两个选项:PublicUnlisted。最后,在第 24 行,我们为来宾用户设置初始值。

现在,修改index()视图以使用更新后的SnippetForm,如下所示:

djangobin/django_project/djangobin/views.py

#...
def index(request):
    if request.method == 'POST':
        f = SnippetForm(request, request.POST)

        if f.is_valid():
            snippet = f.save(request)
            return redirect(reverse('djangobin:snippet_detail', args=[snippet.slug]))

    else:
        f = SnippetForm(request)
    return render(request, 'djangobin/index.html', {'form': f} )

#...

要查看我们的劳动成果,打开浏览器,导航至http://localhost:8000/。您将看到“语言”、“过期”和“暴露”字段预先填充了如下数据:

如果您已经登录,那么这些字段将根据您的偏好进行填充。

删除片段

在本节中,我们将添加一个视图函数来删除片段。

打开views.py,在profile()视图的正下方添加一个名为delete_snippet()的视图,如下所示:

djangobin/django_project/djangobin/views.py

#...

def profile(request, username):
    #....

@login_required
def delete_snippet(request, snippet_slug):
    snippet = get_object_or_404(Snippet, slug=snippet_slug)
    if not snippet.user == request.user:
        raise Http404
    snippet.delete()
    return redirect('djangobin:profile', request.user)

视图功能工作如下:

  1. delete_snippet()接受两个参数requestsnippet_slug
  2. 在第 8 行,我们尝试使用get_object_or_404()函数检索Snippet对象。如果没有找到匹配的片段,get_object_or_404()将返回 HTTP 404 错误。
  3. 在第 9 行,我们测试登录的用户是否是代码片段作者。如果没有,我们显示一个 HTTP 404 错误,否则,我们使用delete()方法删除代码片段(第 11 行)。
  4. 最后,在第 12 行,我们将登录的用户重定向到配置文件页面。

接下来,添加一个新的网址模式来删除urls.py中的片段,如下所示:

决哥/决哥 _ 项目/决哥/URL . py】

#...

urlpatterns = [
    #...
    url(r'^password-change-done/$', auth_views.password_change_done,
        {'template_name': 'djangobin/password_change_done.html'},
        name='password_change_done'
        ),

    url('^settings/$', views.settings, name='settings'),
    url('^delete/(?P<snippet_slug>[\d]+)/$', views.delete_snippet, name='delete_snippet'),
]

现在,让我们在代码片段详细信息页面中添加一个删除代码片段的链接:

djangobin/django _ project/djangobin/templates/djangobin/snippet _ detail . html

{# ... #}
    <div class="toolbar clearfix">
            <span class="at-left"><a href="{% url 'djangobin:trending_snippets' snippet.language.slug %}">{{ snippet.language }}</a></span>
            <span class="at-right">
                {% if snippet.user == request.user %}
                    <a onclick="return confirm('Sure you want to delete this paste? ')"
                   href="{% url 'djangobin:delete_snippet' snippet.slug %}">delete</a>
                {% endif %}
                <a href="{% url 'djangobin:raw_snippet' snippet.slug %}">raw</a>
                <a href="{% url 'djangobin:download_snippet' snippet.slug %}">download</a>
            </span>
        </div>
{# ... #}

我们希望只向创建代码片段的用户显示删除链接。在第 5 行,我们使用{% if %}标签来检查我们检查登录的用户是否是代码片段作者。如果是这样,我们显示链接来删除代码片段。

重访联系人表单

我们的联系人表单由四个字段组成(姓名、电子邮件、目的和消息),并且都是必填字段。

但是,询问登录用户的姓名和电子邮件是不可取的。如果用户登录,我们最好隐藏这些字段,并在发送电子邮件时自动填充它们。

请注意,只是隐藏表单中的字段并不会使它们成为可选的。我们必须动态地将字段的required属性设置为False,使其成为可选的。

为此,我们将覆盖Form类的__init__()方法。

打开forms.py文件,修改ContactForm类,如下所示:

djangobin/django _ project/djangobin/forms . py

class ContactForm(forms.Form):
    #...
    name = forms.CharField()
    email = forms.EmailField()
    purpose = forms.ChoiceField(choices=purpose_choices)
    message = forms.CharField(widget=forms.Textarea(attrs={'cols': 40, 'rows': 5}))

    def __init__(self, request, *args, **kwargs):
        super(ContactForm, self).__init__(*args, **kwargs)
        if request.user.is_authenticated:
            self.fields['name'].required = False
            self.fields['email'].required = False

在第 10 行,我们测试用户是否通过了身份验证。如果是这样,我们通过将名称和电子邮件字段的required属性设置为False,使其成为可选字段。

接下来,修改contact.html如下:

djangobin/django _ project/djangobin/templates/djangobin/contact . html

{# ... #}
    <form method="post">

        {% csrf_token %}

        {% if not request.user.is_authenticated %}

            <div class="form-group row">
                <div class="col-lg-5">
                    {{ form.name.errors }}
                    {{ form.name.label_tag }}
                    {{ form.name }}
                </div>
            </div>

            <div class="form-group row">
                <div class="col-lg-5">
                    {{ form.email.errors }}
                    {{ form.email.label_tag }}
                    {{ form.email }}
                </div>
            </div>

        {% endif %}

        <div class="form-group row">
            <div class="col-lg-5">
                {{ form.purpose.errors }}
                {{ form.purpose.label_tag }}
                {{ form.purpose }}
            </div>
        </div>
{# ... #}

在第 6-24 行中,我们使用{% if %}标签仅在用户未登录时显示姓名和电子邮件字段。

最后,修改contact()视图如下:

djangobin/django_project/djangobin/views.py

#...
def contact(request):
    if request.method == 'POST':
        f = ContactForm(request, request.POST)
        if f.is_valid():

            if request.user.is_authenticated:
                name = request.user.username
                email = request.user.email
            else:
                name = f.cleaned_data['name']
                email = f.cleaned_data['email']

            subject = "You have a new Feedback from {}:<{}>".format(name, email)

            message = "Purpose: {}\n\nDate: {}\n\nMessage:\n\n {}".format(
                dict(f.purpose_choices).get(f.cleaned_data['purpose']),
                datetime.datetime.now(),
                f.cleaned_data['message']
            )

            send_feedback_mail.delay(subject, message)

            messages.add_message(request, messages.INFO, 'Thanks for submitting your feedback.')

            return redirect('djangobin:contact')

    else:
        f = ContactForm(request)

    return render(request, 'djangobin/contact.html', {'form': f})
#...

在第 10 行,我们测试用户是否通过了身份验证。如果是,我们使用request.name对象设置姓名和电子邮件。否则,我们使用表单对象的cleaned_data属性设置名称和电子邮件。

如果您现在登录后访问联系人表单。您应该会看到这样一个表单:

选择目的,输入您的信息,然后点击提交。在运行服务器的 shell 中,您通常会得到如下输出:

[17/Jun/2018 06:18:07] "GET /contact/ HTTP/1.1" 200 9406
[17/Jun/2018 06:18:16] "GET /contact/ HTTP/1.1" 200 9939
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: [Django] You have a new Feedback from noisyboy:<noisyboy@mail.com>
From: infooveriq@gmail.com
To: admin@overiq.com
Date: Sun, 17 Jun 2018 06:20:30 -0000
Message-ID: <20180617062030.13043.69352@pc>

Purpose: Feedback

Date: 2018-06-17 06:20:30.243083

Message:

Great tool!
-------------------------------------------------------------------------------



搜索片段

原文:https://overiq.com/django-1-11/searching-snippets/

最后更新于 2020 年 7 月 27 日


在本课中,我们将添加一个视图,允许用户通过关键字搜索片段。

让我们从创建一个搜索表单开始。

forms.py中,在文件末尾定义SearchForm类,如下所示:

djangobin/django _ project/djangobin/forms . py

#...

class SearchForm(forms.Form):
    query = forms.CharField(widget=forms.TextInput(attrs={'class': 'form-control',
                                                              'placeholder': 'Search'}))
    mysnippet = forms.BooleanField(required=False)

query字段是用户输入搜索词的地方。mysnippet字段仅对登录的用户可见。如果选中,它只允许用户搜索他的片段。

接下来,在profile()视图正下方的views.py中添加一个名为search()的新视图,如下所示:

djangobin/django_project/djangobin/views.py

#...
from django.db.models import Q
from .forms import SnippetForm, ContactForm, LoginForm, CreateUserForm, \
            SettingForm, SearchForm
#...

def search(request):
    f = SearchForm(request.GET)
    snippets = []

    if f.is_valid():

        query = f.cleaned_data.get('query')
        mysnippets = f.cleaned_data.get('mysnippet')

        # if mysnippet field is selected, search only logged in user's snippets
        if mysnippets:
            snippet_list = Snippet.objects.filter(
                Q(user=request.user),
                Q(original_code__icontains=query) | Q(title__icontains=query)
            )

        else:
            qs1 = Snippet.objects.filter(
                Q(exposure='public'),
                Q(original_code__icontains = query) | Q(title__icontains = query)
                # Q(user=request.user)
            )

            # if the user is logged in then search his snippets
            if request.user.is_authenticated:
               qs2 = Snippet.objects.filter(Q(user=request.user),
                                            Q(original_code__icontains=query) | Q(title__icontains=query))
               snippet_list = (qs1 | qs2).distinct()

            else:
                snippet_list = qs1

        snippets = paginate_result(request, snippet_list, 5)

    return render(request, 'djangobin/search.html', {'form': f, 'snippets': snippets })

该视图功能的工作原理如下:

  1. 在第 9 行中,我们通过将request.GET数据传递给表单构造器来实例化一个SearchForm实例。我们一开始将数据绑定到表单的原因是,只有当用户在页面顶部的搜索框中提交查询时,才会调用search()函数。此外,查询是使用 GET 请求提交的,这允许用户根据自己的意愿为搜索添加书签。

  2. 在第 12 行,我们使用is_valid()方法来确定表单是否有效。如果表单无效,我们渲染一个没有任何数据的空表单;否则,操作过程取决于表单的提交方式。有两种可能的情况:

如果提交表单时勾选了mysnippet字段,那么这将触发以下 If 块的执行。

#...
        # if mysnippet field is selected, search only logged in user's snippets
        if mysnippets:
            snippet_list = Snippet.objects.filter(
                Q(user=request.user),
                Q(original_code__icontains=query) | Q(title__icontains=query)
            )
#...

另一方面,如果提交的表单没有勾选mysnippet字段,那么这将触发 else 块的执行。

#...
        else:
            qs1 = Snippet.objects.filter(
                Q(exposure='public'),
                Q(original_code__icontains = query) | Q(title__icontains = query)
                # Q(user=request.user)
            )

            # if the user is logged in then search his snippets
            if request.user.is_authenticated:
               qs2 = Snippet.objects.filter(Q(user=request.user),
                                            Q(original_code__icontains=query) | Q(title__icontains=query))
               snippet_list = (qs1 | qs2).distinct()

            else:
                snippet_list = qs1
#...

如果用户登录,我们会创建一个新的 queryset,其中包含用户创建的代码片段。查询结果qs1qs2可能包含重复的结果。要删除重复项,请使用|(按位或)运算符组合两个查询集,然后对结果查询集应用distinct()方法。我们现在得到了独特的结果。

在第 40 行,我们调用paginate_result()来获取分页结果。

最后,在第 42 行,我们渲染模板。

将名为search的网址模式添加到urls.py中,如下所示:

决哥/决哥 _ 项目/决哥/URL . py】

#...
urlpatterns = [
    #...
    url('^delete/(?P<snippet_slug>[\d]+)/$', views.delete_snippet, name='delete_snippet'),
    url('^search/$', views.search, name='search'),
]

创建一个名为search.html的模板,并添加以下代码:

djangobin/django _ project/djangobin/templates/djangobin/search . html

{% extends 'djangobin/base.html' %}

{% block title %}
    {{ request.GET.query }} - {{ block.super }}
{% endblock %}

{% block main %}

    <form action="" class="form-inline">

        <div class="form-group">
            {{ form.query }}
        </div>

        {% if request.user.is_authenticated %}
            <div class="checkbox">
                <label>
                    {{ form.mysnippet }} Only search my snippets.
                </label>
            </div>
        {% endif %}

        <button type="submit" class="btn btn-primary">Search</button>
    </form>

    <hr>

    {% for snippet in snippets %}

        {% if forloop.first %}
            <h5>{{ snippets.paginator.count }} record{{ snippets.paginator.count|pluralize }} found.</h5>
            <hr>
        {% endif %}

        <h4><a href="{{ snippet.get_absolute_url }}">{{ snippet.title }}</a></h4>
        <p>{{ snippet.original_code|truncatechars:250 }}</p>
        <hr>

    {% empty %}
        <h5>No records found.</h5>
    {% endfor %}

    {% if snippets.paginator.num_pages > 1 %}
        <nav aria-label="...">
            <ul class="pager">

                <li>Page {{ snippets.number }} of {{ snippets.paginator.num_pages }}</li>

                {% if snippets.has_previous %}
                    <li><a href="?query={{ request.GET.query }}&page={{ snippets.previous_page_number }}">Previous</a></li>
                {% endif %}

                {% if snippets.has_next %}
                    <li><a href="?query={{ request.GET.query }}&page={{ snippets.next_page_number }}">Next</a></li>
                {% endif %}
            </ul>
        </nav>
    {% endif %}

{% endblock %}

接下来,更新<form>元素base.html如下:

{# ... #}
                <li {% if request.path == '/about/' %}class='active'{% endif %}>
                    <a href="">About</a>
                </li>
                <li {% if request.path == '/contact/' %}class='active'{% endif %}>
                    <a href="{% url 'djangobin:contact' %}">Contact</a>
                </li>
            </ul>

            <form action="{% url 'djangobin:search' %}" class="navbar-form navbar-left" method="get">
                <div class="form-group">
                    <input type="text" name="query" class="form-control"
                           placeholder="Search" value="{{ request.GET.query }}">
                </div>
            </form>

            <ul class="nav navbar-nav navbar-right">
                <li class="dropdown">
                    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
                       aria-haspopup="true" aria-expanded="false">
                        {% if request.user.is_authenticated %}
                            {{ request.user.username|upper }}
                        {% else %}
{# ... #}

现在,打开浏览器,导航至http://localhost:8000/。在页面顶部的搜索框中输入查询,然后按 Enter 键。您应该会看到如下搜索结果:

如果您已经登录,您将看到搜索框旁边的mysnippet选择框,如下所示:

提交勾选了mysnippet字段的表单时,搜索结果将仅限于您的片段。



Celery 异步任务

原文:https://overiq.com/django-1-11/asynchronous-tasks-with-celery/

最后更新于 2020 年 7 月 27 日


Celery 是什么

Django 是为短期请求设计的。因此,如果您需要执行一个长时间运行的操作,您应该总是在请求-响应周期之外执行它。

以下是长期运行操作的一些示例:

  • 发送电子邮件或群发邮件
  • 转码媒体文件
  • 图像处理
  • 生成报告

等等。

试图在请求-响应周期内执行这些操作将显著增加 HTTP 响应的等待时间和大小。这最终可能会把用户从网站上赶走。此外,这些操作也会失败。出于这个原因,我们可能需要一个重试策略。

解决方案是维护一个长时间运行的操作队列,并异步启动它们。

幸运的是,我们有一个很棒的包装叫 Celery,它可以为我们提供所有的照明。

引用 Celery 文档:

Celery 是一个基于分布式消息传递的异步任务队列。它专注于实时操作,但也支持调度。

除了运行异步任务,Celery 还为您提供了定期执行任务的选项。

要安装 Celery,请执行以下命令:

$ pip install celery

接下来,您需要一个消息代理。代理是将任务存储为队列的地方。代理将这些消息发送给 Celery 工人,然后 Celery 工人执行任务并提供响应。默认情况下,Celery 使用 RabbitMQ 作为消息代理。要安装 RabbitMQ,请执行以下命令:

$ sudo apt-get install rabbitmq-server

安装后,rabbitmq-server 将自动启动。您可以使用以下命令检查 rabbitmq 服务器的状态:

$ sudo service rabbitmq-server status

如果由于某种原因 rabbtmq-server 没有自动启动,请在 shell 中键入以下命令:

$ sudo service rabbitmq-server start

要停止 rabbitmq 服务器,请使用以下命令:

$ sudo service rabbitmq-server stop

Celery 与 Django 的融合

要将 Celery 添加到 Django 项目中,我们必须首先创建一个Celery应用实例以及一些配置。

在 Django 配置目录中的settings.py旁边创建一个名为celery.py的新文件,并向其中添加以下代码:

djangobin/django _ project/django _ project/Celery. py

from __future__ import absolute_import, unicode_literals
import os
from celery import Celery

# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')

app = Celery('djangobin')

# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
#   should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')

# Load task modules from all registered Django app configs.
app.autodiscover_tasks()

在第 1-3 行,我们正在导入必要的功能和模块。

在第 6 行,我们为celery命令行应用设置DJANGO_SETTINGS_MODULE环境变量。

在第 8 行,我们通过传递 Django 应用的名称来创建Celery的实例。

在第 14 行,我们从项目的settings.py文件中加载任何自定义配置。

最后,在第 17 行,我们告诉 Celery 从INSTALLED_APPS设置中列出的应用中自动发现任务。有了这条线,Celery 将在每个安装的应用中寻找一个名为tasks.py的模块来加载任务。

接下来,我们必须在每次 Django 启动时加载Celery实例。为此,在 Django 配置目录中的__init__.py文件中添加以下行。

djangobin/django _ project/django _ project/_ _ init _ _。py

from __future__ import absolute_import, unicode_literals

# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app

__all__ = ['celery_app']

现在,我们准备定义一些任务。

创建异步任务

在本节中,我们将创建发送电子邮件的异步任务。

从目前的情况来看,我们的应用中有三个发送电子邮件的地方。

  • 联系方式
  • 密码重置
  • 帐户激活

联系人表单和账户激活使用mail_admins()send_mail()功能发送邮件。这两个函数都是同步函数,这意味着它们将阻止程序的执行,直到它们完成工作。

但是,如果您已经彻底使用了该应用,您会发现我们根本没有遇到任何延迟。这是因为我们目前使用的是控制台后端,如果我们切换到 SMTP 后端,延迟会很大。

密码重置机制也使用send_mail()功能发送重置密码链接,但是为了保持简单易懂,我们不会更改它。

现在让我们定义一些任务。

djangobin应用目录中创建新文件tasks.py,并添加以下代码:

djangobin/django _ project/djangobin/tasks . py

from celery import task
from django.template.loader import render_to_string
from django.contrib.auth.models import User
from django.core.mail import BadHeaderError, send_mail, mail_admins
from django.contrib.auth.tokens import default_token_generator
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.conf import settings

@task
def send_activation_mail(user_id, context):

    user = User.objects.get(id=user_id)

    context.update({
        'username': user.username,
        'uid': urlsafe_base64_encode(force_bytes(user.pk)),
        'token': default_token_generator.make_token(user),
    })

    subject = render_to_string('djangobin/email/activation_subject.txt', context)
    email = render_to_string('djangobin/email/activation_email.txt', context)

    send_mail(subject, email, settings.DEFAULT_FROM_EMAIL, [user.email])

任务只是一个使用@task装饰器定义的普通函数。send_activation_mail()功能接受user_idcontext参数,并使用send_mail()功能发送电子邮件。

传递给任务函数的参数被序列化并存储在代理中。Celery 的较新版本(从 4.0 开始)使用 JSON 作为默认序列化方法。JSON 序列化程序只能序列化简单的对象,如 int、string、list 等。它不能序列化复杂的对象,如模型实例、HttpRequest实例、字节流等等。

这就是为什么我们发送user_idsend_activation_mail()函数而不是User实例的原因。出于同样的原因,我们正在send_activation_mail()中创建一半的上下文数据,而不是直接从CreateUserForm表单的save()方法中传递。

注意:我们可以通过将默认序列化程序更改为 pickle 来将复杂对象传递给任务函数。事实上,在 Celery 的旧版本(4.0 之前)中,pickle 是默认的序列化程序。但是,由于一些安全漏洞,泡菜不是推荐的做事方式。

下一步是调用这个任务,我们使用delay()方法来完成。以下是save()方法的更新版本。

djangobin/django _ project/djangobin/forms . py

#...
from .tasks import send_activation_mail
#...

class CreateUserForm(UserCreationForm):
    #...

    def save(self, request):

        user = super(CreateUserForm, self).save(commit=False)
        user.is_active = False
        user.save()       

        context = {            
            'protocol': request.scheme,            
            'domain': request.META['HTTP_HOST'],            
        }

        send_activation_mail.delay(user.id, context) ## calling the task

        return user

delay()方法只将任务放在队列中。尽快执行是 Celery 工人的工作。

现在一切就绪,我们所需要的就是启动 Celery 工人。

在终端中,确保您当前的工作目录设置为项目根目录,然后通过键入以下命令启动 Celery 工作器:

$ celery -A django_project worker -l info

输出如下所示:

$ celery -A django_project worker -l info 
 -------------- celery@pc v4.1.0 (latentcall)
---- **** ----- 
--- * ***  * -- Linux-4.10.0-38-generic-x86_64-with-LinuxMint-18.3-sylvia 2018-04-24 07:05:26
-- * - **** --- 
- ** ---------- [config]
- ** ---------- .> app:         djangobin:0x7f39e8f00860
- ** ---------- .> transport:   amqp://guest:**@localhost:5672//
- ** ---------- .> results:     disabled://
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** ----- 
 -------------- [queues]
                .> celery           exchange=celery(direct) key=celery

[tasks]
  . djangobin.tasks.send_activation_mail

[2018-04-24 07:05:27,010: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2018-04-24 07:05:27,020: INFO/MainProcess] mingle: searching for neighbors
[2018-04-24 07:05:28,066: INFO/MainProcess] mingle: all alone
[2018-04-24 07:05:28,087: WARNING/MainProcess] /home/pp/djangobin/env/lib/python3.5/site-packages/celery/fixups/django.py:202: UserWarning: Using settings.DEBUG leads to a memory leak, never use this setting in production environments!
  warnings.warn('Using settings.DEBUG leads to a memory leak, never '
[2018-04-24 07:05:28,088: INFO/MainProcess] celery@pc ready.

Celery 工人现在已经开始工作了。如果尚未运行,启动 Django 开发服务器,并通过访问http://localhost:8000/signup/创建新用户。

在所有字段中输入数据,然后点击提交。在 shell 中,运行 Celery 工作器,您将获得如下输出:

[2018-05-12 04:21:11,012: INFO/MainProcess] Received task: djangobin.tasks.send_activation_mail[79849f4e-2a9b-44b1-bcb3-242ef180e2ef]  
[2018-05-12 04:21:11,096: WARNING/ForkPoolWorker-2] Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Action Required to Complete the Account Creation - djangobin
From: infooveriq@gmail.com
To: pypi@example.com
Date: Sat, 12 May 2018 04:21:11 -0000
Message-ID: <20180512042111.9675.41529@pc>

Hello pypi!

To confirm your registration, visit the following link:

http://localhost:8000/activate/MTU/4w4-8eac5c27a7e29b8190c9/

Welcome to Djanagobin!
[2018-05-12 04:21:11,097: WARNING/ForkPoolWorker-2] ---------------------------------------------------------

我们的异步任务已经执行。要激活用户,请将网址复制并粘贴到浏览器地址栏中。

让我们创建另一个任务,向站点管理员发送反馈电子邮件。打开tasks.py并在send_activation_mail()任务的正下方添加send_feedback_mail()任务,如下所示:

djangobin/django _ project/djangobin/tasks . py

#...

@task
def send_feedback_mail(subject, message):
    mail_admins(subject, message)

接下来,修改contact()视图使用send_feedback_mail()功能,如下所示:

djangobin/django_project/djangobin/views.py

#...
from .tasks import send_feedback_mail
#...

def contact(request):
    if request.method == 'POST':
        f = ContactForm(request.POST)
        if f.is_valid():

            name = f.cleaned_data['name']
            subject = "You have a new Feedback from {}:<{}>".format(name, f.cleaned_data['email'])

            message = "Purpose: {}\n\nDate: {}\n\nMessage:\n\n {}".format(
                dict(f.purpose_choices).get(f.cleaned_data['purpose']),
                datetime.datetime.now(),
                f.cleaned_data['message']
            )

            send_feedback_mail.delay(subject, message)

            messages.add_message(request, messages.INFO, 'Thanks for submitting your feedback.')

            return redirect('djangobin:contact')

    else:
        f = ContactForm()

    return render(request, 'djangobin/contact.html', {'form': f})

重启 Celery 工人,使更改生效,并访问http://localhost:8000/contact/

填写表格并点击提交。这一次,在 shell 中,运行 Celery 工人,您将获得如下输出:

[2018-05-12 04:30:26,682: WARNING/ForkPoolWorker-2] send_feedback_mail
[2018-05-12 04:30:26,691: WARNING/ForkPoolWorker-2] Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: [Django] You have a new Feedback from spike:<spike@example.com>
From: infooveriq@gmail.com
To: admin@overiq.com
Date: Sat, 12 May 2018 04:30:26 -0000
Message-ID: <20180512043026.10707.30774@pc>

Purpose: Feedback

Date: 2018-05-12 04:30:26.644238

Message:

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aut eius ex fuga laborum magni minima nulla quidem suscipit? Atque corporis esse explicabo facilis maxime odit quidem sit tempora velit vitae?
[2018-05-12 04:30:26,692: WARNING/ForkPoolWorker-2] 
-----------------------------------------------------------------------------------------

通过定期任务删除片段

在本节中,我们将添加一个定期任务,从数据库中删除过期的代码片段。

打开tasks.py并向文件末尾添加remove_snippets()功能,如下所示:

#...
from django.utils.http import urlsafe_base64_encode
from django.conf import settings
import datetime, pytz
from .models import Snippet
from .utils import Preference as Pref
#...

@task
def remove_snippets():

    # loop through all the snippets whose expiration is other than never.
    for s in Snippet.objects.exclude(expiration='never').order_by('id'):

        # get the creation time
        creation_time = s.created_on

        if s.expiration == Pref.SNIPPET_EXPIRE_1WEEK:
            tmdelta =  datetime.timedelta(days=7)
        elif s.expiration == Pref.SNIPPET_EXPIRE_1MONTH:
            tmdelta =  datetime.timedelta(days=30)
        elif s.expiration == Pref.SNIPPET_EXPIRE_6MONTH:
            tmdelta = datetime.timedelta(days=30*6)
        elif s.expiration == Pref.SNIPPET_EXPIRE_1YEAR:
            tmdelta = datetime.timedelta(days=30*12)

        # deletion_time is datetime.datetime    
        deletion_time = creation_time + tmdelta

        # now is datetime.datetime    
        now = datetime.datetime.now(pytz.utc)

        # diff is datetime.timedelta
        diff = deletion_time - now

        if diff.days == 0 or diff.days < 0:
            # it's time to delete the snippet
            s.delete()

  1. 该函数以 for 循环开始,该循环迭代其expiration属性包含除never以外的值的片段。

  2. 在第 19-26 行,我们使用if-elif语句创建一个datetime.timedelta对象。datetime.timedelta对象用于对datetime.datetimedatetime.date对象执行基本运算。如果我们给一个datetime.datetime对象添加一个datetime.timedelta对象,结果将是一个新的datetime.datetime对象。另一方面,如果我们减去两个datetime.datetime对象,结果将是一个datetime.timedelta

  3. 在第 29 行,我们通过添加片段创建时间和时间增量来计算片段到期时间。

  4. 第 32 行,我们使用datetime.datetime类的now()方法获取当前日期和时间。

  5. 在第 35 行,我们计算deletion_timenow之间的差值。这种差异产生了一个新的datetime.timedelta物体。如果datetime.timedeltadays属性是0或负数,那么是时候删除代码片段了,这正是我们在第 37 行的 If 语句中所做的。

我们定义了一个周期性任务。为了执行它,我们使用了一种叫做 Celery 拍的东西。Celery 节拍是一个调度器,它定期开始执行任务。

Celery 节拍从beat_schedule设置读取周期性任务。打开celery.py并定义beat_schedule设置如下:

from __future__ import absolute_import, unicode_literals
import os
from celery import Celery
from celery.schedules import crontab

#...

# Load task modules from all registered Django app configs.
app.autodiscover_tasks()

app.conf.beat_schedule = {
    'remove-snippets-daily-at-midnight': {
        'task': 'djangobin.tasks.remove_snippets',        
        'schedule': crontab(minute=0, hour=0),
    },
}

task字段是指要执行的任务名称,schedule字段是指执行频率。crontab(minute=0, hour=0)表示
remove_snippets任务将在每天午夜运行。我们可以将许多其他参数传递给crontab实例,这个页面显示了一些示例。

要启动 Celery 节拍计划程序,请键入以下命令:

$ celery -A django_project beat -l info

输出如下所示:

celery beat v4.1.0 (latentcall) is starting.
__    -    ... __   -        _
LocalTime -> 2018-04-24 12:01:51
Configuration ->
    . broker -> amqp://guest:**@localhost:5672//
    . loader -> celery.loaders.app.AppLoader
    . scheduler -> celery.beat.PersistentScheduler
    . db -> celerybeat-schedule
    . logfile -> [stderr]@%INFO
    . maxinterval -> 5.00 minutes (300s)
[2018-04-24 12:01:51,184: INFO/MainProcess] beat: Starting...

需要注意的是,该命令仅启动调度程序。要执行这些任务,您必须在单独的终端上启动 Celery 工人:

$  celery -A django_project worker -l info

您也可以使用-B选项启动 Celery 工人和节拍:

$ celery -A django_project worker -l info -B

现在,过期的片段将在每天午夜自动删除。

使用 Flower 监控任务

花卉是一个基于网络的工具,用于监测和管理 Celery。要安装 Flower,请键入以下命令:

$ pip install flower

要启动基于网络的工具,请键入以下内容:

$ celery -A django_project flower

默认情况下,该命令在 post 5555 时启动 web 服务器。要更改端口,请使用--port选项。

$ celery -A django_project flower --port=5555

打开浏览器,导航至http://localhost:5555/。您应该会看到这样的仪表板:



Django 的flatpages

原文:https://overiq.com/django-1-11/flatpages-in-django/

最后更新于 2020 年 7 月 27 日


Web 应用通常包含不常更改的页面。例如,关于我们、EULA(最终用户许可协议)、使用条款等。这种页面被称为平面页面。Django 提供了一个名为django.contrib.flatpages的内置应用来处理平板页面。

安装平板应用

默认情况下,flatpages 应用(django.contrib.flatpages)未启用。要安装它,请打开settings.py文件并将django.contrib.flatpages添加到INSTALLED_APPS列表,如下所示:

djangobin/django _ project/django _ project/settings . py

#...

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.humanize',
    'django.contrib.flatpages'
    'djangobin',
]

#...

flatpages 应用依赖于另一个名为 Sites framework ( django.contrib.sites)的应用,默认情况下该应用也未启用。要启用站点框架,请将django.contrib.sites添加到INSTALLED_APPS设置中,并在settings.py文件的末尾定义一个值为 1 的SITE_ID变量。

djangobin/django _ project/django _ project/settings . py

#...

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.humanize',
    'django.contrib.flatpages',
    'django.contrib.sites',
    'djangobin',
]

#...

LOGIN_URL = 'djangobin:login'

SITE_ID = 1

现在像往常一样运行migrate命令,创建必要的表格。

$ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, djangobin, flatpages, sessions, sites
Running migrations:
  Applying sites.0001_initial... OK
  Applying flatpages.0001_initial... OK
  Applying sites.0002_alter_domain_unique... OK

平面页面模型

平面页面在数据库中使用FlatPage模型表示。以下是FlatPage型号的外观:

class FlatPage(models.Model):
    url = models.CharField(maxlength=100)
    title = models.CharField(maxlength=200)
    content = models.TextField(blank=True)
    enable_comments = models.BooleanField(default=False)
    template_name = models.CharField(maxlength=70, blank=True)
    registration_required = models.BooleanField(default=False)
    sites = models.ManyToManyField(Site)

让我们逐一检查每个字段。

  1. url(必选):该字段表示平面页面将出现的网址。它只能接受以斜杠开头和结尾的网址。比如/about//eula//about-us/等。

  2. title(必选):页面标题。

  3. content(可选):HTML 格式的平面页面内容。

  4. enable_comments(可选):一个Boolean字段,指定您是否要启用平面页面的注释。请注意,将此字段设置为True不会自动在平面页面中创建注释框。如果你想有一个评论表,你必须自己编码。我们仅将此字段用作模板中的标志,以检查是否为平面页面启用了注释。

  5. template_name(可选):用于渲染平面页面的模板。如果没有给出,它将回到默认模板flatpages/default.html

  6. registration_required(可选):一个Boolean字段,如果设置为True,那么只有登录的用户才能查看该平面页面。

  7. sites(必选):这个字段很重要。每个平面页面都与一个使用多对多关系的站点相关联。换句话说,一个FlatPage对象可以属于一个或多个Site对象。

网站框架

Sites 框架(django.contrib.sites)允许我们从同一个数据库和 Django 项目运行多个网站。Sites 框架提供了一个名为Site的模型,它包含以下两个字段:

  1. 领域
  2. 名字

domain -用于指定是网站的地址。比如 example.com 或者http://example.com

name -该字段常用于指定一个人可读易记的网站名称。

启用站点框架时,我们定义了值为 1 的SITE_ID变量,因为这是我们的第一个站点。

当我们在settings.py文件中将django.contrib.sites添加到INSTALLED_APPS后运行migrate命令时,Django 自动添加一条记录,其中domainname字段设置为example.com

这就是你需要知道的关于站点框架的全部内容。

了解了平面页面的结构后,让我们创建一些平面页面。此时,有两种方法可以创建平面页面。

  1. Django 管理网站。
  2. Django 奥姆。

让我们尝试通过 Django 管理网站创建平面页面。

使用 Django 管理网站创建平面页面

访问http://127.0.0.1:8000/admin/并登录 Django 管理网站。

您应该会看到Flat PageSites与所有其他已安装的应用一起列出,如下所示:

单击平面页面前面的“添加”链接以创建新的平面页面。您应该会看到这样的添加平面页面表单:

通过输入以下详细信息创建“关于”页面:

如上所述,urltitlesites字段是必需的。

请注意表单底部的高级选项部分。单击显示链接使其可见。“高级”部分允许您为以下字段设置值:

  • registration_required
  • template_name

我们现在不需要在这些字段中输入任何数据。

完成后,点击页面底部的保存按钮。

使用 Django ORM 创建平面页面

平面页面实现为FlatPage模型。要使用FlatPage模型,我们必须首先从django.contrib.flatpages.models模块导入它,如下所示:

from django.contrib.flatpages.models import FlatPage

此时,我们的数据库中有一个名为“关于”的平面页面。要在 shell 中获取它,请键入以下代码:

>>> 
>>> au = FlatPage.objects.get(title='About Us')
>>> 
>>> au.url
'/about/'
>>> 
>>> au.title
'About Us'
>>> 
>>> au.content
'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Adipisci aspernatur beatae cumque delectus dolorem eum ex excepturi fuga hic id laborum nulla optio quam, quasi, quidem quod totam vero voluptas?\r\n\r\nLorem ipsum dolor sit amet, consectetur adipisicing elit. At autem culpa debitis distinctio, dolore fugiat hic laboriosam molestiae, mollitia optio quisquam, voluptate. At dignissimos dolore fuga iusto mollitia quas rem!'
>>> 
>>> au.sites.all()
<QuerySet [<Site: example.com>]>
>>>
>>>

让我们创建一个 EULA 页面。

>>>
>>> eula = FlatPage.objects.create(
...     url='/eula/',
...     title = "EULA",
...     content = "EULA for Djangobin"
... )
>>>
>>> eula
<FlatPage: /eula/ -- EULA>
>>>
>>>

现在我们有两个平面页面“关于”和“EULA”。与关于页面不同, EULA 页面此时不可用,因为它不与任何Site对象相关联。请考虑以下几点:

>>>
>>> au.sites.all()
<QuerySet [<Site: example.com>]>
>>> 
>>> eula.sites.all()
<QuerySet []>
>>> 
>>>

上面的代码演示了我们的“关于”页面与一个站点相关联,但是“EULA”页面与任何站点都不相关联。

要使 EULA 平板可用,将其连接到一个Site对象,如下所示:

>>>
>>> from django.contrib.sites.models import Site  # import Site model
>>>
>>> site = Site.objects.get(id=1)   # get the site object
>>>
>>> site
<Site: example.com>
>>>
>>> eula.sites.add(site)
>>> 
>>> eula.sites.all()
<QuerySet [<Site: example.com>]>
>>> 
>>>

为平面页面创建模板

我们现在有两个平面页面,但无法显示它们。默认情况下,FlatPage对象使用flatpages/default.html模板,除非您在创建/更新FlatPage对象时指定了不同的模板。

Django 不提供flatpages/default.html模板,你得自己创建一个。在 djangobin 的templates目录中,创建一个名为flatpages的新目录。然后在flatpages目录中创建一个名为default.html的新文件,并在其中添加以下代码。

djangobin/django _ project/blog/templates/flat page/default . html

{% extends 'djangobin/base.html' %}

{% block title %}
    {{ flatpage.title }} - {{ block.super }}
{% endblock %}

{% block main %}

    <h1>{{ flatpage.title }}</h1>

    <p>{{ flatpage.content|linebreaks }}</p>

{% endblock %}

注意模板中使用的flatpage变量。所有的平面页面模板都被传递一个名为flatpage的上下文变量,它是一个FlatPage对象。一旦我们有了FlatPage对象,我们可以通过在属性名称后面键入一个(.)点来访问任何相关信息。在我们的例子中,我们只输出FlatPage对象的titlecontent属性的值。

网址模式

一切都准备好了。我们只需要一些网址模式来访问平面页面。打开 djangobin 的urls.py,在urlpatterns列表的末尾添加以下两个网址模式。

djangobin/django _ project/blog/URL . py

#...
from django.contrib.flatpages import views as flat_views
from . import views
#...

urlpatterns = [
    #...
    url('^search/$', views.search, name='search'),
    url(r'^about/$', flat_views.flatpage, {'url': '/about/'}, name='about'),
    url(r'^eula/$', flat_views.flatpage, {'url': '/eula/'}, name='eula'),        
]

平面页面应用(django.contrib.flatpages)提供了一个flatpage()视图,其工作是渲染平面页面。它接受一个名为url的必需参数,这是我们在创建平面页面时输入的模板的网址。

打开 Django 斌的base.html并添加到关于EULA 页面的链接如下:

决哥/决哥 _ project/决哥/样板/决哥/base.html

{# ... #}
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
            <ul class="nav navbar-nav">
                <li {% if request.path == '/' %}class='active'{% endif %} >
                    <a href="{% url 'djangobin:index' %}">Add new</a>
                </li>
                <li {% if request.path == '/trending/' %}class='active'{% endif %}>
                    <a href="{% url 'djangobin:trending_snippets' %}">Trending<span class="sr-only">(current)</span></a>
                </li>
                <li {% if request.path == '/about/' %}class='active'{% endif %}>
                    <a href="{% url 'djangobin:about' %}">About</a>
                </li>
                <li {% if request.path == '/contact/' %}class='active'{% endif %}>
                    <a href="{% url 'djangobin:contact' %}">Contact</a>
                </li>
            </ul>

{# ... #}

    <div class="main-footer">
        <div class="container text-center">
            <ul>
                <li><a href="#">Source Code</a></li>
                <li><a href="https://overiq.com/">OverIQ</a></li>
                <li><a href="{% url 'djangobin:eula' %}">EULA</a></li>
            </ul>
        </div>
    </div>
</footer>
{# ... #}

打开浏览器,访问http://127.0.0.1:8000/about/http://127.0.0.1:8000/eula/。你应该看到关于EULA 页面如下:



在 Django 中创建站点地图

原文:https://overiq.com/django-1-11/creating-sitemaps-in-django/

最后更新于 2020 年 7 月 27 日


网站地图只是网站中的一个链接列表,你希望搜索引擎对其进行抓取和索引。除此之外,网站地图还被用来告诉页面的以下信息:

  1. 页面更改的频率。
  2. 页面的最后修改日期。
  3. 网址相对于其他网址的优先级。

网站地图的类型

网站地图有两种类型:

  1. HTML 网站地图。
  2. XML 站点映射。

html 站点地图

一个 HTML 网站地图是为用户设计的,帮助他们浏览网站。要创建一个 HTML 站点地图,只需使用<ol><ul>标签列出你的网址,如下所示:

<h2>DjangoBin Sitemap</h2>

<ul>
    <li><a href="http://example.com">Home</a></li>
    <li><a href="http://example.com/blog">Blog</a></li>
    <li><a href="http://example.com/contact">contact</a></li>
    <li><a href="http://example.com/careers">Careers</a></li>
    <li><a href="http://example.com/eula">EULA</a></li>
</ul>

请记住,HTML 网站地图是供人类消费的,它们不是为搜索引擎准备的。出于这个原因,谷歌网站管理员工具和其他工具甚至不允许你提交一个 HTML 网站地图。

XML 站点映射

XML 站点地图是当今创建站点地图的最首选方式。各大搜索引擎提供的站长工具都接受 XML sitemap。下面是一个 XML 站点地图的例子:

<?xml version="1.0" encoding="UTF-8"?>
<urlset >
  <url>
    <loc>http://www.example.com/home</loc>
    <lastmod>2017-05-10</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.9</priority>
  </url>
  <url>
    <loc>http://www.example.com/blog/</loc>
    <lastmod>2017-05-10</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.6</priority>
  </url>
  <url>
    <loc>http://www.example.com/contact/</loc>
    <lastmod>2017-05-10</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.6</priority>
  </url>
</urlset>

Django 提供了一个站点地图框架(django.contrib.sitemaps),它自动化了创建站点地图的过程。

安装站点地图框架

要安装站点地图框架,请将'django.contrib.sitemaps'添加到settings.py文件中的INSTALLED_APPS列表中。站点地图框架也依赖于站点框架(django.contrib.sites),我们在上一课已经安装了该框架。此时,INSTALLED_APPS的设定应该是这样的:

djangobin/django _ project/django _ project/settings . py

#...

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.humanize',
    'django.contrib.flatpages',
    'django.contrib.sites',
    'django.contrib.sitemaps',
    'djangobin',
]

#...

sitemaps 框架不需要任何额外的表。所以不需要运行migrate命令。我们可以通过运行migrate命令来验证这个事实,如下所示:

$ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, djangobin, flatpages, sessions, sites
Running migrations:
  No migrations to apply.

请注意输出“没有要应用的迁移”。这告诉我们 sitemap 框架不会创建任何额外的表。

我们现在准备创建站点地图。

创建站点地图类

站点地图类只是一个继承自django.contrib.sitemaps.Sitemap类的 Python 类。sitemap 类表示 sitemap 中条目的一部分。例如,一个站点地图类可以代表博客的所有条目,而另一个站点地图类可以代表所有的平面页面等等。

在我们的例子中,我们希望站点地图包含所有公共片段和平面页面的链接。因此,我们将创建两个站点地图类:SnippetfSitemapFlatPageSitemap

djangobin app 目录中创建新文件sitemaps.py,并添加以下代码。

【django _ project/django gobin/site maps . py】

from django.contrib.sitemaps import Sitemap
from django.contrib.flatpages.models import FlatPage
from .models import Snippet

class SnippetSitemap(Sitemap):
    changefreq = 'monthly'
    priority = 0.9

    def items(self):
        return Snippet.objects.all()

class FlatPageSitemap(Sitemap):
    changefreq = 'monthly'
    priority = 0.9

    def items(self):
        return FlatPage.objects.all()

以下是它的工作原理:

在第 1-3 行,我们导入必要的类和函数。

在第 6-11 行,我们定义了一个名为SnippetSitemap的站点地图类。

在第 7-8 行,我们正在设置changefreqpriority属性。changefreqpriority是可选的类属性,分别指示页面更改的频率和相对于其他网址的优先级。

changefreq属性的其他可能值有:

  • 'always'
  • 'hourly'
  • 'daily'
  • 'weekly'
  • 'monthly'
  • 'yearly'
  • 'never'

同样,priority属性只能包含从0.01.0的值。

changefreqpriority类属性对应于<changefreq><priority> XML 元素。换句话说,站点地图框架将使用来自changefreqpriority属性的数据在站点地图文件中创建<changefreq><priority>元素。

接下来,我们定义items()方法。items()方法的工作是返回一个对象列表,我们希望它的网址在站点地图中。

注意items()方法只是返回一个对象列表。它本身不构建网址。构建Snippet模型的get_absolute_url()方法将被隐式调用。

FlatPageSitemap类的定义方式类似。唯一的区别是这里items()方法返回的是一个FlatPage对象的列表,而不是Snippet对象。

我们的网站地图课程已经准备好了。我们只需要为它创建一个网址模式。

站点地图框架(django.contrib.sitemaps)提供了一个名为sitemap()的视图,它方便了从Sitemap类创建站点地图。sitemap()视图接受一个名为sitemaps的必需参数,它是一个字典对象,映射到其站点地图类的短节标签。

打开 Django 斌的urls.py,修改如下:

决哥/决哥 _ 项目/决哥/URL . py】

#...
from django.contrib.flatpages import views as flat_views
from django.contrib.sitemaps.views import sitemap
from . import views
from .sitemaps import SnippetSitemap, FlatPageSitemap

# app_name = 'djangobin'

sitemaps = {
    'snippets': SnippetSitemap,
    'flatpages': FlatPageSitemap,
}

urlpatterns = [
                #...
    url(r'^about/$', flat_views.flatpage, {'url': '/about/'}, name='about'),
    url(r'^eula/$', flat_views.flatpage, {'url': '/eula/'}, name='eula'),
    url(r'^sitemap\.xml/$', sitemap, {'sitemaps': sitemaps}, name='sitemap'),
]

在第 3 行和第 5 行,我们导入必要的类。

在第 9-12 行,我们定义了一个将短标签映射到 sitemap 类的字典。

最后,在 18 中,我们定义了一个名为sitemap的新 URL 模式,并将sitemaps字典作为关键字参数传递。

我们的 Django 项目已经准备好提供网站地图。打开浏览器,导航至http://127.0.0.1:8000/sitemap.xml/。您应该会看到这样的页面:

我们的网站地图工作正常,但是请注意网址的主机部分包含example.com。这个领域来自于 Django 网站框架(django.contrib.sites)。要更改它,登录 Django 管理,然后访问网站列表页面(http://127.0.0.1:8000/admin/sites/site/)。

单击域名,将显示“更改网站”表单。在表单中,将域名和显示名称更改为127.0.0.1:8000,然后单击保存更新更改。

再次重访站点地图页面(http://127.0.0.1:8000/sitemap.xml/)。在这一点上,你的网站地图应该使用127.0.0.1:8000而不是example.com作为主机来生成网址。您需要在部署时再次更新此设置。



Django 的多种环境设置

原文:https://overiq.com/django-1-11/settings-for-multiple-environments-in-django/

最后更新于 2020 年 7 月 27 日


多个设置文件

到目前为止,单个settings.py文件已经很好地为我们服务了。现在我们正在转移到生产环境,因此settings.py文件中的一些设置需要更改。最值得注意的是,我们将把DEBUG改为False,把ALLOWED_HOSTS改为生产服务器的 IP 或域名。

为了在不同的环境中高效地运行我们的 Django 项目,我们将把我们的单个settings.py分割成多个文件,每个文件代表一个特定环境的设置。

让我们首先将settings.py重命名为old.settings.py,并在 Django 配置目录(djangobin/django_project/django_project)中创建一个名为settings的目录。在settings目录内创建以下四个文件:

  1. __init__.py
  2. base.py
  3. dev.py
  4. prod.py

__init__.py文件告诉 Pythonsettings目录是一个包。base.py包含开发和生产环境通用的设置。dev.pyprod.py分别包含特定于开发和生产的设置。

此时,Django 配置目录应该如下所示:

django_project/
├── celery.py
├── __init__.py
├── old.settings.py
├── settings
│   ├── __init__.py
│   ├── base.py
│   ├── dev.py
│   └── prod.py
├── urls.py
└── wsgi.py

1 directory, 9 files

base.py完整代码如下(变化突出显示):

djangobin/django _ project/django _ project/settings/base . py

"""
Django settings for django_project project.

Generated by 'django-admin startproject' using Django 1.11.

For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""

import os, json
from django.core.exceptions import ImproperlyConfigured

with open(os.path.abspath("djangobin-secrets.json")) as f:
    secrets = json.loads(f.read())

def get_secret_setting(setting, secrets=secrets):
    try:
        return secrets[setting]
    except KeyError:
        raise ImproperlyConfigured("Set the {} setting".format(setting))

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.humanize',
    'django.contrib.flatpages',
    'django.contrib.sites',
    'django.contrib.sitemaps',
    'djangobin',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.middleware.common.BrokenLinkEmailsMiddleware',
]

ROOT_URLCONF = 'django_project.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(BASE_DIR, 'templates'),
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'djangobin.context_processors.recent_snippets',
            ],
        },
    },
]

WSGI_APPLICATION = 'django_project.wsgi.application'

# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#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',
    },
]

# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/

STATIC_URL = '/static/'

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfies')

MEDIA_ROOT  = os.path.join(BASE_DIR, 'media')

MEDIA_URL = '/media/'

LOGIN_REDIRECT_URL = 'djangobin:index'

LOGIN_URL = 'djangobin:login'

SITE_ID = 1

这里有几件事需要注意:

  • 出于安全原因,您绝不能在代码中硬编码敏感配置,如 SECRET_KEY、数据库凭据或 API 密钥。此外,这些配置也会随着部署的不同而变化。如果您将这些配置放在代码中,那么每次迁移到新环境时,您都必须不断更新代码。

    在我们的例子中,我们已经将所有敏感配置存储在名为djangobin-secrets.json的 JSON 文件中,该文件位于项目根目录(djangobin/django_project)中。djangobin-secrets.json的内容是这样的:

    决哥/决哥 _ 项目/决哥的秘密。json】

    {
      "SECRET_KEY": "rj3vhyKiDRNmth75sxJemvOuTy1Hy0ogeKgS9JP8Gp7dGDctfSMUuOt5QbSpsS9xAlvBMTXW3Z6VTODvvFcV3TmtrZUbGkHBcs8I",
      "DATABASE_NAME": "djangobin",
      "DATABASE_USER": "postgres",
      "DATABASE_PASSWORD": "pass",
      "DATABASE_HOST": "127.0.0.1",
      "DATABASE_PORT": "5432",
      "EMAIL_HOST_USER": "apikey",
      "EMAIL_HOST": "smtp.sendgrid.net",
      "EMAIL_HOST_PASSWORD": "IK.qQecgqph1Sa9TkOOljo8pA.5Xrj1oyJKuOGBbHnWFmdDe32G8XXojH45W1loxIsktqY3Nc",
      "EMAIL_PORT": "587",
      "EMAIL_USE_TLS": "True"
    }
    
    

    该文件包含 SECRET_KEY、数据库凭据和电子邮件凭据。如果您正在使用版本控制系统(您应该这样做),请将此文件添加到.gitignore

    要生成密钥,我们使用django.utils.crypto模块的get_random_string()功能:

    >>>
    >>> from django.utils.crypto import get_random_string
    >>>
    >>> get_random_string(80)
    'ZEjwrGbwF9fjFAfTXTvz7LxXnCGUPOJyfPszrk2WHtbrbH3mgBeag2NWUueGYYiA7fTw36F50T2R3F5L'
    >>>
    
    

    在生产中,我们将使用 PostgreSQL 作为我们的数据库,并使用 SendGrid 发送电子邮件。

    如果您已经知道如何安装和配置 PostgreSQL,请继续填充数据库凭据,否则,请等到下一课,我们将了解如何安装和配置 PostgreSQL。

    要获取电子邮件凭据,请在发送网格上注册一个免费帐户。截至本文撰写之时,SendGrid 免费计划允许您每天发送 100 封电子邮件。

    回到base.py文件。

    在第 16-17 行,我们读取djangobin-secrets.json文件的内容,并将其作为字典存储在secrets变量中。

    如果你试图访问一个不存在于secrets目录中的配置,你会得到一个KeyError异常。可悲的是,这没有太大帮助。为了使调试更容易,我们定义了get_secret_setting()函数。如果在secrets目录中找不到该设置,则该方法返回调用它的设置值或一个ImproperlyConfigured异常。

  • 我们的设置文件现在位于 Django 配置目录的一级深处。换句话说,BASE_DIR设置不再指向项目根目录(djangobin/django_project/),而是指向 Django 配置目录(djangobin/django_project/django_project)。这有效地中断了模板、静态文件和媒体文件的路径。为此,在第 27 行中,我们为os.path.dirname()添加了一个额外的呼叫。这将确保BASE_DIR设置指向正确的基本目录。

  • 在第 23 行,我们已经将'django.middleware.common.BrokenLinkEmailsMiddleware'添加到了MIDDLEWARE列表中。每当发生 HTTP 404 错误时,这将通过电子邮件发送给MANAGERS

dev.py的代码如下:

djangobin/django _ project/django _ project/settings/dev . py

from .base import *

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '_5=#=+cl&lp@&ayps6ia0viff)^v$_wvutyyxca!xu0w6d2z3$'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

SERVER_EMAIL = 'infooveriq@gmail.com'
DEFAULT_FROM_EMAIL = SERVER_EMAIL

ADMINS = (
    ('OverIQ', 'admin@overiq.com'),
)

MANAGERS = (
    ('OverIQ', 'manager@overiq.com'),
)

这里没什么花哨的,这个dev.py只是包含开发的具体设置。要从base.py导入常用设置,我们使用from .base import *语句(第 1 行)。请注意,我们特意对一些敏感的配置进行了硬编码,因为这样可以简化开发过程。然而prod.py文件却不是这样。

djangobin/django _ project/django _ project/settings/prod . py

import os
from .base import *

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = get_secret_setting('SECRET_KEY')

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

ALLOWED_HOSTS = ["*"]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': get_secret_setting('DATABASE_NAME'),
        'USER': get_secret_setting('DATABASE_USER'),
        'PASSWORD': get_secret_setting('DATABASE_PASSWORD'),
        'HOST': get_secret_setting('DATABASE_HOST'),
        'PORT': get_secret_setting('DATABASE_PORT'),
    }
}

STATIC_ROOT = 'static'

EMAIL_HOST_USER = get_secret_setting('EMAIL_HOST_USER')
EMAIL_HOST = get_secret_setting('EMAIL_HOST')
EMAIL_HOST_PASSWORD = get_secret_setting('EMAIL_HOST_PASSWORD')
EMAIL_PORT = get_secret_setting('EMAIL_PORT')
EMAIL_USE_TLS = True
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'

DEFAULT_FROM_EMAIL = 'support@overiq.com'
SERVER_EMAIL = 'no-reply@overiq.com'

ADMINS = (
    ('OverIQ', 'infooveriq@gmail.com'),
)

MANAGERS = (
    ('OverIQ', 'manager@overiq.com'),
)

prod.py几乎与dev.py相似,但它定义了特定于生产环境的设置。

这里有几件事需要注意:

  1. dev.py不同,在prod.py中,我们通过base.py文件中定义的get_secret_setting()函数加载敏感配置。

  2. 在第 10 行,我们将ALLOWED_HOSTS设置为[*]ALLOWED_HOSTS设置是一项安全功能,用于验证请求是否来自允许的域。它指定了一个代表这个 Django 项目可以服务的主机/域名的字符串列表。默认情况下,它被设置为空列表。一旦你进入生产,你需要设置它,否则你将得到 500 内部服务器错误。

    如果您拥有example.com并希望允许来自example.comwww.example.com的请求,那么您需要将ALLOWED_HOSTS设置为:

    ALLOWED_HOSTS =  ['example.com', 'www.example.com']
    
    

    如果您想允许来自example.com及其所有子域的请求,那么使用句点作为子域通配符。例如:

    ALLOWED_HOSTS =  ['.example.com']
    
    

    要允许 Django 接受来自任何域的请求,请将ALLOWED_HOSTS设置为“*”。

    ALLOWED_HOSTS =  ['*']
    
    

    但是,在实际部署中,您应该将此设置仅限于您希望允许的主机/域。

  3. 在第 15-19 行,我们定义了连接到 PostgreSQL 数据库的配置。

  4. 在第 25-28 行,我们定义了通过发送网格发送电子邮件所需的设置。

运行项目

我们已经重构了很多代码。现在让我们看看如何使用这个新设置与我们的 Django 项目进行交互。

在终端执行./manage.py文件,会得到如下错误:

$ ./manage.py
...
Note that only Django core commands are listed as settings are not properly configured (error: Requested setting INSTALLED_APPS, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.).

如果您尝试运行runservershell命令,您将会得到相同的错误(只是措辞不同)。

$ ./manage.py runserver
...  
django.core.exceptions.ImproperlyConfigured: Requested setting DEBUG, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.

问题是 Django 不知道我们的设置文件在哪里。我们可以使用--setting选项从命令行指定设置文件的位置。

$  ./manage.py runserver --settings=django_project.settings.dev
Performing system checks...

System check identified no issues (0 silenced).
June 04, 2018 - 06:37:16
Django version 1.11, using settings 'django_project.settings.dev'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

请注意,每次执行manage.py脚本时,您都需要指定--settings选项。例如:

$ ./manage.py shell --settings=django_project.settings.dev

$ ./manage.py makemigrations --settings=django_project.settings.dev

每次执行./manage.py文件时指定--settings选项会很快变得乏味。或者,您可以将DJANGO_SETTINGS_MODULE环境变量设置为所需的设置文件,如下所示:

$ export DJANGO_SETTINGS_MODULE=django_project.settings.dev

您现在可以像往常一样执行./manage.py,而无需指定设置文件的路径。

$ ./manage.py runserver

DJANGO_SETTINGS_MODULE变量将一直存在,直到 shell 会话处于活动状态。不幸的是,如果你开始一个新的 Shell,你将不得不再次设置DJANGO_SETTINGS_MODULE

更好的方法是修改 virtualenv 的activate脚本,并在激活 virtualenv 时设置DJANGO_SETTINGS_MODULE环境变量,在禁用 virtualenv 时取消设置。

打开activate脚本,修改如下:

djangobin/env/bin/激活

# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly

deactivate () {
    unset -f pydoc >/dev/null 2>&1

    # reset old environment variables
    # ! [ -z ${VAR+_} ] returns true if VAR is declared at all
    if ! [ -z "${_OLD_VIRTUAL_PATH+_}" ] ; then
        PATH="$_OLD_VIRTUAL_PATH"
        export PATH
        unset _OLD_VIRTUAL_PATH
    fi
    if ! [ -z "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then
        PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME"
        export PYTHONHOME
        unset _OLD_VIRTUAL_PYTHONHOME
    fi

    # This should detect bash and zsh, which have a hash command that must
    # be called to get it to forget past commands.  Without forgetting
    # past commands the $PATH changes we made may not be respected
    if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ] ; then
        hash -r 2>/dev/null
    fi

    if ! [ -z "${_OLD_VIRTUAL_PS1+_}" ] ; then
        PS1="$_OLD_VIRTUAL_PS1"
        export PS1
        unset _OLD_VIRTUAL_PS1
    fi

    unset VIRTUAL_ENV
    if [ ! "${1-}" = "nondestructive" ] ; then
    # Self destruct!
        unset -f deactivate
    fi

    unset DJANGO_SETTINGS_MODULE
}

# unset irrelevant variables
deactivate nondestructive

VIRTUAL_ENV="/home/pp/django-1.11/djangobin/env"
export VIRTUAL_ENV

_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH

# unset PYTHONHOME if set
if ! [ -z "${PYTHONHOME+_}" ] ; then
    _OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME"
    unset PYTHONHOME
fi

if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then
    _OLD_VIRTUAL_PS1="$PS1"
    if [ "x" != x ] ; then
        PS1="$PS1"
    else
        PS1="(`basename \"$VIRTUAL_ENV\"`) $PS1"
    fi
    export PS1
fi

# Make sure to unalias pydoc if it's already there
alias pydoc 2>/dev/null >/dev/null && unalias pydoc

pydoc () {
    python -m pydoc "$@"
}

# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands.  Without forgetting
# past commands the $PATH changes we made may not be respected
if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ] ; then
    hash -r 2>/dev/null
fi

export DJANGO_SETTINGS_MODULE=django_project.settings.dev

启动新 Shell,激活虚拟环境,使用echo命令检查DJANGO_SETTINGS_MODULE环境变量的存在:

$ echo $DJANGO_SETTINGS_MODULE 
django_project.settings.dev

不出所料,DJANGO_SETTINGS_MODULE环境变量可用。

现在,您可以运行./manage.py文件,而无需设置任何环境变量或指定--settings选项

启动 Django 开发服务器,确保一切都按预期运行。

$ ./manage.py runserver

输出应该如下所示:

Performing system checks...

System check identified no issues (0 silenced).
May 17, 2018 - 14:51:37
Django version 1.11, using settings 'django_project.settings.dev'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

DJANGO_SETTINGS_MODULE变量将在停用虚拟环境时自动移除。

创建需求文件

需求文件是一个包含项目依赖关系的简单文本文件。我们使用需求文件来安装项目依赖项。

要创建需求文件,请执行以下命令:

$ pip freeze > requirements.txt

我们的 DjangoBin 项目现在已经完成。在下一课中,我们将学习如何将其部署到 DigitalOcean 服务器。



将 Django 项目部署到 DigitalOcean

原文:https://overiq.com/django-1-11/deploying-django-project-to-digitalocean/

最后更新于 2020 年 7 月 27 日


本章提供了将 Django 项目部署到 DigitalOcean 服务器的分步指南。

DigitalOcean 是领先的虚拟专用网服务提供商之一,他们的计划非常便宜——起价仅为每月 5 美元。

如果您没有 DigitalOcean 账户,使用这个链接注册,您将获得 10 美元的免费点数。

本章中使用的 Django 项目的 git 存储库可在这里获得。但是如果你想的话,你也可以继续你自己的项目。

创建液滴

登录您的 DigitalOcean 帐户,您将进入仪表板页面。

单击页面顶部的“创建”按钮,并选择水滴。

在“创建水滴”页面的“选择图像”部分下,选择 Ubuntu 16.04。

选择液滴尺寸:

选择服务器将位于的区域:

附加选项和 SSH 密钥是可选的。

给你的小滴命名,然后点击创建按钮。

创建小滴后,您将收到一封电子邮件,其中包含登录服务器所需的凭据。

连接到服务器

要连接到服务器,请启动终端并键入以下命令:

$ ssh root@167.99.235.81

注意: Windows 用户可以使用 PuTTY SSH 客户端连接服务器。

如果您是第一次登录,系统会提示您更改密码。

创建具有有限访问权限的用户

您永远不应该以 root 用户身份运行应用,因为如果攻击者闯入您的应用,他会立即以 root 用户身份访问整个系统。

此外,根用户非常强大,因此可以执行任何操作,即使这会导致系统崩溃。您想格式化磁盘吗?或者删除/usr目录,只要执行命令就搞定了。当你是根时,系统假设你知道自己在做什么。

由于这个原因,Linux 中的大多数应用都是作为具有受限访问权限的系统用户运行的。

为了添加额外的安全层,有些发行版禁用了根访问。要执行管理操作,您必须使用sudo命令提升权限。

要创建新用户,请输入以下命令:

$ adduser django

您将被要求输入密码和一些可选的详细信息。

接下来,通过执行以下命令将用户添加到sudo组:

gpasswd -a django sudo

现在,该用户能够执行管理命令。

要使用新创建的用户登录,请键入su,后跟用户名:

su django

使用cd命令将当前工作目录更改为用户的主目录:

$ cd

下一步,我们将更新我们的系统并安装一些必要的软件包。

安装 PIP、PostgreSQL 和 Nginx

首先,使用以下命令更新系统:

$ sudo apt-get update
$ sudo apt-get upgrade

Ubuntu 16.04 预装了 Python 3.5,所以我们不需要安装 Python。但是,您确实需要安装 pip。

要安装画中画,请键入以下内容:

$ sudo apt-get install python3-pip

Virtualenv(虚拟环境)

就像我们在开发中所做的那样,我们将使用 virtualenv 来创建一个虚拟环境。通过键入以下命令安装 virtualenv:

$ pip3 install virtualenv

一种数据库系统

PostgreSQL 是 Django 社区中首选的数据库。要安装它,请键入:

$ sudo apt-get install postgresql postgresql-contrib

安装后数据库服务器将自动启动。要测试服务器类型的状态:

$ sudo service postgresql status

输出将如下所示:

● postgresql.service - PostgreSQL RDBMS
   Loaded: loaded (/lib/systemd/system/postgresql.service; enabled; vendor preset: enabled)
   Active: active (exited) since Fri 2018-05-18 13:33:21 UTC; 1h 54min ago
 Main PID: 20416 (code=exited, status=0/SUCCESS)
   CGroup: /system.slice/postgresql.service

May 18 13:33:21 djangobin-ubuntu systemd[1]: Starting PostgreSQL RDBMS...
May 18 13:33:21 djangobin-ubuntu systemd[1]: Started PostgreSQL RDBMS.
May 18 13:33:26 djangobin-ubuntu systemd[1]: Started PostgreSQL RDBMS.

Nginx

Nginx 是一款占用空间极小的高性能 web 服务器。我们将使用 Nginx 作为代理服务器,为静态文件提供服务。要按类型安装,请执行以下操作:

$ sudo apt-get install nginx

安装后,Nginx 将自动启动。我们可以通过键入以下命令来检查 Nginx 服务器的状态:

$ sudo service nginx status

输出如下所示:

● nginx.service - A high-performance web server and a reverse proxy server
   Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
   Active: active (running) since Fri 2018-05-18 15:17:51 UTC; 9min ago
 Main PID: 22691 (nginx)
   CGroup: /system.slice/nginx.service
           ├─22691 nginx: master process /usr/sbin/nginx -g daemon on; master_process on
           └─22692 nginx: worker process                           

May 18 15:17:51 djangobin-ubuntu systemd[1]: Starting A high performance web server and a reverse proxy server...
May 18 15:17:51 djangobin-ubuntu systemd[1]: Started A high performance web server and a reverse proxy server.

我们还可以通过直接向 Nginx 请求一个页面来测试它是否正在运行。打开浏览器,访问http://167.99.235.81/(用你的 IP 替换 167.99.235.81)。你应该得到这样一页:

拉比特

安装 rabbitmq:

$ sudo apt-get install rabbitmq-server

创建数据库和用户

当您安装 PostgreSQL 时,它会自动创建一个名为postgres的用户来执行管理任务。

在我们做任何事情之前,让我们通过psql用这个账户登录,创建一个新的数据库。

$ sudo -u postgres psql

输出将如下所示:

psql (9.5.12)
Type "help" for help.

postgres=# 
postgres=#

通过键入以下命令创建新数据库:

postgres=# CREATE DATABASE djangobin;
CREATE DATABASE
postgres=#

接下来,通过键入以下内容创建新用户:

postgres=# 
postgres=# CREATE ROLE db_user WITH LOGIN PASSWORD 'password' CREATEDB;
CREATE ROLE
postgres=#

最后,将数据库djangobin上的所有权限授予db_user:

创建虚拟环境和设置项目

要克隆存储库,请键入以下命令:

$ git clone https://github.com/overiq/djangobin.git

这将在您当前的工作目录中创建一个名为djangobin的目录。使用cd命令将当前工作目录更改为djangobin,并创建一个新的虚拟环境:

$ cd djangobin
$ virtualenv env

完成后,激活虚拟环境并将cd放入django_project目录(与manage.py所在位置相同)。

$ source env/bin/activate
$ cd django_project/

接下来,安装需求文件中的依赖项。

$ pip install -r requirements.txt

由于我们在生产中使用 PostgreSQL 数据库,因此需要为 Python 安装 PostgreSQL 数据库适配器,称为 psycopg2。

$ pip install psycopg2

创建一个 JSON 文件来存储敏感配置。

$ nano djangobin-secrets.json

并向其中添加以下代码:

决哥/决哥 _ 项目/决哥的秘密。json】

{
  "SECRET_KEY": "rj3vhyKiDRNmth75sxJKgS9JP8Gp7SpsS9xAlvBMTXW3Z6VTODvvFcV3TmtrZUbGkHBcs$",
  "DATABASE_NAME": "djangobin",
  "DATABASE_USER": "db_user",
  "DATABASE_PASSWORD": "password",
  "DATABASE_HOST": "127.0.0.1",
  "DATABASE_PORT": "5432",
  "EMAIL_HOST_USER": "apikey",
  "EMAIL_HOST": "smtp.sendgrid.net",
  "EMAIL_HOST_PASSWORD": "TW.qQecgRphQDa3TkLLlj18pqA.5Xrjod3G8XXojH45W4loxAsktdY3Nc",
  "EMAIL_PORT": 587
}

确保适当地替换数据库凭证和应用编程接口密钥。

此时,如果您试图执行./manage.py文件,您将会得到一个错误,因为 Django 不知道您的设置文件位于何处:

使用export命令临时指定设置文件位置:

export DJANGO_SETTINGS_MODULE=django_project.settings.prod

有了这个命令,我们已经将应用置于生产模式。

要在djangobin数据库中创建所有必要的表,运行migrate命令:

$ ./manage.py migrate

键入以下命令,为项目创建超级用户:

$ ./manage.py createsuperuser
Username (leave blank to use 'django'): admin
Email address: admin@mail.com
Password: 
Password (again): 
Superuser created successfully.

接下来,创建一个访客用户,将其is_active属性设置为False,这样该账号就不能用来登录了。

$ ./manage.py createsuperuser
Username (leave blank to use 'django'): guest
Email address: guest@mail.com
Password: 
Password (again): 
Superuser created successfully.
$
$
$ ./manage.py shell
Python 3.5.2 (default, Nov 23 2017, 16:37:01) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> 
>>> from django.contrib.auth.models import User
>>> 
>>> u = User.objects.get(username="guest")
>>> 
>>> u.is_active
True
>>> 
>>> u.is_active = False
>>> 
>>> u.save()
>>> 
>>> u.is_active
False
>>>

如前所述,我们将通过 Nginx 服务器提供静态文件。要收集static目录中项目的所有静态文件,请键入以下命令:

$ ./manage.py collectstatic

Gunicorn

Nginx 将面向外部世界,服务于静态文件。但是,它无法与 Django 应用通信;它需要一些东西来运行应用,从 web 向它提供请求,并返回响应。这就是古尼科恩进入戏剧的地方。

通过键入以下命令安装 Gunicorn:

$ pip install gunicorn

要通过 Gunicorn 为我们的应用提供服务,请键入以下命令:

$ gunicorn -w 3 -b 0.0.0.0:8000 django_project.wsgi
[2018-05-19 07:07:32 +0000] [25653] [INFO] Starting gunicorn 19.8.1
[2018-05-19 07:07:32 +0000] [25653] [INFO] Listening at: http://0.0.0.0:8000 (25653)
[2018-05-19 07:07:32 +0000] [25653] [INFO] Using worker: sync
[2018-05-19 07:07:32 +0000] [25656] [INFO] Booting worker with pid: 25656
[2018-05-19 07:07:32 +0000] [25658] [INFO] Booting worker with pid: 25658
[2018-05-19 07:07:32 +0000] [25659] [INFO] Booting worker with pid: 25659

该命令用三个工作进程启动 Gunicorn,并将套接字绑定到0.0.0.0地址。默认情况下,Gunicorn 只监听本地接口(即127.0.0.1),这意味着您无法从网络上的其他计算机访问您的作品。要告诉 Gunicorn 监听所有接口,请将插座绑定到0.0.0.0

打开浏览器,导航至http://167.99.235.81:8000/。您应该会看到这样的页面:

我们的应用似乎坏了。这是意料之中的,因为我们还没有提供静态文件。

设置 Nginx

Gunicorn 已经启动并运行,现在我们需要配置 Nginx 来将请求传递给它。

首先在/etc/nginx/sites-available/目录中创建一个服务器配置文件:

$  sudo nano /etc/nginx/sites-available/djangobin

接下来,将以下配置添加到文件中:

/etc/nginx/sites-available/djangobin

server {
    server_name 167.99.235.81;

    access_log off;

    location /static/ {
        alias /home/django/djangobin2/django_project/static/;
    }

    location / {
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header X-Forwarded-Host $server_name;
        proxy_set_header X-Real-IP $remote_addr;
        add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
    }
}

167.99.235.81替换为您的 IP 和static目录的路径,以匹配您自己的文件系统。

要启用此配置,请在sites-enabled文件夹中创建一个符号链接。

$ sudo ln -s /etc/nginx/sites-available/djangobin /etc/nginx/sites-enabled/djangobin

测试配置文件语法:

$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

最后,重新启动服务器以使更改生效。

$ sudo service nginx restart

现在,我们准备测试一切是否正常。

首先,通过键入以下命令启动 Celery 工人和 Celery 节拍:

$ celery -A django_project worker -l info -B

按 Ctrl+Z,然后按bg将流程放入后台。然后,通过键入以下命令启动 Gunicorn:

$ gunicorn -w 3 -b 127.0.0.1:8000 django_project.wsgi

请注意,这一次我们将套接字绑定为在本地接口(即 127.0.0.1)上侦听,因为这一次 Nginx 将面对外部世界,而不是 Gunicorn。

打开浏览器,访问http://167.99.235.81/。您应该会看到 DjangoBin 的索引页面,如下所示:

如果你试图访问关于或 EULA 平面页面,你会得到 404 错误,因为这些页面还不存在于数据库中。要创建这些页面,请访问http://167.99.235.81/admin/登录 Django 管理网站。

输入我们在本章前面创建的用户和密码。

单击“平面页面”前面的“添加”链接,并添加关于和 EULA 页面,如下所示:

在此期间,让我们更新 sites 框架(django.contrib.sites)中的域名,以便 sitemap 框架可以生成正确的链接。

访问http://<your_ip_address>/admin/sites/site/的网站列表页面。单击域名进行编辑,并在域名和显示名称字段中输入您的服务器 IP 地址,如下所示:

点击保存按钮更新更改。

最后,访问联系人页面并提交消息。在ADMINS设置中列出的所有管理员将收到如下电子邮件:

事情按预期进行,但是如果 gunicorn 或 Celery 由于某种原因被杀死,或者 DigitalOcean 在执行一些维护后重新启动您的液滴,会发生什么?

在这种情况下,用户将看到 502 错误网关错误:

我们可以通过使用名为 Supervisor 的过程监控工具来防止此类错误。

与主管一起监控流程

Supervisor 是一个允许我们监控流程的工具。它的工作是确保某些进程保持运行。如果进程由于某种原因死亡,主管将自动启动它。

通过键入以下命令安装 Supervisor:

$ sudo apt-get install supervisor

安装后,Supervisor 将自动启动。我们可以通过键入以下内容来检查它的状态:

$ sudo service supervisor status
● supervisor.service - Supervisor process control system for UNIX
   Loaded: loaded (/lib/systemd/system/supervisor.service; enabled; vendor preset: enabled)
   Active: active (running) since Sat 2018-05-19 15:27:28 UTC; 1min 16s ago
     Docs: http://supervisord.org
 Main PID: 592 (supervisord)
   CGroup: /system.slice/supervisor.service
           └─592 /usr/bin/python /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf

May 19 15:27:28 djangobin-ubuntu systemd[1]: Started Supervisor process control system for UNIX.
May 19 15:27:28 djangobin-ubuntu supervisord[592]: 2018-05-19 15:27:28,830 CRIT Supervisor running a
May 19 15:27:28 djangobin-ubuntu supervisord[592]: 2018-05-19 15:27:28,831 WARN No file matches via 
May 19 15:27:28 djangobin-ubuntu supervisord[592]: 2018-05-19 15:27:28,847 INFO RPC interface 'super
May 19 15:27:28 djangobin-ubuntu supervisord[592]: 2018-05-19 15:27:28,847 CRIT Server 'unix_http_se
May 19 15:27:28 djangobin-ubuntu supervisord[592]: 2018-05-19 15:27:28,848 INFO supervisord started

安装 Supervisor 后,我们现在可以访问echo_supervisord_conf命令来创建配置文件。

配置文件是 Windows-INI 风格的文件,定义了要运行的程序、如何处理输出、要传递给程序的环境变量等等。

当 Supervisor 启动时,它会自动从/etc/supervisor/conf.d/目录中读取配置。

通过键入以下命令创建新的配置文件:

$ echo_supervisord_conf > ./djangobin.conf

然后使用mv命令将其移动到/etc/supervisor/conf.d/目录:

$ sudo mv djangobin.conf /etc/supervisor/conf.d/

如果打开djangobin.conf文件,会发现里面包含了大量的小节和注释(以;开头的行)。删除文件顶部除supervisor部分以外的所有部分。此时,文件应该如下所示:

/etc/supervisor/conf . d/djangobin . conf

[supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB        ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10           ; (num of main logfile rotation backups;default 10)
loglevel=info                ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false               ; (start in foreground if true;default false)
minfds=1024                  ; (min. avail startup file descriptors;default 1024)
minprocs=200                 ; (min. avail process descriptors;default 200)

下一步是添加一个或多个[program:x]部分,以便主管知道启动和监控哪些程序。程序段中的x指的是赋予每个段的任意唯一标签。该标签将用于管理程序。

下表列出了我们可以在[program]部分定义的一些常见选项。

[计]选项 描述 需要
command 此选项指定要运行的程序的路径。
directory 它指定了主管在运行程序之前将cd放入的目录
autostart 如果设置为true,则告诉主管在系统启动时启动程序。
autorestart 如果设置为true,如果程序死亡或被杀死,告诉主管启动程序。
stdout_logfile 存储流程标准输出的文件。
stderr_logfile 存储流程标准错误的文件。

打开djangobin.conf文件,在文件末尾添加以下三个[program]部分:

/etc/supervisor/conf . d/djangobin . conf

[program:gunicorn]
command=/home/django/djangobin2/env/bin/gunicorn --access-logfile - --workers 3 --bind 127.0.0.1:8000 django_project.wsgi:application
directory=/home/django/djangobin2/django_project
autostart=true
autorestart=true
stderr_logfile=/var/log/gunicorn.err.log
stdout_logfile=/var/log/gunicorn.out.log

[program:celery_worker]
command=/home/django/djangobin2/env/bin/celery -A django_project worker -l info
directory=/home/django/djangobin2/django_project
autostart=true
autorestart=true
stderr_logfile=/var/log/celery.err.log
stdout_logfile=/var/log/celery.out.log

[program:celery_beat]
command=/home/django/djangobin2/env/bin/celery -A django_project beat -l info
directory=/home/django/djangobin2/django_project
autostart=true
autorestart=true
stderr_logfile=/var/log/celery_beat.err.log
stdout_logfile=/var/log/celery_beat.out.log

我们还希望 Supervisor 将DJANGO_SETTINGS_MODULE环境变量传递给所有三个流程。为此,在[supervisord]部分末尾添加environment选项,如下所示:

/etc/supervisor/conf . d/djangobin . conf

[supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB        ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10           ; (num of main logfile rotation backups;default 10)
loglevel=info                ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false               ; (start in foreground if true;default false)
minfds=1024                  ; (min. avail startup file descriptors;default 1024)
minprocs=200                 ; (min. avail process descriptors;default 200)
environment=DJANGO_SETTINGS_MODULE="django_project.settings.prod"

告诉 Supervisor 加载此新配置,并键入以下两个命令:

$ sudo supervisorctl reread
celery_beat: available
celery_worker: available
gunicorn: available
$ sudo supervisorctl update
celery_beat: added process group
celery_worker: added process group
gunicorn: added process group

每次修改配置文件时,都必须执行这两个命令。

现在,我们所有的程序都在运行。您可以随时通过键入以下命令来检查程序的状态:

$ sudo supervisorctl status
celery_beat                      RUNNING   pid 6027, uptime 1:44:03
celery_worker                    RUNNING   pid 6028, uptime 1:44:03
gunicorn                         RUNNING   pid 6029, uptime 1:44:03
supervisor>

如果我们在不传递任何参数的情况下启动supervisorctl程序,它将启动一个交互式 Shell,允许我们控制当前由 Supervisor 管理的进程。

$ sudo supervisorctl
celery_beat                      RUNNING   pid 6027, uptime 1:48:42
celery_worker                    RUNNING   pid 6028, uptime 1:48:42
gunicorn                         RUNNING   pid 6029, uptime 1:48:42
supervisor>

如您所见,在交互模式下supervisorctl从打印当前管理程序的状态开始。

进入交互式 Shell 后,要查看可用命令,请键入help:

supervisor> 
supervisor> help

default commands (type help <topic>):
=====================================
add    exit      open  reload  restart   start   tail   
avail  fg        pid   remove  shutdown  status  update 
clear  maintail  quit  reread  signal    stop    version

supervisor>

现在,我们可以使用程序标签后面的相应命令来停止、启动和重新启动进程。

supervisor> 
supervisor> stop gunicorn 
gunicorn: stopped
supervisor> 
supervisor> start gunicorn 
gunicorn: started
supervisor> 
supervisor> restart gunicorn 
gunicorn: stopped
gunicorn: started
supervisor>

要获取所有正在运行的进程的状态,请键入status:

celery_beat                      RUNNING   pid 6027, uptime 5:51:00
celery_worker                    RUNNING   pid 6028, uptime 5:51:00
gunicorn                         RUNNING   pid 12502, uptime 0:02:06
supervisor>

我们还可以使用tail命令读取日志文件的内容:

supervisor> 
supervisor> tail gunicorn 
27.0.0.1 - - [20/May/2018:13:56:42 +0000] "GET / HTTP/1.0" 200 10327 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
127.0.0.1 - - [20/May/2018:13:56:42 +0000] "GET / HTTP/1.0" 200 10327 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
127.0.0.1 - - [20/May/2018:13:56:43 +0000] "POST /GponForm/diag_Form?images/ HTTP/1.0" 404 92 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
127.0.0.1 - - [20/May/2018:13:57:31 +0000] "GET / HTTP/1.0" 200 10327 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"

supervisor>

默认情况下,tail命令从stdout读取。以下是我们如何从stderr中阅读。

supervisor> 
supervisor> tail gunicorn stderr
[2018-05-20 17:29:28 +0000] [12492] [INFO] Worker exiting (pid: 12492)
[2018-05-20 17:29:28 +0000] [12493] [INFO] Worker exiting (pid: 12493)
[2018-05-20 17:29:28 +0000] [12490] [INFO] Worker exiting (pid: 12490)
[2018-05-20 17:29:29 +0000] [12487] [INFO] Shutting down: Master
[2018-05-20 17:29:29 +0000] [12502] [INFO] Starting gunicorn 19.8.1
[2018-05-20 17:29:29 +0000] [12502] [INFO] Listening at: http://127.0.0.1:8000 (12502)
[2018-05-20 17:29:29 +0000] [12502] [INFO] Using worker: sync
[2018-05-20 17:29:29 +0000] [12505] [INFO] Booting worker with pid: 12505
[2018-05-20 17:29:29 +0000] [12507] [INFO] Booting worker with pid: 12507
[2018-05-20 17:29:29 +0000] [12508] [INFO] Booting worker with pid: 12508

supervisor>

最后,我们可以一次停止、启动和重新启动所有进程,如下所示:

supervisor> 
supervisor> stop all
celery_beat: stopped
gunicorn: stopped
celery_worker: stopped
supervisor> 
supervisor> 
supervisor> start all
celery_beat: started
celery_worker: started
gunicorn: started
supervisor> 
supervisor> 
supervisor> restart all
celery_beat: stopped
gunicorn: stopped
celery_worker: stopped
celery_beat: started
celery_worker: started
gunicorn: started
supervisor>

完成后,按 Ctrl+C 或键入quit退出监管器 Shell。

主管正在监控我们的所有流程。如果任何进程因为某种原因死亡或被终止,主管将自动启动该进程。

作为测试,尝试使用sudo reboot命令重新启动液滴。您会发现所有进程都会在启动时自动启动。

恭喜,您已经成功部署了 DjangoBin 项目。



Flask 教程

Flask 简介

原文:https://overiq.com/flask-101/intro-to-flask/

最后更新于 2020 年 7 月 27 日


Flask 是阿明·罗纳奇在 2010 年为 Python 编写的一个微框架。微是什么意思?

Micro 简单来说就是 Flask 小。它没有像其他流行的 Python 框架(如 Django 或金字塔)那样提供一套特定的工具或库。Flask 的设计考虑了延展性。它只提供了一组核心特性,并依赖扩展来完成其余的工作。开箱即用 Flask 不提供任何访问数据库、表单验证、身份验证、文件上传等支持。要将这些功能添加到您的应用中,您必须使用扩展。因为您可以选择您想要的扩展,所以您最终会得到精简堆栈,而无需任何额外的步骤。

Flask 对于如何构建应用也没有那么严格。不像像 Django 这样的框架,你必须遵循严格的规则。在 Flask 中,您可以自由地按照自己想要的方式构建应用。

在下一课中,我们将学习如何安装 Flask。

注意:本教程的练习文件可在https://github.com/overiq/flask_app获得



安装 Flask

原文:https://overiq.com/flask-101/installing-flask/

最后更新于 2020 年 7 月 27 日


注意:在继续之前,请确保您的系统上安装了工作正常的 Python 安装和 virtualenv 包。要了解如何安装 Python 和 virtualenv 请点击此处

创建虚拟环境

虚拟环境是 Python 安装的独立副本,我们可以在其中安装软件包,而不会影响全局 Python 安装。创建一个名为flask_app的新目录。该目录将托管我们的 Flask 应用。

overiq@vm:~$ mkdir flask_app
overiq@vm:~$

使用cd命令将当前工作目录更改为flask_app

overiq@vm:~$ cd flask_app/
overiq@vm:~/flask_app$

下一步是使用virtualenv命令在flask_app目录内创建一个虚拟环境。

overiq@vm:~/flask_app$ virtualenv env
Using base prefix '/usr'
New python executable in /home/overiq/flask_app/env/bin/python3
Also creating executable in /home/overiq/flask_app/env/bin/python
Installing setuptools, pip, wheel...done.
overiq@vm:~/flask_app$

执行上述命令后,在flask_app目录中应该有一个名为env的目录。env目录构成了一个单独的 Python 安装。它包含所有的可执行脚本,就像普通的 Python 安装一样。要使用这个虚拟环境,您必须首先激活它。

要在 Linux 和 Mac OS 中激活虚拟环境,请输入以下命令。

overiq@-vm:~/flask_app$ source env/bin/activate
(env) overiq@vm:~/flask_app$

Windows 用户可以通过输入以下命令来激活虚拟环境。

C:\Users\overiq\flask_app>env\Scripts\activate
(env) C:\Users\overiq\flask_app>

请注意提示字符串前括号内的虚拟环境名称,即(env)。这表明我们的虚拟环境已经启动并运行。从现在开始安装的软件包只能在此虚拟环境中使用。

激活虚拟环境会暂时改变PATH环境变量。因此,如果您在终端中键入python,将调用驻留在虚拟环境中的 Python 解释器,即env目录,而不是全局 Python 解释器。

一旦你完成了虚拟环境的工作,你必须使用deactivate命令去激活它。

(env) overiq@vm:~/flask_app$ deactivate
overiq@vm:~/flask_app$

该命令使全局 Python 解释器再次可用。

安装 Flask

要在虚拟环境中安装 Flask,请输入以下命令。

(env) overiq@vm:~/flask_app$ pip install flask

您可以通过调用 Python 解释器和导入 Flask 来验证安装是否成功。

(env) overiq@vm:~/flask_app$ python
Python 3.5.2 (default, Nov 17 2016, 17:05:23) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 
>>> import flask
>>> 
>>> flask.__version__
'0.12.2'
>>>

如果没有出现错误,这意味着 Flask 安装成功。



Flask 基础

原文:https://overiq.com/flask-101/flask-basics/

最后更新于 2020 年 7 月 27 日


Flask 中的你好世界

让我们通过创建一个输出"Hello World"的简单应用来开始我们进入 Flask 的冒险。创建一个名为main.py的新文件,并在其中输入以下代码。

Flask _app/main.py

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello World'

if __name__ == "__main__":
    app.run()

我们刚刚在 Flask 中创建了“你好世界”应用。如果main.py中的代码对你没有意义,那没关系。在接下来的几节中,我们将更详细地讨论一切。要运行main.py,请在虚拟环境中输入以下命令。

(env) overiq@vm:~/flask_app$ python main.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

运行main.py文件在端口 5000 启动 flask 提供的开发服务器。现在打开你最喜欢的浏览器,访问 http://127.0.0.1:5000/ 查看 Hello World app 的运行情况。

要停止服务器,请按 CTRL+C。

实例化 Flask 应用

每个 Flask 应用都必须有一个Flask类的实例。该实例实际上是一个 WSGI(网络服务器网关接口)应用,这仅仅意味着网络服务器将它收到的所有请求传递给这个实例进行进一步处理。我们实例化类Flask的对象如下:

from flask import Flask
app = Flask(__name__)

在第 1 行,我们正在从flask包中导入一个名为Flask的类。

在第 2 行,我们通过将__name__参数传递给Flask构造函数来实例化一个Flask对象。Flask构造函数有一个必需的参数,即应用包的名称。很多时候__name__才是正确的价值观。Flask 使用应用包的名称来查找静态资产、模板等。

创建路线

路由是将网址绑定到视图函数的行为。视图函数只是一个响应请求的函数。在 Flask 中,我们使用Flask实例提供的route装饰器将一个网址与一个视图函数相关联。以下是我们如何在 Flask 中创建路线。

@app.route('/')
def index():
    return 'Hello World'

这段代码将index()视图函数注册为应用根 URL 的处理程序。换句话说,每当应用收到路径为/的请求时,将调用index()函数来完成请求。

或者,您可以使用add_url_rule()方法代替route装饰器来定义路线。add_url_rule()是一种简单的方法,而不是装饰。除了 URL,它还接受端点和视图函数的名称来调用。端点只是指给路线的唯一名称,通常视图函数的名称用作端点。Flask 可以从一个端点生成 URL,我们将在接下来的课程中学习如何做到这一点。前面的代码相当于下面的代码:

def index():
    return 'Hello World'
app.add_url_rule('/', 'index', index)

大多数时候我们会使用route装饰器,但是add_url_rule()也有它的用途。

view 函数必须返回一个字符串。试图返回其他内容将导致 500 内部服务器错误。

我们可以根据应用需要创建任意多的路由。例如,在下面的列表中,我们创建了三条路线。

@app.route('/')
def index():
    return 'Home Page'

@app.route('/career/')
def career():
    return 'Career Page'

@app.route('/feedback/')
def feedback():
    return 'Feedback Page'

当路由中的一个网址以一个尾斜杠(/)结束时,Flask 将把没有尾斜杠的请求重定向到有尾斜杠的网址。这意味着对/career的请求将被重定向到/career/

我们还可以将多个网址映射到同一个视图函数。例如:

@app.route('/contact/')
@app.route('/feedback/')
def feedback():
    return 'Feedback Page'

在这种情况下,每当请求到达/contact//feedback/时,feedback()功能将被调用来完成请求。

如果你试图访问一个没有映射到视图函数的网址,你会得到一个 404 找不到的错误。

到目前为止,我们创建的路线都是静态的。现在大多数应用都是由动态网址组成的。动态网址是由一个或多个影响页面输出的可变部分组成的网址。例如,假设您正在构建一个由用户配置文件组成的 web 应用,每个用户都有一个唯一的 id。您想在/user/1显示用户#1 的简介,在/user/2显示用户#2 的简介,以此类推。解决这个问题的一个笨拙的方法是为每个用户创建一条路线。

相反,我们可以做的是将网址中的动态部分标记为<variable_name>。然后,动态部分作为关键字参数传递给视图函数。下面的列表定义了一个包含动态部分的路径。

@app.route('/user/<id>/')
def user_profile(id):
    return "Profile page of user #{}".format(id)

在本例中,<id>占位符将映射到/user/ URI 之后的任何东西。例如,如果您访问/user/100/,您将获得以下响应。

Profile page of user #100

我们不限于数字身份证。以上路线还将匹配/user/cowboy//user/foobar10//user/@@##/等。然而,它不会像/user//user/12/post/一样匹配 URI。通过指定一个转换器,我们可以将路线限制为仅匹配/user/ URI 之后的数字标识。

默认情况下,网址的动态部分作为字符串传递给视图函数。我们可以通过在网址的动态部分之前指定一个转换器<converter:variable_name>来改变这一点。例如,
/user/<int:id>/路由会匹配/user/1/``/user/2000/等网址。不会匹配/user/cowboy//user/foobar10//user/@@##/等网址。

下表列出了 Flask 中可用的所有转换器:

转换器 描述
string 接受任何字符串,这是默认值。
int 接受整数。
float 接受浮点值。
path 接受带前导斜杠和正斜杠的路径名。
uuid 接受 uuid 字符串。

启动服务器

为了启动开发服务器,我们调用Flask对象的run()方法。

if __name__ == "__main__":
    app.run()

条件__name__ == "__main__"保证了run()方法只有在main.py作为主程序运行时才被调用。如果在另一个 Python 模块中导入main.py,则不会调用run()方法。

注:开发服务器自带 Flask 仅用于开发目的。因此,它将在生产中表现不佳。

现在你应该很清楚main.py是如何工作的了。

调试方式

编程中的 bug 是不可避免的,迟早你会引入一个。这就是为什么知道如何调试应用并快速修复错误是极其重要的。Flask 附带了一个强大的交互式网络调试器,但默认情况下是关闭的。当调试器关闭并且 Flask 遇到任何错误时,它将显示 500 内部服务器错误。让我们通过在我们的main.py文件中故意引入一个 bug 来看看这个行为是如何发生的。打开main.py,修改文件如下(修改突出显示):

Flask _app/main.py

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    print(i)
    return 'Hello World'

if __name__ == "__main__":
    app.run()

这里我们试图打印一个未定义的变量i的值,所以我们必然会得到一个错误。如果尚未运行,请启动服务器并访问。您应该会看到 500 内部服务器错误,如下所示:

尽管如此,浏览器并没有让你看到错误的类型。如果您查看运行服务器的 shell 的标准输出,您将看到刚刚发生的错误的回溯。在这种情况下,回溯如下所示:

File "/home/overiq/flask_app/env/lib/python3.5/site-packages/flask/app.py", line 1612, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/overiq/flask_app/env/lib/python3.5/site-packages/flask/app.py", line 1598, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "main.py", line 13, in index
    print(i)
NameError: name 'i' is not defined

此外,当调试模式关闭时,每次更改代码后,您都必须手动启动服务器才能使更改生效。打开调试模式将在每次更改后自动重新启动服务器。

要打开调试模式,请将debug=True传递给run()方法,如下所示:

if __name__ == "__main__":
    app.run(debug=True)

开启调试模式的另一种方法是将Flask实例的debug属性设置为True

from flask import Flask
app = Flask(__name__)
app.debug = True

如下修改main.py(更改突出显示)并运行。

Flask _app/main.py

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    print(i)
    return 'Hello World'

if __name__ == "__main__":
    app.run(debug=True)

再次访问 http://127.0.0.1:5000/ ,这次应该会看到 Flask 调试器在运行,如下所示:

如果启用调试器后出现任何错误,您将看到问题的详细追溯,而不是一般的 500 内部服务器错误。通常,回溯给出了发生问题的良好指示。在页面底部,我们可以看到打印语句,该语句试图打印未定义变量i的值以及错误类型,这是NameError异常,告诉我们名称i未定义。

单击回溯中的一行代码将展开问题代码周围的源代码。这对于在解释错误时建立一些上下文非常有用。

您可能已经注意到,当您将鼠标放在一行代码上时,会出现回溯终端图标。单击终端图标会打开一个控制台,您可以在其中执行任何 Python 代码。

这个控制台允许我们在异常发生时检查局部变量。

如果您是第一次打开控制台,它会提示您输入个人识别码。

这是一项安全措施,旨在将对控制台的访问权限限制在授权用户。要访问控制台,您必须输入正确的个人识别码。您可以在运行服务器的 Shell 的标准输出中找到个人识别码。

让我们通过创建另一个 Flask 应用来结束这一课,该应用实现了我们到目前为止所学的一切。

使用以下代码创建另一个名为main2.py的文件:

Flask _app/main2.py

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello Flask'

@app.route('/user/<int:user_id>/')
def user_profile(user_id):
    return "Profile page of user #{}".format(user_id)

@app.route('/books/<genre>/')
def books(genre):
    return "All Books in {} category".format(genre)

if __name__ == "__main__":
    app.run(debug=True)

运行文件并访问 http://localhost:5000/ ,您应该会收到如下“Hello Flask”的问候:

这个新版本的应用增加了两条动态的新路线。让我们测试一下。在您的浏览器地址栏中键入http://localhost:5000/user/123/,您应该会得到如下响应:

请注意,路由/user/<int:user_id>/将只匹配动态部分(即user_id)为整数的网址。

要测试第二条动态路线,请访问http://localhost:5000/books/sci-fi/。这次你应该得到这样的回应:

此时,如果您试图访问一个未在路由中定义的网址,您将得到一个 404 未找到错误。例如,在访问http://localhost:5000/products时,应用会以 404 错误进行响应,如下所示:

Flask 如何处理请求?

那么,当 Flask 收到客户端的请求时,它如何知道要执行哪个视图函数呢?

内部 Flask 维护要执行的 URL 和视图函数的映射。这个映射是使用Flask实例的route装饰器或add_url_rule()方法创建的。我们可以使用Flask实例的url_map属性来访问这个映射。

>>>
>>> from main2 import app
>>> app.url_map
Map([<Rule '/' (OPTIONS, GET, HEAD) -> index>,
 <Rule '/static/<filename>' (OPTIONS, GET, HEAD) -> static>,
 <Rule '/books/<genre>' (OPTIONS, GET, HEAD) -> books>,
 <Rule '/user/<user_id>' (OPTIONS, GET, HEAD) -> user_profile>])
>>>
>>>

如输出所示,映射中有 4 个规则。Flask 以以下格式创建网址映射:

url pattern, (comma separated list of HTTP methods handled by the route) -> view function to execute

路线/static/<filename>由 Flask 自动添加,用于服务静态文件。我们将在第中讨论如何服务静态文件。



Flask 中的上下文

原文:https://overiq.com/flask-101/contexts-in-flask/

最后更新于 2020 年 7 月 27 日


Flask 使用上下文使某些变量暂时全局可访问。

如果你来自 Django 背景,那么你可能已经注意到 Flask 中的视图函数不接受request作为第一个参数。在 Flask 中,我们使用request对象访问传入请求中的数据,如下所示:

from flask import Flask, request

@app.route('/')
def requestdata():
    return "Hello! Your IP is {} and you are using {}: ".format(request.remote_addr, request.user_agent)

上面的代码可能会给你一个印象request是全局对象,其实不是。如果request是一个全局对象,那么在多线程程序中,我们的应用将无法区分两个同时发生的请求,因为多线程程序在线程之间共享所有变量。Flask 使用一种叫做 Contexts 的东西来使某些变量表现得像全局变量,当您访问它们时,您就可以访问当前线程的对象。在技术术语中,这样的变量被称为线程本地变量

根据文档,Flask 提供了两种上下文:

  1. 应用上下文。
  2. 请求上下文。

应用上下文用于存储对应用通用的值,如数据库连接、配置等;而请求上下文用于存储特定于每个请求的值。

应用上下文公开了像current_appg这样的对象。current_app指的是处理请求的实例,g用于在请求处理过程中临时存储数据。一旦设置了一个值,您就可以在任何视图函数中访问它。存储在g中的数据在每次请求后都会重置。

就像应用上下文一样,请求上下文也公开了像requestsession这样的对象。如前所述,request对象包含关于当前 web 请求的信息,session是一个类似字典的对象,用于存储请求之间持续存在的值。

Flask 在收到请求时激活(或推送)应用和请求上下文,并在处理请求时移除它们。当应用上下文被推送时,它所公开的所有变量对线程都变得可用。类似地,当请求上下文被推送时,它所公开的所有变量对线程都变得可用。在视图函数中,您可以访问由应用和请求上下文公开的所有对象,因此您不必担心应用或请求上下文是否处于活动状态。但是,如果您试图在视图函数之外或者在 Python Shell 中访问这些对象,您将会得到一个错误。下面的 shell 会话演示了这一点:

>>> 
>>> from flask import Flask, request, current_app
>>> 
>>> request.method   # get the request method 
Traceback (most recent call last):
...
RuntimeError: Working outside of request context.

This typically means that you attempted to use functionality that needed
an active HTTP request.  Consult the documentation on testing for
information about how to avoid this problem.
>>>

request.method返回请求中使用的 HTTP 方法,但是由于没有实际的 HTTP 请求,请求上下文不是活动的。

如果您试图访问应用上下文公开的对象,将会得到类似的错误。

>>> 
>>> current_app.name  # get the name of the application
Traceback (most recent call last):
...
RuntimeError: Working outside of application context.

This typically means that you attempted to use functionality that needed
to interface with the current application object in a way.  To solve
this set up an application context with app.app_context().  See the
documentation for more information.
>>>

要访问视图函数之外的应用/请求上下文公开的对象,必须首先创建应用/请求上下文。

我们可以使用Flask实例的app_context()方法创建应用上下文。

>>>
>>> from main2 import app
>>> from flask import Flask, request, current_app
>>>
>>> app_context = app.app_context()
>>> app_context.push()
>>>
>>> current_app.name
'main2'
>>>
>>>

前面的代码可以使用with语句简化如下:

>>>
>>> from main2 import app
>>> from flask import Flask, request, current_app
>>>
>>> with app.app_context():
...    current_app.name
...
'main2'
>>>
>>>

with语句是创建上下文的首选方式。

同样,我们可以使用Flask实例的test_request_context()方法创建请求上下文。需要记住的重要一点是,每当推送请求上下文时,如果应用上下文还不存在,就会创建它。下面的 shell 会话演示了如何创建请求上下文:

>>>
>>> from main2 import app
>>> from flask import current_app, request
>>>
>>> with app.test_request_context('/products'):
...     request.path  # get the full path of the requested page without the domain name.
...     request.method
...     current_app.name
...
'/products'
'GET'
'main2'
>>>
>>>

网址/products任意选择。

关于 Flask 中的上下文,您只需要知道这些。



Flask 中的自定义响应和挂钩点

原文:https://overiq.com/flask-101/custom-response-and-hook-points-in-flask/

最后更新于 2020 年 7 月 27 日


自定义响应

Flask 提供了三种不同的模式来创建响应:

  1. 作为字符串或使用模板引擎。
  2. 作为回应对象。
  3. 作为形式为(response, status, headers)(response, headers)的元组。

让我们逐一看看这些模式。

将响应创建为字符串

@app.route('/books/<genre>')
def books(genre):
    return "All Books in {} category".format(genre)

到目前为止,我们已经使用这种模式向客户端发送了响应。当 Flask 看到我们正在从视图函数返回一个字符串时,它会自动将该字符串转换为响应对象(使用make_response()方法),其中字符串作为响应的主体,HTTP 状态代码为 200,content-type标头设置为text/html。大多数时候这就是你所需要的。但是,有时您需要在向客户端发送响应之前设置一些额外的头。对于这种情况,您必须使用make_response()功能创建响应。

使用 make_response()创建响应

make_response()的语法如下:

res_obj = make_response(res_body, status_code=200)

res_body是表示响应主体的必需参数,status_code是可选的 HTTP 状态代码,默认为 200。

下面的列表显示了如何使用make_response()函数设置附加标题。

from flask import Flask, make_response, 

@app.route('/books/<genre>')
def books(genre):
    res = make_response("All Books in {} category".format(genre))
    res.headers['Content-Type'] = 'text/plain'
    res.headers['Server'] = 'Foobar'
    return res

下面的列表显示了如何使用make_response()函数返回 HTTP 404 错误。

@app.route('/')
def http_404_handler():
    return make_response("<h2>404 Error</h2>", 400)

设置 cookies 是 web 应用中的另一项常见任务。make_response()功能使该操作非常容易。下面的清单在客户端的浏览器中设置了两个 cookies。

@app.route('/set-cookie')
def set_cookie():
    res = make_response("Cookie setter")    
    res.set_cookie("favorite-color", "skyblue")
    res.set_cookie("favorite-font", "sans-serif")
    return res

注:我们在中详细讨论 Cookie。

以上列表设置的 cookies 将持续到浏览器会话。我们可以通过将秒数作为第三个参数传递给set_cookie()方法来设置 cookie 的过期时间。例如:

@app.route('/cookie')
def set_cookie():
    res = make_response("Cookie setter")
    res.set_cookie("favorite-color", "skyblue", 60*60*24*15)
    res.set_cookie("favorite-font", "sans-serif", 60*60*24*15)
    return res

该列表将 cookies 的过期时间设置为 15 天。

使用元组创建响应

创建响应的最后一种模式是使用以下格式之一的元组:

(response, status, headers) 

(response, headers) 

(response, status)

response是代表响应主体的字符串,status是 HTTP 状态代码,可以是整数或字符串,headers是包含头值的字典。

@app.route('/')
def http_500_handler():
    return ("<h2>500 Error</h2>", 500)

这个视图函数将返回 HTTP 500 内部服务器错误。因为我们可以在创建元组时省略括号,所以上面的代码也可以编写如下:

@app.route('/')
def http_500_handler():
    return "<h2>500 Error</h2>", 500

下面的清单显示了如何使用元组设置头:

@app.route('/')
def render_markdown():
    return "## Heading", 200, {'Content-Type': 'text/markdown'}

你能猜出下面的视图函数是做什么的吗?

@app.route('/transfer')
def transfer():
    return "", 302, {'location': 'http://localhost:5000/login'}

该视图功能使用 HTTP 302 响应代码(临时重定向)将用户重定向到HTTP://localhost:5000/登录。将用户重定向到不同的页面是如此的普遍,以至于 Flask 提供了一个名为redirect()的帮助函数来简化工作。

from flask import Flask, redirect

@app.route('/transfer')
def transfer():
    return redirect("http://localhost:5000/login")

默认情况下,redirect()执行 HTTP 302 重定向,要执行 HTTP 301 重定向,请将 301 的 HTTP 状态代码传递给redirect()函数,如下所示:

from flask import Flask, redirect

@app.route('/transfer')
def transfer():
    return redirect("http://localhost:5000/login", code=301)

挂钩点

在 web 应用中,在每个请求之前和之后执行一些代码是很常见的。例如,假设我们想要记录访问我们的应用的用户的 IP 地址,或者在显示隐藏页面之前验证用户。Flask 没有在每个视图函数中复制这样的代码(这很疯狂),而是为这样的场景提供了以下装饰器:

  • before_first_request:这个装饰器在处理第一个请求之前注册一个要执行的函数。

  • before_request:这个装饰器在处理请求之前注册一个要执行的函数。

  • after_request:这个装饰器注册一个函数,在请求被处理后执行。如果请求处理程序中出现未处理的异常,将不会调用注册的函数。该函数必须接受响应对象并返回相同或新的响应。

  • teardown_request:类似于after_request装饰器,但是无论请求处理程序是否抛出异常,注册的函数都会一直执行。

请注意,如果before_request装饰器注册的函数返回响应,则不会调用请求处理程序。

下面的清单演示了如何利用 Flask 中的钩子点。创建一个名为hooks.py的新文件,代码如下所示:

Flask _app/hooks.py

from flask import Flask, request, g

app = Flask(__name__)

@app.before_first_request
def before_first_request():
    print("before_first_request() called")

@app.before_request
def before_request():
    print("before_request() called")

@app.after_request
def after_request(response):
    print("after_request() called")
    return response

@app.route("/")
def index():
    print("index() called")
    return '<p>Testings Request Hooks</p>'

if __name__ == "__main__":
    app.run(debug=True)

启动服务器,通过访问 http://localhost:5000/ 进行第一次请求。在运行服务器的 shell 的标准输出中,您应该会得到以下输出:

before_first_request() called
before_request() called
index() called
after_request() called

注意:为简洁起见,省略了服务器请求日志。

刷新页面,这次您应该会在 shell 中获得以下输出。

before_request() called
index() called
after_request() called

由于这是我们的第二个请求before_first_request()功能没有执行。

使用中止()中止请求

Flask 提供了一个名为abort()的函数,用于终止带有特定错误代码(如 404、500 等)的请求。例如:

from flask import Flask, abort

@app.route('/')
def index():
    abort(404)
    # code after abort() execution will never be executed

这个视图函数将返回一个通用的 404 页面,如下所示:

对于其他类型的错误,abort()将显示类似的页面。如果您想自定义错误页面,请使用errorhandler装饰器,这将在下面讨论。

自定义错误页面

errorhandler装饰器用于创建自定义错误页面。它接受一个参数,即您正在为其创建自定义错误页的 HTTP 错误代码。打开hooks.py文件,使用errorhandler装饰器为 HTTP 404 和 HTTP 500 错误创建自定义错误页面,如下所示(更改突出显示):

Flask _app/hooks.py

from flask import Flask, request, g, abort
#...
#...
@app.after_request
def after_request(response):
    print("after_request() called")
    return response

@app.errorhandler(404)
def http_404_handler(error):
    return "<p>HTTP 404 Error Encountered</p>", 404

@app.errorhandler(500)
def http_500_handler(error):
    return "<p>HTTP 500 Error Encountered</p>", 500

@app.route("/")
def index():
    # print("index() called")
    # return '<p>Testings Request Hooks</p>'
    abort(404)  

if __name__ == "__main__":
#...

请注意,两个错误处理程序都使用一个名为error的参数,该参数包含关于所发生的错误类型的附加信息。

如果您现在访问根网址,您将得到以下响应:



Flask 中的模板

原文:https://overiq.com/flask-101/templates-in-flask/

最后更新于 2020 年 7 月 27 日


到目前为止,我们一直在视图函数中直接硬编码 HTML 字符串。尽管这种方法对于演示目的来说很好,但是在构建真实世界的应用时并不太合适。如今大多数网页都很长,由许多动态组件组成。我们使用模板,而不是在视图函数中嵌入大块的 HTML 字符串(维护起来会很糟糕)。

模板

模板只是一个文本文件,其中包含静态 HTML 代码以及一些特殊的标记,这些标记表示在请求时已知的动态内容。替换动态标记并生成平面 HTML 页面的过程称为模板渲染。Flask 附带了一个名为 Jinja 的模板引擎,它负责解析模板并将其转换为平面 HTML 页面。

Jinja 模板引擎是 Python 中最强大、最流行的模板引擎之一。如果你曾经使用过 Django 模板,那么你会有宾至如归的感觉。需要注意的是,Jinja 和 Flask 是两个独立的包,可以独立使用。

使用 render_template()渲染模板

默认情况下,Flask 会在应用文件夹内名为templates的子目录中查找模板。我们可以通过在创建应用实例时将template_folder参数传递给Flask构造函数来改变这个默认行为。

app = Flask(__name__, template_folder="jinja_templates")

此代码将模板的默认位置更改为应用目录内的jinja_templates目录。目前,我们没有任何理由这样做,所以我们将继续使用默认的templates目录来存储模板。

flask_app应用目录中创建一个名为templates的新目录。在templates目录内创建一个名为index.html的模板,代码如下:

flask _ app/templates/index . html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    <p>Name: {{ name }}</p>

</body>
</html>

请注意,除了裸露的 HTML 之外,模板还有一个标记为{{ name }}的动态组件。双花括号{{ }}内的name代表一个变量,其值将在渲染模板时指定。假设name的值是Jerry,那么渲染完模板后,你会得到下面的 HTML。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    <p>Name: Jerry</p>

</body>
</html>

Flask 提供了一个名为render_template()的函数来渲染模板。正是这个功能将 Jinja 和 Flask 融为一体。为了渲染一个模板,我们调用render_template(),使用模板名称以及您想要作为关键字参数传递给模板的数据。render_template()函数渲染模板并返回字符串形式的 HTML。我们传递给模板的关键字参数被称为模板上下文,或者简称为模板上下文。下面的列表显示了如何使用render_template()渲染index.html模板(更改被突出显示)。

Flask _app/main2.py

from flask import Flask, request, render_template
app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html', name='Jerry')
#...

注意name='Jerry'中的name是指模板index.html中引用的变量。

如果您现在访问 http://localhost:5000/ ,您将获得以下响应:

如果您有相当多的参数要传递给render_template(),那么不要使用逗号(,)来分隔它们,而是创建一个字典,并对其应用**运算符来将关键字参数传递给函数。例如:

@app.route('/')
def index():
    name, age, profession = "Jerry", 24, 'Programmer'
    template_context = dict(name=name, age=age , profession=profession)
    return render_template('index.html', **template_context)

index.html模板现在可以访问三个模板变量:nameageprofession

如果不指定模板上下文会发生什么?

什么都不会发生,你不会得到任何警告或异常,Jinja 会像往常一样渲染模板,并打印空字符串来代替变量。要在操作中查看此行为,请修改index()查看功能如下:

Flask _app/main2.py

#...
@app.route('/')
def index():
    return render_template('index.html')
#...

并访问 http://localhost:5000/ 。这一次,您将获得以下 HTML 响应:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    <p>Name: </p>

</body>
</html>

现在您应该对模板在 Flask 中的使用有了一个大概的了解,下一节将展示如何在控制台中渲染模板。

控制台内的渲染模板

出于测试目的,在控制台内渲染模板相当容易,而不是创建几个文件。启动 Python shell,从jinja2包导入Template类,如下所示。

>>>
>>> from jinja2 import Template
>>>

要创建Template对象,只需将模板的内容作为原始字符串传递。

>>>
>>> t = Template("Name: {{ name }}")
>>>

要渲染模板,请调用Template对象的render()方法,并将数据作为关键字参数。

>>>
>>> t.render(name='Jerry')
'Name: Jerry'
>>>

在下一课中,我们将讨论 Jinja 模板语言。



Jinja 模板语言基础

原文:https://overiq.com/flask-101/basics-of-jinja-template-language/

最后更新于 2020 年 7 月 27 日


Jinja 模板语言是一组小的结构,它帮助我们自动创建模板。

变量、表达式和函数调用

在 Jinja 中,双曲{{ }}大括号允许我们评估表达式、变量或函数调用,并将结果打印到模板中。例如:

评估表达式

>>>
>>> from jinja2 import Template
>>>
>>> Template("{{ 10 + 3 }}").render()
'13'
>>>
>>> Template("{{ 10 - 3 }}").render()
'7'
>>>
>>> Template("{{ 10 // 3 }}").render()
'3'
>>>
>>> Template("{{ 10 / 3 }}").render()
'3.3333333333333335'
>>>
>>> Template("{{ 10 % 3 }}").render()
'1'
>>>
>>> Template("{{ 10 ** 3 }}").render()
'1000'
>>>

其他 Python 操作符如比较、逻辑和成员操作符也可以在表达式中使用。

评估变量

>>>
>>> Template("{{ var }}").render(var=12)
'12'
>>>
>>> Template("{{ var }}").render(var="hello")
'hello'
>>>

我们不仅限于数字和字符串,Jinja 模板还可以处理复杂的数据结构,如列表、字典、元组,甚至自定义类。

>>>
>>> Template("{{ var[1] }}").render(var=[1,2,3])
'2'
>>>
>>> Template("{{ var['profession'] }}").render(var={'name':'tom', 'age': 25, 'profession': 'Manager' })
'Manager'
>>>
>>>
>>> Template("{{ var[2] }}").render(var=("c", "c++", "python"))
'python'
>>>
>>>
>>> class Foo:
...    def  __str__(self):
...        return "This is an instance of Foo class"
...
>>>
>>> Template("{{ var }}").render(var=Foo())
'This is an instance of Foo class'
>>>
>>>

在索引无效的情况下,Jinja 将无声地输出一个空字符串。

>>>
>>> Template("{{ var[100] }}").render(var=("c", "c++", "python"))
''
>>>

函数调用

在 Jinja 中,为了计算一个函数,我们像往常一样简单地调用它。

>>>
>>> def foo():
...    return "foo() called"
...
>>>
>>> Template("{{ foo() }}").render(foo=foo)
'foo() called'
>>>

属性和方法

要访问对象的属性和方法,请使用点(.)运算符。

>>>
>>> class Foo:
...     def __init__(self, i):
...         self.i = i
...     def do_something(self):
...         return "do_something() called"
...
>>>
>>> Template("{{ obj.i }}").render(obj=Foo(5))
'5'
>>>
>>> Template("{{ obj.do_something() }}").render(obj=Foo(5))
'do_something() called'
>>>

评论

Jinja 使用以下语法在模板中添加单个或多个注释:

{# comment in a line #}

{#
    comment expanded 
    to multiple
    lines
#}

设置变量

在模板内部,我们可以使用set语句定义一个变量。

{% set fruit = 'apple' %}
{% set name, age = 'Tom', 20 %} {# tuple unpacking works inside templates too #}

我们定义变量来存储一些复杂操作的结果,以便可以在模板中重用。在控制结构之外定义的变量(下面讨论)充当全局变量,可以在任何控制结构内部访问。然而,在控制结构内部创建的变量充当局部变量,并且仅在定义它的控制结构内部可见,该规则的唯一例外是if语句。

控制结构

控制结构允许我们在模板中添加控制流和循环。默认情况下,控制结构使用{% ... %}分隔符而不是双花括号{{ ... }}

如果语句

Jinja 中的if语句模仿 Python 中的 if 语句,条件的值决定了语句的流程。例如:

{% if bookmarks %}
    <p>User has some bookmarks</p>
{% endif %}

如果变量bookmarks的值评估为真,则字符串<p>User has some bookmarks</p>将被打印。请记住,在 Jinja 中,如果一个变量没有被定义,它的计算结果将为 false。

我们也可以像普通 Python 代码一样使用elifelse子句。例如:

{% if user.newbie %}
    <p>Display newbie stages</p>
{% elif user.pro %}
    <p>Display pro stages</p>
{% elif user.ninja %}
    <p>Display ninja stages</p>
{% else %}
    <p>You have completed all stages</p>    
{% endif %}

控制语句也可以嵌套。例如:

{% if user %}
    {% if user.newbie %}
        <p>Display newbie stages</p>
    {% elif user.pro %}
        <p>Display pro stages</p>
    {% elif user.ninja %}
        <p>Display ninja stages</p>
    {% else %}
        <p>You have completed all states</p>
    {% endif %}
{% else %}
    <p>User is not defined</p>
{% endif %}

在某些情况下,在一行中添加 if 语句非常有用。Jinja 支持内联 if 语句,但是调用 if 表达式,因为它是使用双花括号{{ ... }}而不是{% ... %}创建的。例如:

{{ "User is logged in" if loggedin else "User is not logged in"  }}

这里,如果变量loggedin评估为真,那么将打印字符串“用户已登录”。否则,将打印字符串“用户未登录”。

else子句是可选的,如果没有提供,那么 else 块将被评估为未定义的对象。

{{ "User is logged in" if loggedin  }}

这里,如果loggedin变量评估为真,那么将打印字符串“用户已登录”。否则,将不会打印任何内容。

就像 Python 一样,我们可以在控制结构中使用比较、逻辑和成员操作符来创建更复杂的条件。以下是一些例子:

{# if user.count is equal to 1000, '<p>User count is 1000</p>' will be printed #}
{% if users.count == 1000 %}
    <p>User count is 1000</p>
{%  endif %}

{# expression 10 >= 2 is true so '<p>10 >= 2</p>' will be printed #}
{% if 10 >= 2 %}
    <p>10 >= 2</p>
{%  endif %}

{# expression "car" <= "train" is true so '<p>car <= train</p>' will be printed #}
{% if "car" <= "train" %}
    <p>car <= train</p>
{%  endif %}

{# 
    if user is logged in and is a superuser, 
    '<p>User is logged in and is a superuser</p>' will be printed 
#}
{% if user.loggedin and user.is_superuser %}
    <p>User is logged in and is a superuser</p>
{%  endif %}

{# 
    if user is superuser or moderator or author 
    '<a href="#">Edit</a>' will be printed 
#}
{% if user.is_superuser or user.is_moderator or user.is_author %}
    <a href="#">Edit</a>
{%  endif %}

{# 
    if user and current_user points to the same object
    <p>user and current_user are same</p> will be printed 
#}
{% if user is current_user  %}
    <p>user and current_user are same</p>
{%  endif %}

{# 
    As "Flask" is one of element in dictionary
    '<p>Flask is in the dictionary</p>' will be printed 
#}
{% if ["Flask"] in ["Django", "web2py", "Flask"] %}
    <p>Flask is in the dictionary</p>
{%  endif %}

如果您的条件变得过于复杂,或者您只想更改运算符优先级,您可以将表达式包装在括号()中,如下所示:

{% if (user.marks > 80) and (user.marks < 90)  %}
    <p>You grade is B</p>
{%  endif %}

For 循环

For 循环允许我们迭代一个序列。例如:

{% set user_list = ['tom','jerry', 'spike'] %}

<ul>
{% for user in user_list %}
    <li>{{ user }}</li>
{% endfor %}
</ul>

输出:

<ul>

    <li>tom</li>

    <li>jerry</li>

    <li>spike</li>

</ul>

以下是如何遍历字典的方法:

{% set employee = { 'name': 'tom', 'age': 25, 'designation': 'Manager' } %}

<ul>
{% for key in employee.items() %}
    <li>{{ key }} : {{ employee[key] }}</li>
{% endfor %}
</ul>

输出:

<ul>

    <li>designation : Manager</li>

    <li>name : tom</li>

    <li>age : 25</li>

</ul>

注意:在 Python 中,字典的元素没有以任何特定的顺序存储,因此输出可能会有所不同。

如果要同时检索字典的关键字和值,使用如下items()方法。

{% set employee = { 'name': 'tom', 'age': 25, 'designation': 'Manager' } %}

<ul>
{% for key, value in employee.items() %}
    <li>{{ key }} : {{ value }}</li>
{% endfor %}
</ul>

输出:

<ul>

    <li>designation : Manager</li>

    <li>name : tom</li>

    <li>age : 25</li>

</ul>

for循环也可以像 Python 一样带一个可选的else子句,不过用法略有不同。回想一下,在 Python 中,当 for 循环后跟一个else子句时,else子句仅在 for 循环在循环完序列后终止或序列为空时执行。当 for 循环被break语句终止时,它不会被执行。

else子句与 Jinja 中的for循环一起使用时,仅当序列为空或未定义时才执行。例如:

{% set user_list = [] %}

<ul>
{% for user in user_list  %}
    <li>{{ user }}</li>
{% else %}
    <li>user_list is empty</li>
{% endfor %}
</ul>

输出:

<ul>

    <li>user_list is empty</li>

</ul>

类似于嵌套if语句,我们也可以有嵌套 for 循环。事实上,我们可以将任何控制结构嵌套在一起。

{% for user in user_list %}
    <p>{{ user.full_name }}</p>
    <p>
        <ul class="follower-list">
            {% for follower in user.followers %}
            <li>{{ follower }}</li>
            {% endfor %}
        </ul>
    </p>
{% endfor %}

for循环使您可以访问一个名为loop的特殊变量来跟踪循环的进度。例如:

<ul>
{% for user in user_list  %}
    <li>{{ loop.index }} - {{ user }}</li>
{% endfor %}
</ul>

for 循环中的loop.index返回从 1 开始的循环的当前迭代。下表列出了loop变量的其他常用属性。

可变的 描述
loop.index0 loop.index相同,但索引为 0,即它从 0 而不是 1 开始计数迭代。
loop.revindex 返回从循环结束(1 个索引)开始的迭代。
loop.revindex0 loop.revindex相同,但索引为 0。
loop.first 如果当前迭代是第一次迭代,则返回True。否则False
loop.last 如果当前迭代是最后一次迭代,则返回True。否则False
loop.length 返回 for 循环迭代的序列长度。

注意:如需完整列表,请访问文档。

过滤

滤镜会在渲染变量之前对其进行修改。使用过滤器的语法如下:

variable_or_value|filter_name

这里有一个例子:

{{ comment|title }}

title过滤器将每个单词的第一个字符大写,所以如果变量comment设置为"dust in the wind",那么输出将是"Dust In The Wind"

我们还可以使用链式滤波器来微调输出。例如:

{{ full_name|striptags|title }}

striptags过滤器从变量中移除所有的 HTML 标签。在上面的代码中,striptags过滤器将首先应用,然后是title过滤器。

一些过滤器也可以接受参数。要将参数传递给筛选器,请像函数一样调用它。例如:

{{ number|round(2) }}

round过滤器将数字舍入到指定的位数。

下表列出了一些常用的过滤器。

名字 描述
upper 将所有字符转换为大写。
lower 将所有字符转换为小写。
capitalize 将第一个字符大写,并将其余字符转换为小写。
escape 转义该值
safe 防止逃跑
length 返回序列中元素的数量
trim 删除前导和尾随空白字符
random 从序列中返回随机项目

注意:内置过滤器的完整列表可在这里获得。

宏指令

Jinja 中的宏类似于 Python 中的函数。这个想法是通过给代码命名来创建可重用的代码。例如:

{% macro render_posts(field, sep=False) %}
    <div>
        {% for post in post_list %}
            <h2>{{ post.title }}</h2>
            <article>
                {{ post.html|safe }}
            </article>
        {% endfor %}
        {% if sep %}<hr>{% endif %}
    </div>
{% endmacro %}

这里我们已经创建了一个名为render_posts的宏,它接受一个名为post_list的必需参数和一个名为sep的可选参数。要使用宏,请按如下方式调用它:

{{ render_posts(posts) }}

宏定义必须在使用前出现,否则会出错。

与其在模板中散布宏,不如将它们存储在单独的文件中,然后根据需要导入文件。

假设我们已经将所有宏存储在templates目录下名为macros.html的文件中。现在要从macros.html导入宏,我们使用如下import语句:

{% import "macros.html" as macros %}

我们现在可以使用macros变量引用macros.html内部定义的宏。例如:

{{ macros.render_posts(posts) }}

导入语句{% import "macros.html" as macros %}macros.html内部定义的所有宏和变量(定义在顶层)导入到模板中。或者,我们也可以使用form语句在模板中导入选定的名称,如下所示:

{% from "macros.html" import render_posts  %}

使用宏时,您会遇到需要向宏传递任意数量的参数的情况。

类似于 Python 中的*args**kwargs,在宏内部你可以访问varargskwargs

varargs :它捕获作为元组传递给宏的附加位置参数。

kwargs :它捕获作为字典传递给宏的附加关键字参数。

尽管您可以在宏中访问这两个变量,但不需要在宏头中显式声明它们。这里有一个例子:

{% macro custom_renderer(para) %}
    <p>{{ para }}</p>
    <p>varargs: {{ varargs }}</p>
    <p>kwargs: {{ kwargs }}</p>
{% endmacro %}

{{ custom_renderer(10, "apple", name='spike', age=15) }}

在这种情况下,额外的位置参数即"apple"被分配给varargs,额外的关键字参数(name='spike', age=15)被分配给kwargs

逃避

默认情况下,出于安全目的,Jinja 会自动转义变量输出。所以如果一个变量有一个像"<p>Escaping in Jinja</p>"这样的包含 HTML 的字符串,那么它将被渲染为"<p>&lt;p&gt;Escaping in Jinja&lt;/p&gt;</p>"。这将导致 HTML 标记显示在浏览器中,而不是被解释。如果您知道数据是安全的,并且想要按原样渲染而不转义,您可以使用safe过滤器。例如:

{% set html = <p>Escaping in Jinja</p> %}
{{ html|safe }}

输出:

<p>Escaping in Jinja</p>

对大块内容重复应用safe过滤器可能会很笨拙和不方便,这就是 Jinja 提供autoescape语句来转义或取消大块内容的原因。它接受truefalse作为分别开启和关闭自动逃逸的参数。例如:

{% autoescape true %}
    Escaping enabled
{% endautoescape %}

{% autoescape false %}
    Escaping disabled
{% endautoescape %}

{% autoescape false %}{% endautoescape %}之间的一切都将按原样渲染,不会逃逸。如果您想在自动转义关闭时转义某些元素,请使用escape滤镜。例如:

{% autoescape false %}
    <div class="post">
        {% for post in post_list %}
            <h2>{{ post.title }}</h2>
            <article>
                {{ post.html }}
            </article>
        {% endfor %}        
    </div>
    <div>
        {% for comment in comment_list %}
            <p>{{ comment|escape }}</p>  # escaping is on for comments
        {% endfor %}
    </div>
{% endautoescape %}

包括模板

include语句在另一个模板中渲染一个模板。include语句通常用于渲染在整个站点重复的静态部分。以下是include语句的语法:

{% include 'path/to/template' %}

假设我们有一个简单的导航栏存储在templates目录内的nav.html中,如下所示:

<nav>
    <a href="/home">Home</a>
    <a href="/blog">Blog</a>
    <a href="/contact">Contact</a>  
</nav>

要在home.html中包含导航栏,我们使用以下代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    {# including the navigation bar from nav.html #}
    {% include 'nav.html' %}   

</body>
</html>

输出:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<nav>
    <a href="/home">Home</a>
    <a href="/blog">Blog</a>
    <a href="/contact">Contact</a>
</nav>

</body>
</html>

模板继承

模板继承是 Jinja 模板化最强大的方面之一。模板继承背后的思想有点类似于面向对象编程。我们首先创建一个包含 HTML 框架和子模板可以覆盖的标记的基础模板。标记是使用block语句创建的。子模板使用extends语句继承或扩展基础模板。让我们举个例子:

{# This is templates/base.html template #}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}Default Title{% endblock %}</title>
</head>
<body>

    {% block nav %}
        <ul>
            <li><a href="/home">Home</a></li>
            <li><a href="/api">API</a></li>
        </ul>
    {% endblock %}

    {% block content %}

    {% endblock %}

</body>
</html>

这是我们的基础模板base.html。它使用block语句定义了三个块,子模板可以在其中填充。block语句只接受一个参数,即块的名称。在模板中,块名必须是唯一的,否则会出错。

子模板是扩展基础模板的模板。子模板可以添加、覆盖或保留父块的内容。下面是我们如何创建子模板。

{# this is templates/child.html template #}
{% extends 'base.html' %}

{% block content %}
    {% for bookmark in bookmarks %}
        <p>{{ bookmark.title }}</p>
    {% endfor %}
{% endblock %}

extends语句告诉金佳child.html是子模板,继承自base.html。当 Jinja 遇到extends语句时,它会加载基础模板,即base.html,然后用子模板中同名的内容块替换父模板中的内容块。如果在子模板中找不到匹配名称的块,则将使用父模板中的块。

请注意,在子模板中,我们只覆盖了content块,因此渲染子模板时将使用titlenav的默认内容。输出应该如下所示:

<head>
    <meta charset="UTF-8">
    <title>Default Title</title>
</head>
<body>

    <ul>
        <li><a href="/home">Home</a></li>
        <li><a href="/api">API</a></li>
    </ul>

    <p>Bookmark title 1</p>
    <p>Bookmark title 2</p>
    <p>Bookmark title 3</p>
    <p>Bookmark title 4</p>

</body>
</html>

如果需要,我们可以通过覆盖child.html中的title块来更改标题的默认值,如下所示:

{# this is templates/child.html template #}
{% extends 'base.html' %}

{% block title %}
    Child Title 
{% endblock %}

{% block content %}
    {% for bookmark in bookmarks %}
        <p>{{ bookmark.title }}</p>
    {% endfor %}
{% endblock %}

覆盖一个块后,您仍然可以通过调用super()函数来引用父模板中的内容。当子模板除了来自父模板的内容之外,还需要添加自己的内容时,一般使用super()调用。例如:

{# this is templates/child.html template #}
{% extends 'base.html' %}

{% block title %}
    Child Title 
{% endblock %}

{% block nav %}
    {{ super() }} {# referring to the content in the parent templates #}
    <li><a href="/contact">Contact</a></li>
    <li><a href="/career">Career</a></li>
{% endblock %}

{% block content %}
    {% for bookmark in bookmarks %}
        <p>{{ bookmark.title }}</p>
    {% endfor %}
{% endblock %}

输出:

<head>
    <meta charset="UTF-8">
    <title>Child Title</title>
</head>
<body>

    <ul>
        <li><a href="/home">Home</a></li>
        <li><a href="/api">API</a></li>
        <li><a href="/contact">Contact</a></li>
        <li><a href="/career">Career</a></li>
    </ul>

    <p>Bookmark title 1</p>
    <p>Bookmark title 2</p>
    <p>Bookmark title 3</p>
    <p>Bookmark title 4</p>

</body>
</html>

关于 Jinja 模板,你只需要知道这些。在接下来的课程中,我们将使用这些知识来创建一些很棒的模板。



在 Flask 中创建网址

原文:https://overiq.com/flask-101/creating-urls-in-flask/

最后更新于 2020 年 7 月 27 日


Flask 可以使用flask包的url_for()功能生成 URL。在模板和视图函数中硬编码 URL 是一种不好的做法。假设,我们想把我们博客的网址从/<id>/<post-title>/重组到/<id>/post/<post-title>/。如果我们的模板和视图函数中有硬编码的 URL,那么我们必须手动访问每个模板和视图函数来进行更改。然而,有了url_for()功能,像这样的改变可以很快完成。

url_for()函数接受端点,并以字符串形式返回网址。回想一下,端点指的是给 URL 的唯一名称,大多数时候它是视图函数的名称。此时,main2.py包含根(/)路由,定义如下:

#...
@app.route('/')
def index():        
    return render_template('index.html', name='Jerry')
#...

生成根网址调用url_for()作为url_for('index')。输出将是'/'。下面的 shell 会话演示了如何在控制台内部使用url_for()

>>>
>>> from main2 import app
>>> from flask import url_for
>>>
>>> with app.test_request_context('/api'): # /api is arbitrarily chosen
...    url_for('index')
...
'/'
>>>

请注意,我们首先创建一个请求上下文(从而隐式创建应用上下文)。试图在没有请求上下文的情况下在控制台内部使用url_for()将导致错误。您可以在这里了解更多关于申请和请求上下文的信息。

如果url_for()无法创建 URL,将抛出BuildError异常。

>>>
>>> with app.test_request_context('/api'):
...    url_for('/api')
...
...  
werkzeug.routing.BuildError: Could not build url for endpoint '/api
Did you mean 'static' instead?
>>>
Traceback (most recent call last):

要生成绝对网址,请将_external=True传递给url_for(),如下所示:

>>>
>>> with app.test_request_context('/api'):
...    url_for('index', _external=True)
...
'http://localhost:5000/'
>>>

不要在redirect()函数中硬编码 URL,你应该总是使用url_for()来生成 URL。例如:

@app.route('/admin/')
def admin():
    if not loggedin:
        return redirect(url_for('login'))  # if not logged in then redirect the user to the login page
    return render_template('admin.html')

要为动态路由生成 URL,请将动态部分作为关键字参数传递。例如:

>>>
>>> with app.test_request_context('/api'):
...    url_for('user_profile', user_id = 100)
...
'/user/100/'
>>>
>>>
>>> with app.test_request_context('/api'): 
...    url_for('books', genre='biography')
...
'/books/biography/'
>>>
>>>

传递给url_for()函数的额外数量的关键字参数将作为查询字符串追加到网址中。

>>>
>>> with app.test_request_context('/api'):
...    url_for('books', genre='biography', page=2, sort_by='date-published')
...
'/books/biography/?page=2&sort_by=date-published'
>>>

url_for()是模板中为数不多的可用功能之一。要在模板内生成网址,只需在双花括号内调用url_for()``{{ ... }},如下所示:

<a href="{{ url_for('books', genre='biography') }}">Books</a>

输出:

<a href="/books/biography/">Books</a>



在 Flask 中提供静态文件

原文:https://overiq.com/flask-101/serving-static-files-in-flask/

最后更新于 2020 年 7 月 27 日


静态文件是不常改变的文件,例如,CSS 文件、JS 文件、字体文件等。默认情况下,Flask 会在应用目录内的static子目录中查找静态文件。我们可以通过在创建应用实例时将目录名传递给static_folder关键字参数来更改此默认值,如下所示:

app = Flask(__name__, static_folder="static_dir")

这会将静态文件的默认位置更改为应用目录内的static_dir目录。

目前,我们将坚持默认的static目录。在flask_app目录中创建一个名为static的目录。在static目录内创建一个 CSS 文件style.css,内容如下。

flask _ app/static/style . CSS

body {
    color: red
}

回想一下,在第课【Flask basic】中,我们讨论过 Flask 会自动添加表单的路径/static/<filename>来处理静态文件。因此,我们需要为静态文件提供的只是使用url_for()函数创建网址,如下所示:

<script src="{{ url_for('static', filename='jquery.js') }}"></script>

输出:

<script src="/static/jquery.js"></script>

打开index.html模板,添加<link>标签如下(更改突出显示):

flask _ app/templates/index . html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>

如果还没有运行就启动服务器,访问 http://localhost:5000/ ,应该会看到页面主体是红色的,如下图:

这里描述的服务静态文件的方法仅用于开发。在生产中,我们使用一个真实的网络服务器,如 Nginx 或 Apache 来服务静态文件。



使用 Flask 脚本扩展 Flask

原文:https://overiq.com/flask-101/extending-flask-with-flask-script/

最后更新于 2020 年 7 月 27 日


Flask 加长件

Flask Extensions 指的是您可以安装来扩展 Flask 的包。Flask 扩展背后的想法是提供一种一致且可预测的方式来将包与 Flask 集成。要查看所有可用的分机,请访问http://flask.pocoo.org/extensions/。该页面包含从发送邮件到构建完整的管理界面的包。请记住,扩展 Flask 并不局限于 Flask Extensions,事实上,您可以使用 Python 标准库或 PyPI 中的任何包。本课的其余部分讨论如何安装和集成一个名为 Flask-Script 的有用的 Flask 扩展。

Flask-脚本扩展

Flask-Script 是一个不错的小扩展,它允许我们创建命令行界面、运行服务器、在应用上下文中启动 Python shell、使某些变量在 shell 中自动可用等等。

回想一下,在第课【Flask 基础知识】中,我们讨论过,为了在特定的主机和端口上运行开发服务器,我们需要将它们作为关键字参数传递给run()方法,如下所示:

if __name__ == "__main__":
    app.run(debug=True, host="127.0.0.10", port=9000)

问题是这种方法不是很灵活,更好的方法是在启动服务器时将主机和端口作为命令行选项传递。Flask-Script 扩展允许我们这样做。不用多说,让我们通过输入以下命令来安装 Flask-Script:

(env) overiq@vm:~/flask_app$ pip install flask-script

要使用 Flask-Script,首先从flask_script包导入Manager类,并通过向其传递应用实例来实例化Manager对象。这就是集成 Flask Extensions 的方法,从扩展包中导入必要的类,并通过将应用实例传递给它来实例化它。打开main2.py,修改文件如下:

Flask _app/main2.py

from flask import Flask, render_template
from flask_script import Manager

app = Flask(__name__)
manager = Manager(app)

...

我们刚刚创建的Manager对象也提供了run()方法,但是除了启动开发服务器之外,它还可以解析命令行参数。将线路app.run(debug=True)更换为manager.run()。此时main2.py应该是这样的样子:

Flask _app/main2.py

from flask import Flask, render_template
from flask_script import Manager

app = Flask(__name__)
manager = Manager(app)

@app.route('/')
def index():
    return render_template('index.html', name='Jerry')

@app.route('/user/<int:user_id>/')
def user_profile(user_id):
    return "Profile page of user #{}".format(user_id)

@app.route('/books/<genre>/')
def books(genre):
    return "All Books in {} category".format(genre)

if __name__ == "__main__":    
    manager.run()

我们的应用现在可以访问一些基本命令。要查看可用的命令,运行main2.py如下:

(env) overiq@vm:~/flask_app$ python main2.py
usage: main2.py [-?] {shell,runserver} ...

positional arguments:
  {shell,runserver}
    shell            Runs a Python shell inside Flask application context.
    runserver        Runs the Flask development server i.e. app.run()

optional arguments:
  -?, --help         show this help message and exit

如输出所示,目前我们有两个命令:shellrunserver。让我们从runserver命令开始。

runserver命令启动网络服务器。默认情况下,它在端口 5000 上的 127.0.0.1 启动开发服务器。查看任何命令的可用选项。键入--help,后跟命令名称。例如:

(env) overiq@vm:~/flask_app$ python main2.py runserver --help
usage: main2.py runserver [-?] [-h HOST] [-p PORT] [--threaded]
                          [--processes PROCESSES] [--passthrough-errors] [-d]
                          [-D] [-r] [-R] [--ssl-crt SSL_CRT]
                          [--ssl-key SSL_KEY]

Runs the Flask development server i.e. app.run()

optional arguments:
  -?, --help            show this help message and exit
  -h HOST, --host HOST
  -p PORT, --port PORT
  --threaded
  --processes PROCESSES
  --passthrough-errors
  -d, --debug           enable the Werkzeug debugger (DO NOT use in production
                        code)
  -D, --no-debug        disable the Werkzeug debugger
  -r, --reload          monitor Python files for changes (not 100% safe for
                        production use)
  -R, --no-reload       do not monitor Python files for changes
  --ssl-crt SSL_CRT     Path to ssl certificate
  --ssl-key SSL_KEY     Path to ssl key

runserver命令最常用的选项是--host--post,可以让我们在特定的接口和端口启动开发服务器。例如:

(env) overiq@vm:~/flask_app$ python main2.py runserver --host=127.0.0.2 --port 8000
 * Running on http://127.0.0.2:8000/ (Press CTRL+C to quit)

默认情况下,runserver命令在没有调试器和重新加载器的情况下启动服务器。我们可以手动打开调试器和重新加载器,如下所示:

(env) overiq@vm:~/flask_app$ python main2.py runserver -d -r
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 250-045-653
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

启动调试器和重新加载器的一个更简单的方法是将应用实例(app)的debug属性设置为True。打开main2.py,修改文件如下:

Flask _app/main2.py

#...
app = Flask(__name__)
app.debug = True
manager = Manager(app)
#...

现在我们来探索shell命令。

shell命令启动 Flask 应用上下文中的 Python Shell。这仅仅意味着应用和请求上下文提供的所有对象都可以在控制台中使用,而无需创建应用或请求上下文。要启动 shell,请输入以下命令。

>>>
(env) overiq@vm:~/flask_app$ python main2.py shell
>>>

现在让我们尝试访问一些对象。

>>>
>>> from flask import current_app, url_for, request
>>>
>>> current_app.name
'main2'
>>>
>>>
>>> url_for("user_profile", user_id=10)
'/user/10/'
>>>
>>> request.path
'/'
>>>

正如预期的那样,我们可以在不创建应用或请求上下文的情况下访问所需的对象。

创建命令

一旦创建了Manager实例,我们就可以创建自己的命令了。有两种方法可以创建命令:

  1. 使用Command类。
  2. 使用@command装饰器。

使用Command类创建命令

打开main2.py并添加Faker类,如下所示:

Flask _app/main2.py

#...
from flask_script import Manager, Command
#...

manager = Manager(app)

class Faker(Command):
    'A command to add fake data to the tables'
    def run(self):
        # add logic here
        print("Fake data entered")

@app.route('/')
#...

这里我们通过继承Command类创建了一个命令Faker。执行命令时会调用run()方法。现在,要从命令行运行该命令,请使用add_command()方法将其添加到Manager实例,如下所示:

Flask _app/main2.py

#...
class Faker(Command):
    'A command to add fake data to the tables'
    def run(self):
        # add logic here
        print("Fake data entered")

manager.add_command("faker", Faker())
#...

现在返回终端,再次运行main2.py:

(env) overiq@vm:~/flask_app$ python main2.py
usage: main2.py [-?] {faker,shell,runserver} ...

positional arguments:
  {faker,shell,runserver}
    faker               A command to add fake data to the tables
    shell               Runs a Python shell inside Flask application context.
    runserver           Runs the Flask development server i.e. app.run()

optional arguments:
  -?, --help            show this help message and exit

注意,除了shellrunserver,我们现在还有一个faker命令的条目。faker 命令前面的描述来自Faker类中的 doc 字符串。要运行它,请输入以下命令:

(env) overiq@vm:~/flask_app$ python main2.py faker
Fake data entered

使用@command装饰器创建命令

使用Command类创建命令有点冗长。或者,我们可以使用Manager实例的@command装饰器。打开main2.py,修改文件如下:

Flask _app/main2.py

#...
manager.add_command("faker", Faker())

@manager.command
def foo():
    "Just a simple command"
    print("foo command executed")

@app.route('/')
#...

我们已经创建了一个简单的命令foo,当被调用时会打印foo command executed@command装饰器会自动将命令添加到现有的Manager实例中,因此我们不需要调用add_command()方法。再次返回终端,运行main2.py文件查看命令使用情况。

(env) overiq@vm:~/flask_app$ python main2.py 
usage: main2.py [-?] {faker,foo,shell,runserver} ...

positional arguments:
  {faker,foo,shell,runserver}
    faker               A command to add fake data to the tables
    foo                 Just a simple command
    shell               Runs a Python shell inside Flask application context.
    runserver           Runs the Flask development server i.e. app.run()

optional arguments:
  -?, --help            show this help message and exit

如您所见,我们的foo命令现在可用,我们可以通过输入以下命令来执行它。

(env) overiq@vm:~/flask_app$ python main2.py foo
foo command executed

自动导入对象

在一个 shell 中导入大量对象可能会很乏味。使用 Flask-Script,我们可以在不显式导入对象的情况下使对象在 Shell 内可用。

Shell命令启动一个 Shell。Shell构造函数接受一个名为make_context的关键字参数。传递给make_context的参数必须是返回字典的可调用参数。默认情况下,可调用返回一个只包含应用实例的字典,即app。这意味着,默认情况下,在 shell 中,您只能访问应用实例(app),而不能显式导入它。要覆盖这个缺省值,给make_context分配一个新的可调用函数,它会返回一个你想在 shell 中访问的对象的字典。

打开main2.py,在foo()功能后添加如下代码。

Flask _app/main2.py

#...
from flask_script import Manager, Command, Shell

#...
def shell_context():
    import os, sys
    return dict(app=app, os=os, sys=sys)

manager.add_command("shell", Shell(make_context=shell_context))
#...

这里我们给make_context关键字参数分配一个名为shell_context的可调用函数。shell_context()函数返回包含三个对象的字典:appossys。因此,在 shell 中,我们可以访问这些对象,而无需显式导入它们。

(env) overiq@vm:~/flask_app$ python main2.py shell
>>>
>>> app
<Flask 'main2'>
>>>
>>> os.name
'posix'
>>>
>>> sys.platform
'linux'
>>>
>>>



Flask 中的表单处理

原文:https://overiq.com/flask-101/form-handling-in-flask/

最后更新于 2020 年 7 月 27 日


表单是任何 web 应用必不可少的一部分,但不幸的是,使用表单非常困难。这一切都从客户端开始,首先,您必须在客户端验证数据,然后在服务器端。如果这还不够,您还需要考虑所有的安全问题,如 CSRF、XSS、SQL 注入等等。总而言之,这是一个很大的工作量。幸运的是,我们有一个名为 WTForms 的优秀库来为我们做这项繁重的工作。在我们了解更多关于 WTForms 的知识之前,下一节将为您介绍如何在 Flask 中处理表单,而无需使用任何库或包。

表单处理-艰难之路

使用以下代码创建名为login.html的新模板:

flask _ app/模板/login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>

    {% if message %}
        <p>{{ message }}</p>
    {% endif %}

    <form action="" method="post">
        <p>
            <label for="username">Username</label>
            <input type="text" name="username">
        </p>
        <p>
            <label for="password">Password</label>
            <input type="password" name="password">
        </p>
        <p>
            <input type="submit">
        </p>
    </form>

</body>
</html>

接下来,在main2.py中的books()查看功能后添加以下代码。

Flask _app/main2.py

from flask import Flask, render_template, request
#...
@app.route('/login/', methods=['post', 'get'])
def login():
    message = ''
    if request.method == 'POST':
        username = request.form.get('username')  # access the data inside 
        password = request.form.get('password')

        if username == 'root' and password == 'pass':
            message = "Correct username and password"
        else:
            message = "Wrong username or password"

    return render_template('login.html', message=message)
#...

注意传递给route()装饰器的methods参数。默认情况下,只有当request.method为 GET 或 HEAD 时,才会调用请求处理程序。这可以通过将允许的 HTTP 方法列表传递给methods关键字参数来更改。从现在开始,login()视图功能
将只在使用 GET、POST 或 HEAD 方法请求/login/时调用。尝试使用任何其他方法访问/login/网址将导致 HTTP 405 方法不允许错误。

在之前的课程中,我们已经讨论过request对象提供了关于当前 web 请求的信息。通过表单提交的数据存储在request对象的form属性中。request.form是一个像字典一样不可变的对象,被称为ImmutableMultiDict

启动服务器,访问http://localhost:5000/log in/。你应该看到这样一个表格。

使用 GET 请求请求页面,因此跳过了login()视图功能中 if 块
内的代码。

提交表单时无需输入任何内容,您应该会看到如下页面:

这一次页面是使用 POST 方法提交的,所以 if 块中的代码被执行。在 if 主体中,我们访问用户名和密码,并相应地设置message变量的值。因为我们提交了一个空表单,所以会显示一条错误消息。

用正确的用户名和密码填写表格,然后点击回车。您应该会收到如下"Correct username and password"信息:

这就是我们在 Flask 中处理表单的方式。现在让我们把注意力转移到 WTForms 包上。

WTForms

WTForms 是一个用 Python 编写的强大的框架无关(框架无关)库。它允许我们生成 HTML 表单、验证表单、用数据预填充表单(对编辑有用)等等。除此之外,它还为 CSRF 提供保护。为了安装 WTForms,我们使用了 Flask-WTF。

Flask-WTF 是一个将 Flask 和 WTForms 集成在一起的 Flask 扩展。Flask-WTF 还提供了一些附加功能,如文件上传、reCAPTCHA、国际化(i18n)等。要安装 Flask-WTF,请输入以下命令。

(env) overiq@vm:~/flask_app$ pip install flask-wtf

创建表单类

我们首先将表单定义为 Python 类。每个表单类都必须扩展flask_wtf包的FlaskForm类。FlaskForm是一个包装器,包含一些围绕原始wtform.Form类的有用方法,这是创建表单的基类。在表单类中,我们将表单字段定义为类变量。通过创建与字段类型相关联的对象来定义表单字段。wtform包提供了几个类来表示表单域,如StringFieldPasswordFieldSelectFieldTextAreaFieldSubmitField等。

flask_app字典中创建一个新文件forms.py,并添加以下代码。

Flask _app/forms.py

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField
from wtforms.validators import DataRequired, Email

class ContactForm(FlaskForm):
    name = StringField("Name: ", validators=[DataRequired()])
    email = StringField("Email: ", validators=[Email()])
    message = TextAreaField("Message", validators=[DataRequired()])
    submit = SubmitField("Submit")

这里我们定义了一个表单类ContactForm,它包含四个表单域:nameemailmessagesubmit。这些变量将用于渲染表单字段,以及在字段之间设置和检索数据。使用两个StringField创建表单,一个TextAreaField和一个SubmitField。每次我们创建一个字段对象,我们都会传递一些参数给它的构造函数。第一个参数是一个包含标签的字符串,当表单域被渲染时,它将显示在<label>标签中。第二个可选参数是作为关键字参数传递给构造函数的验证器列表。验证器是确定字段中的数据是否有效的函数或类。我们可以通过逗号(,)将多个验证器应用于一个字段。wtforms.validators模块提供了一些基本的验证器,但是我们也可以创建自己的验证器。在这个表单中,我们使用了两个内置验证器DataRequiredEmail

数据要求:保证用户必须在字段中输入一些数据。

邮件:检查输入的数据是否为有效的邮件地址。

字段中的数据将不会被接受,直到对其应用的所有验证器都得到满足。

注意:我们几乎没有触及表单域和验证器的表面,要查看完整列表,请访问https://wtforms.readthedocs.io/en/master/

设置密钥

默认情况下,Flask-WTF 阻止所有形式的 CSRF 攻击。它通过在表单中隐藏的<input>元素中嵌入一个标记来实现这一点。然后使用令牌来验证请求的真实性。在 Flask-WTF 可以生成 csrf 令牌之前,我们必须添加一个密钥。打开main2.py并按如下方式设置密钥:

Flask _app/main2.py

#...
app.debug = True
app.config['SECRET_KEY'] = 'a really really really really long secret key'

manager = Manager(app)
#...

这里我们使用的是Flask对象的config属性。config属性就像字典一样工作,它用于放置 Flask 和 Flask 扩展的配置选项,但是如果您愿意,也可以放置您自己的配置。

密钥应该是一个很长很难猜测的字符串。SECRET_KEY的使用不仅仅局限于创建 CSRF 代币,它还被 Flask 和许多其他扩展使用。密钥应该保密。与其将密钥存储在应用中,不如将它存储在环境变量中。我们将在后面的章节中学习如何做到这一点。

控制台中的表单

通过输入以下命令打开 Python shell:

(env) overiq@vm:~/flask_app$ python main2.py shell

这将在应用上下文中启动 Python shell。

现在导入ContactForm类,并通过向其传递表单数据来实例化一个新的表单对象。

>>>
>>> from forms import ContactForm
>>> from werkzeug.datastructures import MultiDict
>>>
>>>
>>> form1 = ContactForm(MultiDict([('name', 'jerry'),('email', 'jerry@mail.com')]))
>>>

请注意,我们将表单数据作为MultiDict对象传递,因为wtforms.Form类的构造函数接受类型为MultiDict的参数。如果在实例化表单对象时未指定表单数据,并且表单是使用 POST 请求提交的,则wtforms.Form将使用来自request.form属性的数据。回想一下request.form返回一个类型为ImmutableMultiDict的对象,该对象与MultiDict对象相同,但不可变。

表单对象的validate()方法验证表单。成功后返回True,否则返回False

>>>
>>> form1.validate()
False
>>>

我们的表单未能通过验证,因为我们在创建表单对象时没有向所需的message字段提供任何数据。我们可以使用表单对象的errors属性来访问表单错误:

>>>
>>> form1.errors
{'message': ['This field is required.'], 'csrf_token': ['The CSRF token is missing.']}
>>>

请注意,除了message字段的错误消息外,输出还包含缺失 csrf 令牌的错误消息。这是因为我们在表单数据中没有带有 csrf 令牌的实际 POST 请求。

我们可以在实例化表单类时通过传递csrf_enabled=False来关闭表单上的 CSRF 保护。这里有一个例子:

>>>
>>> form3 = ContactForm(MultiDict([('name', 'spike'),('email', 'spike@mail.com')]), csrf_enabled=False)
>>>
>>> form3.validate()
False
>>>
>>> form3.errors
{'message': ['This field is required.']}
>>>
>>>

不出所料,现在我们只得到丢失的message字段的错误。让我们创建另一个表单对象,但这次我们将向所有表单字段提供有效数据。

>>>
>>> form4 = ContactForm(MultiDict([('name', 'jerry'), ('email', 'jerry@mail.com'), ('message', "hello tom")]), csrf_enabled=False)
>>>
>>> form4.validate()
True
>>>
>>> form4.errors
{}
>>>

这次表单验证成功。

我们的下一个逻辑步骤将是渲染接下来讨论的表单。

渲染表单

有两种方法可以渲染表单域:

  1. 逐个渲染字段。
  2. 使用 for 循环渲染字段。

逐个渲染字段

在模板中,一旦我们访问了表单实例,我们就可以使用字段名来渲染字段、标签和错误,如下所示:

{# render the label tag associated with field #}
{{ form.field_name.label()  }}  

{# render the field itself #}
{{ form.field_name()  }}  

{# render the validation errors associated with the field #}
{% for error in form.field_name.errors %}
    {{ error }}  
{% endfor %}

让我们在控制台内部测试一下:

>>>
>>> from forms import ContactForm
>>> from jinja2 import Template
>>>
>>> form = ContactForm()
>>>

这里我们已经实例化了没有任何请求数据的表单对象,这通常是第一次使用 GET 请求显示表单的情况。

>>>
>>>
>>> Template("{{ form.name.label() }}").render(form=form)
'<label for="name">Name: </label>'
>>>
>>> Template("{{ form.name() }}").render(form=form)
'<input id="name" name="name" type="text" value="">'
>>>
>>>
>>> Template("{{ form.email.label() }}").render(form=form)
'<label for="email">Email: </label>'
>>>
>>> Template("{{ form.email() }}").render(form=form)
'<input id="email" name="email" type="text" value="">'
>>>
>>>
>>> Template("{{ form.message.label() }}").render(form=form)
'<label for="message">Message</label>'
>>>
>>> Template("{{ form.message() }}").render(form=form)
'<textarea id="message" name="message"></textarea>'
>>>
>>>
>>> Template("{{ form.submit() }}").render(form=form)
'<input id="submit" name="submit" type="submit" value="Submit">'
>>>
>>>

由于表单是第一次显示,因此它的任何字段都不会有任何验证错误。下面的代码演示了这一点:

>>>
>>>
>>> Template("{% for error in form.name.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>
>>>
>>> Template("{% for error in form.email.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>
>>>
>>> Template("{% for error in form.message.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>
>>>

您可以使用form.errors来访问与表单相关的所有验证错误,而不是显示每个字段的验证错误。forms.errors常用于在表单顶部显示验证错误。

>>>
>>> Template("{% for error in form.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>

在渲染字段和标签时,我们还可以提供额外的关键字参数,这些参数将作为键值对注入到 HTML 中。例如:

>>>
>>> Template('{{ form.name(class="input", id="simple-input") }}').render(form=form)
'<input class="input" id="simple-input" name="name" type="text" value="">'
>>>
>>>
>>> Template('{{ form.name.label(class="lbl") }}').render(form=form)
'<label class="lbl" for="name">Name: </label>'
>>>
>>>

现在假设我们的表单已经提交。这次让我们尝试渲染字段,看看会发生什么。

>>>
>>> from werkzeug.datastructures import MultiDict
>>>
>>> form = ContactForm(MultiDict([('name', 'spike'),('email', 'spike@mail.com')]))
>>>
>>> form.validate()
False
>>>
>>>
>>> Template("{{ form.name() }}").render(form=form)
'<input id="name" name="name" type="text" value="spike">'
>>>
>>>
>>> Template("{{ form.email() }}").render(form=form)
'<input id="email" name="email" type="text" value="spike@mail.com">'
>>>
>>>
>>> Template("{{ form.message() }}").render(form=form)
'<textarea id="message" name="message"></textarea>'
>>>
>>>

请注意,nameemail字段的value属性已填充数据。然而,message字段的<textarea>元素是空的,因为我们没有向它提供任何数据。我们可以访问message字段的验证错误,如下所示:

>>>
>>> Template("{% for error in form.message.errors %}{{ error }}{% endfor %}").render(form=form)
'This field is required.'
>>>

或者,您也可以使用form.errors一次遍历所有验证错误。

>>>
>>> s ="""\
... {% for field_name in form.errors %}\
...         {% for error in form.errors[field_name] %}\
...             <li>{{ field_name }}: {{ error }}</li>
...         {% endfor %}\
... {% endfor %}\
... """
>>>
>>> Template(s).render(form=form)
'<li>csrf_token: The CSRF token is missing.</li>\n
<li>message: This field is required.</li>\n'
>>>
>>>

请注意,由于提交的请求没有 csrf 令牌,因此出现了 csrf 令牌丢失错误。我们可以像正常场一样渲染 csrf 场,如下所示:

>>>
>>> Template("{{ form.csrf_token() }}").render(form=form)
'<input id="csrf_token" name="csrf_token" type="hidden" value="IjZjOTBkOWM4ZmQ0M
GMzZTY3NDc3ZTNiZDIxZTFjNDAzMGU1YzEwOTYi.DQlFlA.GQ-PrxsCJkQfoJ5k6i5YfZMzC7k">'
>>>

如果您有相当多的表单域,逐个渲染域可能会很麻烦。对于这种情况,可以使用 For 循环来渲染字段。

使用循环渲染字段

下面的 shell 会话演示了如何使用 for 循环渲染字段。

>>>
>>> s = """\
...     <div>
...         {{ form.csrf_token }}
...     </div>
... {% for field in form if field.name != 'csrf_token' %}
...     <div>
...         {{ field.label() }}
...         {{ field() }}
...         {% for error in field.errors %}
...             <div class="error">{{ error }}</div>
...         {% endfor %}
...     </div>
... {% endfor %}
... """
>>>
>>>
>>> print(Template(s).render(form=form))
    <div>
        <input id="csrf_token" name="csrf_token" type="hidden" value="IjZjOTBkOW
M4ZmQ0MGMzZTY3NDc3ZTNiZDIxZTFjNDAzMGU1YzEwOTYi.DQlFlA.GQ-PrxsCJkQfoJ5k6i5YfZMzC7
k">
    </div>

    <div>
        <label for="name">Name: </label>
        <input id="name" name="name" type="text" value="spike">

    </div>

    <div>
        <label for="email">Email: </label>
        <input id="email" name="email" type="text" value="spike@mail.com">

    </div>

    <div>
        <label for="message">Message</label>
        <textarea id="message" name="message"></textarea>

            <div class="error">This field is required.</div>

    </div>

    <div>
        <label for="submit">Submit</label>
        <input id="submit" name="submit" type="submit" value="Submit">

    </div>

>>>
>>>

需要注意的是,无论使用哪种方法,都必须手动添加<form>标签来包装表单字段。

现在我们知道如何创建、验证和渲染表单。让我们利用这些知识创造一些真实的形式。

首先用以下代码创建一个新模板contact.html:

flask _ app/templates/contact . html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<form action="" method="post">

    {{ form.csrf_token() }}

    {% for field in form if field.name != "csrf_token" %}
        <p>{{ field.label() }}</p>
        <p>{{ field }}
            {% for error in field.errors %}
                {{ error }}
            {% endfor %}
        </p>
    {% endfor %}

</form>

</body>
</html>

拼图中唯一缺少的部分是我们接下来将创建的视图功能。

提交表格

打开main2.py,在login()查看功能后添加以下代码。

Flask _app/main2.py

from flask import Flask, render_template, request, redirect, url_for
from flask_script import Manager, Command, Shell
from forms import ContactForm
#...
@app.route('/contact/', methods=['get', 'post'])
def contact():
    form = ContactForm()
    if form.validate_on_submit():
        name = form.name.data
        email = form.email.data
        message = form.message.data
        print(name)
        print(email)
        print(message)
        # db logic goes here
        print("\nData received. Now redirecting ...")
        return redirect(url_for('contact'))

    return render_template('contact.html', form=form)
#...

在第 7 行,我们正在创建一个表单对象。在第 8 行,我们正在检查validate_on_submit()方法的返回值,以执行 if 语句体中的一些代码。

为什么我们使用validate_on_submit()而不是validate(),就像我们在控制台中做的那样?

validate()方法只是检查表单数据是否有效,并不检查请求是否使用 POST 方法提交。这意味着如果我们使用validate()方法,那么对/contact/的 GET 请求将触发表单验证,用户将在表单中看到验证错误。一般来说,只有在使用 POST 请求提交数据时,我们才会触发验证例程。当使用开机自检请求提交表单并且数据有效时,validate_on_submit()方法返回True。否则Falsevalidate_on_submit()方法内部调用validate()方法。此外,请注意,我们在实例化表单对象时没有传递任何数据,因为当使用 POST 请求提交表单时,WTForms 会从request.form属性中读取表单数据。

表单类中定义的表单字段成为表单对象的属性。要访问字段数据,我们使用表单字段的data属性:

form.name.data   # access the data in the name field.
form.email.data   # access the data in the email field.

要一次访问所有表单数据,请使用表单对象的data属性:

form.data   # access all the form data

当您使用 GET 请求访问 URL /contact/时,validate_on_submit()方法返回False,跳过 if 主体内的代码,向用户显示一个空的 HTML 表单。

当使用 POST 请求提交表单时,validate_on_submit()返回True,假设数据有效。if 主体内的print()调用打印用户输入的数据,redirect()功能将用户重定向到/contact/页面。另一方面,如果validate_on_submit()返回False,则跳过 if 主体内部语句的执行,并显示带有验证错误的表单。

启动服务器,如果还没有运行,访问http://localhost:5000/contact/。您应该会看到这样的联系人表单:

无需输入任何内容,点击提交,您将看到如下验证错误:

在名称和消息字段中输入一些数据,在电子邮件字段中输入无效数据,然后再次提交表单。

请注意,所有字段仍然包含来自上一个请求的数据。

在电子邮件字段中输入有效的电子邮件,然后点击提交。这一次我们的验证将会成功,在运行服务器的 shell 中,您应该会看到如下输出:

Spike
spike@gmail.com
A Message

Data received. Now redirecting ...

在 shell 中显示提交的数据后,查看功能会再次将用户重定向到/contact/ URL。此时,您应该会看到一个没有任何验证错误的空表单,就像您第一次使用 GET 请求访问/contact/网址一样。

成功提交表单后,向用户显示一些反馈是一个很好的做法。在 Flask 中,我们使用 flash 消息创建这样的反馈,这将在下面讨论。

Flash 消息

Flash 消息是另一种依赖于密钥的功能。密钥是必要的,因为在幕后,闪存消息存储在会话中。我们将在 Flask 中的课程中深入学习什么是会话以及如何使用它们。既然我们已经设置了密匙,我们就准备出发了。

要闪烁消息,我们使用flask包中的flash()功能。flash()函数接受两个参数,消息到 flash 和一个可选类别。类别表示消息类型,如成功错误警告等。该类别可在模板中用于确定要显示的警报消息的类型。

contact()视图功能中打开main2.py并在redirect()调用前添加flash("Message Received", "success"),如下所示:

Flask _app/main2.py

from flask import Flask, render_template, request, redirect, url_for, flash
#...
        # db logic goes here
        print("\nData received. Now redirecting ...")
        flash("Message Received", "success")
        return redirect(url_for('contact'))
    return render_template('contact.html', form=form)

flash()功能设置的消息仅适用于后续请求,然后将被删除。

我们现在正在设置 flash 消息,为了显示它,我们也必须修改我们的模板。

打开contact.html,修改文件如下:

flask _ app/templates/contact . html

<body>

{% for category, message in get_flashed_messages(with_categories=true) %}
    <p class="{{ category }}">{{ message }}</p>
{% endfor %}

<form action="" method="post">

Jinja 提供了一个名为get_flashed_messages()的函数,该函数返回一个没有类别的未决 flash 消息列表。拨打get_flashed_messages()时,要获得带类别通行证with_categories=True的闪光信息。当with_categories设置为真时,get_flashed_messages()返回形式为(category, message)的元组列表。

进行这些更改后,再次访问http://localhost:5000/contact/。填写表格并点击提交。这一次,您应该会在表单顶部看到一条成功消息,如下所示:



Flask 中的 Cookie

原文:https://overiq.com/flask-101/cookies-in-flask/

最后更新于 2020 年 7 月 27 日


到目前为止,我们构建的页面非常简单。浏览器将请求发送给服务器,服务器用 HTML 页面进行响应,仅此而已。HTTP 是一种无状态协议。这意味着 HTTP 没有内置的方式来告诉服务器,这两个请求来自同一个用户。因此,服务器不知道您是第一次还是第一千次请求页面。它平等地为每个人服务,就像他们第一次请求页面一样。

访问一个电子商务网站,浏览一些项目。下次访问该网站时,您将根据以前的浏览模式获得一些产品推荐。那么网站怎么会知道你的存在呢?

答案在 Cookies 和会话中。

本课讨论 Cookies,下一课将讨论会话。

cookie 只是服务器在浏览器中设置的一段数据。以下是它的工作原理:

  1. 浏览器向服务器发送网页请求。
  2. 服务器通过发送所请求的网页以及一个或多个 cookies 来响应浏览器请求。
  3. 收到响应后,浏览器会渲染网页并将 cookie 保存在用户计算机中。
  4. 对服务器的后续请求将在Cookie头中包含来自所有 cookies 的数据。此过程将持续到 cookie 过期。一旦 cookie 过期,就会从浏览器中删除。

在 Flask 中,我们使用响应对象的set_cookie()方法来设置 cookies。set_cookie()方法的语法如下:

set_cookie(key, value="", max_age=None)

key是必需的参数,指的是 cookie 的名称。value是您想要存储在 cookie 中的数据,默认为空字符串。max_age指的是 cookie 的过期时间,以秒为单位,如果没有设置,cookie 将在用户关闭浏览器时停止存在。

打开main2.py,在contact()查看功能后添加以下代码:

Flask _app/main2.py

from flask import Flask, render_template, request, redirect, url_for, flash, make_response
#...
@app.route('/cookie/')
def cookie():
    res = make_response("Setting a cookie")
    res.set_cookie('foo', 'bar', max_age=60*60*24*365*2)
    return res
#...

这里我们正在创建一个名为foo的 cookie,其值bar将持续 2 年。

启动服务器,访问http://localhost:5000/cookie/。你应该会看到一个以"Setting a cookie"作为回应的页面。点击Shift + F9,在火狐中打开存储检查器,查看服务器设置的 cookie。浏览器窗口底部将出现一个新窗口。在左侧,选择“Cookies”存储类型,然后点击http://localhost:5000/查看服务器在设置的所有 Cookies http://localhost:5000/

从现在开始,cookie foo将与任何请求一起发送到服务器 http://localhost:5000/ 。我们可以使用火狐中的网络监视器来验证这一点。按 Ctrl+Shift+E 打开网络监视器,访问 http://localhost:5000/ 。在左侧的网络请求列表中,选择第一个请求,您将在右侧窗格中获得如下请求详细信息:

请注意,一旦设置了 cookie,后续对http://localhost:5000/cookie的请求将更新 cookie 的过期时间。

访问 Cookies

要访问 cookie,我们使用request对象的cookie属性。cookie属性是一个类似字典的属性,包含浏览器发送的所有 cookies。打开main2.py并修改cookie()查看功能如下:

Flask _app/main2.py

#...
@app.route('/cookie/')
def cookie():
    if not request.cookies.get('foo'):
        res = make_response("Setting a cookie")
        res.set_cookie('foo', 'bar', max_age=60*60*24*365*2)
    else:
        res = make_response("Value of cookie foo is {}".format(request.cookies.get('foo')))
    return res
#...

我们修改了我们的视图函数来显示 cookie 值,假设有一个 cookie。否则,它会为我们设置一个 cookie。

访问http://localhost:5000/cookie/这次应该会得到如下的回应。

模板内部也有request对象。这意味着,在模板内部,我们可以使用与 Python 代码中相同的方式来访问 cookies。我们将在下一节看到这方面的一个例子。

删除 Cookies

要删除 cookie,使用 cookie 的名称和任意值调用set_cookie()方法,并将max_age参数设置为 0。打开main2.py文件,在cookie()查看功能后添加以下代码。

Flask _app/main2.py

#...
@app.route('/delete-cookie/')
def delete_cookie():
    res = make_response("Cookie Removed")
    res.set_cookie('foo', 'bar', max_age=0)
    return res
#...

访问http://localhost:5000/delete-cookie/会得到如下响应:

现在你应该对 Cookie 的工作原理有了很好的理解。下面的清单给出了一个如何使用 cookie 存储用户首选项的实际例子。

main2.py中,在delete_cookie()查看功能后添加以下代码。

Flask _app/main2.py

#...
@app.route('/article/', methods=['POST', 'GET'])
def article():
    if request.method == 'POST':
        print(request.form)
        res = make_response("")
        res.set_cookie("font", request.form.get('font'), 60*60*24*15)
        res.headers['location'] = url_for('article')
        return res, 302

    return render_template('article.html')
#...

使用以下代码创建新模板article.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Article</title>
</head>
<body style="{% if request.cookies.get('font') %}font-family:{{ request.cookies.get('font') }}{% endif %}">

Select Font Preference: <br>
<form action="" method="post">
    <select name="font" onchange="submit()">
        <option value="">----</option>
        <option value="consolas" {% if request.cookies.get('font') == 'consolas' %}selected{% endif %}>consolas</option>
        <option value="arial" {% if request.cookies.get('font') == 'arial' %}selected{% endif %}>arial</option>
        <option value="verdana" {% if request.cookies.get('font') == 'verdana' %}selected{% endif %}>verdana</option>
    </select>
</form>

<h1>Festus, superbus toruss diligenter tractare de brevis, dexter olla.</h1>

<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aperiam blanditiis debitis doloribus eos magni minus odit, provident tempora. Expedita fugiat harum in incidunt minus nam nesciunt voluptate. Facilis nesciunt, similique!
</p>

<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias amet animi aperiam inventore molestiae quos, reiciendis voluptatem. Ab, cum cupiditate fugit illo incidunt ipsa neque quam, qui quidem vel voluptatum.</p>

</body>
</html>

用户第一次访问http://localhost:5000/article,页面使用默认浏览器字体显示。当用户使用下拉菜单改变字体时,我们提交表单。if 条件request.method == 'POST'变为真,我们设置一个名为font的 cookie,所选字体的值将在 15 天后过期,然后用户被重定向到http://localhost:5000/article。并且用户以所选择的字体显示页面。

在浏览器中,访问http://localhost:5000/article,你会看到一个浏览器默认字体的页面。

从下拉列表中选择字体,您将以所选字体显示页面。

在项目中广泛使用 cookies 之前,您必须意识到它的缺点。

  1. Cookies 不安全。存储在 cookie 中的数据对任何人都是可见的,所以您不应该使用它来存储敏感数据,如密码、信用卡详细信息等。

  2. Cookies 可以被禁用。大多数浏览器给用户禁用 cookies 的选项。当 cookie 被禁用时,您将不会收到任何警告或错误消息,相反,设置 cookie 的响应头将被丢弃。为了解决这些问题,您可以像下面这样编写 Javascript 代码,提醒用户您的应用需要 cookies 才能正常工作。

    <script>
        document.cookie = "foo=bar;";
        if (!document.cookie) 
        {
            alert("This website requires cookies to function properly");
        }
    </script>
    
    
  3. 每个 Cookie 只能存储不超过 4KB 的数据。除此之外,浏览器还对网站可以设置的 cookies 数量进行了限制。该限制因浏览器而异。有些浏览器每个网站接受 50 个 cookies,有些接受 30 个。

  4. 每次您向服务器请求页面时,都会发送 Cookies。假设你有 20 个 cookiess,每个 cookie 都存储 4KB 的数据。这意味着每次请求你都有 80KB 的额外负载!

我们可以使用会话来解决其中的一些问题。这是下一课的主题。



Flask 中的会话

原文:https://overiq.com/flask-101/sessions-in-flask/

最后更新于 2020 年 7 月 27 日


会话是在请求之间存储用户特定数据的另一种方式。它的工作原理类似于 Cookie。要使用会话,您必须首先设置密钥。flask包的session对象用于设置和获取会话数据。session对象像字典一样工作,但它也可以跟踪修改。

当我们使用会话时,数据作为 cookie 存储在浏览器中。用于存储会话数据的 cookie 称为会话 cookie。然而,与普通的 cookie 不同,Flask 对会话 cookie 进行加密签名。这意味着任何人都可以查看 cookie 的内容,但不能修改 cookie,除非他有用于签名 cookie 的密钥。这就是为什么建议设置一个又长又难猜的字符串作为密钥。一旦设置了会话 cookie,对服务器的每个后续请求都通过使用相同的密钥取消分配来验证 cookie 的真实性。如果 Flask 未能取消 cookie 的设计,那么它的内容将被丢弃,一个新的会话 cookie 将被发送到浏览器。

如果您使用过像 PHP 这样的语言的会话,那么 Flask 中的会话就有点不同了。在 PHP 中,会话 cookie 不存储会话数据,而是只存储会话 id。会话 id 是 PHP 创建的唯一字符串,用于将会话数据与 cookie 相关联。会话数据本身存储在服务器的一个文件中。收到客户端的请求后,PHP 使用会话 id 来检索会话数据,并使其在您的代码中可用。这种类型的会话称为服务器端会话,默认情况下 Flask 提供的会话类型称为客户端会话。

默认情况下,cookies 和 Flask 中基于客户端的会话没有太大区别。因此,基于客户端的会话与 cookies 具有相同的缺点:

  • 不能存储像密码这样的敏感数据。
  • 每个请求都有额外的负载。
  • 不能存储超过 4KB 的数据。
  • 限制每个网站的 cookie 数量等等。

cookie 和基于客户端的会话之间唯一真正的区别是 Flask 保证会话 cookie 的内容不会被用户篡改(除非他有密钥)。

如果您想在 Flask 中使用服务器端会话,您可以编写自己的会话接口,也可以使用 Flask-Session 和 Flask-KVSession 这样的扩展。

如何读取、写入和删除会话数据

下面的清单演示了如何读取、写入和删除会话数据。打开main2.py文件,在article()查看功能后添加以下代码:

Flask _app/main2.py

from flask import Flask, render_template, request, redirect, \
url_for, flash, make_response, session)
#...
@app.route('/visits-counter/')
def visits():
    if 'visits' in session:
        session['visits'] = session.get('visits') + 1  # reading and updating session data
    else:
        session['visits'] = 1 # setting session data
    return "Total visits: {}".format(session.get('visits'))

@app.route('/delete-visits/')
def delete_visits():
    session.pop('visits', None) # delete visits
    return 'Visits deleted'
#...

请注意,我们使用的session对象就像一个普通的字典。如果尚未运行,请启动服务器并访问http://localhost:5000/访问量-计数器/ 。您应该看到总访问量如下所示:

多刷新页面几次以增加访问次数。

Flask 仅在您创建新会话或修改现有会话时向客户端发送会话 cookie。当您第一次访问http://localhost:5000/visits-counter/页面时,else 块的主体在visits()视图功能中执行,并创建一个新的会话。当我们创建一个新的会话时,Flask 向客户端发送会话 cookie。对http://localhost:5000/visitos-counter/的后续请求执行 if 块中的代码,其中正在更新会话中visits计数器的值。修改会话意味着需要创建新的 cookie,这就是为什么 Flask 再次将新的会话 cookie 发送给客户端。

要删除会话数据,请访问

如果您现在访问http://localhost:5000/访问量-计数器/ ,访问量计数器将再次从 1 开始。

默认情况下,会话 cookie 会持续到浏览器关闭。为了延长会话 cookie 的寿命,将session对象的permanent属性设置为True。当permanent设置为True时,会话 cookie 将持续permanent_session_lifetimepermanent_session_lifetimeFlask对象的datetime.timedelta属性,默认值为 31 天。我们可以通过为permanent_session_lifetime属性指定一个新值或通过设置PERMANENT_SESSION_LIFETIME配置键来改变它。

import datetime

app = Flask(__name__)
app.permanent_session_lifetime = datetime.timedelta(days=365)
# app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=365) # you can also do this

就像request对象一样,session对象在模板中也是可用的。

修改会话数据

注意:在跟随之前,删除本地主机设置的所有 cookies。

大多数时候session对象会自动拾取其上的修改。然而,在某些情况下,比如对可变数据结构的修改不会自动获得。对于这种情况,您必须将session对象的modified属性设置为True。如果不将modified属性设置为True,Flask 将不会向客户端发送更新的会话 cookie。下面的清单演示了如何使用session对象的modified属性。打开main2.py文件,在delete_visits()查看功能前添加以下代码。

Flask _app/main2.py

#...
@app.route('/session/')
def updating_session():
    res = str(session.items())

    cart_item = {'pineapples': '10', 'apples': '20', 'mangoes': '30'}
    if 'cart_item' in session:
        session['cart_item']['pineapples'] = '100'
        session.modified = True
    else:
        session['cart_item'] = cart_item

    return res
#...

第一次访问http://localhost:5000/session/时,else 块中的代码会被执行并创建一个新的 session,其中 session 数据是一个字典。对的后续请求 http://localhost:5000/session/通过将菠萝计数设置为 100 来更新会话数据。在下一行中,我们将modified属性设置为True,因为如果没有它,Flask 将不会向客户端发送更新的会话 cookie。

如果尚未运行,请启动服务器,并访问。您将看到一个空的session字典,因为浏览器没有会话 cookie 要发送到服务器:

再次重新加载页面,会显示 10 个菠萝的session字典,如下所示:

第三次重新加载页面,你会看到session字典中有 100 个菠萝,而不是 10 个,如下所示:

由于modified属性,会话对象接受了此修改。我们可以通过删除会话 cookie 并注释掉将modified属性设置为True的行来验证这一点。现在,在第一次请求后,您将总是看到包含 10 个菠萝的会话字典。

这就完成了所有你需要知道的关于 Flask 会话的内容。别忘了默认情况下,Flask 中的会话是客户端会话。



Flask 中的数据库建模

原文:https://overiq.com/flask-101/database-modelling-in-flask/

最后更新于 2020 年 7 月 27 日


在本课中,我们将学习如何与数据库交互。今天,我们有两种相互竞争的数据库系统:

  1. 关系数据库。
  2. 非关系数据库或 NoSQL 数据库。

关系数据库传统上用于 web 应用。许多像脸书这样的网络巨头仍在使用它。关系数据库将数据存储在表和列中,并使用外键在一个或多个表之间建立关系。关系数据库也支持事务,这意味着您可以执行一组需要原子化的 SQL 语句。atomic我的意思是要么事务中的所有语句都成功执行,要么什么都不执行。

近年来,NoSQL 数据库越来越受欢迎。NoSQL 数据库不在表和列中存储数据,而是使用文档存储、键值存储、图形等结构。大多数 NoSQL 也不支持交易,但他们提供了很多速度。

与 NoSQL 数据库相比,关系数据库非常成熟。他们已经在许多行业证明了自己的可靠性和安全性。因此,本课的剩余部分将专门讨论如何在 Flask 中使用关系数据库。这并不意味着 NoSQL 的数据库没有用。事实上,在某些情况下,NoSQL 数据库比关系数据库更有意义,但目前,我们的讨论将仅限于关系数据库。

SQLAlchemy 和 Flask-SQLAlchemy

SQLAlchemy 是 Python 中处理关系数据库的事实框架。它是由迈克·拜尔在 2005 年创建的。SQLAlchemy 支持 MySQL、PostgreSQL、Oracle、MS-SQL、SQLite 等数据库。

SQLAlchemy 附带了一个强大的 ORM(对象关系映射器),它允许我们使用面向对象的代码来处理各种数据库,而不是编写原始的 SQL。当然,我们不一定要以任何方式使用 ORM,如果有需要,我们也可以使用 SQL。

Flask-SQLAlchemy 是将 SQLAlchemy 框架与 Flask 集成在一起的扩展。除此之外,它还提供了一些帮助方法,使使用 SQLAlchemy 变得更加容易。使用以下命令安装 Flask-SQLAlchemy 及其依赖项:

(env) overiq@vm:~/flask_app$ pip install flask-sqlalchemy

使用 Flask-SQLAlchemy 从flask_sqlalchemy包导入SQLAlchemy类,并通过向其传递应用实例来实例化SQLAlchemy对象。打开main2.py文件,修改如下(修改突出显示):

#...
from forms import ContactForm
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'a really really really really long secret key'

manager = Manager(app)
db = SQLAlchemy(app)

class Faker(Command):
#...

SQLAlchemy实例db提供对所有 SQLAlchemy 函数的访问。

接下来,我们需要告诉 Flask-SQLAlchemy 我们想要用作 URI 的数据库的位置。URI 数据库的格式如下:

dialect+driver://username:password@host:port/database

dialect指的是mysqlmssqlpostgresql等数据库的名称。

driver是指用于连接数据库的DBAPI。默认情况下,SQLAlchemy 仅适用于 SQLite,不需要任何额外的驱动程序。要使用其他数据库,您必须安装特定于该数据库的符合 DBAPI 的驱动程序。

那么什么是 DBAPI 呢?

DBAPI 只是一个标准,它定义了一个通用的 Python 应用编程接口来访问来自不同供应商的数据库。

下表列出了一些数据库及其符合 DBAPI 的驱动程序:

数据库ˌ资料库 数据库驱动程序
关系型数据库 PyMysql
一种数据库系统 Psycopg 2
数据库备份方法 pyodbc
神谕 cx_Oracle

usernamepassword是可选的,如果指定,将用于登录数据库。

host指的是数据库服务器的位置。

port是可选的数据库服务器端口。

database指的是数据库的名称。

以下是一些流行数据库的 URIs 数据库示例:

# database URI for MySQL using PyMysql driver
'mysql+pymysql://root:pass@localhost/my_db'  

# database URI for PostgreSQL using psycopg2 
'postgresql+psycopg2://root:pass@localhost/my_db' 

# database URI for MS-SQL using pyodbc driver
'mssql+pyodbc://root:pass@localhost/my_db' 

 # database URI for Oracle using cx_Oracle driver
'oracle+cx_oracle://root:pass@localhost/my_db'

SQLite 数据库的 URI 数据库的格式略有不同。因为 SQLite 是一个基于文件的数据库,不需要用户名和密码,所以在 URI 数据库中,我们只指定数据库文件的路径名。

# For Unix/Mac we use 4 slashes
sqlite:////absolute/path/to/my_db.db  

# For Windows we use 3 slashes
sqlite:///c:/absolute/path/to/mysql.db

Flask-SQLAlchemy 使用SQLALCHEMY_DATABASE_URI配置键指定数据库 URI。打开main2.py并添加SQLALCHEMY_DATABASE_URI配置键,如下所示(更改突出显示):

#...
app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'a really really really really long secret key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:pass@localhost/flask_app_db'

manager = Manager(app)
db = SQLAlchemy(app)
#...

在本课程中,我们将使用 MySQL 数据库。因此,在进入下一部分之前,请确保您的计算机上有一个正在运行的 MySQL 安装。

创建模型

模型是一个 Python 类,代表数据库表,它的属性映射到表的列。模型类继承自db.Model,并将列定义为db.Column类的实例。打开main2.py文件,在updating_session()查看功能下添加以下类:

Flask _app/main2.py

#...
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

#...

class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer(), primary_key=True)
    title = db.Column(db.String(255), nullable=False)
    slug = db.Column(db.String(255), nullable=False)
    content = db.Column(db.Text(), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)    

    def __repr__(self):
        return "<{}:{}>".format(self.id, self.title[:10])

这里我们创建了一个有 5 个类变量的Post模型类。除了__tablename__之外的每个类变量都是db.Column类的一个实例。__tablename__是一个特殊的类变量,用于定义数据库表的名称。默认情况下,SQLAlchemy 不符合创建复数名称的惯例,而是在模型名称之后创建表名。如果不想依赖这种行为,请使用__tablename__变量显式命名该表。

db.Column()构造函数的第一个参数是要创建的列的类型。SQLAlchemy 提供了大量的列类型,如果这还不够,您甚至可以定义自己的自定义类型。下表列出了 SQLAlchemy 提供的一些泛型列类型及其在 Python 和 SQL 中的关联类型。

sqllcemy(SQL 语法) 计算机编程语言 结构化查询语言
BigInteger(大整数) int 比吉斯本
布尔代数学体系的 bool BOOLEAN 或 SMALLINT
日期 datetime.date 日期
日期时间 datetime.date DATETIME
整数 int 整数
浮动 float 浮动还是真实
数字的 decimal.Decimal 数字的
文本 str 文本

我们还可以通过将列作为关键字参数传递给db.Column构造函数来设置列的附加约束。下表列出了一些常用的约束:

限制 描述
可空的 当设置为False时,使列成为必需的。其默认值为True
系统默认值 它为列提供默认值。
指数 布尔属性。如果设置为True,将创建一个索引列。
联合国更新 它在更新记录时为列提供默认值。
主键 布尔属性。如果设置为True,则将该列标记为表的主键。
独一无二的 布尔属性。如果设置为True,列中的每个值必须是唯一的。

在第 16-17 行,我们定义了一个__repr__()方法。它不是一个要求,但是当被定义时,它提供了对象的字符串表示。

您可能已经注意到,我们将created_onupdated_on的默认值设置为方法名(datetime.utcnow),而不是调用方法(datetime.utcnow())。这是因为我们不想在执行代码时调用datetime.utcnow()方法。相反,我们希望它在添加或更新实际记录时调用它。

定义关系

在前一节中,我们已经创建了一个包含几个字段的 Post 模型。然而,在现实世界中,模型类很少单独存在。大多数情况下,它们通过一对一、一对多和多对多等各种关系与其他模型联系在一起。

让我们扩展一下博客网站的类比。通常,博客文章属于一个类别和一个或多个标签。换句话说,类别和帖子之间存在一对多关系,帖子和标签之间存在多对多关系。下图展示了这种关系。

打开main2.py并添加CategoryTag型号,如下所示(更改突出显示):

Flask _app/models.py

#...
def updating_session():
    #...
    return res

class Category(db.Model):
    __tablename__ = 'categories'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    slug = db.Column(db.String(255), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)

    def __repr__(self):
        return "<{}:{}>".format(id, self.name)

class Posts(db.Model):
    # ...

class Tag(db.Model):
    __tablename__ = 'tags'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    slug = db.Column(db.String(255), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)

    def __repr__(self):
        return "<{}:{}>".format(id, self.name)
#...

一对多关系

一对多关系是通过在子表上放置外键来创建的。这是您在使用数据库时会遇到的最常见的关系类型。为了在 SQLAlchemy 中创建一对多关系,我们执行以下操作:

  1. 在子类中使用db.ForeignKey约束创建一个新的db.Column实例。
  2. 在父类中使用db.relationship指令定义一个新属性。此属性将用于访问相关对象。

打开main2.py并修改PostCategory模型,如下所示(更改突出显示):

Flask _app/models.py

#...
class Category(db.Model):
    # ...
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    posts = db.relationship('Post', backref='category')

class Post(db.Model):
    # ...
    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)
    category_id = db.Column(db.Integer(), db.ForeignKey('categories.id'))
#...

这里我们分别给CategoryPost模型增加了两个新属性postscategory_id

db.ForeignKey()接受您要在其上定义外键的列的名称。这里我们将categories.id传递给db.ForeignKey(),这意味着Post模型的category_id属性只能从categories表的id列中取值。

接下来,我们在使用db.relationship()指令定义的Category模型中有posts属性。db.relationship()用于增加双向关系。换句话说,它在模型类上添加了一个属性来访问相关的对象。最简单的是,它接受至少一个位置参数,即关系另一端的类名。

class Category(db.Model):
    # ...    
    posts = db.relationship('Post')

现在,如果我们有一个Category对象(比如c),那么我们可以以c.posts的身份访问它下面的所有帖子。如果您想从关系的另一方访问数据,即从帖子对象中获取类别,该怎么办?这就是backref发挥作用的地方。所以代码:

posts = db.relationship('Post', backref='category')

Post对象添加category属性。也就是说,如果我们有一个Post对象(比如p,那么我们可以将它归类为p.category

PostCategory对象上的categoryposts属性只是为了您的方便而存在,它们不是表中的实际列。

请注意,与表示外键的属性(必须在关系的多侧定义)不同,您可以在关系的任何一侧定义db.relationship()

一对一的关系

在 SQLAlchemy 中建立一对一的关系几乎和一对多的关系一样,唯一的区别就是我们给db.relationship()指令传递了一个额外的参数uselist=False。这里有一个例子:

class Employee(db.Model):
    __tablename__ = 'employees'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    designation = db.Column(db.String(255), nullable=False)
    doj = db.Column(db.Date(), nullable=False)
    dl = db.relationship('DriverLicense', backref='employee', uselist=False)

class DriverLicense(db.Model):
    __tablename__ = 'driverlicense'
    id = db.Column(db.Integer(), primary_key=True)
    license_number = db.Column(db.String(255), nullable=False)
    renewed_on = db.Column(db.Date(), nullable=False)
    expiry_date = db.Column(db.Date(), nullable=False)
    employee_id = db.Column(db.Integer(), db.ForeignKey('employees.id'))  # Foreign key

:在这些类中,我们假设一个员工不能有多个驾驶证。所以员工和驾照是一对一的关系。

现在,如果我们有一个Employee对象e,那么e.dl将返回一个DriverLicense对象。如果我们没有将uselist=False传递给db.relationship()指令,那么员工和DriverLicense之间的关系将是一对多,并且e.dl将返回一个DriverLicense对象的列表,而不是单个对象。uselist=False参数对DriverLicense对象的employee属性没有任何影响。像往常一样,它将返回一个对象。

多对多关系

多对多关系需要一个额外的表,称为关联表。考虑一个博客网站的例子:

一篇博文通常与一个或多个标签相关联。类似地,标签也与一个或多个帖子相关联。所以poststags之间是多对多的关系。在 tags 表中添加引用帖子 id 的外键是不够的,因为一个标签可以有一个或多个帖子。

解决方案是通过定义 2 个引用post.idtag.id列的外键来创建一个名为关联表的新表。

如图所示,帖子和标签之间的多对多关系被实现为两个一对多关系。第一种是postspost_tags表之间的一一对应关系,第二种是tagspost_tags表之间的一一对应关系。下面的代码显示了如何在 SQLAlchemy 中创建多对多关系。打开main2.py文件并添加以下代码(更改突出显示)。

Flask _app/main2.py

# ...
class Category(db.Model):
    # ...
        def __repr__(self):
        return "<{}:{}>".format(id, self.name)

post_tags = db.Table('post_tags',
    db.Column('post_id', db.Integer, db.ForeignKey('posts.id')),
    db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'))
)

class Post(db.Model):
    # ...

class Tag(db.Model):
    # ...
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    posts = db.relationship('Post', secondary=post_tags, backref='tags')
#...

在第 7-10 行,我们将关联表定义为db.Table()的对象。db.Table()的第一个参数是表的名称,其他参数是由db.Column()的实例表示的列。与模型类相比,创建关联表的语法可能显得有些奇怪。这是因为关联表是使用 SQLAlchemy Core 创建的,这是 SQLAlchemy 的另一个方面。要了解更多关于 SQLAlchemy 的信息,请访问我们的 SQLAlchemy 教程

接下来,我们必须告诉我们的模型类我们想要使用的关联表。这就是secondary关键字论证的工作。在第 18 行,我们将secondary参数设置为post_tags来调用db.relationship()。虽然我们已经在Tag模型中定义了这种关系,但我们也可以在Post模型中轻松定义它。

假设我们有一个Post对象p,那么我们可以访问它的所有标签为p.tags。同样,给定一个Tag对象t,我们可以访问它下面的所有帖子作为t.posts

现在是时候在其中创建我们的数据库和表了。

创建表格

为了完成本课的剩余部分,您应该有一个运行良好的 MySQL 安装。如果没有,点击这里学习如何安装 MySQL。

回想一下,默认情况下,SQLAlchemy 只适用于 SQLite 数据库。要使用其他数据库,我们必须安装一个符合 DBAPI 的驱动程序。当我们使用 MySQL 时,我们将安装 PyMySql 驱动程序。

(env) overiq@vm:~/flask_app$ pip install pymysql

接下来登录 MySQL 服务器,使用以下命令创建一个名为flask_app_db的数据库:

(env) overiq@vm:~/flask_app$ mysql -u root -p
mysql>
mysql> CREATE DATABASE flask_app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Query OK, 1 row affected (0.26 sec)

mysql> \q
Bye
(env) overiq@vm:~/flask_app$

该命令创建完全支持 Unicode 的flask_app_db数据库。

要从模型中创建必要的表,请调用SQLAlchemy对象(db)的create_all()方法。启动 Python shell 并输入以下命令:

(env) overiq@vm:~/flask_app$ python main2.py shell
>>>
>>> from main2 import db
>>>
>>> db.create_all()
>>>

create_all()方法只在数据库中不存在表的情况下创建表。所以你可以安全地运行它多次。除此之外,create_all()方法没有考虑到在创建表格时对模型所做的修改。这意味着一旦在数据库中创建了表,在修改其模型后运行create_all()方法将不会改变表模式。为此,我们使用像 Alembic 这样的迁移工具。我们将在第课中学习如何使用 Alembic 执行数据库迁移。

要查看创建的表,请登录 MySQL 服务器并执行以下命令:

mysql>
mysql> use flask_app_db
Database changed
mysql>
mysql> show tables;
+------------------------+
| Tables_in_flask_app_db |
+------------------------+
| categories             |
| post_tags              |
| posts                  |
| tags                   |
+------------------------+
4 rows in set (0.02 sec)

mysql>

查看表的另一种方法是使用数据库管理工具,如 HeidiSQL。HeidiSQL 是一个跨平台的开源软件,用于管理 MySQL、MS-SQL 和 PostgreSQL。它允许我们浏览数据、编辑数据、查看模式、更改表等等,而无需编写一行 SQL。可以从这里下载 HeidiSQL。

安装完成后,在 HeidiSQL 中打开flask_app_db数据库,您将看到如下表格列表:

flask_app_db数据库现在有 4 个表。表即categoriespoststags是直接从模型创建的,表post_tags是一个关联表,表示PostTag模型之间的多对多关系。

SQLAlchemy类还定义了一个名为drop_all()的方法来删除数据库中的所有表。记住drop_all()不在乎表格是否包含任何数据。它会立即删除所有数据和表,所以要谨慎使用。

我们现在已经准备好了所有的桌子。让我们输入一些数据。



SQLAlchemy ORM 基础

原文:https://overiq.com/flask-101/sqlalchemy-orm-basics/

最后更新于 2020 年 7 月 27 日


插入数据

要使用 SQLAlchemy 创建新记录,我们需要执行以下步骤:

  1. 创建一个对象。
  2. 将对象添加到会话中。
  3. 提交会话。

在 SQLAlchemy 中,我们使用会话与数据库交互。幸运的是,我们不需要手动创建会话,Flask-SQLAlchemy 为我们管理它。我们以db.session的形式访问会话对象。它是处理数据库连接的会话对象。会话对象也是事务的处理程序。默认情况下,事务隐式启动,并将保持打开状态,直到会话被提交或回滚。

启动 Python shell 并创建一些模型对象,如下所示:

(env) overiq@vm:~/flask_app$ python main2.py shell
>>>
>>> from main2 import db, Post, Tag, Category
>>>
>>>
>>> c1 = Category(name='Python', slug='python')
>>> c2 = Category(name='Java', slug='java')
>>>

这里我们已经创建了两个Category对象。我们可以使用点(.)运算符访问对象的属性,如下所示:

>>>
>>> c1.name, c1.slug
('Python', 'python')
>>>
>>> c2.name, c2.slug
('Java', 'java')
>>>

接下来,我们将对象添加到会话中。

>>>
>>> db.session.add(c1)
>>> db.session.add(c2)
>>>

向会话中添加对象实际上并没有将它们写入数据库,它只是准备要在下一次提交中保存的对象。我们可以通过检查对象的主键来验证这一点。

>>>
>>> print(c1.id)
None
>>>
>>> print(c2.id)
None
>>>

两个对象的id属性的值都是None。这意味着我们的对象还没有保存在数据库中。

我们可以使用add_all()方法,而不是一次向会话中添加一个对象。add_all()方法接受要添加到会话中的对象列表。

>>>
>>> db.session.add_all([c1, c1])
>>>

多次向会话添加对象不会引发任何错误。您可以随时使用db.session.new查看会话中的对象。

>>>
>>> db.session.new
IdentitySet([<None:Python>, <None:java>])
>>>

最后,要将对象保存到数据库中,调用commit()方法如下:

>>>
>>> db.session.commit()
>>>

访问Category对象的id属性现在将返回主键,而不是None

>>>
>>> print(c1.id)
1
>>>
>>> print(c2.id)
2
>>>

此时,HeidiSQL 中的categories表应该是这样的:

我们新创建的类别与任何帖子都没有关联。因此c1.postsc2.posts将返回一个空列表。

>>>
>>> c1.posts
[]
>>>
>>> c2.posts
[]
>>>

现在让我们创建一些帖子。

>>>
>>> p1 = Post(title='Post 1', slug='post-1', content='Post 1', category=c1)
>>> p2 = Post(title='Post 2', slug='post-2', content='Post 2', category=c1)
>>> p3 = Post(title='Post 3', slug='post-3', content='Post 3', category=c2)
>>>

在创建Post对象时,我们也可以如下设置,而不是传递类别:

>>>
>>> p1.category = c1
>>>

将对象添加到会话并提交。

>>>
>>> db.session.add_all([p1, p2, p3])
>>> db.session.commit()
>>>

再次访问Category对象的posts属性,这次会得到一个非空列表,如下图:

>>>
>>> c1.posts
[<1:Post 1>, <2:Post 2>]
>>>
>>> c2.posts
[<3:Post 3>]
>>>

从关系的另一面,我们可以使用Post对象上的category属性访问帖子所属的Category对象。

>>>
>>> p1.category
<1:Python>
>>>
>>> p2.category
<1:Python>
>>>
>>> p3.category
<2:Java>
>>>

请记住,由于Category模型中的relationship()指令,所有这些都成为可能。我们的数据库中现在有三篇文章,但是没有一篇与任何标签相关联。

>>>
>>> p1.tags, p2.tags, p3.tags
([], [], [])
>>>

是时候创建一些标签了。在 Shell 中创建Tag对象,如下所示:

>>>
>>> t1 = Tag(name="refactoring", slug="refactoring")
>>> t2 = Tag(name="snippet", slug="snippet")
>>> t3 = Tag(name="analytics", slug="analytics")
>>>
>>> db.session.add_all([t1, t2, t3])
>>> db.session.commit()
>>>

这段代码创建三个标记对象,并将它们提交给数据库。我们的帖子仍然没有连接到任何标签。这里他告诉我们如何将一个Post对象连接到一个Tag对象。

>>>
>>> p1.tags.append(t1)
>>> p1.tags.extend([t2, t3])
>>> p2.tags.append(t2)
>>> p3.tags.append(t3)
>>>
>>> db.session.add_all([p1, p2, p3])
>>>
>>> db.session.commit()
>>>

该提交在post_tags表中添加了以下五条记录。

我们的帖子现在与一个或多个标签相关联:

>>>
>>> p1.tags
[<1:refactoring>, <2:snippet>, <3:analytics>]
>>>
>>> p2.tags
[<2:snippet>]
>>>
>>> p3.tags
[<3:analytics>]
>>>

反过来,我们可以访问属于标签的帖子,如下所示:

>>>
>>> t1.posts
[<1:Post 1>]
>>>
>>> t2.posts
[<1:Post 1>, <2:Post 2>]
>>>
>>> t3.posts
[<1:Post 1>, <3:Post 3>]
>>>
>>>

需要注意的是,我们不需要首先提交Tag对象,然后将其与Post对象相关联,而是可以一次完成所有这些,如下所示:

>>>
>>> t1 = Tag(name="refactoring", slug="refactoring")
>>> t2 = Tag(name="snippet", slug="snippet")
>>> t3 = Tag(name="analytics", slug="analytics")
>>> 
>>> p1.tags.append(t1)
>>> p1.tags.extend([t2, t3])
>>> p2.tags.append(t2)
>>> p3.tags.append(t3)
>>>
>>> db.session.add(p1)
>>> db.session.add(p2)
>>> db.session.add(p3)
>>>
>>> db.session.commit()
>>>

请注意,在第 11-13 行中,我们只向会话添加了Post对象。TagPost对象通过多对多关系连接。因此,将一个Post对象添加到会话中,也会隐式地将其关联的Tag对象添加到会话中。即使您仍然将Tag对象手动添加到会话中,也不会出现任何错误。

更新数据

要更新对象,只需将其属性设置为新值,将对象添加到会话中并提交更改。

>>>
>>> p1.content   # initial value
'Post 1'
>>>
>>> p1.content = "This is content for post 1"   # setting new value
>>> db.session.add(p1)
>>>
>>> db.session.commit()
>>>
>>> p1.content  # final value
'This is content for post 1'
>>>

删除数据

要删除对象,请使用会话对象的delete()方法。它接受一个对象,并将其标记为在下一次提交时删除。

创建一个名为seo的新临时标签,并将其与帖子p1p2相关联,如下所示:

>>>
>>> tmp = Tag(name='seo', slug='seo') # creating a temporary Tag object
>>>
>>> p1.tags.append(tmp)
>>> p2.tags.append(tmp)
>>>
>>> db.session.add_all([p1, p2])
>>> db.session.commit()
>>>

该提交总共添加了 3 行。一个在tags表,两个在post_tags表。在数据库中,这三行如下所示:

现在我们删除seo标记:

>>>
>>> db.session.delete(tmp)
>>> db.session.commit()
>>>

此提交会删除上一步中添加的所有三行。但是,它不会删除标签关联的帖子。

默认情况下,如果删除父表中的对象(如categories),则子表中其关联对象的外键(如posts)被设置为NULL。下面的清单通过创建一个新的类别对象和一个 post 对象,然后删除该类别对象来演示这种行为:

>>>
>>> c4 = Category(name='css', slug='css')
>>> p4 = Post(title='Post 4', slug='post-4', content='Post 4', category=c4)
>>>
>>> db.session.add(c4)
>>>
>>> db.session.new
IdentitySet([<None:css>, <None:Post 4>])
>>>
>>> db.session.commit()
>>>

这个提交增加了两行。一个在categories表,一个在posts表。

现在让我们看看当我们删除一个Category对象时会发生什么。

>>>
>>> db.session.delete(c4)
>>> db.session.commit()
>>>

该提交从categories表中删除css类别,并将其关联帖子的外键(category_id)设置为NULL

在某些情况下,一旦父记录被删除,您可能希望删除所有子记录。我们可以通过将cascade='all,delete-orphan'传递给db.relationship()指令来实现这一点。打开main2.py文件,修改Category模型中的db.relationship()指令如下(更改突出显示):

Flask _app/main2.py

#...
class Category(db.Model):
    #...
    posts = db.relationship('Post', backref='category', cascade='all,delete-orphan')
#...

从现在开始,删除一个类别也会删除与之相关的所有帖子。重新启动 shell 以使更改生效,导入必要的对象,并创建一个新的类别和帖子,如下所示:

(env) overiq@vm:~/flask_app$ python main2.py shell
>>>
>>> from main2 import db, Post, Tag, Category
>>>
>>> c5 = Category(name='css', slug='css')
>>> p5 = Post(title='Post 5', slug='post-5', content='Post 5', category=c5)
>>>
>>> db.session.add(c5)
>>> db.session.commit()
>>>

下面是数据库应该如何处理这个提交。

立即删除该类别。

>>>
>>> db.session.delete(c5)
>>> db.session.commit()
>>>

提交后,数据库应该如下所示:

查询数据

要查询数据库,我们使用session对象的query()方法。query()方法返回一个flask_sqlalchemy.BaseQuery对象,它只是原sqlalchemy.orm.query.Query对象的扩展。flask_sqlalchemy.BaseQuery对象表示将用于查询数据库的SELECT语句。下表列出了flask_sqlalchemy.BaseQuery类的一些常用方法。

方法 描述
all() 以列表形式返回查询结果(用flask_sqlalchemy.BaseQuery表示)。
count() 返回查询中记录的总数。
first() 返回查询的第一个结果,如果结果中没有行,则返回None
first_or_404() 如果结果中没有行,则返回查询的第一个结果或 HTTP 404 错误。
get(pk) 如果没有找到匹配给定主键(pk)或None的对象,则返回该对象。
get_or_404(pk) 如果没有找到与给定主键(pk)匹配的对象,则返回该对象。
filter(*criterion) WHERE子句应用于查询后,返回一个新的flask_sqlalchemy.BaseQuery实例。
limit(limit) LIMIT子句应用于查询后,返回一个新的flask_sqlalchemy.BaseQuery实例。
offset(offset) OFFSET子句应用于查询后,返回一个新的flask_sqlalchemy.BaseQuery实例。
order_by(*criterion) 在查询中应用ORDER BY子句后,返回一个新的flask_sqlalchemy.BaseQuery实例。
join() 在查询上创建 SQL JOIN 后,返回一个新的flask_sqlalchemy.BaseQuery实例。

all()方法

最简单的形式是query()方法可以将一个或多个模型类或列作为参数。以下代码返回posts表中的所有记录。

>>>
>>> db.session.query(Post).all()
[<1:Post 1>, <2:Post 2>, <3:Post 3>, <4:Post 4>]
>>>

同样,下面的代码返回来自categoriestags表的所有记录。

>>>
>>> db.session.query(Category).all()
[<1:Python>, <2:Java>]
>>>
>>>
>>> db.session.query(Tag).all()
[<1:refactoring>, <2:snippet>, <3:analytics>]
>>>

要获取用于查询数据库的原始 SQL,只需按如下方式打印flask_sqlalchemy.BaseQuery对象:

>>>
>>> print(db.session.query(Post))
SELECT
    posts.id AS posts_id,
    posts.title AS posts_title,
    posts.slug AS posts_slu g,
    posts.content AS posts_content,
    posts.created_on AS posts_created_on,
    posts.u pdated_on AS posts_updated_on,
    posts.category_id AS posts_category_id
FROM
    posts
>>>
>>>

在前面的示例中,数据从表的所有列中返回。我们可以通过将列名显式传递给query()方法来防止这种情况,如下所示:

>>>
>>> db.session.query(Post.id, Post.title).all()
[(1, 'Post 1'), (2, 'Post 2'), (3, 'Post 3'), (4, 'Post 4')]
>>>

count()方法

count()方法返回查询返回的结果数。

>>>
>>> db.session.query(Post).count() # get the total number of records in the posts table
4
>>> db.session.query(Category).count()  # get the total number of records in the categories table
2
>>> db.session.query(Tag).count()  # get the total number of records in the tags table
3
>>>

first()方法

first()方法只返回查询的第一个结果,如果查询返回零个结果,则返回None

>>>
>>> db.session.query(Post).first()
<1:Post 1>
>>>
>>> db.session.query(Category).first()
<1:Python>
>>>
>>> db.session.query(Tag).first()
<1:refactoring>
>>>

get()方法

get()方法返回与传递给它的主键匹配的实例,如果没有找到这样的对象,则返回None

>>>
>>> db.session.query(Post).get(2)
<2:Post 2>
>>>
>>> db.session.query(Category).get(1)
<1:Python>
>>>
>>> print(db.session.query(Category).get(10))  # no result found for primary key 10
None
>>>

get_or_404()方法

get()方法相同,但是当没有找到对象时,它不会返回None,而是返回 HTTP 404 Error。

>>>
>>> db.session.query(Post).get_or_404(1)
<1:Post 1>
>>>
>>>
>>> db.session.query(Post).get_or_404(100)
Traceback (most recent call last):
...
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on
the server.  If you entered the URL manually please check your spelling and try
again.
>>>

filter()方法

filter()方法允许我们通过在查询中添加WHERE子句来过滤结果。它至少接受一列、一个运算符和值。这里有一个例子:

>>>
>>> db.session.query(Post).filter(Post.title == 'Post 1').all()
[<1:Post 1>]
>>>

该查询返回标题为"Post 1"的所有帖子。该查询的 SQL 等价物是:

>>>
>>> print(db.session.query(Post).filter(Post.title == 'Post 1'))
SELECT
    posts.id AS posts_id,
    posts.title AS posts_title,
    posts.slug AS posts_slu g,
    posts.content AS posts_content,
    posts.created_on AS posts_created_on,
    posts.u pdated_on AS posts_updated_on,
    posts.category_id AS posts_category_id
FROM
    posts
WHERE
    posts.title = % (title_1) s
>>> 
>>>

WHERE子句中的字符串% (title_1) s是一个占位符,在执行查询时将被实际值替换。

我们可以将多个过滤器传递给filter()方法,它们将使用 SQL AND运算符连接在一起。例如:

>>>
>>> db.session.query(Post).filter(Post.id >= 1, Post.id <= 2).all()
[<1:Post 1>, <2:Post 2>]
>>>
>>>

此查询返回主键大于或等于 1 但小于或等于 2 的所有帖子。它的 SQL 等价物是:

>>>
>>> print(db.session.query(Post).filter(Post.id >= 1, Post.id <= 2))
SELECT
    posts.id AS posts_id,
    posts.title AS posts_title,
    posts.slug AS posts_slu g,
    posts.content AS posts_content,
    posts.created_on AS posts_created_on,
    posts.u pdated_on AS posts_updated_on,
    posts.category_id AS posts_category_id
FROM
    posts
WHERE
    posts.id >= % (id_1) s
AND posts.id <= % (id_2) s
>>>

first_or_404()方法

first()方法相同,但是当查询没有返回结果时,它不会返回None,而是返回 HTTP 404 Error。

>>>
>>> db.session.query(Post).filter(Post.id > 1).first_or_404()
<2:Post 2>
>>>
>>> db.session.query(Post).filter(Post.id > 10).first_or_404().all()
Traceback (most recent call last):
...
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on
the server.  If you entered the URL manually please check your spelling and try
again.
>>>

极限()方法

limit()方法将LIMIT子句添加到查询中。它接受您希望从查询中返回的行数。

>>>
>>> db.session.query(Post).limit(2).all()
[<1:Post 1>, <2:Post 2>]
>>>
>>> db.session.query(Post).filter(Post.id >= 2).limit(1).all()
[<2:Post 2>]
>>>

上述查询的 SQL 等价物如下:

>>>
>>> print(db.session.query(Post).limit(2))
SELECT
    posts.id AS posts_id,
    posts.title AS posts_title,
    posts.slug AS posts_slu g,
    posts.content AS posts_content,
    posts.created_on AS posts_created_on,
    posts.u pdated_on AS posts_updated_on,
    posts.category_id AS posts_category_id
FROM
    posts
LIMIT % (param_1) s
>>>
>>>
>>> print(db.session.query(Post).filter(Post.id >= 2).limit(1))
SELECT
    posts.id AS posts_id,
    posts.title AS posts_title,
    posts.slug AS posts_slu g,
    posts.content AS posts_content,
    posts.created_on AS posts_created_on,
    posts.u pdated_on AS posts_updated_on,
    posts.category_id AS posts_category_id
FROM
    posts
WHERE
    posts.id >= % (id_1) s
LIMIT % (param_1) s
>>>
>>>

offset()方法

offset()方法将OFFSET子句添加到查询中。它接受偏移量作为参数。它常用于limit()从句。

>>>
>>> db.session.query(Post).filter(Post.id > 1).limit(3).offset(1).all()
[<3:Post 3>, <4:Post 4>]
>>>

上述查询的等效 SQL 如下:

>>>
>>> print(db.session.query(Post).filter(Post.id > 1).limit(3).offset(1))
SELECT
    posts.id AS posts_id,
    posts.title AS posts_title,
    posts.slug AS posts_slu g,
    posts.content AS posts_content,
    posts.created_on AS posts_created_on,
    posts.u pdated_on AS posts_updated_on,
    posts.category_id AS posts_category_id
FROM
    posts
WHERE
    posts.id > % (id_1) s
LIMIT % (param_1) s, % (param_2) s
>>>

字符串% (param_1) s% (param_2) s分别是偏移和限制的占位符。

order_by()方法

通过在查询中添加ORDER BY子句,使用order_by()方法对结果进行排序。它接受订单应该基于的列名。默认情况下,它按升序排序。

>>>
>>> db.session.query(Tag).all()
[<1:refactoring>, <2:snippet>, <3:analytics>]
>>>
>>> db.session.query(Tag).order_by(Tag.name).all()
[<3:analytics>, <1:refactoring>, <2:snippet>]
>>>

要按降序排序,使用db.desc()功能,如下所示:

>>>
>>> db.session.query(Tag).order_by(db.desc(Tag.name)).all()
[<2:snippet>, <1:refactoring>, <3:analytics>]
>>>

join()方法

join()方法用于创建 SQL JOIN。它接受要为其创建 SQL JOIN 的表名。

>>>
>>> db.session.query(Post).join(Category).all()
[<1:Post 1>, <2:Post 2>, <3:Post 3>]
>>>

该查询相当于以下 SQL:

>>>
>>> print(db.session.query(Post).join(Category))
SELECT
    posts.id AS posts_id,
    posts.title AS posts_title,
    posts.slug AS posts_slu g,
    posts.content AS posts_content,
    posts.created_on AS posts_created_on,
    posts.u pdated_on AS posts_updated_on,
    posts.category_id AS posts_category_id
FROM
    posts
INNER JOIN categories ON categories.id = posts.category_id

join()方法通常用于在单个查询中从一个或多个表中获取数据。例如:

>>>
>>> db.session.query(Post.title, Category.name).join(Category).all()
[('Post 1', 'Python'), ('Post 2', 'Python'), ('Post 3', 'Java')]
>>>

我们可以通过如下链接join()方法为两个以上的表创建 SQL JOIN:

db.session.query(Table1).join(Table2).join(Table3).join(Table4).all()

让我们通过填写联系表来结束本课。

回想一下,在第课“Flask 中的表单处理”中,我们创建了一个联系表单来接收用户的反馈。从目前的情况来看,contact()视图功能并没有将提交的反馈保存到数据库中。它只将反馈打印到控制台。为了将反馈保存到数据库,我们必须首先创建一个新表。打开main2.py并在Tag模型的正下方添加Feedback模型,如下所示:

Flask _app/main2.py

#...
class Feedback(db.Model):
    __tablename__ = 'feedbacks'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(1000), nullable=False)
    email = db.Column(db.String(100), nullable=False)
    message = db.Column(db.Text(), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)

    def __repr__(self):
        return "<{}:{}>".format(self.id, self.name)
#...

重启 Python shell,调用db对象的create_all()方法,创建feedbacks表。

(env) overiq@vm:~/flask_app$ python main2.py shell
>>>
>>> from main2 import db
>>>
>>> db.create_all()
>>>

接下来,修改contact()视图功能如下(更改突出显示):

Flask _app/main2.py

#...
@app.route('/contact/', methods=['get', 'post'])
def contact():
    form = ContactForm()
    if form.validate_on_submit():
        name = form.name.data
        email = form.email.data
        message = form.message.data
        print(name)
        print(Post)
        print(email)
        print(message)

        # db logic goes here
        feedback = Feedback(name=name, email=email, message=message)
        db.session.add(feedback)
        db.session.commit()

        print("\nData received. Now redirecting ...")
        flash("Message Received", "success")
        return redirect(url_for('contact'))

    return render_template('contact.html', form=form)
#...

启动服务器,访问http://127 . 0 . 0 . 1:5000/联系/ ,填写表格,提交反馈。

HeidiSQL 中提交的反馈应该如下所示:



将 Alembic 用于数据库迁移

原文:https://overiq.com/flask-101/database-migrations-with-alembic/

最后更新于 2020 年 7 月 27 日


Alembic 是一个用于 SQLAlchemy 的数据库迁移工具。将数据库迁移视为数据库的版本控制。回想一下,SQLAlchemy create_all()方法只创建模型中缺失的表。一旦创建了表,它就不会根据模型的变化来改变表模式。

开发应用时,更改表模式是很常见的。这就是 Alembic 进入戏剧的地方。像 Alembic 这样的工具允许我们随着应用的发展改变数据库模式。它还跟踪对数据库所做的更改,以便您可以及时向前或向后移动。如果我们不使用像 Alembic 这样的工具,那么我们必须跟踪所有的变化,并通过输入ALTER语句手动更改数据库模式。

Flask-Migrate 是将 Alembic 与 Flask 应用集成在一起的扩展。使用以下命令安装 Flask-Migrate 及其依赖项。

(env) overiq@vm:~/flask_app$ pip install flask-migrate

要将 Flask-Migrate 与我们的应用集成,请从flask_migrate包导入MigrateMigrateCommand类,并通过传递应用实例(app)和SQLAlchemy对象(db)来创建Migrate类的实例,如下所示(更改突出显示):

Flask _app/main2.py

#...
from flask_migrate import Migrate, MigrateCommand

app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'a really really really really long secret key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:pass@localhost/flask_app_db'

manager = Manager(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
#...

MigrateCommand类定义了一些可以通过 Flask-Script 获得的数据库迁移命令。在第 12 行,我们通过db命令行参数公开这些命令。要返回终端查看新添加的命令头,请输入以下命令:

(env) overiq@vm:~/flask_app$ python main2.py
positional arguments:
  {db,faker,foo,shell,runserver}
    db                  Perform database migrations
    faker               A command to add fake data to the tables
    foo                 Just a simple command
    shell               Runs a Python shell inside Flask application context.
    runserver           Runs the Flask development server i.e. app.run()

optional arguments:
  -?, --help            show this help message and exit

(env) overiq@vm:~/flask_app$

如您所见,我们现在有了一个名为db的新命令来执行数据库迁移。要查看db所有可能子命令的完整列表,请键入以下内容:

(env) overiq@vm:~/flask_app$  python main2.py db -?
Perform database migrations

positional arguments:
  {init,revision,migrate,edit,merge,upgrade,downgrade,show,history,heads,branche
s,current,stamp}
    init                Creates a new migration repository
    revision            Create a new revision file.
    migrate             Alias for 'revision --autogenerate'
    edit                Edit current revision.
    merge               Merge two revisions together. Creates a new migration
                        file
    upgrade             Upgrade to a later version
    downgrade           Revert to a previous version
    show                Show the revision denoted by the given symbol.
    history             List changeset scripts in chronological order.
    heads               Show current available heads in the script directory
    branches            Show current branch points
    current             Display the current revision for each database.
    stamp               'stamp' the revision table with the given revision;
                        don't run any migrations

optional arguments:
  -?, --help            show this help message and exit

这些是我们在执行数据库迁移时将使用的实际命令。

在 Alembic 开始跟踪更改之前,我们必须初始化迁移存储库。迁移存储库只是一个包含 Alembic 配置和迁移脚本的目录。要创建迁移存储库,请执行init命令:

(env) overiq@vm:~/flask_app$ python main2.py db init
  Creating directory /home/overiq/flask_app/migrations ... done
  Creating directory /home/overiq/flask_app/migrations/versions ... done
  Generating /home/overiq/flask_app/migrations/README ... done
  Generating /home/overiq/flask_app/migrations/env.py ... done
  Generating /home/overiq/flask_app/migrations/alembic.ini ... done
  Generating /home/overiq/flask_app/migrations/script.py.mako ... done
  Please edit configuration/connection/logging settings in
  '/home/overiq/flask_app/migrations/alembic.ini' before proceeding.
(env) overiq@vm:~/flask_app$

该命令将在flask_app目录下创建一个“迁移”目录。migrations目录的结构应该是这样的:

migrations
├── alembic.ini
├── env.py
├── README
├── script.py.mako
└── versions

以下是每个文件和文件夹的简要介绍:

  • alembic.ini-Alembic 的配置文件。
  • env.py -每次调用 Alembic 时运行的 Python 文件。它负责连接到数据库、启动事务和调用迁移引擎。
  • README -一个自述文件。
  • script.py.mako -将用于创建迁移脚本的樱井真子模板文件。
  • version -存储迁移脚本的目录。

创建迁移脚本

Alembic 将数据库迁移存储在迁移脚本中,这些脚本只是 Python 文件。迁移脚本定义了两个函数upgrade()downgrade()upgrade()功能的工作是将一组更改应用到数据库中,而downgrade()功能则逆转这些更改。当我们应用迁移时,它的upgrade()函数被执行,当我们回滚迁移时,它的downgrade()函数被执行。

Alembic 提供了两种创建迁移的方法:

  1. 通过revision命令手动。
  2. 自动通过migrate命令。

手动迁移

手动或空迁移创建具有空upgrade()downgrade()功能的迁移脚本。我们的工作是使用 Alembic 指令填充这些方法,这些指令将对数据库应用一组更改。当我们想要完全控制迁移过程时,使用手动迁移。要创建空迁移,请输入以下命令:

(env) overiq@vm:~/flask_app$ python main2.py db revision -m "Initial migration"

该命令将在migrations/version目录中创建新的迁移脚本。文件的名称应为someid_initial_migration.py形式。打开文件,应该是这样的:

"""Initial migration

Revision ID: 945fc7313080
Revises: 
Create Date: 2017-12-29 14:39:27.854291

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = '945fc7313080'
down_revision = None
branch_labels = None
depends_on = None

def upgrade():
    pass

def downgrade():
    pass

文件以注释部分开始,注释部分包含我们使用-m标志、修订标识和文件创建时的时间戳指定的消息。下一个重要部分是版本标识符。每个迁移脚本都有一个存储在revision变量中的修订标识。在下一行中,我们设置了down_revision变量None。Alembic 使用down_revision变量来确定迁移应该以什么顺序运行。down_revision变量指向父级迁移的版本号。在我们的例子中,它被设置为None,因为这是我们的第一个迁移脚本。文件末尾有空的upgrade()downgrade()功能。

有了迁移脚本。让我们编辑迁移文件,分别给upgrade()downgrade()功能添加创建表和删除表操作。

def upgrade():
    op.create_table(
        'users',
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('name', sa.String(50), nullable=False),
    )

def downgrade():
    op.drop_table('users')

upgrade()功能中,我们使用的是 Alembic 的create_table()指令。create_table()指令发布CREATE TABLE声明。

downgrade()功能中,我们使用发布DROP TABLE声明的drop_table()指令。

当您运行此迁移时,它会创建users表,当您回滚此迁移时,它会删除users表。

我们现在准备应用我们的第一次迁移。输入以下命令运行迁移:

(env) overiq@vm:~/flask_app$ python main2.py db upgrade

该命令将调用迁移脚本的upgrade()功能。db upgrade命令将数据库迁移到最新的迁移。请注意,db upgrade不仅运行最新的迁移,还运行所有尚未运行的迁移。这意味着,如果我们已经创建了一系列迁移,那么db upgrade将按照它们被创建的顺序运行所有这些迁移。

您也可以传递要运行的迁移的版本 id,而不是运行最新的迁移。在这种情况下,db upgrade将在运行指定的迁移后停止,并且不会继续运行最新的迁移。

(env) overiq@vm:~/flask_app$ python main2.py db upgrade 945fc7313080

由于这是第一次应用迁移,Alembic 还会创建一个名为alembic_version的表。该表由一个名为version_num的列组成,该列存储最新应用的迁移的修订 id。这就是 Alembic 如何知道迁移的当前状态以及应该从哪里开始。目前,alembic_version表是这样的:

我们可以使用db current命令来确定上次应用的迁移。它返回上次应用的迁移的修订 id。如果您没有应用任何迁移,它将不会返回任何内容。

(env) overiq@vm:~/flask_app$ python main2.py db current
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
945fc7313080 (head)

(env) overiq@vm:~/flask_app$

输出显示我们当前正在迁移945fc7313080。另外,请注意修订 id 后面的字符串(head),它表示迁移945fc7313080是最新的迁移。

使用db revision命令创建另一个空迁移,如下所示:

(env) overiq@vm:~/flask_app$ python main2.py db revision -m "Second migration"

再次运行db current命令。这一次,您将获得不带字符串(head)的修订 id,因为迁移945fc7313080不再是最新的了。

(env) overiq@vm:~/flask_app$ python main2.py db current
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
945fc7313080

(env) overiq@vm:~/flask_app$

要查看完整的迁移列表(已应用和未应用),请使用db history命令。它以相反的时间顺序(即最晚优先)返回迁移列表。

(env) overiq@vm:~/flask_app$ python main2.py db history
945fc7313080 -> b0c1f3d3617c (head), Second migration
<base> -> 945fc7313080, Initial migration

(env) overiq@vm:~/flask_app$

输出显示945fc7313080是我们的第一次迁移,然后是b0c1f3d3617c是最近的一次迁移。像往常一样(head)表示最近的迁徙。

我们通过迁移创建的users表纯粹是为了测试。我们可以通过降级迁移将数据库恢复到执行db upgrade命令之前的原始状态。要降级或回滚上次应用的迁移,我们使用db downgrade命令。

(env) overiq@vm:~/flask_app$ python main2.py db downgrade
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running downgrade 945fc7313080 -> , Initial mi
gration

(env) overiq@vm:~/flask_app$

这将调用迁移945fc7313080downgrade()方法,从数据库中删除users表。就像db upgrade命令一样,我们也可以传递想要降级到的迁移的修订 id。例如,要降级到迁移645fc5113912,我们将使用以下命令。

(env) overiq@vm:~/flask_app$ python main2.py db downgrade 645fc5113912

要回滚所有应用的迁移,请使用以下命令:

(env) overiq@vm:~/flask_app$ python main2.py db downgrade base

目前,我们还没有对数据库进行迁移。我们可以通过运行db current命令来验证这一点,如下所示:

(env) overiq@vm:~/flask_app$ python main2.py db current
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.

(env) overiq@vm:~/flask_app$

如您所见,输出不返回修订 id。请注意,降级迁移只会撤消对数据库所做的更改,不会删除迁移脚本本身。因此,我们还有两个迁移脚本,要查看它们运行db history命令。

(env) overiq@vm:~/flask_app$ python main2.py db history
945fc7313080 -> b0c1f3d3617c (head), Second migration
<base> -> 945fc7313080, Initial migration

(env) overiq@vm:~/flask_app$

那么如果我们现在运行db upgrade命令会发生什么呢?

db upgrade命令将首先运行迁移945fc7313080,然后是迁移b0c1f3d3617c

如果你是这么想的。干得好!现在,您应该对迁移有了很好的了解。我们的数据库再次处于完美状态,我们不想在迁移脚本中应用更改,这样我们就可以安全地删除它们。

自动迁移

注意:在继续之前,请确保您已经删除了上一节中的所有迁移。

自动迁移在将模型与数据库的当前版本进行比较后,为upgrade()downgrade()功能创建代码。为了创建自动迁移,我们使用migrate命令,它只是revision --autogenerate的别名。在终端输入migrate命令如下:

(env) overiq@vm:~/flask_app$ python main2.py db migrate
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.env] No changes in schema detected.

(env) overiq@vm:~/flask_app$

注意输出的最后一行,它说"No changes in schema detected."这意味着我们的模型和数据库是同步的。

打开main2.py,在Feedback模型后添加Employee模型,如下所示:

Flask _app/main2.py

#...
class Employee(db.Model):
    __tablename__ = 'employees'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    designation = db.Column(db.String(255), nullable=False)
    doj = db.Column(db.Date(), nullable=False)    
#...

再次运行db migrate命令,这次 Alembic 将检测到新的employees表的添加,并将生成一个迁移脚本以及创建和删除employees表的逻辑。

(env) overiq@vm:~/flask_app$ python main2.py db migrate -m "Adding employees table"

前面命令创建的迁移脚本应该如下所示:

"""Adding employees table

Revision ID: 6e059688f04e
Revises: 
Create Date: 2017-12-30 16:01:28.030320

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = '6e059688f04e'
down_revision = None
branch_labels = None
depends_on = None

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('employees',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.String(length=255), nullable=False),
    sa.Column('designation', sa.String(length=255), nullable=False),
    sa.Column('doj', sa.Date(), nullable=False),
    sa.PrimaryKeyConstraint('id')
    )
    # ### end Alembic commands ###

def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('employees')
    # ### end Alembic commands ###

这里没有什么新的,函数upgrade()使用create_table指令创建表,downgrade()函数使用drop_table指令删除表。

让我们使用db upgrade命令运行这个迁移:

(env) overiq@vm:~/flask_app$ python main2.py db upgrade
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 6e059688f04e, Adding emplo
yees table

(env) overiq@vm:~/flask_app$

这将在您的数据库中添加employees表。我们可以通过查看数据库来验证这些更改,如下所示:

自动迁移的局限性

自动迁移并不完美。它无法检测到所有可能的变化。

Alembic 可以检测到的操作:

  • 添加或删除表格
  • 添加或删除列
  • 外键的更改
  • 列类型的更改
  • 索引和显式命名的唯一约束的更改

Alembic 无法检测到的操作:

  • 更改表名
  • 更改列名
  • 没有显式名称的约束

要为 Alembic 无法检测到的操作创建迁移脚本,我们必须创建一个空的迁移脚本,然后相应地填充upgrade()downgrade()函数。



在 Flask 中发送电子邮件

原文:https://overiq.com/flask-101/sending-email-in-flask/

最后更新于 2020 年 7 月 27 日


Web 应用一直在发送电子邮件,在本课中,我们将把电子邮件发送功能集成到 Flask 应用中。

Python 标准库有一个名为smtplib的模块,可以用来发送邮件。虽然,直接使用smtplib模块并没有那么复杂,但是仍然需要你做一些工作。为了简化这个过程,人们创建了一个名为 Flask-Mail 的扩展。Flask-Mail 是围绕 Python smtplib模块构建的,公开了一个发送电子邮件的简单界面。它还为批量电子邮件和附件提供支持。使用以下命令安装 Flask 邮件:

(env) overiq@vm:~/flask_app$ pip install flask-mail

flask_mail包中初始化扩展导入Mail类,并如下创建Mail类的实例(更改突出显示):

Flask _app/main2.py

#...
from flask_mail import Mail, Message

app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'a really really really really long secret key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:pass@localhost/flask_app_db'

manager = Manager(app)
manager.add_command('db', MigrateCommand)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
mail = Mail(app)
#...

接下来,我们必须设置一些配置选项,让 Flask-Mail 知道要连接到哪个 SMTP 服务器。在main2.py文件中添加以下配置(更改突出显示):

Flask _app/main2.py

#...
app.config['SECRET_KEY'] = 'a really really really really long secret key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:pass@localhost/flask_app_db'
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = 'infooveriq@gmail.com'  # enter your email here
app.config['MAIL_DEFAULT_SENDER'] = 'infooveriq@gmail.com' # enter your email here
app.config['MAIL_PASSWORD'] = 'password' # enter your password here

manager = Manager(app)
manager.add_command('db', MigrateCommand)
db = SQLAlchemy(app)
mail = Mail(app)
#...

这里我们使用的是 Gmail SMTP 服务器。请注意,Gmail 每天只允许您发送 100-150 封电子邮件。如果这还不够,您可能需要查看 SendGrid 或 MailChimp 之类的替代方法。

而不是像我们在这里所做的那样,在应用中硬编码电子邮件和密码。更好的方法是将电子邮件和密码存储在环境变量中。这样,如果电子邮件或密码改变,我们就不必更新我们的代码。我们将在后面的课程中看到如何做到这一点。

Flask-邮件基础

为了编写一封电子邮件,我们创建了一个Message类的实例,如下所示:

msg = Message("Subject", sender="sender@example.com", recipients=['recipient_1@example.com'])

如果您已经设置了MAIL_DEFAULT_SENDER配置,那么您就不需要在创建Message实例时显式地传递sender

msg = Message("Subject", recipients=['recipient@example.com'])

要设置邮件正文,请使用Message实例的body属性:

msg.body = "Mail body"

如果邮件正文是 HTML,则改为设置为html属性。

msg.body = "<p>Mail body</p>"

最后,我们可以通过将Message实例传递给Mail实例的send()方法来发送邮件:

mail.send(msg)

现在让我们通过命令行发送电子邮件来测试我们的配置。

发送测试邮件

打开终端并输入以下命令:

(env) overiq@vm:~/flask_app$ python main2.py shell
>>>
>>> from main2 import mail, Message
>>>
>>> msg = Message("Subject", recipients=["infooveriq@gmail.com"])
>>> msg.html = "<h2>Email Heading</h2>\n<p>Email Body</p>"
>>>
>>> mail.send(msg)
>>>

成功后,您会收到一封如下所示的电子邮件:

请注意,除非您禁用双因素身份验证并允许不太安全的应用访问您的帐户,否则通过 Gmail SMTP 服务器发送电子邮件将不起作用。

将电子邮件与我们的应用集成

从目前的情况来看,每当用户提交反馈时,它都会被保存到数据库中,用户会得到一条成功的消息,仅此而已,很无聊,你不觉得吗?理想情况下,应用应该将反馈通知管理员或版主。我们开始吧。打开main2.py修改contact()查看功能发送邮件如下(更改突出显示):

Flask _app/main2.py

#...
@app.route('/contact/', methods=['get', 'post'])
def contact():
    #...        
        db.session.commit()

        msg = Message("Feedback", recipients=[app.config['MAIL_USERNAME']])
        msg.body = "You have received a new feedback from {} <{}>.".format(name, email)
        mail.send(msg)

        print("\nData received. Now redirecting ...")
    #...

启动服务器,访问http://localhost:5000/contact/。填写表格并点击提交。成功后,你会收到这样一封邮件:

您可能已经注意到在点击提交按钮和后续请求的成功消息之间有很长的延迟。问题是mail.send()方法会阻止视图功能执行几秒钟。因此,在mail.send()方法返回之前,不会执行重定向页面的代码。我们可以使用线程来解决这个问题。

在此过程中,我们还将重构代码以发送电子邮件。现在,如果我们想在代码中的任何地方发送电子邮件,我们必须复制并粘贴完全相同的代码行。我们可以通过在函数中包装发送电子邮件的逻辑来节省一些代码行。

打开main2.py并在index路线前添加以下代码如下(更改突出显示):

Flask _app/main2.py

#...
from threading import Thread
#...
def shell_context():
    import os, sys
    return dict(app=app, os=os, sys=sys)

manager.add_command("shell", Shell(make_context=shell_context))

def async_send_mail(app, msg):
    with app.app_context():
        mail.send(msg)

def send_mail(subject, recipient, template, **kwargs):
    msg = Message(subject, sender=app.config['MAIL_DEFAULT_SENDER'], recipients=[recipient])
    msg.html = render_template(template, **kwargs)
    thr = Thread(target=async_send_mail, args=[app, msg])
    thr.start()
    return thr

@app.route('/')
def index():
    return render_template('index.html', name='Jerry')
#...

我们在这方面做了很多改进。send_mail()功能封装了发送邮件的逻辑。它接受主题、收件人和电子邮件模板。您还可以将任何附加参数作为关键字参数传递给它。为什么要附加参数?附加参数表示要传递给模板的数据。在第 17 行,我们正在渲染一个模板,并将其结果分配给msg.html属性。在第 18 行,我们正在创建一个Thread对象,传递函数的名称和参数函数必须用。下一行开始线程。线程启动时,调用async_send_mail()。有趣的部分来了。在我们的代码中,我们第一次在应用之外(即视图函数之外)的新线程中工作。with app.app_context():创建应用上下文,最后mail.send()发送电子邮件。

接下来,我们需要为反馈电子邮件创建一个模板。在templates目录中创建一个名为mail的目录。该目录将存储我们的电子邮件模板。在目录中创建一个名为feedback.html的模板,其代码如下:

模板/邮件/反馈. html

<p>You have received a new feedback from {{ name }} &lt;{{ email }}&gt; </p>

修改contact()视图功能,使用send_mail()功能,如下所示(更改突出显示):

Flask _app/main2.py

@app.route('/contact/', methods=['get', 'post'])
def contact():
        #...
        db.session.commit()

        send_mail("New Feedback", app.config['MAIL_DEFAULT_SENDER'], 'mail/feedback.html',
                  name=name, email=email)

        print("\nData received. Now redirecting ...")
        #...

更改后,再次访问http://localhost:5000/contact/。填写表格并点击提交。这一次你不会经历任何延迟。



Flask 中的认证

原文:https://overiq.com/flask-101/authentication-in-flask/

最后更新于 2020 年 7 月 27 日


身份验证是 web 应用最关键和最重要的方面之一。它防止未经授权的人远离网站的保护区。如果你对 cookies 有很好的理解,并且知道如何正确地散列密码,你可以推出你自己的认证系统。这可能是一个测试你技能的有趣的小项目。

正如你可能已经猜到的,已经有一个扩展让你的生活变得更容易。Flask-Login 是一个扩展,允许您轻松地将身份验证系统集成到 Flask 应用中。使用以下命令安装 Flask-Login 及其依赖项:

(env) overiq@vm:~/flask_app$ pip install flask-login

创建用户模型

目前,我们没有存储任何关于将成为我们网站的管理员/发布者的用户的数据。所以我们的第一个任务是创建一个User模型来存储用户数据。打开main2.py文件,在Employee模型下面添加User模型,如下所示:

Flask _app/main2.py

#..
class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(100))
    username = db.Column(db.String(50), nullable=False, unique=True)
    email = db.Column(db.String(100), nullable=False, unique=True)
    password_hash = db.Column(db.String(100), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)

    def __repr__(self):
        return "<{}:{}>".format(self.id, self.username)
#...

为了更新我们的数据库,我们需要创建一个新的迁移。在终端中,输入以下命令创建迁移脚本:

(env) overiq@vm:~/flask_app$ python main2.py db migrate -m "Adding users table"

使用upgrade命令运行迁移,如下所示:

(env) overiq@vm:~/flask_app$ python main2.py db upgrade
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade 6e059688f04e -> 0f0002bf91cc,
Adding users table

(env) overiq@vm:~/flask_app$

这将在数据库中创建users表。

散列密码

您绝不能将用户密码以纯文本形式存储在数据库中。万一一个恶意用户闯入你的数据库,他将能够读取与之相关的所有密码和电子邮件。众所周知,大多数人在多个网站上使用相同的密码,这意味着攻击者也可以访问用户的其他在线帐户。

我们将存储密码哈希,而不是直接将密码存储在数据库中。哈希只是一个看起来很随机的长字符串,如下所示:

pbkdf2:sha256:50000$Otfe3YgZ$4fc9f1d2de2b6beb0b888278f21a8c0777e8ff980016e043f3eacea9f48f6dea

使用单向散列函数创建散列。单向散列函数接受可变长度的输入,并返回固定长度的输出,我们称之为散列。使它安全的是,一旦我们有了一个散列,我们就不能得到生成它的原始字符串(因此有一种方法)。对于相同的输入,单向散列函数将总是返回相同的结果。

以下是使用密码哈希时涉及的工作流程:

当用户给你他们的密码时(在注册阶段),散列它,然后将散列保存到数据库。当用户登录时,根据输入的密码创建哈希,然后将其与存储在数据库中的哈希进行比较。如果匹配,请登录用户。否则,显示错误消息。

Flask 附带了一个名为 Werkzeug 的包,它为密码散列提供了以下两个助手函数。

方法 描述
generate_password_hash(password) 它接受密码并返回一个散列值。默认情况下,它使用 pbkdf2 单向函数来生成哈希。
check_password_hash(password_hash, password) 它接受密码哈希和纯文本密码,然后将password的哈希与password_hash进行比较。如果两者相同,则返回True,否则返回False

下面的 shell 会话显示了如何使用这些函数:

>>>
>>> from werkzeug.security import generate_password_hash, check_password_hash
>>>
>>> hash = generate_password_hash("secret password")
>>>
>>> hash
'pbkdf2:sha256:50000$zB51O5L3$8a43788bc902bca96e01a1eea95a650d9d5320753a2fbd16be
a984215cdf97ee'
>>>
>>> check_password_hash(hash, "secret password")
True
>>>
>>> check_password_hash(hash, "pass")
False
>>>
>>>

注意用正确的密码("secret password")调用check_password_hash()时,返回True,用错误的密码("pass")调用时,返回False

接下来,更新User模型以实现如下密码散列(更改突出显示):

Flask _app/main2.py

#...
from werkzeug.security import generate_password_hash, check_password_hash
#...

#...
class User(db.Model):
    #...
    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)

    def __repr__(self):
        return "<{}:{}>".format(self.id, self.username)

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)        
    #...

让我们创建一些用户并测试密码哈希。

(env) overiq@vm:~/flask_app$ python main2.py shell
>>>
>>> from main2 import db, User
>>>
>>> u1 = User(username='spike', email='spike@example.com')
>>> u1.set_password("spike")
>>>
>>> u2 = User(username='tyke', email='tyke@example.com')
>>> u2.set_password("tyke")
>>>
>>> db.session.add_all([u1, u2])
>>> db.session.commit()
>>>
>>> u1, u2
(<1:spike>, <2:tyke>)
>>>
>>>
>>> u1.check_password("pass")
False
>>> u1.check_password("spike")
True
>>>
>>> u2.check_password("foo")
False
>>> u2.check_password("tyke")
True
>>>
>>>

如输出所示,一切都如预期的那样工作,现在我们的数据库中有两个用户。

整合 Flask-登录

要初始化 Flask-Login 从flask_login包导入LoginManager类并创建LoginManager的新实例,如下所示(更改突出显示):

#...
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import LoginManager

app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'a really really really really long secret key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:pass@localhost/flask_app_db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = 'infooveriq@gmail.com'
app.config['MAIL_DEFAULT_SENDER'] = 'infooveriq@gmail.com'
app.config['MAIL_PASSWORD'] = 'password'

manager = Manager(app)
manager.add_command('db', MigrateCommand)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
mail = Mail(app)
login_manager = LoginManager(app)
#...

为了验证用户,Flask-Login 要求您在User类中实现一些特殊的方法。下表列出了所需的方法:

方法 描述
is_authenticated() 如果用户通过验证(即登录),则返回True。否则False
is_active() 如果账户未暂停,返回True。否则False
is_anonymous() 对于匿名用户(即未登录的用户),返回True。否则False
get_id() 返回User对象的唯一标识符。

Flask-Login 还通过UserMixin类提供了这些方法的默认实现。因此,我们可以直接从UserMixin类继承它们,而不是手动定义所有这些方法。打开main2.py并修改User模型标题,如下所示:

Flask _app/main2.py

#...
from flask_login import LoginManager, UserMixin

#...
class User(db.Model, UserMixin):
    __tablename__ = 'users'
#...

唯一剩下的就是增加一个user_loader回调。在User模型的正上方添加以下方法。

Flask _app/main2.py

#...
@login_manager.user_loader
def load_user(user_id):
    return db.session.query(User).get(user_id)
#...

每次服务器收到请求时,都会调用用user_loader装饰器装饰的函数。它从存储在会话 cookie 中的用户 id 加载用户。Flask-登录使加载的用户可以通过current_user代理访问。使用current_userflask_login包导入。它就像一个全局变量,可以在视图函数和模板中使用。在任何时候,current_user要么引用登录用户,要么引用匿名用户。我们可以使用current_useris_authenticated属性来区分两者。对于匿名用户is_authenticated属性返回False,否则返回True

限制访问视图

就目前情况来看,我们的网站没有任何管理区。在本课中,管理区域将由虚拟页面表示。为了防止未经授权的用户访问受保护的页面,Flask-Login 提供了一个名为login_required的装饰器。在main2.py中,在updating_session()视图功能的正下方添加以下代码:

Flask _app/main2.py

#...
from flask_login import LoginManager, UserMixin, login_required
#...
@app.route('/admin/')
@login_required
def admin():
    return render_template('admin.html')
#...

login_required装饰器确保admin()视图功能仅在用户登录时调用。默认情况下,当匿名用户(未登录的用户)试图访问受保护的视图时,将显示 HTTP 401 未授权页面。

启动服务器,如果还没有运行,访问http://localhost:5000/admin/。您将看到如下页面:

与其显示 401 个未经授权的错误,更好的方法是将用户重定向到登录页面。为此,将LoginManager实例的login_view属性设置为login()视图功能,如下所示(更改会突出显示):

Flask _app/main2.py

#...
migrate = Migrate(app, db)
mail = Mail(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'

class Faker(Command):
    'A command to add fake data to the tables'
#...

目前login()函数的定义如下(我们很快会更改):

Flask _app/main2.py

#...
@app.route('/login/', methods=['post', 'get'])
def login():
    message = ''
    if request.method == 'POST':
        print(request.form)
        username = request.form.get('username')
        password = request.form.get('password')

        if username == 'root' and password == 'pass':
            message = "Correct username and password"
        else:
            message = "Wrong username or password"

    return render_template('login.html', message=message)
#...

现在访问http://localhost:5000/admin/,您将被重定向到登录页面:

Flask-Login 在用户重定向到登录页面时也设置了 flash 消息,但是我们没有看到任何消息,因为登录模板(template/login.html)没有显示任何 flash 消息。打开login.html并在<form>标签前添加如下代码(更改突出显示):

flask _ app/templates/log in . html

#...
    {% endif %}

    {% for category, message in get_flashed_messages(with_categories=true) %}
        <spam class="{{ category }}">{{ message }}</spam>
    {% endfor %}

    <form action="" method="post">
#...

再次访问http://localhost:5000/admin/。这一次,您将在登录页面上看到如下 flash 消息:

要更改 flash 消息,只需将新消息分配给LoginManager实例的login_message属性。

在此期间,让我们创建admin()视图功能使用的模板。使用以下代码创建新的模板名称admin.html:

flask _ app/templates/admin . html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h2>Logged in User details</h2>

<ul>
    <li>Username: {{ current_user.username }}</li>
    <li>Email: {{ current_user.email }}</li>
    <li>Created on: {{ current_user.created_on }}</li>
    <li>Updated on: {{ current_user.updated_on }}</li>
</ul>

</body>
</html>

这里我们使用current_user变量来打印登录用户的详细信息。

创建登录表单

在我们登录之前,我们需要一个登录表单。登录表单将有三个字段:用户名、密码和记住我。打开forms.py并在ContactForm类的正下方添加LoginForm类,如下所示(更改突出显示):

Flask _app/forms.py

#...
from wtforms import StringField, SubmitField, TextAreaField, BooleanField, PasswordField
#...
#...
class LoginForm(FlaskForm):
    username = StringField("Username", validators=[DataRequired()])
    password = PasswordField("Password", validators=[DataRequired()])
    remember = BooleanField("Remember Me")
    submit = SubmitField()

登录用户

登录用户 Flask-登录提供login_user()功能。它接受用户对象登录。成功后,它返回True并建立会话。否则,返回False。默认情况下,login_user()建立的会话在浏览器关闭时到期。要让用户长时间保持登录状态,请在用户登录时通过remember=Truelogin_user()功能。打开main2.py并修改login()查看功能如下(更改突出显示):

Flask _app/main2.py

#...
from forms import ContactForm, LoginForm
#...
from flask_login import LoginManager, UserMixin, login_required, login_user, current_user

#...
@app.route('/login/', methods=['post', 'get'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = db.session.query(User).filter(User.username == form.username.data).first()
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember.data)
            return redirect(url_for('admin'))

        flash("Invalid username/password", 'error')
        return redirect(url_for('login'))
    return render_template('login.html', form=form)
#...

接下来,我们需要更新login.html来使用LoginForm()类。打开login.html并进行如下修改(修改突出显示):

flask _ app/templates/log in . html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>

    {% for category, message in get_flashed_messages(with_categories=true) %}
        <spam class="{{ category }}">{{ message }}</spam>
    {% endfor %}

    <form action="" method="post">
        {{ form.csrf_token }}
        <p>
            {{ form.username.label() }}
            {{ form.username() }}
            {% if form.username.errors %}
                {% for error in form.username.errors %}
                    {{ error }}
                {% endfor %}
            {% endif %}
        </p>
        <p>
            {{ form.password.label() }}
            {{ form.password() }}
            {% if form.password.errors %}
                {% for error in form.password.errors %}
                    {{ error }}
                {% endfor %}
            {% endif %}
        </p>
        <p>
            {{ form.remember.label() }}
            {{ form.remember() }}
        </p>
        <p>
            {{ form.submit() }}
        </p>
    </form>

</body>
</html>

我们现在可以登录了。访问http://localhost:5000/admin会被重定向到登录页面。

输入正确的用户名和密码,然后点击提交。您将被重定向到如下所示的管理页面:

如果您在登录时没有选中“记住我”复选框,浏览器一关闭,您就会被注销。否则,您将保持登录状态。

输入无效的用户名或密码后,您将被重定向到登录页面,并显示一条类似如下的提示消息:

注销用户

Flask-log log 的logout_user()功能通过删除存储在会话中的用户 id 来注销用户。在main2.py文件中,在login()视图功能下面添加以下代码:

Flask _app/main2.py

#...
from flask_login import LoginManager, UserMixin, login_required, login_user, current_user, logout_user
#...
@app.route('/logout/')
@login_required
def logout():
    logout_user()    
    flash("You have been logged out.")
    return redirect(url_for('login'))
#...

接下来,更新admin.html模板,使其包含到logout路线的链接,如下所示(更改突出显示):

flask _ app/templates/admin . html

#...
<ul>
    <li>Username: {{ current_user.username }}</li>
    <li>Email: {{ current_user.email }}</li>
    <li>Created on: {{ current_user.created_on }}</li>
    <li>Updated on: {{ current_user.updated_on }}</li>
</ul>

<p><a href="{{ url_for('logout') }}">Logout</a></p>

</body>
</html>

如果您现在访问http://localhost:5000/admin/(假设您已经登录),您将在页面底部看到一个注销链接。

要注销,请单击链接,您将被重定向到登录页面。

最后的接触

登录页面有一个小问题。现在,如果登录用户访问http://localhost:5000/log in/,他将再次看到登录页面。向已经登录的用户显示登录表单没有意义。要解决此问题,请在login()视图功能中进行以下更改。

Flask _app/main2.py

#...
@app.route('/login/', methods=['post', 'get'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('admin'))
    form = LoginForm()
    if form.validate_on_submit():
#...

进行这些更改后,如果登录用户访问登录页面,他将被重定向到管理页面。



Flask 中的应用结构和蓝图

原文:https://overiq.com/flask-101/application-structure-and-blueprint-in-flask/

最后更新于 2020 年 7 月 27 日


到目前为止,我们整个 Flask 应用主要驻留在一个文件main2.py中。这对小应用来说没问题,但是随着项目的增长,它变得很难管理。当我们将一个单一的文件分解成多个文件时,其中的代码变得更加可维护和可预测。

Flask 没有对您应该如何构建应用施加任何限制。但是,它确实提供了一些准则来使应用模块化。

在本课程中,我们将使用以下应用结构。

/app_dir
    /app
        __init__.py
        /static
        /templates
        views.py
    config.py
    runner.py

以下是每个文件和文件夹的详细内容:

文件 描述
app_dir app_dir是你的 Flask 项目的根目录。
app app目录是一个 Python 包,包含视图、模板和静态文件。
__init__.py __init__.py告诉 Pythonapp目录是 Python 包。
static static目录包含项目的静态文件。
templates templates目录包含模板。
views.py views.py包含路线和视图功能。
config.py config.py包含 Flask 应用的设置和配置。
runner.py Flask 应用的入口点。

在本课的剩余部分,我们将转换我们的项目以符合这个目录结构。我们将从创建config.py开始。

基于类的配置

软件项目通常在三种不同的环境中运行:

  1. 发展。
  2. 测试。
  3. 生产。

随着项目的发展,您将需要为不同的环境指定不同的配置选项。您还会注意到,无论您在哪个环境中,某些配置总是保持不变。我们可以使用类来实现这样的配置系统。

首先在基类中定义默认配置,然后创建从基类继承的特定于环境的类。特定于环境的类可以重写或添加特定于环境的配置。

flask_app目录下新建一个名为config.py的文件,并在其中添加以下代码:

Flask _app/config.py

import os

app_dir = os.path.abspath(os.path.dirname(__file__))

class BaseConfig:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'A SECRET KEY'
    SQLALCHEMY_TRACK_MODIFICATIONS = False

    ##### Flask-Mail configurations #####
    MAIL_SERVER = 'smtp.googlemail.com'
    MAIL_PORT = 587
    MAIL_USE_TLS = True
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'infooveriq@gmail.com'
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'password'
    MAIL_DEFAULT_SENDER = MAIL_USERNAME

class DevelopementConfig(BaseConfig):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEVELOPMENT_DATABASE_URI') or  \
        'mysql+pymysql://root:pass@localhost/flask_app_db'

class TestingConfig(BaseConfig):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TESTING_DATABASE_URI') or \
                              'mysql+pymysql://root:pass@localhost/flask_app_db'    

class ProductionConfig(BaseConfig):
    DEBUG = False
    SQLALCHEMY_DATABASE_URI = os.environ.get('PRODUCTION_DATABASE_URI') or  \
        'mysql+pymysql://root:pass@localhost/flask_app_db'

请注意,我们第一次从环境变量中读取一些配置的值。如果没有设置环境变量,我们也提供默认值。当您有一些不想在应用本身中硬编码的敏感数据时,这种方法特别有用。

要从类中读取配置,请使用如下from_object()方法:

app.config.from_object('config.Create')

创建应用包

flask_app目录内创建一个新目录app目录,并将所有文件和目录移动到这个目录(除了envmigrations目录以及我们新创建的config.py文件)。在app目录内用以下代码创建__init__.py文件:

Flask _app/app/init。py

from flask import Flask
from flask_migrate import Migrate, MigrateCommand
from flask_mail import Mail, Message
from flask_sqlalchemy import SQLAlchemy
from flask_script import Manager, Command, Shell
from flask_login import LoginManager
import os, config

# create application instance
app = Flask(__name__)
app.config.from_object(os.environ.get('FLASK_ENV') or 'config.DevelopementConfig')

# initializes extensions
db = SQLAlchemy(app)
mail = Mail(app)
migrate = Migrate(app, db)
login_manager = LoginManager(app)
login_manager.login_view = 'login'

# import views
from . import views
# from . import forum_views
# from . import admin_views

__init__.py创建应用实例并初始化扩展。如果没有设置FLASK_ENV环境变量,Flask 应用将在调试模式下运行(即app.debug = True)。要将应用置于生产模式,请将FLASK_ENV环境变量设置为config.ProductionConfig

初始化扩展后,第 21 行的import语句导入所有视图。这是将应用实例连接到视图函数所必需的,否则,Flask 将不会知道您的视图函数。

main2.py文件重命名为views.py并更新,使其只包含路线和视图功能。这是更新后的views.py文件的完整代码。

flask _ app/app/view . py

from app import app
from flask import render_template, request, redirect, url_for, flash, make_response, session
from flask_login import login_required, login_user,current_user, logout_user
from .models import User, Post, Category, Feedback, db
from .forms import ContactForm, LoginForm
from .utils import send_mail

@app.route('/')
def index():
    return render_template('index.html', name='Jerry')

@app.route('/user/<int:user_id>/')
def user_profile(user_id):
    return "Profile page of user #{}".format(user_id)

@app.route('/books/<genre>/')
def books(genre):
    return "All Books in {} category".format(genre)

@app.route('/login/', methods=['post', 'get'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('admin'))
    form = LoginForm()
    if form.validate_on_submit():
        user = db.session.query(User).filter(User.username == form.username.data).first()
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember.data)
            return redirect(url_for('admin'))

        flash("Invalid username/password", 'error')
        return redirect(url_for('login'))
    return render_template('login.html', form=form)

@app.route('/logout/')
@login_required
def logout():
    logout_user()
    flash("You have been logged out.")
    return redirect(url_for('login'))

@app.route('/contact/', methods=['get', 'post'])
def contact():
    form = ContactForm()
    if form.validate_on_submit():
        name = form.name.data
        email = form.email.data
        message = form.message.data

        # db logic goes here
        feedback = Feedback(name=name, email=email, message=message)
        db.session.add(feedback)
        db.session.commit()

        send_mail("New Feedback", app.config['MAIL_DEFAULT_SENDER'], 'mail/feedback.html',
                  name=name, email=email)

        flash("Message Received", "success")
        return redirect(url_for('contact'))

    return render_template('contact.html', form=form)

@app.route('/cookie/')
def cookie():
    if not request.cookies.get('foo'):
        res = make_response("Setting a cookie")
        res.set_cookie('foo', 'bar', max_age=60*60*24*365*2)
    else:
        res = make_response("Value of cookie foo is {}".format(request.cookies.get('foo')))
    return res

@app.route('/delete-cookie/')
def delete_cookie():
    res = make_response("Cookie Removed")
    res.set_cookie('foo', 'bar', max_age=0)
    return res

@app.route('/article', methods=['POST', 'GET'])
def article():
    if request.method == 'POST':
        res = make_response("")
        res.set_cookie("font", request.form.get('font'), 60*60*24*15)
        res.headers['location'] = url_for('article')
        return res, 302

    return render_template('article.html')

@app.route('/visits-counter/')
def visits():
    if 'visits' in session:
        session['visits'] = session.get('visits') + 1
    else:
        session['visits'] = 1
    return "Total visits: {}".format(session.get('visits'))

@app.route('/delete-visits/')
def delete_visits():
    session.pop('visits', None) # delete visits
    return 'Visits deleted'

@app.route('/session/')
def updating_session():
    res = str(session.items())

    cart_item = {'pineapples': '10', 'apples': '20', 'mangoes': '30'}
    if 'cart_item' in session:
        session['cart_item']['pineapples'] = '100'
        session.modified = True
    else:
        session['cart_item'] = cart_item

    return res

@app.route('/admin/')
@login_required
def admin():
    return render_template('admin.html')

views.py文件现在只包含视图功能。我们已经将模型、表单类和实用函数的代码移到了它们各自的文件中,如下所示:

Flask _app/app/models.py

from app import db, login_manager
from datetime import datetime
from flask_login import (LoginManager, UserMixin, login_required,
                          login_user, current_user, logout_user)
from werkzeug.security import generate_password_hash, check_password_hash

class Category(db.Model):
    __tablename__ = 'categories'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255), nullable=False, unique=True)
    slug = db.Column(db.String(255), nullable=False, unique=True)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    posts = db.relationship('Post', backref='category', cascade='all,delete-orphan')

    def __repr__(self):
        return "<{}:{}>".format(self.id, self.name)

post_tags = db.Table('post_tags',
    db.Column('post_id', db.Integer, db.ForeignKey('posts.id')),
    db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'))
)

class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer(), primary_key=True)
    title = db.Column(db.String(255), nullable=False)
    slug = db.Column(db.String(255), nullable=False)
    content = db.Column(db.Text(), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)
    category_id = db.Column(db.Integer(), db.ForeignKey('categories.id'))

    def __repr__(self):
        return "<{}:{}>".format(self.id, self.title[:10])

class Tag(db.Model):
    __tablename__ = 'tags'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    slug = db.Column(db.String(255), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    posts = db.relationship('Post', secondary=post_tags, backref='tags')

    def __repr__(self):
        return "<{}:{}>".format(self.id, self.name)

class Feedback(db.Model):
    __tablename__ = 'feedbacks'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(1000), nullable=False)
    email = db.Column(db.String(100), nullable=False)
    message = db.Column(db.Text(), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)

    def __repr__(self):
        return "<{}:{}>".format(self.id, self.name)

class Employee(db.Model):
    __tablename__ = 'employees'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    designation = db.Column(db.String(255), nullable=False)
    doj = db.Column(db.Date(), nullable=False)

@login_manager.user_loader
def load_user(user_id):
    return db.session.query(User).get(user_id)

class User(db.Model, UserMixin):
    __tablename__ = 'users'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(100))
    username = db.Column(db.String(50), nullable=False, unique=True)
    email = db.Column(db.String(100), nullable=False, unique=True)
    password_hash = db.Column(db.String(100), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)

    def __repr__(self):
        return "<{}:{}>".format(self.id, self.username)

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

Flask _app/app/forms.py

from flask_wtf import FlaskForm
from wtforms import Form, ValidationError
from wtforms import StringField, SubmitField, TextAreaField, BooleanField
from wtforms.validators import DataRequired, Email

class ContactForm(FlaskForm):
    name = StringField("Name: ", validators=[DataRequired()])
    email = StringField("Email: ", validators=[Email()])
    message = TextAreaField("Message", validators=[DataRequired()])
    submit = SubmitField()

class LoginForm(FlaskForm):
    username = StringField("Username", validators=[DataRequired()])
    password = StringField("Password", validators=[DataRequired()])
    remember = BooleanField("Remember Me")
    submit = SubmitField()

Flask _app/app/utils.py

from . import mail, db
from flask import render_template
from threading import Thread
from app import app
from flask_mail import Message

def async_send_mail(app, msg):
    with app.app_context():
        mail.send(msg)

def send_mail(subject, recipient, template, **kwargs):
    msg = Message(subject, sender=app.config['MAIL_DEFAULT_SENDER'], recipients=[recipient])
    msg.html = render_template(template, **kwargs)
    thrd = Thread(target=async_send_mail, args=[app, msg])
    thrd.start()
    return thrd

最后,要启动应用,请将以下代码添加到runner.py文件中:

Flask _app/runner.py

import os
from app import app, db
from app.models import User, Post, Tag, Category, Employee, Feedback
from flask_script import Manager, Shell
from flask_migrate import MigrateCommand

manager = Manager(app)

# these names will be available inside the shell without explicit import
def make_shell_context():
    return dict(app=app,  db=db, User=User, Post=Post, Tag=Tag, Category=Category,
                Employee=Employee, Feedback=Feedback)

manager.add_command('shell', Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

if __name__ == '__main__':
    manager.run()

runner.py是我们项目的切入点。文件从创建Manager()对象的实例开始。然后定义make_shell_context()函数。make_shell_context()函数返回的对象将在 Shell 内可用,无需显式导入语句。最后调用Manager实例上的run()方法启动服务器。

导入流

在本课中,我们已经创建了相当多的文件,很容易忘记哪个文件做什么以及文件执行的顺序。为了使事情变得清楚,本节解释并展示了一切是如何协同工作的。

事情从runner.py文件的执行开始。runner.py档第二行从app包导入appdb。当 Python 解释器遇到这一行时,程序控制转移到__init__.py开始执行。第 7 行,__init__.py导入config模块,将程序控制转到config.py。当config.py的执行完成时,程序控制再次回到__init__.py。在第 21 行中,__init__.py文件导入views模块,该模块将程序控制转移到views.pyviews.py第一行再次从app包导入应用实例app。应用实例app已经在内存中,不再导入。在第 4、5、6 行,views.py分别导入模型、表单和send_mail函数,依次将程序控制临时转移到各自的文件中。当views.py执行完毕,程序控制回到__init__.py。这就完成了__init__.py的执行。程序控制返回到runner.py并开始执行第 3 行的语句。

runner.py第三行导入models.py模块中定义的类。由于views.py已有车型,models.py文件将不再执行。

由于我们将runner.py作为主模块运行,第 17 行的条件评估为Truemanager.run()开始应用。

运行项目

我们现在准备运行我们的项目。在终端中,输入以下命令启动服务器。

(env) overiq@vm:~/flask_app$ python runner.py runserver
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 391-587-440
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

如果没有设置FLASK_ENV环境变量,前面的命令将在调试模式下启动应用。导航到 http://127.0.0.1:5000/ 您应该会看到当前的主页,如下所示:

浏览应用的其余页面,确保一切都按预期运行。

我们的应用现在非常灵活。它可以通过读取一个环境变量来获得一组完全不同的配置。例如,假设我们想要将我们的站点置于生产模式。为此,只需创建一个值为config.ProductionConfig的环境变量FLASK_ENV

在终端中,输入以下命令创建FLASK_ENV环境变量:

(env) overiq@vm:~/flask_app$ export FLASK_ENV=config.ProductionConfig

该命令在 Linux 和 Mac OS 中创建一个环境变量。窗口用户可以使用以下命令:

(env) C:\Users\overiq\flask_app>set FLASK_ENV=config.ProductionConfig

再次运行应用。

(env) overiq@vm:~/flask_app$ python runner.py runserver
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

现在,我们的应用正在生产模式下运行。此时,如果 Python 代码引发异常,您将看到 500 内部服务器错误,而不是堆栈跟踪。

因为还在开发中,不得不删除FLASK_ENV环境变量。一关闭终端FLASK_ENV就会自动删除。要手动删除它,请输入以下命令:

(env) overiq@vm:~/flask_app$ unset FLASK_ENV

窗口用户可以使用此命令:

(env) C:\Users\overiq\flask_app>set FLASK_ENV=

我们的项目进展顺利得多。现在,事情的组织方式比以前更加可预测。这里设计的技术对中小型项目很有用。然而,Flask 还有一些锦囊妙计可以帮助你变得更有效率。

蓝图

蓝图是组织应用的另一种方式。蓝图提供了视图级别的关注点分离。就像 Flask 应用一样,蓝图可以有自己的视图、静态文件和模板。我们也可以用他们自己的 URIs 绘制蓝图。例如,假设我们正在处理一个博客和它的管理面板。博客的蓝图将包含视图功能、模板和只针对博客的静态资产。而管理面板的蓝图将包含视图、静态文件和特定于管理面板的模板。蓝图可以通过使用模块或包来实现。

是时候给我们的项目添加一个蓝图了。

创建蓝图

flask_app/app目录内创建一个名为main的目录,并将views.pyforms.py移动到该目录。在main目录内用以下代码创建__init__.py文件:

Flask _app/app/main/init。py

from flask import Blueprint

main = Blueprint('main', __name__)

from . import views

我们正在使用Blueprint类创建蓝图对象。Blueprint()构造函数取两个参数,蓝图名称和蓝图所在包的名称;对于大多数应用来说,将__name__传递给它就足够了。

默认情况下,蓝图中的视图函数将分别在应用的templatesstatic目录中查找模板和静态资产。

我们可以通过在创建Blueprint对象时指定模板和静态资产的位置来进行更改,如下所示:

main = Blueprint('main', __name__
                template_folder='templates_dir')
                static_folder='static_dir')

在这种情况下,Flask 将在蓝图包内的templates_dirstatic_dir目录中寻找模板和静态资产。

蓝图添加的模板路径的优先级低于应用的模板目录。这意味着如果在templates_dirtemplates目录中有两个同名的模板,Flask 将使用templates目录中的模板。

关于蓝图,以下是一些值得注意的要点:

  1. 当使用蓝图时,路线是使用蓝图对象的route装饰器而不是应用实例(app)定义的。

  2. 要在使用蓝图时创建网址,您必须在端点前加上蓝图名称和一个点(.)。无论您是在 Python 代码中还是在模板中创建 URL,都是如此。例如:

    url_for("main.index")
    
    

    这将返回main蓝图的index路线的网址。

    蓝图的名称可以省略,以防您位于要为其创建 URL 的同一蓝图中。例如:

    url_for(".index")
    
    

    这将返回main蓝图的index路线的网址,假设您在main蓝图的视图功能或模板中。

为了适应这些变化,我们必须更新views.py文件中的import语句、url_for()呼叫和路由。以下是views.py文件的更新版本。

flask _ app/app/main/view . py

from app import app, db
from . import main
from flask import Flask, request, render_template, redirect, url_for, flash, make_response, session
from flask_login import login_required, login_user, current_user, logout_user
from app.models import User, Post, Category, Feedback, db
from .forms import ContactForm, LoginForm
from app.utils import send_mail

@main.route('/')
def index():
    return render_template('index.html', name='Jerry')

@main.route('/user/<int:user_id>/')
def user_profile(user_id):
    return "Profile page of user #{}".format(user_id)

@main.route('/books/<genre>/')
def books(genre):
    return "All Books in {} category".format(genre)

@main.route('/login/', methods=['post', 'get'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('.admin'))
    form = LoginForm()
    if form.validate_on_submit():
        user = db.session.query(User).filter(User.username == form.username.data).first()
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember.data)
            return redirect(url_for('.admin'))

        flash("Invalid username/password", 'error')
        return redirect(url_for('.login'))
    return render_template('login.html', form=form)

@main.route('/logout/')
@login_required
def logout():
    logout_user()    
    flash("You have been logged out.")
    return redirect(url_for('.login'))

@main.route('/contact/', methods=['get', 'post'])
def contact():
    form = ContactForm()
    if form.validate_on_submit():
        name = form.name.data
        email = form.email.data
        message = form.message.data
        print(name)
        print(email)
        print(message)

        # db logic goes here
        feedback = Feedback(name=name, email=email, message=message)
        db.session.add(feedback)
        db.session.commit()

        send_mail("New Feedback", app.config['MAIL_DEFAULT_SENDER'], 'mail/feedback.html',
                  name=name, email=email)

        print("\nData received. Now redirecting ...")
        flash("Message Received", "success")
        return redirect(url_for('.contact'))

    return render_template('contact.html', form=form)

@main.route('/cookie/')
def cookie():
    if not request.cookies.get('foo'):
        res = make_response("Setting a cookie")
        res.set_cookie('foo', 'bar', max_age=60*60*24*365*2)
    else:
        res = make_response("Value of cookie foo is {}".format(request.cookies.get('foo')))
    return res

@main.route('/delete-cookie/')
def delete_cookie():
    res = make_response("Cookie Removed")
    res.set_cookie('foo', 'bar', max_age=0)
    return res

@main.route('/article/', methods=['POST', 'GET'])
def article():
    if request.method == 'POST':
        print(request.form)
        res = make_response("")
        res.set_cookie("font", request.form.get('font'), 60*60*24*15)
        res.headers['location'] = url_for('.article')
        return res, 302

    return render_template('article.html')

@main.route('/visits-counter/')
def visits():
    if 'visits' in session:
        session['visits'] = session.get('visits') + 1  # reading and updating session data
    else:
        session['visits'] = 1 # setting session data
    return "Total visits: {}".format(session.get('visits'))

@main.route('/delete-visits/')
def delete_visits():
    session.pop('visits', None) # delete visits
    return 'Visits deleted'

@main.route('/session/')
def updating_session():
    res = str(session.items())

    cart_item = {'pineapples': '10', 'apples': '20', 'mangoes': '30'}
    if 'cart_item' in session:
        session['cart_item']['pineapples'] = '100'
        session.modified = True
    else:
        session['cart_item'] = cart_item

    return res

@main.route('/admin/')
@login_required
def admin():
    return render_template('admin.html')

请注意,在整个views.py文件中,我们在创建网址时没有指定蓝图名称,因为我们在为其创建网址的同一个蓝图中。

同样更新admin.html中的url_for()调用,如下所示:

flask _ app/app/templates/admin . html

#...
<p><a href="{{ url_for('.logout') }}">Logout</a></p>
#...

views.py中的视图功能现在与main蓝图相关联。接下来,我们必须在 Flask 应用上注册蓝图。打开app/__init__.py,修改如下:(修改突出显示):

Flask _app/app/init。py

#...
# create application instance
app = Flask(__name__)
app.config.from_object(os.environ.get('FLASK_ENV') or 'config.DevelopementConfig')

# initializes extensions
db = SQLAlchemy(app)
mail = Mail(app)
migrate = Migrate(app, db)
login_manager = LoginManager(app)
login_manager.login_view = 'main.login'

# register blueprints
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)

#from .admin import main as admin_blueprint
#app.register_blueprint(admin_blueprint)

应用实例的register_blueprint()方法用于注册蓝图。我们可以通过为每个蓝图调用register_blueprint()来注册多个蓝图。请注意,在第 11 行,我们将main.login分配给login_manager.login_view。在这种情况下,有必要指定蓝图名称,否则 Flask 将无法判断您指的是哪个蓝图。

此时,应用结构应该如下所示:

├── flask_app/
├── app/
│   ├── __init__.py
│   ├── main/
│   │   ├── forms.py
│   │   ├── __init__.py
│   │   └── views.py
│   ├── models.py
│   ├── static/
│   │   └── style.css
│   ├── templates/
│   │   ├── admin.html
│   │   ├── article.html
│   │   ├── contact.html
│   │   ├── index.html
│   │   ├── login.html
│   │   └── mail/
│   │       └── feedback.html
│   └── utils.py
├── migrations/
│   ├── alembic.ini
│   ├── env.py
│   ├── README
│   ├── script.py.mako
│   └── versions/
│       ├── 0f0002bf91cc_adding_users_table.py
│       ├── 6e059688f04e_adding_employees_table.py
├── runner.py
├── config.py
├── env/

应用工厂

我们已经在应用中使用了包和蓝图。我们可以通过将实例化应用实例的任务委托给应用工厂来进一步改进我们的应用。应用工厂只是一个创建对象的函数。

那么我们通过这样做能得到什么:

  1. 它使测试更容易,因为我们可以用不同的设置创建应用实例。
  2. 我们可以在同一个过程中运行同一个应用的多个实例。当负载平衡器在不同服务器之间分配流量时,这很方便。

让我们更新app/__init__.py来实现一个应用工厂,如下所示(更改被突出显示):

Flask _app/app/init。py

from flask import Flask
from flask_migrate import Migrate, MigrateCommand
from flask_mail import Mail, Message
from flask_sqlalchemy import SQLAlchemy
from flask_script import Manager, Command, Shell
from flask_login import LoginManager
import os, config

db = SQLAlchemy()
mail = Mail()
migrate = Migrate()
login_manager = LoginManager()
login_manager.login_view = 'main.login'

# application factory
def create_app(config):

    # create application instance
    app = Flask(__name__)
    app.config.from_object(config)

    db.init_app(app)
    mail.init_app(app)
    migrate.init_app(app, db)
    login_manager.init_app(app)

    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    #from .admin import main as admin_blueprint
    #app.register_blueprint(admin_blueprint)

    return app

我们已经将创建应用实例的任务委托给了create_app()功能。create_app()函数接受一个名为config的参数,并返回一个应用实例。

应用工厂将扩展的实例化与其配置分开。实例化发生在调用create_app()之前,配置发生在使用init_app()方法的create_app()函数内部。

接下来,更新runner.py以使用应用工厂,如下所示:

Flask _app/runner.py

import os
from app import db, create_app
from app.models import User, Post, Tag, Category, Employee, Feedback
from flask_script import Manager, Shell
from flask_migrate import MigrateCommand

app = create_app(os.getenv('FLASK_ENV') or 'config.DevelopementConfig')
manager = Manager(app)

def make_shell_context():
    return dict(app=app,  db=db, User=User, Post=Post, Tag=Tag, Category=Category,
                Employee=Employee, Feedback=Feedback)

manager.add_command('shell', Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

if __name__ == '__main__':
    manager.run()

需要注意的是,当使用应用工厂时,我们在导入时不再能够访问蓝图中的应用实例。要访问蓝图中的应用,请使用flask包中的current_app代理。让我们更新我们的项目以使用current_app变量,如下所示:

flask _ app/app/main/view . py

from app import db
from . import main
from flask import (render_template, request, redirect, url_for, flash,
                   make_response, session, current_app)
from flask_login import login_required, login_user, current_user, logout_user
from app.models import User, Feedback
from app.utils import send_mail
from .forms import ContactForm, LoginForm

@main.route('/')
def index():
    return render_template('index.html', name='Jerry')

@main.route('/user/<int:user_id>/')
def user_profile(user_id):
    return "Profile page of user #{}".format(user_id)

@main.route('/books/<genre>/')
def books(genre):
    return "All Books in {} category".format(genre)

@main.route('/login/', methods=['post', 'get'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('.admin'))
    form = LoginForm()
    if form.validate_on_submit():
        user = db.session.query(User).filter(User.username == form.username.data).first()
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember.data)
            return redirect(url_for('.admin'))

        flash("Invalid username/password", 'error')
        return redirect(url_for('.login'))
    return render_template('login.html', form=form)

@main.route('/logout/')
@login_required
def logout():
    logout_user()
    flash("You have been logged out.")
    return redirect(url_for('.login'))

@main.route('/contact/', methods=['get', 'post'])
def contact():
    form = ContactForm()
    if form.validate_on_submit():
        name = form.name.data
        email = form.email.data
        message = form.message.data

        # db logic goes here
        feedback = Feedback(name=name, email=email, message=message)
        db.session.add(feedback)
        db.session.commit()

        send_mail("New Feedback", current_app.config['MAIL_DEFAULT_SENDER'], 'mail/feedback.html',
                  name=name, email=email)

        flash("Message Received", "success")
        return redirect(url_for('.contact'))

    return render_template('contact.html', form=form)

@main.route('/cookie/')
def cookie():
    if not request.cookies.get('foo'):
        res = make_response("Setting a cookie")
        res.set_cookie('foo', 'bar', max_age=60*60*24*365*2)
    else:
        res = make_response("Value of cookie foo is {}".format(request.cookies.get('foo')))
    return res

@main.route('/delete-cookie/')
def delete_cookie():
    res = make_response("Cookie Removed")
    res.set_cookie('foo', 'bar', max_age=0)
    return res

@main.route('/article', methods=['POST', 'GET'])
def article():
    if request.method == 'POST':
        res = make_response("")
        res.set_cookie("font", request.form.get('font'), 60*60*24*15)
        res.headers['location'] = url_for('.article')
        return res, 302

    return render_template('article.html')

@main.route('/visits-counter/')
def visits():
    if 'visits' in session:
        session['visits'] = session.get('visits') + 1
    else:
        session['visits'] = 1
    return "Total visits: {}".format(session.get('visits'))

@main.route('/delete-visits/')
def delete_visits():
    session.pop('visits', None) # delete visits
    return 'Visits deleted'

@main.route('/session/')
def updating_session():
    res = str(session.items())

    cart_item = {'pineapples': '10', 'apples': '20', 'mangoes': '30'}
    if 'cart_item' in session:
        session['cart_item']['pineapples'] = '100'
        session.modified = True
    else:
        session['cart_item'] = cart_item

    return res

@main.route('/admin/')
@login_required
def admin():
    return render_template('admin.html')

Flask _app/app/utils.py

from . import mail, db
from flask import render_template, current_app
from threading import Thread
from flask_mail import Message

def async_send_mail(app, msg):
    with app.app_context():
        mail.send(msg)

def send_mail(subject, recipient, template, **kwargs):
    msg = Message(subject, sender=current_app.config['MAIL_DEFAULT_SENDER'], recipients=[recipient])
    msg.html = render_template(template, **kwargs)
    thr = Thread(target=async_send_mail, args=[current_app._get_current_object(), msg])
    thr.start()
    return thr

现在,您应该对 Flask、它的不同组件以及所有组件如何配合有了一个坚实的了解。信不信由你,我们已经探索了很多。在本课程的下一部分,我们将使用我们所学的知识来创建一个名为 Flask-Marks 的美味克隆。美味是一个社交书签网站,于 2003 年推出。2017 年,它被 Pinboard 收购,此后一直以只读模式运行。翻开这一页,让我们开始吧。



posted @ 2024-11-02 15:51  绝不原创的飞龙  阅读(18)  评论(0编辑  收藏  举报