django[一] Django应用-迁
Django应用基础
请求与响应
新建项目
$ django-admin startproject mysite
注意:在给项目命名的时候必须避开Django和Python的保留关键字,比如“django”,“test”等,否则会引起冲突和莫名的错误。对于mysite的放置位置,不建议放在传统的/var/www目录下,它会具有一定的数据暴露危险,因此Django建议你将项目文件放在例如/home/mycode类似的位置。
一个新建立的项目结构大概如下:
mysite/ manage.py mysite/ __init__.py settings.py urls.py wsgi.py
- 外层的
mysite/
目录与Django无关,只是你项目的容器,可以任意命名。 manage.py
:一个命令行工具,用于与Django进行不同方式的交互脚本,非常重要!- 内层的
mysite/
目录是真正的项目文件包裹目录,它的名字是你引用内部文件的包名,例如:mysite.urls
。 mysite/__init__.py
:一个定义包的空文件。mysite/settings.py
:项目的主配置文件,非常重要!mysite/urls.py
:路由文件,所有的任务都是从这里开始分配,相当于Django驱动站点的内容表格,非常重要!mysite/wsgi.py
:一个基于WSGI的web服务器进入点,提供底层的网络通信功能,通常不用关心。
启动开发服务器
python manage.py runserver 0.0.0.0:8000
Django提供了一个用于开发的web服务器,使你无需配置一个类似Ngnix的线上服务器,就能让站点运行起来。这是一个由Python编写的轻量级服务器,简易并且不安全,因此不要将它用于生产环境。
指定settings文件
python manage.py runserver 0.0.0.0:8000 --settings=webResty2.settings_dev
这时,Django将运行在8000端口,整个局域网内都将可以访问站点,而不只是是本机。
注意: Django的开发服务器具有自动重载功能,当你的代码有修改,每隔一段时间服务器将自动更新。但是,有一些例如增加文件的动作,不会触发服务器重载,这时就需要你自己手动重启。
报错
出现 ImportError: No module named django 但是 import django却是成功的,为啥呢? 原来在 /usr/lib/python2.7/importlib/__init__.py中有一个动态导入方法:__import__(name) 在setting app_install中有任意一个包出现问题都会导致报这个错,排了很久后来最原始的方法 print name,"=======" 才发现是一个包有问题
创建投票应用(app)
python manage.py startapp polls
polls/views.py
from django.http import HttpResponse def index(request): return HttpResponse("Hello, world. You're at the polls index.")
为了调用该视图,我们还需要编写urlconf,也就是路由路径。现在,在polls目录中新建一个文件,名字为urls.py
,在其中输入代码如下:
from django.conf.urls import url from . import views urlpatterns = [ url(r'^$', views.index, name='index'), ]
接下来,在项目的主urls文件中添加urlpattern
条目,指向我们刚才建立的polls这个app独有的urls文件,这里需要导入include模块
from django.conf.urls import include, url from django.contrib import admin urlpatterns = [ url(r'^polls/', include('polls.urls')), url(r'^admin/', admin.site.urls), ]
模型与管理后台
数据库安装
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'mysite', 'HOST': '192.168.1.1', 'USER': 'root', 'PASSWORD': 'pwd', 'PORT': '3306', } }
- 在使用非SQLite的数据库时,请务必预先在数据库管理系统的提示符交互模式下创建数据库,你可以使用命令:“CREATE DATABASE database_name;”。Django不会自动帮你做这一步工作。
- 确保你在settings文件中提供的数据库用户具有创建数据库表的权限,因为在接下来的教程中,我们需要自动创建一个test数据表。(在实际项目中也需要确认这一条要求。)
- 如果你使用的是SQLite,那么你无需做任何预先配置,直接使用就可以了。
在修改settings文件时,请顺便将TIME_ZONE
设置为国内所在的时区Asia/Shanghai
。
同时,请注意settings文件中顶部的INSTALLED_APPS
设置项。它列出了所有的项目中被激活的Django应用(app)。你必须将你自定义的app注册在这里。每个应用可以被多个项目使用,并且可以打包和分发给其他人在他们的项目中使用。
默认情况,INSTALLED_APPS
中会自动包含下列条目,它们都是Django自动生成的:
- django.contrib.admin:admin管理后台站点
- django.contrib.auth:身份认证系统
- django.contrib.contenttypes:内容类型框架
- django.contrib.sessions:会话框架
- django.contrib.messages:消息框架
- django.contrib.staticfiles:静态文件管理框架
上面的一些应用也需要建立一些数据库表,所以在使用它们之前我们要在数据库中创建这些表。使用下面的命令创建数据表:
python manage.py migrate
创建模型
# 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)
启用模型
# mysite/settings.py INSTALLED_APPS = [ 'polls', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ]
加入到项目列表
python manage.py makemigrations polls
通过运行makemigrations
命令,相当于告诉Django你对模型有改动,并且你想把这些改动保存为一个“迁移(migration)”。
migrations
是Django保存模型修改记录的文件,这些文件保存在磁盘上。在例子中,它就是polls/migrations/0001_initial.py
,你可以打开它看看,里面保存的都是人类可读并且可编辑的内容,方便你随时手动修改。
接下来有一个叫做migrate
的命令将对数据库执行真正的迁移动作。但是在此之前,让我们先看看在migration的时候实际执行的SQL语句是什么。有一个叫做sqlmigrate
的命令可以展示SQL语句,例如:
python manage.py sqlmigrate polls 0001
最后提交到数据库,就可以做一个真实的改动
$ python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, dashboard, djcelery, nginxadmin, sessions, vsvms Running migrations: Applying vsvms.0002_auto_20190114_1550... OK
如果migrate报错且无法解决,删除db数据库,删除各个app下的__pycache__和migrations目录下的0001*文件和,然后确保migrations和里面的__init__.py文件存在就可以自动migrations了
使用模型的API
python manage.py shell
检查代码的有效性 python manage.py validate 产生对应模型的sql python manage.py sqlall books django 1.6 之前 同步数据库 python manage.py syncdb django 1.6 之后 同步数据库 python manage.py makemigrations <app> //产生sql及修改记录 python manage.py migrate <app> // 应用到数据库 python manage.py showmigrations <app> // 查看所有的历史版本 python manage.py migrate <app> zero // 某个版本回到初始状态,一般用于建表错误,然后又有migrations文件的情况下 python manage.py migrate <app> --fake // 一般情况下由于之前zero了之后,表还存在,继续migrate会报错,这里加--fake,说明仅仅产生这个版本但是不执行到数据库 python manage.py sqlmigrate select_related 0001 // 提取sql语句,到数据库自己执行
命令测试
>>> from polls.models import Question, Choice # 导入我们写的模型类 # 现在系统内还没有questions对象 >>> Question.objects.all() <QuerySet []> # 创建一个新的question对象 # Django推荐使用timezone.now()代替python内置的datetime.datetime.now() # 这个timezone就来自于Django唯一的依赖库pytz from django.utils import timezone >>> q = Question(question_text="What's new?", pub_date=timezone.now()) # 你必须显式的调用save()方法,才能将对象保存到数据库内 >>> q.save() # 默认情况,你会自动获得一个自增的名为id的主键 >>> q.id 1 # 通过python的属性调用方式,访问模型字段的值 >>> q.question_text "What's new?" >>> q.pub_date datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>) # 通过修改属性来修改字段的值,然后显式的调用save方法进行保存。 >>> q.question_text = "What's up?" >>> q.save() # objects.all() 用于查询数据库内的所有questions >>> Question.objects.all() <QuerySet [<Question: Question object>]>
admin后台管理站点
python manage.py createsuperuser
注意:Django1.10版本后,超级用户的密码强制要求具备一定的复杂性,不能再偷懒了
在实际环境中,为了站点的安全性,我们不能将管理后台的url随便暴露给他人,不能用/admin/
这么简单的路径。
打开根url路由文件mysite/urls.py
,修改其中admin.site.urls对应的正则表达式,换成你想要的
from django.conf.urls import url from django.contrib import admin admin.autodiscover() # 很重要 urlpatterns = [ url(r'^my/set/', admin.site.urls), ]
这样,我们必须访问http://127.0.0.1:8000/my/set/
才能进入admin界面。
必须先在admin中进行注册,告诉admin站点,请将polls的模型加入站点内,接受站点的管理。打开polls/admin.py
文件,加入下面的内容
from django.contrib import admin from .models import Question admin.site.register(Question)
视图和模板
编写视图
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)
然后,在polls/urls.py
文件中加入下面的url模式,将其映射到我们上面新增的视图。
from django.conf.urls import url from . import views urlpatterns = [ # ex: /polls/ url(r'^$', views.index, name='index'), # ex: /polls/5/ url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'), # ex: /polls/5/results/ url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'), # ex: /polls/5/vote/ url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'), ]
现在去浏览器中访问/polls/34/
(注意:这里省略了域名。另外,使用了二级路由后,url中都要添加polls部分,参考前面的章节),它将调用detail()
函数,然后在页面中显示你在url里提供的ID。访问/polls/34/results/
和/polls/34/vote/
,将分别显示预定义的伪结果和投票页面。(PS:这里就不贴图了,请大家务必自己动手测试,多实践。)
上面访问的路由过程如下:当有人访问/polls/34/
地址时,Django将首先加载mysite.urls
模块,因为它是settings文件里设置的根URL配置文件。在该文件里,Django发现了urlpatterns
变量,于是在其内按顺序进行匹配。当它匹配上了^polls/
,就裁去url中匹配的文本polls/
,然后将剩下的文本“34/”,传递给polls.urls
进行下一步的处理。在polls.urls
中,又匹配到了r’^(?P<question_id>[0-9]+)/$’
,最终结果就是调用该模式对应的detail()视图,也就是下面的函数:
detail(request=<HttpRequest object>, question_id='34')
视图举例
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) # 下面是那些没改动过的视图(detail, results, vote)
将下列代码写入文件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/views.py
,让新的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()
在实际运用中,加载模板、传递参数,返回HttpResponse对象是一整套再常用不过的操作了,为了节省力气,Django提供了一个快捷方式:render函数,一步到位!看如下代码:
polls/views.py
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)
返回404错误
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})
这里有个新知识点,如果请求的问卷ID不存在,那么会弹出一个Http404错误。
新建polls/detail.html
文件,暂时写入下面的代码:
{{ question }}
快捷方式:get_object_or_404()
就像render函数一样,Django同样为你提供了一个偷懒的方式,替代上面的多行代码,那就是get_object_or_404()
方法,参考下面的代码:
polls/views.py
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})
get_object_or_404()
方法将一个Django模型作为第一个位置参数,后面可以跟上任意个数的关键字参数,如果对象不存在则弹出Http404错误。
同样,还有一个get_list_or_404()
方法,和上面的get_object_or_404()
类似,只不过是用来替代filter()
函数,当查询列表为空时弹出404错误。(filter是模型API中用来过滤查询结果的函数,它的结果是一个列表集。而get则是查询一个结果的方法,和filter是一个和多个的区别!)
使用模板系统
detail()
视图会将上下文变量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>
在模板系统中圆点.
是万能的魔法师,你可以用它访问对象的属性。在例子{{ question.question_text }}
中,DJango首先会在question对象中尝试查找一个字典,如果失败,则尝试查找属性,如果再失败,则尝试作为列表的索引进行查询。
在 {% for %}
循环中的方法调用——question.choice_set.all
其实就是Python的代码question.choice_set.all()
,它将返回一组可迭代的Choice
对象,并用在{% for %}
标签中。
使用 {{ forloop.counter }} 可以获取本次循环的计数,可以用作id值
删除模板中硬编码的URLs
在polls/index.html
文件中,还有一部分硬编码存在,也就是href里的“/polls/”部分:
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
它对于代码修改非常不利。设想如果你在urls.py文件里修改了正则表达式,那么你所有的模板中对这个url的引用都需要修改,这是无法接受的!
我们前面给urls定义了一个name别名,可以用它来解决这个问题。具体代码如下:
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
Django会在polls.urls
文件中查找name='detail'
的url,具体的就是下面这行:
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
举个栗子,如果你想将polls的detail视图的URL更换为polls/specifics/12/
,那么你不需要在模板中重新修改url地址了,仅仅只需要在polls/urls.py
文件中,将对应的正则表达式改成下面这样的就行了,所有模板中对它的引用都会自动修改成新的链接:
# 添加新的单词'specifics' url(r'^specifics/(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
URL names的命名空间
本教程例子中,只有一个app也就是polls,但是在现实中很显然会有5个、10个、更多的app同时存在一个项目中。Django是如何区分这些app之间的URL name呢?
答案是使用URLconf的命名空间。在polls/urls.py文件的开头部分,添加一个app_name
的变量来指定该应用的命名空间
from django.conf.urls import url from . import views app_name = 'polls' # 关键是这行 urlpatterns = [ url(r'^$', views.index, name='index'), url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'), url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'), url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'), ]
现在,让我们将代码修改得更严谨一点,将polls/templates/polls/index.html
中的
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
修改为:
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
url()方法
url()方法可以接收4个参数,其中2个是必须的:regex
和view
,以及2个可选的参数:kwargs
和name
。
regex:
regex是正则表达式的通用缩写,它是一种匹配字符串或url地址的语法。Django拿着用户请求的url地址,在urls.py
文件中对urlpatterns
列表中的每一项条目从头开始进行逐一对比,一旦遇到匹配项,立即执行该条目映射的视图函数或下级路由,其后的条目将不再继续匹配。因此,url路由的编写顺序非常重要!
需要注意的是,regex不会去匹配GET或POST参数或域名,例如对于https://www.example.com/myapp/
,regex只尝试匹配myapp/
。对于https://www.example.com/myapp/?page=3
,regex也只尝试匹配myapp/
。
如果你想深入研究正则表达式,可以读一些相关的书籍或专论,但是在Django的实际应用中,你不需要多高深的正则表达式知识,在Python教程部分有正则表达式相关专题,可供学习参考。
当URLconf模块加载的时候会预先编译正则表达式,因此它的匹配搜索速度非常快,你通常感觉不到。
view:
view指的是处理当前url请求的视图函数。当正则表达式匹配到某个条目时,自动将封装的HttpRequest
对象作为第一个参数,正则表达式“捕获”到的值作为第二个参数,传递给该条目指定的视图view。如果是简单捕获,那么捕获值将作为一个位置参数进行传递,如果是命名捕获,那么将作为关键字参数进行传递。
kwargs:
任意数量的关键字参数可以作为一个字典传递给目标视图。
name:
对你的URL进行命名,让你能够在Django的任意处,尤其是模板内显式地引用它。这是一个非常强大的功能,相当于给URL取了个全局变量名,不会将url匹配地址写死。
url()方法的四个参数,每个都非常有讲究,这里先做基本的介绍,在后面有详细的论述。
表单和类视图
表单form
为了接收用户的投票选择,我们需要在前端页面显示一个投票界面。让我们重写先前的polls/detail.html
文件,代码如下:
<h1>{{ question.question_text }}</h1> {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %} <form action="{% url 'polls:vote' question.id %}" method="post"> {% csrf_token %} {% 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 %} <input type="submit" value="Vote" /> </form>
- 上面的模板显示一系列单选按钮,按钮的值是选项的ID,按钮的名字是字符串"choice"。这意味着,当你选择了其中某个按钮,并提交表单,一个包含数据
choice=#
的POST请求将被发送到指定的url,#
是被选择的选项的ID。这就是HTML表单的基本概念。 - 如果你有一定的前端开发基础,那么form标签的action属性和method属性你应该很清楚它们的含义,action表示你要发送的目的url,method表示提交数据的方式,一般分POST和GET。
- forloop.counter是DJango模板系统专门提供的一个变量,用来表示你当前循环的次数,一般用来给循环项目添加有序数标。
- 由于我们发送了一个POST请求,就必须考虑一个跨站请求伪造的安全问题,简称CSRF(具体含义请百度)。Django为你提供了一个简单的方法来避免这个困扰,那就是在form表单内添加一条{% csrf_token %}标签,标签名不可更改,固定格式,位置任意,只要是在form表单内。这个方法对form表单的提交方式方便好使,但如果是用ajax的方式提交数据,那么就不能用这个方法了。
现在,让我们创建一个处理提交过来的数据的视图。前面我们已经写了一个“占坑”的vote视图的url(polls/urls.py):
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
以及“占坑”的vote视图函数(polls/views.py),我们把坑填起来:
from django.shortcuts import get_object_or_404, render from django.http import HttpResponseRedirect, HttpResponse 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): # 发生choice未找到异常时,重新返回表单页面,并给出提示信息 return render(request, 'polls/detail.html', { 'question': question, 'error_message': "You didn't select a choice.", }) else: selected_choice.votes += 1 selected_choice.save() # 成功处理数据后,自动跳转到结果页面,防止用户连续多次提交。 return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
有些新的东西,我们要解释一下:
- request.POST是一个类似字典的对象,允许你通过键名访问提交的数据。本例中,
request.POST[’choice’]
返回被选择选项的ID,并且值的类型永远是string字符串,那怕它看起来像数字!同样的,你也可以用类似的手段获取GET请求发送过来的数据,一个道理。 request.POST[’choice’]
有可能触发一个KeyError异常,如果你的POST数据里没有提供choice键值,在这种情况下,上面的代码会返回表单页面并给出错误提示。PS:通常我们会给个默认值,防止这种异常的产生,例如request.POST[’choice’,None]
,一个None解决所有问题。- 在选择计数器加一后,返回的是一个
HttpResponseRedirect
而不是先前我们常用的HttpResponse
。HttpResponseRedirect需要一个参数:重定向的URL。这里有一个建议,当你成功处理POST数据后,应当保持一个良好的习惯,始终返回一个HttpResponseRedirect。这不仅仅是对Django而言,它是一个良好的WEB开发习惯。 - 我们在上面HttpResponseRedirect的构造器中使用了一个
reverse()
函数。它能帮助我们避免在视图函数中硬编码URL。它首先需要一个我们在URLconf中指定的name,然后是传递的数据。例如'/polls/3/results/'
,其中的3是某个question.id
的值。重定向后将进入polls:results
对应的视图,并将question.id
传递给它。白话来讲,就是把活扔给另外一个路由对应的视图去干。
当有人对某个问题投票后,vote()视图重定向到了问卷的结果显示页面。下面我们来写这个处理结果页面的视图(polls/views.py):
from django.shortcuts import get_object_or_404, render def results(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/results.html', {'question': question})
同样,还需要写个模板polls/templates/polls/results.html
。(路由、视图、模板、模型!都是这个套路....)
<h1>{{ question.question_text }}</h1> <ul> {% for choice in question.choice_set.all %} <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li> {% endfor %} </ul> <a href="{% url 'polls:detail' question.id %}">Vote again?</a>
.......... 以上部分略,请参考原站:http://www.liujiangblog.com/course/django/90
使用类视图:减少重复代码
通常在写一个Django的app时,我们一开始就要决定使用类视图还是不用,而不是等到代码写到一半了才重构你的代码成类视图
打开polls/urls.py
文件,将其修改成下面的样子:
from django.conf.urls import url from . import views app_name = 'polls' urlpatterns = [ url(r'^$', views.IndexView.as_view(), name='index'), url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'), url(r'^(?P<pk>[0-9]+)/results/$', views.ResultsView.as_view(), name='results'), url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'), ]
polls/views.py
文件,删掉index、detail和results视图,替换成Django的类视图
from django.shortcuts import get_object_or_404, render from django.http import HttpResponseRedirect 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): """返回最近发布的5个问卷.""" 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): ... # 这个视图未改变!!!
在这里,我们使用了两种类视图ListView
和DetailView
(它们是作为父类被继承的)。这两者分别代表“显示一个对象的列表”和“显示特定类型对象的详细页面”的抽象概念
-
每一种类视图都需要知道它要作用在哪个模型上,这通过model属性提供。
-
DetailView
类视图需要从url捕获到的称为"pk"的主键值,因此我们在url文件中将2和3条目的<question_id>
修改成了<pk>
。
默认情况下,DetailView
类视图使用一个称作<app name>/<model name>_detail.html
的模板。在本例中,实际使用的是polls/detail.html
。template_name
属性就是用来指定这个模板名的,用于代替自动生成的默认模板名。(一定要仔细观察上面的代码,对号入座,注意细节。)同样的,在resutls列表视图中,指定template_name
为'polls/results.html'
,这样就确保了虽然resulst视图和detail视图同样继承了DetailView类,使用了同样的model:Qeustion,但它们依然会显示不同的页面。(模板不同嘛!so easy!)
类似的,ListView类视图使用一个默认模板称为<app name>/<model name>_list.html
。我们也使用template_name
这个变量来告诉ListView使用我们已经存在的"polls/index.html"
模板,而不是使用它自己默认的那个。
在教程的前面部分,我们给模板提供了一个包含question
和latest_question_list
的上下文变量。而对于DetailView,question变量会被自动提供,因为我们使用了Django的模型(Question),Django会智能的选择合适的上下文变量。然而,对于ListView,自动生成的上下文变量是question_list
。为了覆盖它,我们提供了context_object_name
属性,指定说我们希望使用latest_question_list
而不是question_list
。
测试
编写测试程序
很巧,在我们的投票应用中有一个小bug需要修改:在Question.was_published_recently()
方法的返回值中,当Qeustion在最近的一天发布的时候返回True(这是正确的),然而当Question在未来的日期内发布的时候也返回True(这是错误的)
>>> import datetime >>> from django.utils import timezone >>> from polls.models import Question >>> # 创建一个发布日期在30天后的问卷 >>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30)) >>> # 测试一下返回值 >>> future_question.was_published_recently() True
问题的核心在于我们允许创建在未来时间才发布的问卷,由于“未来”不等于“最近”,因此这显然是个bug。
创建一个测试来暴露这个bug
刚才我们是在shell中测试了这个bug,那如何通过自动化测试来发现这个bug呢?
通常,我们会把测试代码放在应用的tests.py
文件中,测试系统将自动地从任何名字以test开头的文件中查找测试程序。每个app在创建的时候,都会自动创建一个tests.py
文件,就像views.py
等文件一样。
将下面的代码输入投票应用的polls/tests.py
文件中:
import datetime from django.utils import timezone from django.test import TestCase from .models import Question class QuestionMethodTests(TestCase): def test_was_published_recently_with_future_question(self): """ 在将来发布的问卷应该返回False """ time = timezone.now() + datetime.timedelta(days=30) future_question = Question(pub_date=time) self.assertIs(future_question.was_published_recently(), False)
我们在这里创建了一个django.test.TestCase
的子类,它具有一个方法,该方法创建一个pub_date
在未来的Question实例。最后我们检查was_published_recently()
的输出,它应该是 False。
运行测试程序
python manage.py test polls
你将看到结果如下:
Creating test database for alias 'default'... F ====================================================================== FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question self.assertIs(future_question.was_published_recently(), False) AssertionError: True is not False ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1) Destroying test database for alias 'default'..
这其中都发生了些什么?:
python manage.py test polls
命令会查找投票应用中所有的测试程序- 发现一个
django.test.TestCase
的子类 - 为测试创建一个专用的数据库
- 查找名字以
test
开头的测试方法 - 在
test_was_published_recently_with_future_question
方法中,创建一个Question实例,该实例的pub_data字段的值是30天后的未来日期。 - 然后利用
assertIs()
方法,它发现was_published_recently()
返回了True,而不是我们希望的False。
最后,测试程序会通知我们哪个测试失败了,错误出现在哪一行。
整个测试用例基本上和Python内置的unittest非常相似,大家可以参考Python教程中测试相关的章节。
修复bug
# polls/models.py def was_published_recently(self): now = timezone.now() return now - datetime.timedelta(days=1) <= self.pub_date <= now
再次运行测试程序:
Creating test database for alias 'default'... . ---------------------------------------------------------------------- Ran 1 test in 0.001s OK Destroying test database for alias 'default'...
更加全面的测试
事实上,前面的测试用例还不够完整,为了使was_published_recently()
方法更加可靠,我们在上面的测试类中再额外添加两个其它的方法,来更加全面地进行测试
# polls/tests.py def test_was_published_recently_with_old_question(self): """ 只要是超过1天的问卷,返回False """ time = timezone.now() - datetime.timedelta(days=1, seconds=1) old_question = Question(pub_date=time) self.assertIs(old_question.was_published_recently(), False) def test_was_published_recently_with_recent_question(self): """ 最近一天内的问卷,返回True """ time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59) recent_question = Question(pub_date=time) self.assertIs(recent_question.was_published_recently(), True)
现在我们有三个测试来保证无论发布时间是在过去、现在还是未来Question.was_published_recently()
都将返回正确的结果。
关于测试的内容,虽然很重要,但是对于刚入门者而言却不是最首要的知识点,等将主体内容掌握后,我们再回头来梳理这一部分。
静态文件
除了由服务器生成的HTML文件外,WEB应用一般需要提供一些其它的必要文件,比如图片文件、JavaScript脚本和CSS样式表等等,用来为用户呈现出一个完整的网页。在Django中,我们将这些文件统称为“静态文件”,因为这些文件的内容基本是固定不变的,不需要动态生成。
对于小项目,这些都不是大问题,你可以将静态文件放在任何你的web服务器能够找到的地方。但是对于大型项目,尤其是那些包含多个app在内的项目,处理那些由app带来的多套不同的静态文件是个麻烦活。
但这正是django.contrib.staticfiles
的用途:它收集每个应用(和任何你指定的地方)的静态文件到一个统一指定的地方,并且易于访问。
使用静态文件
首先在你的polls目录中创建一个static
目录。Django将在那里查找静态文件,这与Django在polls/templates/中寻找对应的模板文件的方式是一致的。
Django的STATICFILES_FINDERS
设置项中包含一个查找器列表,它们知道如何从各种源中找到静态文件。 其中一个默认的查找器是AppDirectoriesFinder
,它在每个INSTALLED_APPS
下查找static
子目录,例如我们刚创建的那个static
目录。admin管理站点也为它的静态文件使用相同的目录结构。
在刚才的static
目录中新建一个polls
子目录,再在该子目录中创建一个style.css
文件。换句话说,这个css样式文件应该是polls/static/polls/style.css
。你可以通过书写polls/style.css
在Django中访问这个静态文件,与你如何访问模板的路径类似。
静态文件的命名空间:
与模板类似,我们可以将静态文件直接放在polls/static
(而不是创建另外一个polls 子目录),但实际上这是一个坏主意。Django将使用它所找到的第一个匹配到的静态文件,如果在你的不同应用中存在两个同名的静态文件,Django将无法区分它们。我们需要告诉Django该使用其中的哪一个,最简单的方法就是为它们添加命名空间。也就是说,将这些静态文件放进以它们所在的应用的名字同名的另外一个子目录下(白话讲:多建一层与应用同名的子目录)。
PS:良好的目录结构是每个应用都应该创建自己的urls、views、models、templates和static,每个templates包含一个与应用同名的子目录,每个static也包含一个与应用同名的子目录。
添加背景图
下面,我们在polls/static/polls/
目录下创建一个用于存放图片的images
子目录,在这个子目录里放入`background.gif文件。换句话说,这个文件的路径是polls/static/polls/images/background.gif。(你可以使用任何你想要的图片)
在css样式文件polls/static/polls/style.css
中添加下面的代码:
body { background: white url("images/background.gif") no-repeat right bottom; }
重新加载http://localhost:8000/polls/
(CTRL+F5或者直接F5),你会在屏幕的右下方看到载入的背景图片。
提示:
很显然,{% static %}
模板标签不能用在静态文件,比如样式表中,因为他们不是由Django生成的。 你应该使用相对路径来相互链接静态文件,因为这样你可以改变STATIC_URL ( static模板标签用它来生成URLs)而不用同时修改一大堆静态文件中路径相关的部分。
直接访问静态文件
实际上不管是在Django开发服务器上,还是在nginx+uwsgi+django部署的服务器上,都可以直接通过url访问静态文件,不需要在Django中专门为每个静态文件编写url路由和视图。
比如,通过http://www.liujiangblog.com/static/images/default_avatar_male_50.gif你就可以直接获得网站用户的默认头像图片了。
本节简要的介绍了如何使用静态文件,更多的内容留待后续。
自定义admin站点
定制模型表单
在前面的学习过程中,通过admin.site.register(Question)
语句,我们在admin站点中注册了Question模型。Django会自动生成一个该模型的默认表单页面。如果你想自定义该页面的外观和工作方式,可以在注册对象的时候告诉Django你的自定义选项。
下面是一个修改admin表单默认排序方式的例子。修改polls/admin.py
的代码::
from django.contrib import admin from .models import Question class QuestionAdmin(admin.ModelAdmin): fields = ['pub_date', 'question_text'] admin.site.register(Question, QuestionAdmin)
你只需要创建一个继承admin.ModelAdmin
的模型管理类,然后将它作为第二个参数传递给admin.site.register()
,第一个参数则是Question模型本身。
上面的修改让Publication date
字段显示在Question
字段前面了(默认是在后面)。如下图所示:
对于只有2个字段的情况,效果看起来还不是很明显,但是如果你有一打的字段,选择一种直观的符合我们人类习惯的排序方式则非常有用。
还有,当表单含有大量字段的时候,你也许想将表单划分为一些字段的集合。再次修改polls/admin.py
:
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)
在3个插槽的最后,还有一个Add another Choice
链接。点击它,又可以获得一个新的插槽。如果你想删除新增的插槽,点击它最右边的灰色X图标即可。但是,默认的三个插槽不可删除。
这里还有点小问题。上面页面中插槽纵队排列的方式需要占据大块的页面空间,查看起来很不方便。为此,Django提供了一种扁平化的显示方式,你仅仅只需要修改一下ChoiceInline
继承的类为admin.TabularInline
替代先前的StackedInline
类(其实,从类名上你就能看出两种父类的区别)
# polls/admin.py class ChoiceInline(admin.TabularInline): #...
页面方法举例
class BookAdmin(admin.ModelAdmin): list_display = ('title', 'publisher', 'publication_date') #显示列,authors 是 manyTomany不能直接display出来,会出错,我看看怎么解决 list_filter = ('publication_date','title') #页面过滤器 date_hierarchy = 'publication_date' #日期层次划分 search_fields = ['question_text'] # 查询框 ordering = ('-publication_date',) #逆序 filter_horizontal = ('authors',) #新增条目提供多对多字段的复选框。 filter_horizontal和filter_vertical选项只能用在多对多字段 raw_id_fields = ('publisher',) #异步加载publisher元祖
定制admin整体界面
很明显,在每一个项目的admin页面顶端都显示Django administration
是很可笑的,它仅仅是个占位文本。利用Django的模板系统,我们可以快速修改它。
在manage.py
文件同级下创建一个templates
目录。然后,打开设置文件mysite/settings.py
,在TEMPLATES条目中添加一个DIRS选项:
# mysite/settings.py TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')], # 要有这一行,如果已经存在请保持原样 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
DIRS是一个文件系统目录的列表,是模板的搜索路径。当加载Django模板时,会在DIRS中进行查找
注意
django正式上线后,需要将settings.py中的"DEBUG = True"注释掉免得内存泄漏,同时在下面加上"ALLOWED_HOSTS = ['10.0.0.219']",使得admin网页可用(10.0.0.219是django server运行的主机ip)。这时候admin网页会比较丑陋而且有些功能不正常,因为非debug模式下django server不会帮忙处理静态文件,临时解决方案是在启动django server的时候添加--insecure选项(http://stackoverflow.com/questions/5836674/why-does-debug-false-setting-make-my-django-static-files-access-fail ),最好的解决方案是settings.py的INSTALLED_APPS中有'django.contrib.staticfiles'。
urls.py中添加 if settings.DEBUG is False: urlpatterns += patterns('', url(r'^static/(?P<path>.*)$', 'django.views.static.serve', { 'document_root': settings.STATIC_ROOT}), )
PS:模板的组织方式
就像静态文件一样,我们可以把所有的模板都放在一起,形成一个大大的模板文件夹,并且工作正常。但是请一定不要这么做!强烈建议每一个模板都应该存放在它所属应用的模板目录内(例如polls/templates)而不是整个项目的模板目录(templates),因为这样每个应用才可以被方便和正确的重用。只有对整个项目有作用的模板文件才放在根目录的templates中,比如admin界面。
回到刚才创建的templates目录中,再创建一个admin目录,将admin/base_site.html
模板文件拷贝到该目录内。这个HTML文件来自Django源码,它位于django/contrib/admin/templates
目录内。 (在我的windows系统中,它位于C:\Python36\Lib\site-packages\django\contrib\admin\templates\admin
,请大家参考。)
Django的源代码在哪里?
如果你无法找到Django源代码文件的存放位置,可以使用下面的命令:
python -c "import django; print(django.__path__)"
编辑base_site.html
文件,用你喜欢的站点名字替换掉{{ site_header|default:_(’Django administration’) }}
(包括两个大括号一起替换掉),看起来像下面这样:
{% extends "admin/base.html" %} {% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} {% block branding %} <h1 id="site-name"><a href="{% url 'admin:index' %}">投票站点管理界面</a></h1> {% endblock %} {% block nav-global %}{% endblock %}
在这里,我们使用的是硬编码,强行改名为"投票站点管理界面"。但是在实际的项目中,你可以使用django.contrib.admin.AdminSite.site_header
属性,方便的对这个页面title进行自定义。
修改完后,刷新页面,效果如下:
定制admin首页
默认情况下,admin首页显示所有INSTALLED_APPS
内并在admin应用中注册过的app,以字母顺序进行排序。
要定制admin首页,你需要重写admin/index.html
模板,就像前面修改base_site.html
模板的方法一样,从源码目录拷贝到你指定的目录内。编辑该文件,你会看到文件内使用了一个app_list
模板变量。该变量包含了所有已经安装的Django应用。你可以硬编码链接到指定对象的admin页面,使用任何你认为好的方法,用于替代这个app_list
。