# Django Python Web应用程序框架简介

## 关于Django

Django称自己为“鼓励快速开发和简洁实用的设计的高级Python Web框架。它由经验丰富的开发人员构建，它解决了Web开发的许多麻烦，因此您可以专注于编写应用程序而无需重新发明轮子。” 他们真的是真的！ 这个庞大的Web框架附带了如此多的电池，因此在开发过程中经常会迷惑到一切如何协同工作。

## Django启动和配置



$mkdir django_todo$ cd django_todo

$pipenv install --python 3.6$ pipenv shell

(django-someHash) $pipenv install django djangorestframework   作为参考，我们正在使用django-2.0.7djangorestframework-3.8.2 与Flask，Tornado和Pyramid不同，我们不需要编写自己的setup.py文件。 我们不会制作可安装的Python发行版。 与许多事情一样，Django会以自己的Django方式为我们解决这一问题。 我们仍然需要requirements.txt文件来跟踪所有必要的安装，以便在其他地方进行部署。 但是，就我们的Django项目中的定位模块而言，Django将让我们列出我们要访问的子目录，然后允许我们从这些目录中进行导入，就好像它们是已安装的软件包一样。 首先，我们必须创建一个Django项目。 安装Django时，还安装了命令行脚本django-admin 。 它的工作是管理所有与Django相关的各种命令，这些命令有助于将我们的项目放在一起并在我们继续开发时对其进行维护。 django-admin无需让我们从头开始构建整个Django生态系统，而是可以让我们开始使用标准Django项目所需的所有绝对必要的文件（以及更多）。 调用django-admin的start-project命令的语法为django-admin startproject <project name> <directory where we want the files> 。 我们希望文件存在于当前工作目录中，因此：  (django-someHash)$ django-admin startproject django_todo . 




(django-someHash) $ls manage.py django_todo   manage.py是一个命令行可执行的Python文件，最终仅是django-admin的包装。 因此，它的工作是相同的：帮助我们管理项目。 因此，名称为manage.py 它创建的目录django_todo内的django_todo表示项目的配置根目录 。 现在让我们深入研究。 ## 配置Django 通过将django_todo目录称为“配置根目录”，我们的意思是该目录包含通常配置Django项目所需的文件。 该目录之外的几乎所有内容都将仅专注于与项目的模型，视图，路线等相关的“业务逻辑”。将项目连接在一起的所有点都将在此处引导。 django_todo调用ls django_todo显示四个文件：  (django-someHash)$ cd django_todo

