Django官方教程

django学习记录

参考Django documentationDjango 简介

初识Django

特点

  • 强大的数据库功能
  • 自带强大的后台功能
  • 优雅的网址
  • 基于MTV模型

MVC与MTV

MVC 模式(Model–view–controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。

img

MTV本质上与MVC相似,也是为了保持各个组件的松耦合关系。

  • M 表示模型(Model):编写程序应有的功能,负责业务对象与数据库的映射(ORM)。
  • T 表示模板 (Template):负责如何把页面(html)展示给用户。
  • V 表示视图(View):负责业务逻辑,并在适当时候调用 Model和 Template。
img

ORM

Django对于各种数据库都有很好的支持,且提供了统一调用的api。

Django 模型使用自带的 ORM。

对象关系映射(Object Relational Mapping,简称 ORM )用于实现面向对象编程语言里不同类型系统的数据之间的转换。

ORM 在业务逻辑层和数据库层之间充当了桥梁的作用。

ORM 是通过使用描述对象和数据库之间的映射的元数据,将程序中的对象自动持久化到数据库中。

img

使用 ORM 的好处:

  • 提高开发效率。
  • 不同数据库可以平滑切换。

使用 ORM 的缺点:

  • ORM 代码转换为 SQL 语句时,需要花费一定的时间,执行效率会有所降低。
  • 长期写 ORM 代码,会降低编写 SQL 语句的能力。

ORM 解析过程:

  1. ORM 会将 Python 代码转成为 SQL 语句。

  2. SQL 语句通过 pymysql 传送到数据库服务端。

  3. 在数据库中执行 SQL 语句并将结果返回。

img

安装

python -m pip install Django

安装完成后运行

django-admin --version

如果要在生产站点上使用 Django,需要将 Apachemod_wsgi 一起使用。Django 将在任何支持 mod_wsgi 的 Apache 版本上工作。

第一个项目

创建项目

首先打开命令行,cd到一个放置代码的目录,然后运行

django-admin startproject mysite

就会生成一个mysite目录

mysite/
    manage.py
    mysite/
        __init__.py
        settings.py
        urls.py
        asgi.py
        wsgi.py

用于开发的简易服务器

在cmd中运行,django就会启动自带的用于开发的简易服务器,他只是为了快速开发,但是不能够用于投入生产环境。

manage.py runserver

现在,服务器正在运行,浏览器访问 https://127.0.0.1:8000/。你将会看到一个“祝贺”页面,随着一只火箭发射,服务器已经运行了。

创建应用

在Django中,每一个应用都是一个Python包,并且遵循相同的约定。

项目与应用

项目是一个网站使用的配置以及应用的集合,它包含了许多应用,应用是专门做某事的网络程序,可以用于很多项目。

应用可以存放在Python搜索路径下。

在manage.py的目录中,创建应用:

python manage.py startapp polls

这将会创建一个 polls 目录,它的目录结构大致如下:

polls/
    __init__.py
    admin.py
    apps.py
    migrations/
        __init__.py
    models.py
    tests.py
    views.py

这个目录结构包括了投票应用的全部内容。

HelloWorld

在views.py中,编写

from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")

这是最简单的HelloWorld视图

为了看见它,我们需要指定路由,在polls目录里新建一个urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
]

接下来需要在根URLconf中指定我们创建的polls.urls模块,在mysite/urls.py的urlpatterns中插入

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('polls/', include('polls.urls')),
    path('admin/', admin.site.urls),
]

函数 include() 允许引用其它 URLconfs。每当 Django 遇到 include 时,它会截断与此项匹配的 URL 的部分,并将剩余的字符串发送到 URLconf 以供进一步处理。

这时再运行服务器,进入 http://localhost:8000/polls/, 就能看见我们定义的视图了。

path

函数 path() 具有四个参数,两个必须参数: routeview ,两个可选参数: kwargsname

  • route: 是一个匹配URL的准则(类似于正则)Django响应请求时,它会从urlpatterns第一项开始,按顺序依次匹配列表中的项。它不会匹配url中的参数
  • view: Django找到匹配准则后就会调用这个视图函数,并传入HttpRequest 对象作为第一个参数,被“捕获”的参数以关键字参数的形式传入。
  • kwargs: 任意关键字参数传递给目标视图函数。
  • name: 为URl取名,能够在任意地方引用它。

settings配置

在mysite/settings.py中进行各种配置:

数据库

