Django Python Web应用程序框架简介

在这个由四部分组成的系列文章的前三篇文章中,比较了不同的Python Web框架,我们介绍了PyramidFlaskTornado Web框架。 我们已经构建了同一个应用程序3次,最终进入了Django 。 如今,Django基本上已成为Python开发人员的主要Web框架,并且不难理解为什么。 它擅长隐藏许多配置逻辑,使您专注于能够快速构建大型配置。

就是说,当涉及到诸如“待办事项列表”应用之类的小型项目时,Django有点像为水枪大战带来了水火。 让我们看看它们如何结合在一起。

关于Django

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

专门用于第三方软件包的整个网站 ,这些软件包是人们设计用来插入Django来做很多事情的。 这包括从身份验证和授权到完全由Django支持的内容管理系统,电子商务附加组件以及与Stripe集成的所有内容。 谈论不重新发明轮子; 如果您希望使用Django完成某项工作,则可能已经有人完成了,您可以将其放入您的项目中。

为此,我们希望使用Django构建REST API,因此我们将利用一直流行的Django REST框架 。 它的工作是将Django框架转变为专门用于有效处理REST交互的系统,该框架是为使用Django自己的模板引擎构建的完全渲染HTML页面提供服务。 让我们开始吧。

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 . 

键入ls将显示一个新文件和一个新目录。


   
(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 

我们希望像使用Flask,Pyramid和Tornado一样使用PostgreSQL数据库。 这意味着我们必须更改DATABASES设置,以允许我们的服务器访问PostgreSQL数据库。 首先:引擎。 默认情况下,数据库引擎是django.db.backends.sqlite3 。 我们将其更改为django.db.backends.postgresql

有关Django可用引擎的更多信息, 请参阅docs 。 请注意,虽然从技术上讲可以将NoSQL解决方案合并到Django项目中,但是Django强烈偏向于SQL解决方案。

接下来,我们必须为连接参数的不同部分指定键值对。

  • 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' , '' ) ,
        'PASSWORD' : os . environ . get ( 'DB_PASS' , '' ) ,
        'HOST' : os . environ . get ( 'DB_HOST' , '' ) ,
        'PORT' : os . environ . get ( 'DB_PORT' , '' ) ,
    }
}

在继续之前,请确保设置环境变量,否则Django将无法正常工作。 另外,我们需要将psycopg2安装到此环境中,以便我们可以与数据库对话。

Django路线和视图

让我们在该项目中执行一些功能。 我们将使用Django REST Framework来构建REST API,因此我们必须通过在settings.pyINSTALLED_APPS末尾添加rest_framework来确保可以使用它。


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

尽管Django REST Framework并不只需要基于类的视图(如Tornado)来处理传入请求,但它是编写视图的首选方法。 让我们定义一个。

让我们在django_todo创建一个名为views.py的文件。 在views.py ,我们将创建“您好,世界!” 视图。


   
# 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!" )

每个基于Django REST Framework的类视图都直接或间接继承自APIViewAPIView处理大量内容,但出于我们的目的,它执行以下特定操作:

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

是的,我们有意见! 它本身不执行任何操作。 我们需要将其连接到路线。

如果跳入django_todo/urls.py ,我们将到达默认的URL配置文件。 如前所述:如果我们的Django项目中未包含路由,则该路由不存在

我们通过将所需的URL添加到给定的urlpatterns列表中来添加它们。 默认情况下,我们获得了Django内置站点管理后端的整套URL。 我们将其完全删除。

我们还获得了一些非常有用的文档字符串,这些字符串准确地告诉我们如何向Django项目添加路由。 我们需要使用以下三个参数提供对path()的调用:

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

让我们导入HelloWorld视图并将其附加到本地路由"/" 。 我们也可以从urlpatterns删除admin的路径,因为我们不会使用它。


   
# 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" ) ,
]

好吧,这是不同的。 我们指定的路由只是一个空字符串。 为什么行得通? Django假设我们声明的每条路径均以斜杠开头。 我们只是在初始域名之后指定到资源的路由。 如果路由不去特定资源,而只是主页,则该路由只是"" ,或者实际上是“无资源”。

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_todo目录中编写我们自己的models.py文件并将其导入视图中来规避此问题。 但是,我们正在尝试以“正确”的方式编写Django项目,因此我们应将模型尽可能地分成自己的小包装Django Way™。

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

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