(django-someHash) $ls __init__.py settings.py urls.py wsgi.py   • __init__.py为空，仅存在即可将这个目录转换为可导入的Python包。 • settings.pysettings.py大多数配置项的位置，例如项目是否处于DEBUG模式，正在使用哪些数据库，Django应该在哪里查找文件等。它是配置根目录的“主要配置”部分，我们会立即进行深入探讨。 • 顾名思义， urls.py是设置URL的位置。 尽管我们不必在该文件中显式写入项目的每个 URL，但确实需要使该文件知道已声明URL的其他任何位置。 如果此文件未指向其他URL，则这些URL不存在。 期。 • wsgi.py用于在生产中服务该应用程序。 就像Pyramid，Tornado和Flask如何公开某些“应用”对象一样，该对象是要提供的已配置应用程序，Django也必须公开一个。 在这里完成。 然后，可使用类似服务的Gunicorn服务员 ，或uWSGI ### 设定设定 看看settings.py会发现它的大小，这些只是默认值！ 这甚至不包括数据库钩子，静态文件，媒体文件，任何云集成或可以配置Django项目的其他数十种方式中的任何一种。 让我们从上到下看，我们得到了什么： • BASE_DIR设置基本目录或manage.py所在目录的绝对路径。 这对于查找文件很有用。 • SECRET_KEY是Django项目中用于加密签名的密钥。 实际上，它用于会话，cookie，CSRF保护和身份验证令牌之类的东西。 尽快，最好在第一次提交之前，应更改SECRET_KEY的值并将其移入环境变量。 • DEBUG告诉Django是在开发模式下还是在生产模式下运行项目。 这是一个极其关键的区别。 • 在开发模式下，当错误弹出时，Django将显示导致错误的完整堆栈跟踪以及运行项目所涉及的所有设置和配置。 如果在生产环境中将DEBUG设置为True ，则这可能是一个严重的安全问题。 • 在生产中，当出现问题时，Django会显示一个简单的错误页面。 除了错误代码外，没有给出任何信息。 • 保护项目的一种简单方法是将DEBUG设置为一个环境变量，例如bool(os.environ.get('DEBUG', '')) • ALLOWED_HOSTS是从其提供应用程序的主机名的文字列表。 在开发中，该字段可以为空，但在生产中， 如果为该项目提供服务的主机不在ALLOWED_HOSTS列表中，则我们的Django项目将无法运行 。 环境变量框的另一件事。 • INSTALLED_APPS是我们的Django项目可以访问的Django“应用”的列表（将其视为子目录；稍后将进行详细介绍）。 默认情况下，我们会提供一些… • 内置的Django管理网站 • Django的内置身份验证系统 • Django的“一刀切”数据管理器 • 会话管理 • Cookie和基于会话的消息传递 • 网站固有的静态文件的使用，例如css文件， js文件，属于我们网站设计的任何图像等。 • 听起来像MIDDLEWARE ：可以帮助Django项目运行的中间件。 尽管我们可以根据需要添加其他类型的安全性，但是其中大部分用于处理各种类型的安全性。 • ROOT_URLCONF设置基本级别URL配置文件的导入路径。 我们之前看到的urls.py ？ 默认情况下，Django指向该文件以收集我们的所有URL。 如果我们希望Django在其他地方查找，我们将在此处将导入路径设置为该位置。 • 如果我们依靠Django来构建HTML，则TEMPLATES是Django将用于我们网站前端的模板引擎列表。 由于我们不是，所以无关紧要。 • WSGI_APPLICATION设置WSGI应用程序的导入路径，即在生产环境中提供的东西。 默认情况下，它指向wsgi.pyapplication对象。 这很少（如果有的话）需要修改。 • DATABASES设置我们的Django项目将访问哪些数据库。 必须设置default数据库。 只要提供HOSTUSERPASSWORDPORT ，数据库NAME和适当的ENGINE ，我们就可以按名称设置其他人。 可以想象，这些都是敏感的信息，因此最好将它们隐藏在环境变量中。 查看Django文档了解更多详细信息。 • 注意：如果要提供完整的数据库URL，而不是提供数据库位置的各个部分，请签出dj_database_url • AUTH_PASSWORD_VALIDATORS是运行以检查输入密码的功能列表。 默认情况下，我们会提供一些密码，但是如果还有其他更复杂的验证需求，则不仅仅是检查密码是否与用户属性匹配，密码是否超过最小长度，密码是否为最常用的1000种密码之一，或者密码完全是数字-我们可以在此处列出。 • LANGUAGE_CODE将设置网站的语言。 默认情况下为美国英语，但我们可以将其切换为其他语言。 • TIME_ZONE是Django项目中所有自动生成的时间戳的时区。 我不能强调我们坚持UTC并在其他地方执行任何特定于时区的处理，而不是尝试重新配置此设置有多么重要 。 正如本文所述，UTC是所有时区中的共同点，因为没有任何偏移可担心。 如果偏移量非常重要，我们可以根据需要计算，并与UTC进行适当的偏移量。 • USE_I18N将允许Django使用其自己的翻译服务来翻译前端的字符串。 I18N =国际化（“ i”和“ n”之间的18个字符） • 如果设置为True ，则USE_L10N （L10N =本地化[“ l”和“ n”之间的10个字符]）将使用数据的通用本地格式。 日期是一个很好的例子：在美国，日期是MM-DD-YYYY。 在欧洲，日期通常写成DD-MM-YYYY • STATIC_URL是用于提供静态文件的大量设置的一部分。 我们将构建一个REST API，因此我们无需担心静态文件。 通常，这将为每个静态文件在域名之后设置根路径。 因此，如果我们要投放徽标图片，则该图片为http://<domainname>/<STATIC_URL>/logo.gif 这些设置在默认情况下几乎可以使用。 我们必须更改的一件事是DATABASES设置。 首先，我们创建将与之一起使用的数据库：  (django-someHash)$ createdb django_todo 


• NAME是我们刚刚创建的数据库的名称。
• USER是个人的Postgres数据库用户名
• PASSWORD是访问数据库所需的密码
• HOST是数据库的主机。 随着我们在本地开发，本地localhost127.0.0.1将可以正常工作。
• PORT是我们为Postgres开放的任何PORT； 通常是5432

settings.py希望我们为每个键提供字符串值。 但是，这是高度敏感的信息。 这对任何负责任的开发人员都行不通。 有几种方法可以解决此问题，但是我们仅设置环境变量。



DATABASES
=
{

'default' :
{

'ENGINE' :
'django.db.backends.postgresql'
,

'NAME' :
os .
environ .
get
(
'DB_NAME'
,
''
)
,

'USER' :
os .
environ .
get
(
'DB_USER'
,
''
)
,

os .
environ .
get
(
'DB_PASS'
,
''
)
,

'HOST' :
os .
environ .
get
(
'DB_HOST'
,
''
)
,

'PORT' :
os .
environ .
get
(
'DB_PORT'
,
''
)
,

}

}




## Django路线和视图



INSTALLED_APPS
=
[

,

'django.contrib.auth'
,

'django.contrib.contenttypes'
,

'django.contrib.sessions'
,

'django.contrib.messages'
,

'django.contrib.staticfiles'
,

'rest_framework'

]






# in django_todo/views.py

from rest_framework.
response
import JsonResponse

from rest_framework.
views
import APIView

class HelloWorld
( APIView
) :

def get
(
self
, request
, format
=
None
) :

"""Print 'Hello, world!' as the response body."""

return JsonResponse
(
"Hello, world!"
)




