Django 是一款基于 Python 编写并且采用 MVC 设计模式的开源的 Web 应用框架,早期是作为劳伦斯出版集团新闻网站的 CMS 内容管理系统而开发,后于 2005 年 7 月在 BSD 许可协议下开源,并于 2017 年 12 月 2 日 发布 2.0 正式版。
本文基于《Django 官方 Tutorials》以及《Django REST framework 官方 Tutorials》编写,发稿时所使用的 Django 版本为 2.1.4,Python 版本为 3.6.6,文中涉及的代码都已经由笔者验证运行通过,最终形成了一个简单项目并推送至笔者Github上的jungle项目当中,需要的朋友可以基于此来逐步步完善成为一个产品化的项目。
新建 Django 项目
下面的命令行展示了在 Windows 操作系统下,基于 venv 虚拟环境搭建一个 Django 项目的步骤:
# 建立虚拟环境 C:\Workspace\django λ python -m venv venv # 激活虚拟环境 C:\Workspace\django λ .\venv\Scripts\activate.bat (venv) λ # 安装Django C:\Workspace\django (venv) λ pip install Django Looking in indexes: https://mirrors.aliyun.com/pypi/simple/ Collecting Django Using cached https://mirrors.aliyun.com/pypi/packages/fd/9a/0c028ea0fe4f5803dda1a7afabeed958d0c8b79b0fe762ffbf728db3b90d/Django-2.1.4-py3-none-any.whl Collecting pytz (from Django) Using cached https://mirrors.aliyun.com/pypi/packages/f8/0e/2365ddc010afb3d79147f1dd544e5ee24bf4ece58ab99b16fbb465ce6dc0/pytz-2018.7-py2.py3-none-any.whl Installing collected packages: pytz, Django Successfully installed Django-2.1.4 pytz-2018.7 # 进入虚拟环境目录,新建一个Django项目 C:\Workspace\django (venv) λ django-admin startproject mysite C:\Workspace\django (venv) λ ls mysite/ venv/ # 进入新建的Django项目,建立一个应用 C:\Workspace\django (venv) λ cd mysite C:\Workspace\django\mysite (venv) λ python manage.py startapp demo C:\Workspace\django\mysite (venv) λ ls demo/ manage.py* mysite/ # 同步数据库 C:\Workspace\django\mysite (venv) λ python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions 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 admin.0003_logentry_add_action_flag_choices... 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 # 启动开发服务 (venv) λ python manage.py runserver 8080 Performing system checks... System check identified no issues (0 silenced). January 03, 2019 - 21:31:48 Django version 2.1.4, using settings 'mysite.settings' Starting development server at http://127.0.0.1:8080/ Quit the server with CTRL-BREAK. # 返回uinika虚拟环境目录,并将当前虚拟环境的依赖导入至requirements.txt C:\Workspace\django\mysite (venv) λ cd .. C:\Workspace\django (venv) λ pip freeze > requirements.txt C:\Workspace\django (venv) λ ls mysite/ requirements.txt venv/
通过 django-admin startproject
命令创建的外部 mysite/
目录是 Web 项目的容器,而 manage.py
文件是用于与 Django 项目交互的命令行工具,更多的使用方式可以参阅 django-admin 文档 。。
mysite/ manage.py mysite/ __init__.py settings.py urls.py wsgi.py
内部嵌套的 mysite/
目录是用于放置项目中具体的 Python 包,它的名称是您需要用来导入其中任何内容的 Python 包名称,例如 mysite.urls
。
mysite/__init__.py
: 空文件,用于提示系统将当前目录识别为一个 Python 包。mysite/settings.py
: Django 项目的配置文件,更多配置请查阅 Django settings 。mysite/urls.py
: 当前 Django 项目的 URL 声明,更多内容请参阅 URL dispatcher 。mysite/wsgi.py
: 兼容 WSGI 规范的当前项目入口点,更多细节可以阅读 如果使用 WSGI 进行部署 。
建立 mysite
项目之后,上面的命令行还通过了 py manage.py startapp
建立了一个 demo/
应用目录,Django 当中一个项目( mysite
)可以拥有多个应用( demo
), demo/
目录下的文件结构如下:
demo/ __init__.py admin.py apps.py migrations/ __init__.py models.py tests.py views.py
请求与响应
首先进入 Python 虚拟环境并进入 mysite
目录后,执行如下命令:
C:\Workspace\django\mysite (master -> origin) (venv) λ python manage.py startapp polls
新建一个 polls
应用之后,打开该目录下的 polls/views.py
源码文件,输入以下代码:
from django.http import HttpResponse def index(request): return HttpResponse("你好,这是一个投票应用!")
接下来,我们需要将上面修改的视图文件 views.py
映射到一个 URL,先在 polls/
目录下新建一个 urls.py
文件,然后键入下面这段代码:
from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index'), ]
最后,将上面定义的应用的 URL 声明文件 polls/urls.py
包含至项目的 mysite/urls.py
当中,
from django.contrib import admin from django.urls import include, path urlpatterns = [ path('polls/', include('polls.urls')), path('admin/', admin.site.urls), ]
上面代码中出现的 include()
函数主要用于引入其它 URL 配置文件,这样我们就可以通过 http://localhost:8080/polls/
路径访问到如下信息了:
模型和管理页面
mysite/settings.py
文件包含了项目的基本配置,该文件通过如下声明默认使用 Django 内置的 SQLite 作为项目数据库。
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } }
如果使用其它数据库,则可以将配置书写为下面的格式:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', # 数据库引擎名称 'NAME': 'db', # 数据库连接名称 'USER': 'uinika', # 数据库连接用户名 'PASSWORD': 'test', # 数据库连接密码 'HOST': 'localhost', # 数据库主机地址 'PORT': '3306', # 数据库端口 } }
其中 ENGINE
属性可以根据项目所使用数据库的不同而选择如下值:
- SQLite:django.db.backends.sqlite3
- MySQL:django.db.backends.mysql
- PostgreSQL:django.db.backends.postgresql
- Oracle:django.db.backends.oracle
接下来继续修改 mysite/settings.py
,设置 TIME_ZONE
属性为项目使用国家的时区。
LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Chongqing'
mysite/settings.py
文件头部的 INSTALLED_APPS
属性定义了当前项目使用的应用程序。
INSTALLED_APPS = [ 'django.contrib.admin', # 管理员站点 'django.contrib.auth', # 认证授权系统 'django.contrib.contenttypes', # 内容类型框架 'django.contrib.sessions', # 会话框架 'django.contrib.messages', # 消息框架 'django.contrib.staticfiles', # 静态文件管理 ]
在前面命令行中执行的 python manage.py migrate
命令会检查 INSTALLED_APPS
属性的设置,并为其中的每个应用创建所需的数据表,实际上 migrate
命令只会为对 INSTALLED_APPS
里声明了的应用进行数据库迁移 。
了解项目配置文件的一些设置之后,现在来编辑 polls/models.py
文件新建 Question(问题)
和 Choice(选项)
两个数据模型:
from django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField('date published') class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
每个自定义模型都是 django.db.models.Model
的子类,模型里的类变量都表示一个数据库字段,每个字段实质都是 Field
类的实例。注意在 Choice
使用了 ForeignKey
属性定义了一个与 Question
的外键关联关系,Django 支持所有常用的多对一、多对多和一对一数据库关系。
数据库模型建立完成之后,由于 PollsConfig
类位于 polls/apps.py
文件当中,所以其对应的点式路径为 polls.apps.PollsConfig
,现在我们需要将该路径添加至 mysite/settings.py
文件的 INSTALLED_APPS
属性:
INSTALLED_APPS = [ 'polls.apps.PollsConfig', # 添加PollsConfig 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ]
通过 manage.py
提供的 makemigrations
命令,将模型的修改迁移到 SQLite 数据库当中。
C:\Workspace\django\mysite (master -> origin) (venv) λ python manage.py makemigrations polls Migrations for 'polls': polls\migrations\0001_initial.py - Create model Choice - Create model Question - Add field question to choice
我们还可以通过 manage.py
提供的 sqlmigrate
命令,查看数据迁移过程中执行了哪些 SQL 语句,该命令并不会实质性执行 Django 模型到数据库的迁移任务。
C:\Workspace\django\mysite (master -> origin) (venv) λ python manage.py sqlmigrate polls 0001 BEGIN; -- -- Create model Choice -- CREATE TABLE "polls_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "v otes" integer NOT NULL); -- -- Create model Question -- CREATE TABLE "polls_question" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "question_text" varchar(200) NOT NULL , "pub_date" datetime NOT NULL); -- -- Add field question to choice -- ALTER TABLE "polls_choice" RENAME TO "polls_choice__old"; CREATE TABLE "polls_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "v otes" integer NOT NULL, "question_id" integer NOT NULL REFERENCES "polls_question" ("id") DEFERRABLE INITIALLY DEFERR ED); INSERT INTO "polls_choice" ("id", "choice_text", "votes", "question_id") SELECT "id", "choice_text", "votes", NULL FR OM "polls_choice__old"; DROP TABLE "polls_choice__old"; CREATE INDEX "polls_choice_question_id_c5b4b260" ON "polls_choice" ("question_id"); COMMIT;
Django 模型的数据库主键 ID 会被自动创建, 并会在外键字段名称后追加 _id
字符串作为后缀。
接下来运行 manage.py
提供的 migrate
命令,在根据新定义的模型创建相应的数据库表。
C:\Workspace\django\mysite (master -> origin) (venv) λ python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying polls.0001_initial... OK
为了便于在版本管理系统提交迁移数据,Django 将模型的修改分别独立为 生成 和 应用 两个命令,因此修改 Django 模型会涉及如下 3 个步骤:
- 编辑models.py文件修改模型。
- 运行python manage.py makemigrations为模型的改变生成迁移文件。
- 运行python manage.py migrate来应用数据库迁移。
完成上述 Django 模型与数据库的同步之后,接下来可以通过 manage.py
提供的 shell
命令,在命令行工具内运行 Django 提供的交互式 API。
C:\Workspace\django\mysite (master -> origin) (venv) λ python manage.py shell Python 3.6.6 (v3.6.6:4cf1f54eb7, Jun 27 2018, 03:37:03) [MSC v.1900 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) > from polls.models import Choice, Question > Question.objects.all() <QuerySet []> > from django.utils import timezone > q = Question(question_text="What's new", pub_date=timezone.now()) > q.save() > q.id 1 > q.question_text "What's new" > q.pub_date datetime.datetime(2019, 1, 4, 9, 10, 1, 955820, tzinfo=<UTC>) > q.question_text = "What's up" > q.save() > Question.objects.all() <QuerySet [<Question: Question object (1)>]>
上面命令行执行结果中的 <Question: Question object (1)>
对于实际开发没有意义,因此可以考虑为上面建立的 Django 模型增加 __str__()
方法直接打印模型对象的属性数据。为了便于进一步测试,这里还为 Question
类添加一个自定义的 was_published_recently()
方法:
import datetime from django.db import models from django.utils import timezone class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField('date published') # 自定义was_published_recently()方法 def was_published_recently(self): return self.pub_date >= timezone.now() - datetime.timedelta(days=1) # 添加__str__()方法 def __str__(self): return self.question_text class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) # 添加__str__()方法 def __str__(self): return self.choice_text
完成修改工作之后,再一次运行 python manage.py shell
命令:
C:\Workspace\django\mysite (master -> origin) (venv) λ python manage.py shell Python 3.6.6 (v3.6.6:4cf1f54eb7, Jun 27 2018, 03:37:03) [MSC v.1900 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) > from polls.models import Choice, Question > Question.objects.all() <QuerySet [<Question: What's up?>]> > Question.objects.filter(id=1) <QuerySet [<Question: What's up?>]> > Question.objects.filter(question_text__startswith='What') <QuerySet [<Question: What's up?>]> > from django.utils import timezone > current_year = timezone.now().year > Question.objects.get(pub_date__year=current_year) <Question: What's up?> > Question.objects.get(id=2) Traceback (most recent call last): File "<console>", line 1, in <module> File "C:\Workspace\django\venv\lib\site-packages\django\db\models\manager.py", line 82, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) File "C:\Workspace\django\venv\lib\site-packages\django\db\models\query.py", line 399, in get self.model._meta.object_name polls.models.Question.DoesNotExist: Question matching query does not exist. > Question.objects.get(pk=1) <Question: What's up?> > q = Question.objects.get(pk=1) > q.was_published_recently() True > q = Question.objects.get(pk=1) > q.choice_set.all() <QuerySet []> > q.choice_set.create(choice_text='Not much', votes=0) <Choice: Not much> > q.choice_set.create(choice_text='The sky', votes=0) <Choice: The sky> > c = q.choice_set.create(choice_text='Just hacking again', votes=0) > c.question <Question: What's up?> > q.choice_set.all() <QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]> > q.choice_set.count() 3 > Choice.objects.filter(question__pub_date__year=current_year) <QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]> > c = q.choice_set.filter(choice_text__startswith='Just hacking') > c.delete() (1, {'polls.Choice': 1})
管理站点
Django 能够根据模型自动创建后台管理界面, 这里我们执行 manage.py
提供的 createsuperuser
命令创建一个管理用户:
C:\Workspace\django\mysite (master -> origin) (venv) λ python manage.py createsuperuser Username (leave blank to use 'zhenghang'): hank Email address: uinika@outlook.com Password: ******** Password (again): ******** Superuser created successfully.
启动 Django 服务之后,就可以通过 URL 地址 http://localhost:8080/admin/login
并使用上面新建的用户名和密码进行登陆管理操作:
登陆后默认只能对权限相关的 User
和 Group
进行管理,如果我们需要将 Question
数据模型纳入管理,那么必须要在 polls/admin.py
文件对其进行注册。
from django.contrib import admin from .models import Question admin.site.register(Question)
完成注册之后,刷新管理站点页面即可查看到 Question
管理选项:
视图与模板
Django 使用 URLconfs
配置将 URL 与视图关联,即将 URL 映射至视图,下面我们将向 polls/views.py
文件添加一些能够接收参数的视图:
from django.http import HttpResponse def index(request): return HttpResponse("你好,这是一个投票应用!") def detail(request, question_id): return HttpResponse("你正在查看问题 %s 。" % question_id) def results(request, question_id): response = "你看到的是问题 %s 的结果。" return HttpResponse(response % question_id) def vote(request, question_id): return HttpResponse("你正在对问题 %s 进行投票。" % question_id)
然后将这些新的视图添加至 polls.urls
模块:
from django.urls import path from . import views urlpatterns = [ # 访问 http://localhost:8080/polls/ path('', views.index, name='index'), # 访问 http://localhost:8080/polls/5/ path('<int:question_id>/', views.detail, name='detail'), # 访问 http://localhost:8080/polls/5/results/ path('<int:question_id>/results/', views.results, name='results'), # 访问 http://localhost:8080/polls/5/vote/ path('<int:question_id>/vote/', views.vote, name='vote'), ]
Django 的每个视图只会完成两个任务:第 1 是返回一个包含被请求页面内容的 HttpResponse
对象,或者抛出一个 Http404
这样的异常。这里为了展示数据库里按照发布日期排序的最近五个投票问题,我们再向 polls/views.py
代码文件的 index()
函数添加如下内容:
from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] output = ', '.join([q.question_text for q in latest_question_list]) return HttpResponse(output)
这样直接将数据库查询结果输出到页面的方式并不优雅,实际开发环境当中我们通常会使用模板页面来展示数据,首先在 polls
应用目录下创建一个用来存放模板文件的 templates
目录。由于站点配置文件 mysite/settings.py
里 TEMPLATES
属性的默认设置,能够让 Django 在每个 INSTALLED_APPS
文件夹中自动寻找 templates
子目录,从而正确定位出模板的位置。
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], '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', ], }, }, ]
接下来继续在 templates
下面新建一个 polls
目录,然后在里边放置一个 index.html
文件,此时通过 URL 地址 http://localhost:8080/polls/
就可以访问到这个模板文件,模板文件会将按照发布日期排序了的 Question
列表 latest_question_list
放置到 HttpResponse
上下文,并在 polls/index.html
模板当中完成数据绑定。
from django.http import HttpResponse from django.template import loader from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] template = loader.get_template('polls/index.html') context = { 'latest_question_list': latest_question_list, } return HttpResponse(template.render(context, request))
事实上,通过使用 render()
方法,Django 能够以更加简化的方式完成载入模板、填充上下文、返回 HttpResponse 对象这一系列步骤:
from django.shortcuts import render from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context)
接下来处理投票详情页面,这里会有一个新原则,即如果指定 ID
所对应的 Question
不存在,那么视图就会抛出一个 Http404
异常。在 polls/views.py
添加如下代码,
from django.http import Http404 from django.shortcuts import render from .models import Question def detail(request, question_id): try: question = Question.objects.get(pk=question_id) except Question.DoesNotExist: raise Http404("问题不存在!") return render(request, 'polls/detail.html', {'question': qu
然后暂时向 polls/templates/polls/detail.html
添加一行简单的 代码便于测试上面的代码。
Django 提供了诸如 get_object_or_404()
、 get_list_or_404()
这样的快捷函数语法糖来解决 Http404
判断的问题,因而上一步的代码依然可以进一步简化为下面这样:
from django.shortcuts import get_object_or_404, render from .models import Question def detail(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/detail.html', {'question': question})
让我们进一步完善polls/templates/polls/detail.html,填充完整的视图代码:
<h1>{{ question.question_text }}</h1> <ul> {% for choice in question.choice_set.all %} <li>{{ choice.choice_text }}</li> {% endfor %} </ul>
通过在模板代码中使用.符号来访问变量属性,例如对于上面代码中的, Django 首先会尝试对question对象使用字典查找(既obj.get(str)),如果失败再尝试属性查找(既obj.str),如果依然失败就会尝试列表查找(即obj[int])。另外循环for中的question.choice_set.all语句会被解析为question.choice_set.all()的 Python 的函数调用,完成后将返回一个可迭代的Choice对象,该对象仅限于for循环标签内部使用。
在polls/templates/polls/index.html编写的投票链接里使用了诸如<a href="/polls//" rel="external nofollow" ></a>这样的硬编码,但是这样容易造成视图与后端业务的耦合,因此 Django 提供了url标签来解决这个问题。
<a href="{% url 'detail' question.id %}" rel="external nofollow" >{{ question.question_text }}</a>
实际上在mysite/polls/urls.py内的函数调用path('<int:question_id>/', views.detail, name='detail')当中,path()的name属性就是作用于url标签中的这个特性的。
为了避免项目当中各种应用的 URL 重名,避免url标签被使用时产生歧义,需要在polls/urls.py上添加应用的命名空间作为区分。
from django.urls import path from . import views app_name = 'polls' urlpatterns = [ path('', views.index, name='index'), path('<int:question_id>/', views.detail, name='detail'), path('<int:question_id>/results/', views.results, name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ]
然后编辑polls/templates/polls/index.html文件,为每个url标签添加上面声明的polls:命名空间。
<li><a href="{% url 'polls:detail' question.id %}" rel="external nofollow" >{{ question.question_text }}</a></li>
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。