Django使用SQLite作为默认数据库, 要使用其他数据库,需要安装合适的 database bindings ,然后改变设置文件中 DATABASES 'default' 项目中的一些键值:

  • ENGINE -- 可选值有 'django.db.backends.sqlite3''django.db.backends.postgresql''django.db.backends.mysql',或 'django.db.backends.oracle'。其它 可用后端
  • NAME -- 数据库的名称。如果你使用 SQLite,数据库将是你电脑上的一个文件,在这种情况下,NAME 应该是此文件完整的绝对路径,包括文件名。默认值 BASE_DIR / 'db.sqlite3' 将把数据库文件储存在项目的根目录。

如果不使用 SQLite,则必须添加一些额外设置,比如 USERPASSWORDHOST 等等。

如果使用了 SQLite 以外的数据库,请确认在使用前已经创建了数据库。可以通过在数据库交互式命令行中使用 " CREATE DATABASE database_name; " 命令来完成这件事。

使用 MySQL

  1. 使用 MySQL 终端提前创建好数据库
>mysql create database myDatabase
  1. 修改 settings.py 在 DATABASES 中将 default 改为
'default': {
    'ENGINE': 'django.db.backends.mysql',
    'NAME': 'myDatabase',
    'HOST': '127.0.0.1',
    'PORT': 3306,
    'USER': '你的用户名(管理员)',
    'PASSWORD': '密码'
}
  1. 修改 init.py 文件 这里我们使用 pymysql, 因此需要进行初始化,即在 init.py 中:

pymysql.install_as_MySQLdb()
  1. 进行迁移

基础设置

默认开启的应用有一些需要至少一个数据表,因此在使用之前需要在数据库中创建:

python manage.py migrate

模型

在投票应用中需要两个模型:问题 Question 和选项 Choice。

Question 包括问题描述和发布时间,Choice包括选项描述和当前得票数,每个选项属于一个问题。

polls/models.py

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)

每个模型是 Model 类的子类,每个属性都是数据库的一个字段,这些字段都是 Field 类的实例。例如:CharField 表示 字符字段,DateTimeField 表示日期时间字段。

激活模型

在 INSTALLED_APPS 中添加

'polls.apps.PollsConfig',

PollsConfig 是在polls/apps.py 中定义的类。

然后cmd中运行

python manage.py makemigrations polls
> Migrations for 'polls':
  polls\migrations\0001_initial.py
    - Create model Question
    - Create model Choice

通过运行makemigrations,Django会把修改的模型文件储存为一次迁移

然后再运行

python manage.py migrate

就会在数据库里创建新定义的模型的数据表。

migrate 通过选中所有未执行过的迁移(在数据库中创建一个特殊的表跟踪)并应用在数据库上。

迁移使得在开发过程中能够持续的改变数据库结构而不需要重新删除和创建表。

总之,改变模型需要三步:

API

Django创建了各种API

python manage.py shell

进入交互界面,我们就可以进行增删改查了:

>>> from polls.models import Choice, Question  # Import the model classes we just wrote.

# No questions are in the system yet.
>>> Question.objects.all()
<QuerySet []>

# Create a new Question.
# Support for time zones is enabled in the default settings file, so
# Django expects a datetime with tzinfo for pub_date. Use timezone.now()
# instead of datetime.datetime.now() and it will do the right thing.
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())

# Save the object into the database. You have to call save() explicitly.
>>> q.save()

# Now it has an ID.
>>> q.id
1

# Access model field values via Python attributes.
>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)

# Change values by changing the attributes, then calling save().
>>> q.question_text = "What's up?"
>>> q.save()

# objects.all() displays all the questions in the database.
>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>

此外,通过给模型增加__str__() 方法,可以更加方便,而且 Django 自动生成的管理页面中也使用这个方法表示对象。

import datetime

from django.db import models
from django.utils import timezone
class Question(models.Model):
    # ...
    def __str__(self):
        return self.question_text
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

class Choice(models.Model):
    # ...
    def __str__(self):
        return self.choice_text

保存后继续进入交互页面:

>>> from polls.models import Choice, Question

# Make sure our __str__() addition worked.
>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>

# Django provides a rich database lookup API that's entirely driven by
# keyword arguments.

### 查询的函数在属性后面加双下划线