• 设置基于HTTP方法引导流量所需的方法（例如GET，POST，PUT，DELETE）
• 使用解析和处理任何传入请求所需的所有数据和属性填充request对象
• 接受ResponseJsonResponse ，使每个调度方法（即名为getpostputdelete ）返回并构造一个格式正确的HTTP响应。

• 所需的路线，以字符串形式（不带斜杠）
• 可以处理该路由的视图函数（只有一个函数！）
• Django项目中路线的名称



# django_todo/urls.py, after the big doc string

from django.
urls
import path

from django_todo.
views
import HelloWorld

urlpatterns
=
[

path
(
''
, HelloWorld.
as_view
(
)
, name
=
"hello"
)
,

]




HelloWorld视图是从我们刚刚创建的views.py文件导入的。 为了执行此导入，我们需要更新settings.py以将django_todo包括在INSTALLED_APPS列表中。 是的，这有点奇怪。 这是一种思考方式。

INSTALLED_APPS引用Django视为可导入的目录或软件包的列表。 这是Django处理项目中各个组件（如已安装的软件包）的方式，而无需通过setup.py 。 我们希望将django_todo目录视为可导入的软件包，因此我们将该目录包含在INSTALLED_APPS 。 现在，该目录中的任何模块都可以导入。 因此，我们得到了我们的看法。

path函数将仅将视图函数用作第二个参数，而不仅仅是基于类的视图。 幸运的是，所有有效的基于Django类的视图都包含此.as_view()方法。 它的工作是将基于类的视图的所有优点汇总到一个视图函数中并返回该视图函数。 因此，我们不必担心进行该翻译。 相反，我们只需要考虑业务逻辑，就可以让Django和Django REST Framework处理其余的事情。

Django随附了自己的本地开发服务器，可通过manage.py访问。 让我们导航到包含manage.py的目录并输入：



(django-someHash) $./manage.py runserver Performing system checks... System check identified no issues (0 silenced). August 01, 2018 - 16:47:24 Django version 2.0.7, using settings 'django_todo.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.   执行runserver ，Django会进行检查以确保（或多或少）正确地将项目连接在一起。 它不是万无一失的，但是确实存在一些明显的问题。 它还会通知我们数据库是否与代码不同步。 毫无疑问，这是因为我们尚未将应用程序的任何内容提交给数据库，但是现在还可以。 让我们访问http://127.0.0.1:8000以查看HelloWorld视图的输出。 嗯 那不是我们在Pyramid，Flask和Tornado中看到的明文数据。 当使用Django REST Framework时，HTTP响应（在浏览器中查看）是这种呈现HTML，以红色显示我们的实际JSON响应。 但是不要烦恼！ 如果我们做一个快速的curl看着http://127.0.0.1:8000在命令行中，我们没有得到任何花哨HTML。 只是内容。  # Note: try this in a different terminal window, outside of the virtual environment above$ curl http://127.0.0.1:8000

"Hello, world!"




Django REST Framework希望我们在使用浏览器时具有人性化的界面。 这很有道理； 如果在浏览器中查看了JSON，则通常是因为人们在设计API使用者时想要检查它的外观是否正确或了解JSON响应的外观。 这很像您从Postman之类的服务中获得的东西。

1. 使用django-admin startproject <project name>启动了项目
2. 更新了django_todo/settings.py以将环境变量用于DEBUGSECRET_KEYDATABASES dict中的值
3. 安装了Django REST Framework ，并将其添加到INSTALLED_APPS列表中
4. 创建了django_todo/views.py以包含我们的第一个视图类，向世界问好
5. 更新了django_todo/urls.py其中包含我们新的本地路线的路径
6. 更新了django_todo/settings.py INSTALLED_APPS以包括django_todo软件包

## 建立模型

Django项目的整个基础架构都是围绕数据模型构建的。 编写该文档是为了使每个数据模型都可以拥有自己的小世界，自己的视图，自己的与资源相关的URL集合，甚至是自己的测试（如果我们愿意的话）。

Django方式涉及创建所谓的Django“应用”。 Django“应用程序”本身并不是独立的应用程序。 他们没有自己的设置，但没有（尽管可以）。 但是，它们可以拥有人们可能想到的独立应用程序中的所有其他内容：

• 一组独立的URL
• 一组独立HTML模板（如果我们要提供HTML）
• 一个或多个数据模型
• 一组独立视图
• 整套独立测试

 (django-someHash) $./manage.py startapp todo   startapp命令将以静默方式成功执行。 我们可以使用ls来检查它是否完成了应该做的事情。  (django-someHash)$ ls

Pipfile      Pipfile.lock django_todo  manage.py    todo