它们是独立的,因此可以像独立应用程序一样轻松共享。 实际上,Django REST Framework是Django应用的示例。 它带有自己的视图和HTML模板,用于提供JSON。 我们只是利用该Django应用程序,将我们的项目转变为功能全面的RESTful API,而麻烦却很少。

要为待办事项列表项创建Django应用,我们需要将startapp命令与manage.py

 (django-someHash) $ ./manage.py startapp todo 

startapp命令将以静默方式成功执行。 我们可以使用ls来检查它是否完成了应该做的事情。


   
(django-someHash) $ ls
Pipfile      Pipfile.lock django_todo  manage.py    todo

看一下:我们有一个全新的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 

在继续前进之前,我们应该帮自己一个忙,并将这个新的Django应用添加到django_todo/settings.py中的INSTALLED_APPS列表中。


   
# in settings.py
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' # <--- the line was added
]

检查todo/models.py看到manage.py已经写了一些代码供我们入门。 与在Flask,Tornado和Pyramid实现中创建模型的方式不同,Django不利用第三方来管理数据库会话或其对象实例的构造。 所有这些都放入了Django的django.db.models子模块中。

但是,模型的构建方式大致相同。 要在Django中创建模型,我们需要构建一个从models.Model继承的class 。 适用于该模型实例的所有字段都应显示为类属性。 而不是像过去一样从SQLAlchemy导入列和字段类型,我们所有的字段都将直接来自django.db.models


   
# todo/models.py
from django. db import models

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 )

尽管Django和基于SQLAlchemy的系统之间存在一定的区别,但总体内容和结构基本相同。 让我们指出差异。

我们不再需要为对象实例的自动递增的ID号声明一个单独的字段。 Django会为我们构建一个,除非我们指定其他字段作为主键。

无需实例化传递给数据类型对象的Column对象,我们仅将数据类型直接引用为列本身。

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有其自己的数据库管理方式。 无需编写关于数据库的任何代码,实际上,我们可以利用Django在构建时提供的manage.py脚本。 它不仅可以管理数据库表的构造,而且还可以管理我们希望对这些表进行的任何更新, 而不必将整个工作付诸东流!