>>> Question.objects.filter(id=1)
<QuerySet [<Question: What's up?>]>
>>> Question.objects.filter(question_text__startswith='What')
<QuerySet [<Question: What's up?>]>

# Get the question that was published this year.
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
<Question: What's up?>

# Request an ID that doesn't exist, this will raise an exception.
>>> Question.objects.get(id=2)
Traceback (most recent call last):
    ...
DoesNotExist: Question matching query does not exist.

# Lookup by a primary key is the most common case, so Django provides a
# shortcut for primary-key exact lookups.
# The following is identical to Question.objects.get(id=1).
>>> Question.objects.get(pk=1)
<Question: What's up?>

# Make sure our custom method worked.
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True

# Give the Question a couple of Choices. The create call constructs a new
# Choice object, does the INSERT statement, adds the choice to the set
# of available choices and returns the new Choice object. Django creates
# a set to hold the "other side" of a ForeignKey relation
# (e.g. a question's choice) which can be accessed via the API.

### Django 为Question实例创建了一个集合来存储所有引用它的choice

>>> q = Question.objects.get(pk=1)

# Display any choices from the related object set -- none so far.
>>> q.choice_set.all()
<QuerySet []>

# Create three choices.
>>> 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)

### Choice 实例可以访问和它相关的API

# Choice objects have API access to their related Question objects.
>>> c.question
<Question: What's up?>

# And vice versa: Question objects get access to Choice objects.
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
>>> q.choice_set.count()
3

# The API automatically follows relationships as far as you need.
# Use double underscores to separate relationships.
# This works as many levels deep as you want; there's no limit.
# Find all Choices for any question whose pub_date is in this year
# (reusing the 'current_year' variable we created above).
>>> Choice.objects.filter(question__pub_date__year=current_year)
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

# Let's delete one of the choices. Use delete() for that.
>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
>>> c.delete()

阅读 访问关系对象 文档可以获取关于数据库关系的更多内容。关于双下划线的更多用法,参见 查找字段 文档。数据库 API 的所有细节可以在 数据库 API 参考 文档中找到。

管理页面

Django能够全自动地根据模型创建后台界面,这个管理界面不是为了网站的访问者而是为管理者准备的。

创建管理员账号

python manage.py createsuperuser

管理界面在启动服务器时就默认就是启动的,因此运行服务器

python manage.py runserver

转到本地管理员目录,"http://127.0.0.1:8000/admin/" ,登录后,接下来就会进入管理站点的界面。

Django admin index page

会发现有几个可编辑的内容:组和用户,它们是由 django.contrib.auth 提供的,这是 Django 开发的认证框架。

添加应用

我们需要给模型添加后台接口

polls/admin.py

from django.contrib import admin

from .models import Question

admin.site.register(Question)

此时就可以通过管理页面改变Question了

视图

Django 中视图的概念是「一类具有相同功能和模板的网页的集合」。在Django中,网页和其他内容都是由视图派生而来,每个视图表现为一个Python的函数/方法。

向 polls/views.py 添加

def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)

然后把视图添加到urls模块里

polls/urls.py

from django.urls import path

from . import views

urlpatterns = [
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('<int:question_id>/', views.detail, name='detail'),
    # ex: /polls/5/results/
    path('<int:question_id>/results/', views.results, name='results'),
    # ex: /polls/5/vote/
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

当某人请求你网站的某一页面时——比如说, "/polls/34/" ,Django 将会载入 mysite.urls 模块,因为这在配置项 ROOT_URLCONF 中设置了。然后 Django 寻找名为 urlpatterns 变量并且按序匹配正则表达式。在找到匹配项 'polls/' ,它切掉了匹配的文本( "polls/" ),将剩余文本—— "34/" ,发送至 'polls.urls' URLconf 做进一步处理。在这里剩余文本匹配了 '<int:question_id>/' ,使得我们 Django 以如下形式调用 detail() :

detail(request=<HttpRequest object>, question_id=34)

question_id=34<int:question_id> 匹配生成。使用尖括号“捕获”这部分 URL,且以关键字参数的形式发送给视图函数。上述字符串的 :question_id> 部分定义了将被用于区分匹配模式的变量名,而 int: 则是一个转换器决定了应该以什么变量类型匹配这部分的 URL 路径。

引入模板

在 polls/views.py

from django.http import HttpResponse

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)

# Leave the rest of the views (detail, results, vote) unchanged

但是我们页面的设计依然只是简单的HttpResponse,我们可以使用 Django 的模板系统,只需要创建一个视图就可以将页面的设计从代码中分离出来。

创建polls/templates/polls/index.html,因为 app_directories 模板加载器是通过上述描述的方法运行的,所以 Django 可以引用到 polls/index.html 这一模板了。

polls/templates/polls/index.html

{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

模板命名空间

虽然我们现在可以将模板文件直接放在 polls/templates 文件夹中(而不是再建立一个 polls 子文件夹),但是这样做不太好。Django 将会选择第一个匹配的模板文件,如果你有一个模板文件正好和另一个应用中的某个模板文件重名,Django 没有办法 区分 它们。我们需要帮助 Django 选择正确的模板,最好的方法就是把他们放入各自的 命名空间 中,也就是把这些模板放入一个和 自身 应用重名的子文件夹里。

现在更新一下 views.py:

from django.http import 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)