(django-someHash) $ls todo __init__.py admin.py apps.py migrations models.py tests.py views.py   以下是manage.py startapp创建的文件： • __init__.py为空； 它存在，因此该目录可以被视为模型，视图等的有效导入路径。 • admin.py不是很空； 它用于在Django管理员中格式化此应用程序的模型，在本文中我们不再赘述。 • apps.py …在这里也没有太多工作要做； 它有助于格式化Django管理员的模型。 • migrations是一个目录，其中包含我们数据模型的快照； 它用于更新我们的数据库。 这是内置的数据库管理附带的少数框架之一，其中一部分是使我们能够更新数据库，而不必拆除数据库并进行重建以更改架构。 • models.py是数据模型所在的位置。 • tests.py是进行测试的地方（如果我们编写了测试的话）。 • views.py适用于我们编写的与此应用程序中的模型有关的视图。 他们不必在这里写。 例如，我们可以将所有视图都写在django_todo/views.py 。 但是，它在这里，所以更容易区分我们的关注点。 这对于涵盖许多概念空间的庞大应用程序变得更加重要。 还没有为我们创建此应用程序的urls.py文件。 我们可以自己做。  (django-someHash)$ touch todo/urls.py 




# in settings.py

INSTALLED_APPS
=
[

,

'django.contrib.auth'
,

'django.contrib.contenttypes'
,

'django.contrib.sessions'
,

'django.contrib.messages'
,

'django.contrib.staticfiles'
,

'rest_framework'
,

'django_todo'
,

'todo'
# <--- the line was added

]






# todo/models.py

from django.
db
import models

( models.
Model
) :

"""Tasks for the To Do list."""

name
= models.
CharField
( max_length
=
256
)

note
= models.
TextField
( blank
=
True
, null
=
True
)

creation_date
= models.
DateTimeField
=
True
)

due_date
= models.
DateTimeField
( blank
=
True
, null
=
True
)

completed
= models.
BooleanField
( default
=
False
)




Unicode字段成为models.CharFieldmodels.TextFieldCharField用于特定最大长度的小型文本字段，而TextField用于任意数量的文本。

TextField应该可以为空，我们以两种方式指定它。 blank=True表示，当构造此模型的实例，并且正在验证附加到此字段的数据时，可以将该数据为空。 这不同于null=True ，后者表示在构造此模型类的表时，与note对应的列将允许空白或NULL条目。 因此，总而言之， blank=True控制如何将数据添加到模型实例，而null=True控制如何首先构建保存该数据的数据库表。

DateTime字段变得很强大，可以为我们做一些工作，而不必为类修改__init__方法。 对于creation_date字段，我们指定auto_now_add=True 。 从实际意义上讲，这意味着当创建新的模型实例时， Django会自动现在的日期和时间记录为该字段的值。 方便！

auto_now_add及其近亲auto_now设置为TrueDateTimeField将期望像其他任何字段一样的数据。 它需要输入正确的datetime对象才能生效。 due_date列的blanknull都设置为True因此待办事项列表上的项目可以只是将来要在某个时间完成的项目，没有定义日期或时间。

BooleanField只是一个可以采用以下两个值之一的字段： TrueFalse 。 在这里，默认值设置为False

### 管理数据库



(django-someHash) $./manage.py makemigrations Migrations for 'todo': todo/migrations/0001_initial.py - Create model Task   这将查看INSTALLED_APPS列出的每个应用，并检查这些应用中是否存在模型。 然后，它将检查相应的migrations目录中是否包含迁移文件，并将它们与每个INSTALLED_APPS应用程序中的模型进行比较。 如果模型已升级到超出最新迁移要求的范围，则将创建一个继承自最新模型的新迁移文件。 它会被自动命名，并显示一条消息，说明自上次迁移以来发生了什么变化。 如果自从您上一次从事Django项目以来已经有一段时间了，并且不记得您的模型是否与迁移同步，那么您就不必担心。 makemigrations是一个幂等的操作； 无论您运行一次或20次makemigrations您的migrations目录都只有一个当前模型配置的副本。 甚至比这更好的是，当我们运行./manage.py runserver ，Django将检测到我们的模型与我们的迁移不同步，并且会以彩色文本告诉我们，以便我们做出适当的选择。 下一点是使每个人至少绊倒一次的事情： 创建迁移文件不会立即影响我们的数据库 。 运行makemigrations ，我们准备了Django项目，以定义如何创建给定表并最终查找。 将这些更改应用到数据库仍由我们决定。 这就是migrate命令的作用。  (django-someHash)$ ./manage.py migrate

Operations to perform:

Apply all migrations: admin, auth, contenttypes, sessions, todo

Running migrations:

Applying contenttypes.0001_initial... OK

Applying auth.0001_initial... OK

Applying contenttypes.0002_remove_content_type_name... OK

Applying auth.0002_alter_permission_name_max_length... OK

Applying auth.0003_alter_user_email_max_length... OK

Applying auth.0006_require_contenttypes_0002... OK

Applying auth.0009_alter_user_last_name_max_length... OK

Applying sessions.0001_initial... OK

Applying todo.0001_initial... OK




• 继承自Django的User对象，通过添加token字段制作我们自己的对象以对其进行扩展
• 创建一个与Django的User对象存在一对一关系的新对象，其唯一目的是持有令牌

 (django-someHash) $./manage.py startapp owner   不要忘记将其添加到settings.py中的INSTALLED_APPS列表中。  INSTALLED_APPS = [ 'django.contrib.admin' , 'django.contrib.auth' , 'django.contrib.contenttypes' , 'django.contrib.sessions' , 'django.contrib.messages' , 'django.contrib.staticfiles' , 'rest_framework' , 'django_todo' , 'todo' , 'owner' ]   如果我们查看Django项目的根目录，那么现在有两个Django应用程序：  (django-someHash)$ ls