因为我们已经构建了一个模型,所以我们需要使数据库知道它。 首先,我们需要将与该模型相对应的架构放入代码中。 manage.pymakemigrations命令将为我们构建的模型类及其所有字段拍摄快照。 它将获取这些信息并将其打包到一个Python脚本中,该脚本将存在于此特定Django应用的migrations目录中。 永远没有理由直接运行此迁移脚本。 它仅存在,以便Django在更新模型类时可以将其用作更新数据库表或继承信息的基础。


   
(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 admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... 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.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK
  Applying todo.0001_initial... OK

应用迁移时,Django首先检查其他INSTALLED_APPS是否要应用迁移。 它以大致列出的顺序检查它们。 我们希望我们的应用程序排在最后,因为我们要确保在我们的模型依赖于Django的任何内置模型的情况下,我们进行的数据库更新不会遇到依赖关系问题。

我们还有另一个模型要构建:用户模型。 但是,自从我们使用Django以来,游戏发生了一些变化。 如此多的应用程序需要某种用户模型,因此Django的django.contrib.auth软件包已构建了自己的模型供我们使用。 如果不是我们用户需要的身份验证令牌,我们可以继续使用并使用它,而不必重新发明轮子。

但是,我们需要该令牌。 我们可以通过几种方法来处理此问题。

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

我有建立对象关系的习惯,所以让我们选择第二种方法。 我们称其为Owner因为它基本上具有与User类似的含义,这就是我们想要的。

出于懒惰,我们可以只在todo/models.py包含这个新的Owner对象,但我们不todo/models.pyOwner不必明确地与任务列表中项目的创建或维护有关。 从概念上讲, Owner只是任务的所有者 。 甚至有时候我们想要扩展此Owner以包括与任务完全无关的其他数据。

为了安全起见,让我们创建一个owner应用,其工作是容纳和处理此Owner对象。

 (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 ) :
    """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 )

这表示Owner对象链接到User对象,每个user实例一个owner实例。 on_delete=models.CASCADE指示,如果删除了相应的User ,则与之链接的Owner实例也将被删除。 让我们运行makemigrations并进行migrate以将这个新模型烘焙到我们的数据库中。


   
(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

现在,我们的Owner需要拥有一些Task对象。 它与上面的OneToOneField非常相似,不同之处在于,我们将ForeignKey字段粘贴在Task对象上,指向Owner


   
# 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 )

每个任务列表任务只有一个所有者,可以拥有多个任务。 删除该所有者后,他们拥有的所有任务都会随之而来。

现在让我们运行makemigrations来获取数据模型设置的新快照,然后进行migrate以将这些更改应用于我们的数据库。


   
(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':
  todo/migrations/0002_task_owner.py
    - Add field owner to task
(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
Type "help", "copyright", "credits" or "license" for more information.
(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 ( username = 'kenyattamurphy' , email = 'kenyatta.murphy@gmail.com' )
>>> new_user. set_password ( '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 ) > ] >

好极了! We've got data!

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:


   
class Task ( 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 import Task

class TaskSerializer ( serializers. ModelSerializer ) :
    """Serializer for the Task model."""

    class Meta:
        model = Task
        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 import Task
from owner. models import Owner

class TaskSerializer ( serializers. ModelSerializer ) :
    """Serializer for the Task model."""
    owner = serializers. PrimaryKeyRelatedField ( queryset = Owner. objects . all ( ) )

    class Meta:
        model = Task
        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 import Task
>>> from todo. serializers import TaskSerializer
>>> from owner. models import Owner
>>> from django. contrib . auth . models import User
>>> new_user = User ( username = 'kenyatta' , email = 'kenyatta@gmail.com' )
>>> new_user. save_password ( 'wakandaforever' )
>>> new_user. save ( ) # creating the User that builds the Owner
>>> kenyatta = Owner. objects . first ( ) # grabbing the Owner that is kenyatta
>>> new_task = Task ( name = "Buy roast beef for the Sunday potluck" , owner = kenyatta )
>>> new_task. save ( )
>>> TaskSerializer ( new_task ) . 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' : 'GET /api/v1/accounts/<username>' ,
            'edit profile' : 'PUT /api/v1/accounts/<username>' ,
            'delete profile' : 'DELETE /api/v1/accounts/<username>' ,
            'login' : 'POST /api/v1/accounts/login' ,
            'logout' : 'GET /api/v1/accounts/logout' ,
            "user's tasks" : 'GET /api/v1/accounts/<username>/tasks' ,
            "create task" : 'POST /api/v1/accounts/<username>/tasks' ,
            "task detail" : 'GET /api/v1/accounts/<username>/tasks/<id>' ,
            "task update" : 'PUT /api/v1/accounts/<username>/tasks/<id>' ,
            "delete task" : 'DELETE /api/v1/accounts/<username>/tasks/<id>'
        }
        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 import TaskListView

urlpatterns = [
    path ( '' , TaskListView. as_view ( ) , name = "list_tasks" )
]

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 ( 'api/v1/accounts/<str:username>/tasks' , 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 import Task
from todo. serializers import TaskSerializer


class TaskListView ( APIView ) :
    def get ( self , request , username , format = None ) :
        """Get all of the tasks for a given user."""
        owner = get_object_or_404 ( Owner , user__username = username )
        tasks = Task. objects . filter ( owner = owner ) . all ( )
        serialized = TaskSerializer ( tasks , many = True )
        return JsonResponse ( {
            'username' : username ,
            'tasks' : serialized. 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

class TaskListView ( APIView ) :
    def get ( self , request , username , format = None ) :
        ...

    def post ( self , request , username , format = None ) :
        """Create a new Task."""
        owner = get_object_or_404 ( Owner , user__username = username )
        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' )

        new_task = TaskSerializer ( data = data )
        if new_task. is_valid ( ) :
            new_task. save ( )
            return JsonResponse ( { 'msg' : 'posted' } , status = 201 )

        return JsonResponse ( new_task. 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.

而已! We've reached the end of this series! I hope it has been an enlightening adventure and will help you make more than just the most-familiar choice when you're thinking about how to build out your next project. Make sure to read the documentation for each framework to expand on anything covered in this series (as it's not even the least bit comprehensive). There's a wide world of stuff to get into for each. 编码愉快!

翻译自: https://opensource.com/article/18/8/django-framework

表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页