render()

render() 可以方便快捷的进行「载入模板,填充上下文,再返回由它生成的 HttpResponse 对象」的操作流程,它的第一个参数是request类型,第二个是模板的地址,第三个是一个字典,提供了模板所需要的变量。

404错误

继续修改 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("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})

如果问题不存在就会抛出一个404异常

get_object_or_404()

尝试用 get() 函数获取一个对象,如果不存在就抛出 Http404 错误,这个快捷函数可以简化代码:

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

同样,也有 get_list_or_404() 函数,不过是get换成了filter

现在创建detail的模板,

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>

模板系统统一使用点符号来访问变量的属性。在示例 {{ question.question_text }} 中,首先 Django 尝试对 question 对象使用字典查找(也就是使用 obj.get(str) 操作),如果失败了就尝试属性查找(也就是 obj.str 操作),结果是成功了。如果这一操作也失败的话,将会尝试列表查找(也就是 obj[int] 操作)。

{ % for %} 循环中发生的函数调用: question.choice_set.all 被解释为 Python 代码 question.choice_set.all() ,将会返回一个可迭代的 Choice 对象,这一对象可以在 { % for %} 标签内部使用。

查看 模板指南 可以了解关于模板的更多信息。

去除硬编码

由于在 polls.urls 模块的URL定义中定义了name参数,因此可以使用 {% url %} 标签代替

<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

命名空间

由于可能有若干个应用,这些应用的url的命名可能相同,因此可以在根 URLconf 中添加命名空间。

只需要在urls.py 中加上app_name设置命名空间:

app_name = 'polls'

然后修改 index.html:

<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

创建表单

修改 polls/templates/polls/detail.html

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
    <legend><h1>{{ question.question_text }}</h1></legend>
    {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
    {% for choice in question.choice_set.all %}
        <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
        <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
    {% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>

简要说明:

  • 上面的模板在 Question 的每个 Choice 前添加一个单选按钮。 每个单选按钮的 value 属性是对应的各个 Choice 的 ID。每个单选按钮的 name"choice" 。这意味着,当有人选择一个单选按钮并提交表单提交时,它将发送一个 POST 数据 choice=# ,其中# 为选择的 Choice 的 ID。这是 HTML 表单的基本概念。
  • 我们设置表单的 action{% url 'polls:vote' question.id %} ,并设置 method="post" 。使用 method="post" (与其相对的是 method="get")是非常重要的,因为这个提交表单的行为会改变服务器端的数据。 无论何时,当你需要创建一个改变服务器端数据的表单时,请使用 `method="post" 。这不是 Django 的特定技巧;这是优秀的网站开发技巧。
  • forloop.counter 指示 for 标签已经循环多少次。
  • 由于我们创建一个 POST 表单(它具有修改数据的作用),所以我们需要小心跨站点请求伪造, 不过 Django 自带了一个非常有用的防御系统。 简而言之,所有针对内部 URL 的 POST 表单都应该使用 { % csrf_token %} 模板标签。

修改 polls/views.py

from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse

from .models import Choice, Question
# ...
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
  • request. POST 是一个类字典对象,让你可以通过关键字的名字获取提交的数据。 这个例子中, request.POST['choice'] 以字符串形式返回选择的 Choice 的 ID。它 的值永远是字符串。

    注意,Django 还以同样的方式提供 request. GET 用于访问 GET 数据 —— 但我们在代码中显式地使用 POST ,以保证数据只能通过 POST 调用改动。

  • 如果在 request.POST['choice'] 数据中没有提供 choice , POST 将引发一个 KeyError 。如果没有给出 choice 将重新显示 Question 表单和一个错误信息。

  • 在增加 Choice 的得票数之后,代码返回一个 HttpResponseRedirect 而不是常用的 HttpResponse. HttpResponseRedirect 只接收一个参数:用户将要被重定向的 URL(。

    应当始终返回 HttpResponseRedirect 当进行 POST 的修改,这样会避免重复提交表单。

  • 在这个例子中,我们在 HttpResponseRedirect 的构造函数中使用 reverse() 函数。这个函数避免了我们在视图函数中硬编码 URL。它需要我们给出我们想要跳转的视图的名字和该视图所对应的 URL 模式中需要给该视图提供的参数。

然后再修改 results:

def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})

我们的 vote() 视图代码有一个小问题。代码首先从数据库中获取了 selected_choice 对象,接着计算 vote 的新值,最后把值存回数据库。如果网站有两个方可同时投票在 同一时间 ,可能会导致问题。同样的值,42,会被 votes 返回。然后,对于两个用户,新值43计算完毕,并被保存,但是期望值是44。

这个问题被称为 竞争条件使用 F() 避免竞争条件

通用视图

基本的 Web 开发中的一个常见情况:根据 URL 中的参数从数据库中获取数据、载入模板文件然后返回渲染后的模板。 由于这种情况特别常见,Django 提供一种快捷方式,叫做“通用视图”系统。通用视图将常见的模式抽象化,可以使你在编写应用时甚至不需要编写Python代码。

改良 URLconf

polls/urls.py

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

改良视图

polls/views.py

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from .models import Choice, Question

class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]

class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'

class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'

def vote(request, question_id):
    ... # same as above, no changes needed.

我们在这里使用两个通用视图: ListViewDetailView 。这两个视图分别抽象“显示一个对象列表”和“显示一个特定类型对象的详细信息页面”这两种概念。

  • 每个通用视图需要知道它将作用于哪个模型。 这由 model 属性提供。
  • DetailView 期望从 URL 中捕获名为 "pk" 的主键值,所以我们为通用视图把 question_id 改成 pk

如果我们提供了template_name,那么通用视图就会使用这个模板。对于DetailView, question 变量会自动提供,因为我们使用了Django的模型,它能为context定一个合适的名字。而对于 ListView, 自动生成的变量是question_list,因此我们需要主动提供context_object_name这一属性。

更多关于通用视图的详细信息,查看 通用视图的文档

测试

按照惯例,Django 应用的测试应该写在应用的 tests.py 文件里。测试系统会自动的在所有以 tests 开头的文件里寻找并执行测试代码。

polls/tests.py]

import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question

class QuestionModelTests(TestCase):

    def test_was_published_recently_with_future_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is in the future.
        """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

在终端中,我们通过输入以下代码运行测试:

python manage.py test polls

以下是自动化测试的运行过程:

  • python manage.py test polls 将会寻找 polls 应用里的测试代码
  • 它找到了 django.test. TestCase 的一个子类
  • 它创建一个特殊的数据库供测试使用
  • 它在类中寻找测试方法——以 test 开头的方法。
  • test_was_published_recently_with_future_question 方法中,它创建了一个 pub_date 值为 30 天后的 Question 实例。
  • 接着使用 assertls() 方法,发现 was_published_recently() 返回了 True,而我们期望它返回 False

测试系统通知我们哪些测试样例失败了,和造成测试失败的代码所在的行号。

当需要测试的时候,测试用例越多越好

如果你对测试有个整体规划,那么它们就几乎不会变得混乱。下面有几条好的建议:

  • 对于每个模型和视图都建立单独的 TestClass
  • 每个测试方法只测试一个功能
  • 给每个测试方法起个能描述其功能的名字

文档 Django 中的测试 里有关于测试的更多信息。

静态文件

除了服务端生成的 HTML 以外,网络应用通常需要一些额外的文件——比如图片,脚本和样式表——来帮助渲染网络页面。在 Django 中,我们把这些文件统称为“静态文件”。

polls 目录下创建一个名为 static 的目录。Django 将在该目录下查找静态文件。

更多关于设置和框架的资料,参考 静态文件解惑静态文件指南部署静态文件 介绍了如何在真实服务器上使用静态文件。

自定义后台表单

通过 admin.site.register(Question) 注册 Question 模型,Django 能够构建一个默认的表单用于展示。通常来说,我们期望能自定义表单的外观和工作方式。可以在注册模型时将这些设置告诉 Django。

from django.contrib import admin

from .models import Question

class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['question_text']}),
        ('Date information', {'fields': ['pub_date']}),
    ]

admin.site.register(Question, QuestionAdmin)

这会改变表单的顺序,并且分类展示

我们还可以增加投票选项

polls/admin.py

from django.contrib import admin

from .models import Choice, Question

class ChoiceInline(admin.StackedInline):
    model = Choice
    extra = 3

class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['question_text']}),
        ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
    ]
    inlines = [ChoiceInline]

admin.site.register(Question, QuestionAdmin)

Add question page now has choices on it

extra定义了3个预先的选项插槽。

此外通过更改

class ChoiceInline(admin.TabularInline):

可以将关联对象以表格形式呈现,更加紧凑。

Add question page now has more compact choices

增加过滤器

list_filter = ['pub_date']

增加搜索

search_fields = ['question_text']