Pipfile      Pipfile.lock django_todo  manage.py    owner        todo




owner/models.py ，构建此Owner模型。 如前所述，它将与Django的内置User对象具有一对一的关系。 我们可以使用Django的models.OneToOneField来加强这种关系。



# owner/models.py

from django.
db
import models

from django.
contrib .
auth .
models
import User

import secrets

class Owner
( models.
Model
) :

user
= models.
OneToOneField
( User
, on_delete
= models.
)

token
= models.
CharField
( max_length
=
256
)

def
__init__
(
self
, *args
, **kwargs
) :

"""On construction, set token."""

self .
token
= secrets.
token_urlsafe
(
64
)

super
(
) .
__init__
( *args
, **kwargs
)






(django-someHash) $./manage.py makemigrations Migrations for 'owner': owner/migrations/0001_initial.py - Create model Owner (django-someHash)$ ./manage.py migrate

Operations to perform:

Apply all migrations: admin, auth, contenttypes, owner, sessions, todo

Running migrations:

Applying owner.0001_initial... OK






# todo/models.py

from django.
db
import models

from owner.
models
import Owner

( models.
Model
) :

"""Tasks for the To Do list."""

name
= models.
CharField
( max_length
=
256
)

note
= models.
TextField
( blank
=
True
, null
=
True
)

creation_date
= models.
DateTimeField
=
True
)

due_date
= models.
DateTimeField
( blank
=
True
, null
=
True
)

completed
= models.
BooleanField
( default
=
False
)

owner
= models.
ForeignKey
( Owner
, on_delete
= models.
)






(django-someHash) django $./manage.py makemigrations You are trying to add a non-nullable field 'owner' to task 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   不好了！ 我们出现了问题！ 发生了什么？ 好了，当我们创建Owner对象并将其作为ForeignKey添加到Task ，我们基本上要求每个Task需要一个Owner 。 但是，我们为Task对象所做的第一次迁移并不包括该要求。 因此，即使我们数据库的表中没有数据，Django仍在对我们的迁移进行预检查，以确保它们兼容，而我们建议的新迁移不兼容。 有几种方法可以解决此类问题： 1. 吹走当前的迁移并构建一个包含当前模型配置的新迁移 2. 将默认值添加到Task对象上的owner字段 3. 允许任务的owner字段具有NULL值。 选项2在这里没有多大意义； 我们建议，默认情况下，创建的所有Task都将链接到某些默认所有者，尽管不一定存在。 选项1要求我们销毁并重建迁移。 我们应该不理会那些人。 让我们选择选项3。在这种情况下，如果允许Task表的所​​有者为空值，这将不是世界末日。 从此以后创建的任何任务都必须具有所有者。 如果您的数据库表的架构不适合这种情况，请取消迁移，删除表，然后重新构建迁移。  # todo/models.py from django. db import models from owner. models import Owner class Task ( models. Model ) : """Tasks for the To Do list.""" name = models. CharField ( max_length = 256 ) note = models. TextField ( blank = True , null = True ) creation_date = models. DateTimeField ( auto_now_add = True ) due_date = models. DateTimeField ( blank = True , null = True ) completed = models. BooleanField ( default = False ) owner = models. ForeignKey ( Owner , on_delete = models. CASCADE , null = True )    (django-someHash)$ ./manage.py makemigrations

Migrations for 'todo':

(django-someHash) $./manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, owner, sessions, todo Running migrations: Applying todo.0002_task_owner... OK   ！ 我们有我们的模型！ 欢迎使用Django声明对象的方式。 为了确保一切正常，我们确保每当创建一个User ，它都会自动与新的Owner对象链接。 我们可以使用Django的signals系统来做到这一点。 基本上，我们确切地说出我们的意图：“当我们收到一个信号，表明已经构造了一个新User ，构造一个新Owner ，并将该新User设置为该Owneruser字段。” 在实践中看起来像：  # owner/models.py from django. contrib . auth . models import User from django. db import models from django. db . models . signals import post_save from django. dispatch import receiver import secrets class Owner ( models. Model ) : """The object that owns tasks.""" user = models. OneToOneField ( User , on_delete = models. CASCADE ) token = models. CharField ( max_length = 256 ) def __init__ ( self , *args , **kwargs ) : """On construction, set token.""" self . token = secrets. token_urlsafe ( 64 ) super ( ) . __init__ ( *args , **kwargs ) @ receiver ( post_save , sender = User ) def link_user_to_owner ( sender , **kwargs ) : """If a new User is saved, create a corresponding Owner.""" if kwargs [ 'created' ] : owner = Owner ( user = kwargs [ 'instance' ] ) owner. save ( )   我们设置了一个函数，该函数侦听要从Django内置的User对象发送的信号。 等待User对象保存之后。 这可以来自新User ，也可以来自现有User的更新。 我们在侦听功能中区分这两种情况。 如果发送信号的对象是新创建的实例，则kwargs['created']的值为True 。 如果这是True我们只想做某事。 如果是新实例，我们将创建一个新的Owner ，将其user字段设置为已创建的新User实例。 After that, we save() the new Owner . This will commit our change to the database if all is well. It'll fail if the data doesn't validate against the fields we declared. Now let's talk about how we're going to access the data. ## Accessing model data In the Flask, Pyramid, and Tornado frameworks, we accessed model data by running queries against some database session. Maybe it was attached to a request object, maybe it was a standalone session object. Regardless, we had to establish a live connection to the database and query on that connection. This isn't the way Django works. Django, by default, doesn't leverage any third-party object-relational mapping (ORM) to converse with the database. Instead, Django allows the model classes to maintain their own conversations with the database. Every model class that inherits from django.db.models.Model will have attached to it an objects object. This will take the place of the session or dbsession we've become so familiar with. Let's open the special shell that Django gives us and investigate how this objects object works.  (django-someHash)$ ./manage.py shell

Python 3.7.0 (default, Jun 29 2018, 20:13:13)

[Clang 9.1.0 (clang-902.0.39.2)] on darwin

(InteractiveConsole)

>>>




The Django shell is different from a normal Python shell in that it's aware of the Django project we've been building and can do easy imports of our models, views, settings, etc. without having to worry about installing a package. We can access our models with a simple import .



>>>
from owner.
models
import Owner

>>> Owner

<
class
'owner.models.Owner'
>




Currently, we have no Owner instances. We can tell by querying for them with Owner.objects.all() .



>>> Owner.
objects .
all
(
)

< QuerySet
[
]
>




Anytime we run a query method on the <Model>.objects object, we'll get a QuerySet back. For our purposes, it's effectively a list , and this list is showing us that it's empty. Let's make an Owner by making a User .



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

>>> new_user
= User
=
'kenyattamurphy'
,
email
=
'kenyatta.murphy@gmail.com'
)

>>> new_user.
(
'wakandaforever'
)

>>> new_user.
save
(
)




If we query for all of our Owner s now, we should find Kenyatta.



>>> Owner.
objects .
all
(
)

< QuerySet
[
< Owner: Owner
object
(
1
)
>
]
>




## Serializing models

We'll be passing data back and forth beyond just "Hello World." As such, we'll want to see some sort of JSON-ified output that represents that data well. Taking that object's data and transforming it into a JSON object for submission across HTTP is a version of data serialization . In serializing data, we're taking the data we currently have and reformatting it to fit some standard, more-easily-digestible form.

If I were doing this with Flask, Pyramid, and Tornado, I'd create a new method on each model to give the user direct access to call to_json() . The only job of to_json() would be to return a JSON-serializable (ie numbers, strings, lists, dicts) dictionary with whatever fields I want to be displayed for the object in question.

It'd probably look something like this for the Task object:



( Base
) :

...
all the fields...

def to_json
(
self
) :

"""Convert task attributes to a JSON-serializable dict."""

return
{

'id' :
self .
id
,

'name' :
self .
name
,

'note' :
self .
note
,

'creation_date' :
self .
creation_date .
strftime
(
'%m/%d/%Y %H:%M:%S'
)
,

'due_date' :
self .
due_date .
strftime
(
'%m/%d/%Y %H:%M:%S'
)
,

'completed' :
self .
completed
,

'user' :
self .
user_id

}




It's not fancy, but it does the job.

Django REST Framework, however, provides us with an object that'll not only do that for us but also validate inputs when we want to create new object instances or update existing ones. It's called the ModelSerializer .

Django REST Framework's ModelSerializer is effectively documentation for our models. They don't have lives of their own if there are no models attached (for that there's the Serializer class). Their main job is to accurately represent our model and make the conversion to JSON thoughtless when our model's data needs to be serialized and sent over a wire.

Django REST Framework's ModelSerializer works best for simple objects. As an example, imagine that we didn't have that ForeignKey on the Task object. We could create a serializer for our Task that would convert its field values to JSON as necessary with the following declaration:



# todo/serializers.py

from rest_framework
import serializers

from todo.
models

( serializers.
ModelSerializer
) :

class Meta:

model

fields
=
(
'id'
,
'name'
,
'note'
,
'creation_date'
,
'due_date'
,
'completed'
)




Inside our new TaskSerializer , we create a Meta class. Meta 's job here is just to hold information (or metadata ) about the thing we're attempting to serialize. Then, we note the specific fields that we want to show. If we wanted to show all the fields, we could just shortcut the process and use '__all__' . We could, alternatively, use the exclude keyword instead of fields to tell Django REST Framework that we want every field except for a select few. We can have as many serializers as we like, so maybe we want one for a small subset of fields and one for all the fields? Go wild here.

In our case, there is a relation between each Task and its owner Owner that must be reflected here. As such, we need to borrow the serializers.PrimaryKeyRelatedField object to specify that each Task will have an Owner and that relationship is one-to-one. Its owner will be found from the set of all owners that exists. We get that set by doing a query for those owners and returning the results we want to be associated with this serializer: Owner.objects.all() . We also need to include owner in the list of fields, as we always need an Owner associated with a Task



# todo/serializers.py

from rest_framework
import serializers

from todo.
models

from owner.
models
import Owner

( serializers.
ModelSerializer
) :

owner
= serializers.
PrimaryKeyRelatedField
( queryset
= Owner.
objects .
all
(
)
)

class Meta:

model

fields
=
(
'id'
,
'name'
,
'note'
,
'creation_date'
,
'due_date'
,
'completed'
,
'owner'
)




Now that this serializer is built, we can use it for all the CRUD operations we'd like to do for our objects:

• If we want to GET a JSONified version of a specific Task , we can do TaskSerializer(some_task).data
• If we want to accept a POST with the appropriate data to create a new Task , we can use TaskSerializer(data=new_data).save()
• If we want to update some existing data with a PUT , we can say TaskSerializer(existing_task, data=data).save()

We're not including delete because we don't really need to do anything with information for a delete operation. If you have access to an object you want to delete, just say object_instance.delete() .

Here is an example of what some serialized data might look like:



>>>
from todo.
models

>>>
from todo.
serializers

>>>
from owner.
models
import Owner

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

>>> new_user
= User
=
'kenyatta'
,
email
=
'kenyatta@gmail.com'
)

>>> new_user.
(
'wakandaforever'
)

>>> new_user.
save
(
)
# creating the User that builds the Owner

>>> kenyatta
= Owner.
objects .
first
(
)
# grabbing the Owner that is kenyatta

( name
=
"Buy roast beef for the Sunday potluck"
, owner
= kenyatta
)

save
(
)

) .
data

{
'id' :
1
,
'name' :
'Go to the supermarket'
,
'note' :
None
,
'creation_date' :
'2018-07-31T06:00:25.165013Z'
,
'due_date' :
None
,
'completed' :
False
,
'owner' :
1
}




There's a lot more you can do with the ModelSerializer objects, and I suggest checking the docs for those greater capabilities. Otherwise, this is as much as we need. It's time to dig into some views.

## Views for reals

We've built the models and the serializers, and now we need to set up the views and URLs for our application. After all, we can't do anything with an application that has no views. We've already seen an example with the HelloWorld view above. However, that's always a contrived, proof-of-concept example and doesn't really show what can be done with Django REST Framework's views. Let's clear out the HelloWorld view and URL so we can start fresh with our views.

The first view we'll build is the InfoView . As in the previous frameworks, we just want to package and send out a dictionary of our proposed routes. The view itself can live in django_todo.views since it doesn't pertain to a specific model (and thus doesn't conceptually belong in a specific app).



# django_todo/views.py

from rest_framework.
response
import JsonResponse

from rest_framework.
views
import APIView

class InfoView
( APIView
) :

"""List of routes for this API."""

def get
(
self
, request
) :

output
=
{

'info' :
'GET /api/v1'
,

'register' :
'POST /api/v1/accounts'
,

'single profile detail' :
,

'edit profile' :
,

'delete profile' :
,

,

'logout' :
'GET /api/v1/accounts/logout'
,

,

,

,

,

}

return JsonResponse
( output
)




This is pretty much identical to what we had in Tornado. Let's hook it up to an appropriate route and be on our way. For good measure, we'll also remove the admin/ route, as we won't be using the Django administrative backend here.



# in django_todo/urls.py

from django_todo.
views
import InfoView

from django.
urls
import path

urlpatterns
=
[

path
(
'api/v1'
, InfoView.
as_view
(
)
, name
=
"info"
)
,

]




### Connecting models to views

Let's figure out the next URL, which will be the endpoint for either creating a new Task or listing a user's existing tasks. This should exist in a urls.py in the todo app since this has to deal specifically with Task objects instead of being a part of the whole project.



# in todo/urls.py

from django.
urls
import path

from todo.
views

urlpatterns
=
[

path
(
''
as_view
(
)
, name
=
)

]




What's the deal with this route? We didn't specify a particular user or much of a path at all. Since there would be a couple of routes requiring the base path /api/v1/accounts/<username>/tasks , why write it again and again when we can just write it once?

Django allows us to take a whole suite of URLs and import them into the base django_todo/urls.py file. We can then give every one of those imported URLs the same base path, only worrying about the variable parts when, you know, they vary.



# in django_todo/urls.py

from django.
urls
import include
, path

from django_todo.
views
import InfoView

urlpatterns
=
[

path
(
'api/v1'
, InfoView.
as_view
(
)
, name
=
"info"
)
,

path
(
, include
(
'todo.urls'
)
)

]




And now every URL coming from todo/urls.py will be prefixed with the path api/v1/accounts/<str:username>/tasks .

Let's build out the view in todo/views.py



# todo/views.py

from django.
shortcuts
import get_object_or_404

from rest_framework.
response
import JsonResponse

from rest_framework.
views
import APIView

from owner.
models
import Owner

from todo.
models

from todo.
serializers

( APIView
) :

def get
(
self
, request
, format
=
None
) :

"""Get all of the tasks for a given user."""

owner
= get_object_or_404
( Owner
)

objects .
filter
( owner
= owner
) .
all
(
)

serialized
, many
=
True
)

return JsonResponse
(
{

,

data

}
)




There's a lot going on here in a little bit of code, so let's walk through it.

We start out with the same inheritance of the APIView that we've been using, laying the groundwork for what will be our view. We override the same get method we've overridden before, adding a parameter that allows our view to receive the username from the incoming request.

Our get method will then use that username to grab the Owner associated with that user. This get_object_or_404 function allows us to do just that, with a little something special added for ease of use.

It would make sense that there's no point in looking for tasks if the specified user can't be found. In fact, we'd want to return a 404 error. get_object_or_404 gets a single object based on whatever criteria we pass in and either returns that object or raises an Http404 exception . We can set that criteria based on attributes of the object. The Owner objects are all attached to a User through their user attribute. We don't have a User object to search with, though. We only have a username . So, we say to get_object_or_404 "when you look for an Owner , check to see that the User attached to it has the username that I want" by specifying user__username . That's TWO underscores. When filtering through a QuerySet, the two underscores mean "attribute of this nested object." Those attributes can be as deeply nested as needed.

We now have the Owner corresponding to the given username. We use that Owner to filter through all the tasks, only retrieving the ones it owns with Task.objects.filter . We could've used the same nested-attribute pattern that we did with get_object_or_404 to drill into the User connected to the Owner connected to the Tasks ( tasks = Task.objects.filter(owner__user__username=username).all() ) but there's no need to get that wild with it.

Task.objects.filter(owner=owner).all() will provide us with a QuerySet of all the Task objects that match our query. 大。 The TaskSerializer will then take that QuerySet and all its data, along with the flag of many=True to notify it as being a collection of items instead of just one item, and return a serialized set of results. Effectively a list of dictionaries. Finally, we provide the outgoing response with the JSON-serialized data and the username used for the query.

### Handling the POST request

The post method will look somewhat different from what we've seen before.



# still in todo/views.py

# ...other imports...

from rest_framework.
parsers
import JSONParser

from
datetime
import
datetime

( APIView
) :

def get
(
self
, request
, format
=
None
) :

...

def post
(
self
, request
, format
=
None
) :

owner
= get_object_or_404
( Owner
)

data
= JSONParser
(
) .
parse
( request
)

data
[
'owner'
]
= owner.
id

if data
[
'due_date'
] :

data
[
'due_date'
]
=
datetime .
strptime
( data
[
'due_date'
]
,
'%d/%m/%Y %H:%M:%S'
)

( data
= data
)

is_valid
(
) :

save
(
)

return JsonResponse
(
{
'msg' :
'posted'
}
, status
=
201
)

return JsonResponse
errors
, status
=
400
)




When we receive data from the client, we parse it into a dictionary using JSONParser().parse(request) . We add the owner to the data and format the due_date for the task if one exists.

Our TaskSerializer does the heavy lifting. It first takes in the incoming data and translates it into the fields we specified on the model. It then validates that data to make sure it fits the specified fields. If the data being attached to the new Task is valid, it constructs a new Task object with that data and commits it to the database. We then send back an appropriate "Yay! We made a new thing!" response. If not, we collect the errors that TaskSerializer generated and send those back to the client with a 400 Bad Request status code.

If we were to build out the put view for updating a Task , it would look very similar to this. The main difference would be that when we instantiate the TaskSerializer , instead of just passing in the new data, we'd pass in the old object and the new data for that object like TaskSerializer(existing_task, data=data) . We'd still do the validity check and send back the responses we want to send back.

## 结语

Django as a framework is highly customizable , and everyone has their own way of stitching together a Django project. The way I've written it out here isn't necessarily the exact way that a Django project needs to be set up; it's just a) what I'm familiar with, and b) what leverages Django's management system. Django projects grow in complexity as you separate concepts into their own little silos. You do that so it's easier for multiple people to contribute to the overall project without stepping on each other's toes.

The vast map of files that is a Django project, however, doesn't make it more performant or naturally predisposed to a microservice architecture. On the contrary, it can very easily become a confusing monolith. That may still be useful for your project. It may also make it harder for your project to be manageable, especially as it grows.

Consider your options carefully and use the right tool for the right job. For a simple project like this, Django likely isn't the right tool.

Django is meant to handle multiple sets of models that cover a variety of different project areas that may share some common ground. This project is a small, two-model project with a handful of routes. If we were to build this out more, we'd only have seven routes and still the same two models. It's hardly enough to justify a full Django project.

It would be a great option if we expected this project to expand. This is not one of those projects. This is choosing a flamethrower to light a candle. It's absolute overkill.

Still, a web framework is a web framework, regardless of which one you use for your project. It can take in requests and respond as well as any other, so you do as you wish. Just be aware of what overhead comes with your choice of framework.

02-24 1万+
03-23 5608
08-30 388
05-22 4649
08-19 787
04-27 329
08-21 1816
08-02 3310
07-08 7230
10-04 1万+