通过一个调查问卷的例子来学习Django,它主要包括两部分:
- 一个公共站点,允许用户查看问卷并投票
- 一个管理员站点,允许添加、修改和删除问卷
假设你已经安装了Django,可以使用python -m django --version
来检测
创建一个项目
通过django-admin startproject mysite
创建一个项目
该命令会在当前目录创建一个
mysite
目录,切记不要用django
这个关键字来给项目命名。
创建得到的目录结构如下:
mysite/
manage.py
mysite/
__init__.py
settings.py
urls.py
wsgi.py
接下来一一解释这些文件的作用:
- 外部的
mysite/
根目录是整个项目的容器,它的名字并不重要,你可以自行重命名; manage.py
:一个命令行的组件,提供多种方式来和Django项目进行交互- 内部的
mysite/
目录是项目实际的Python包,它的名字就是将来import
时要用的,比如mysite.urls
mysite/settings.py
:当前项目的配置文件mysite/urls.py
:当前项目的url
声明语句mysite/wsgi.py
:WSGI兼容的web服务器的entry-point
开发服务器
通过python manage.py runserver
运行开发服务器
默认条件下便可以在浏览器中访问http://127.0.0.1:8000/
当然也可以更改端口python manage.py runserver 8080
, 这样的话在浏览器中也需要对应更改端口号
如果想更改服务器的ip,也可以python manage.py runserver 0:8000
服务器的自动重新加载机制
大多数情况下,当你更改了项目代码后,开发服务器会自动重新加载Python代码,这样就不用重启服务器了。当然,一些添加文件的操作时不会触发重新加载。
创建polls应用
在manage.py
所处目录中输入python manage.py startapp polls
来创建应用
该命令会创建如下目录结构:
polls/
__init__.py
admin.py
apps.py
migrations/
__init__.py
models.py
tests.py
views.py
编写第一个view
打开polls/views.py
文件,然后编写如下代码:
from django.http import HttpResponse
def index(request):
return HttpResponse("hello, world. You're at the polls index.")
这是Django中最简单的view,为了调用这个view,我们需要将其映射到一个URL上,因此我们需要一个URLconf
为了创建一个URLConf,创建urls.py
文件,此时应用的目录结构如下:
polls/
__init__.py
admin.py
apps.py
migrations/
__init__.py
models.py
tests.py
urls.py
views.py
在polls/urls.py
文件中编写如下代码:
from django.urls import path
from . import views
urlpatterns = {
path('', views.index, name='index'),
}
下一步,在项目的urls.py
文件中添加一个include()
:
from django.contrib import admin
from django.urls import include, path
urlpatterns = {
path('polls/', include('polls.urls')),
path('admin/', admin.site.urls),
}
这里的include()
函数允许引用其他的URLConf,无论何时Django遇到include()
函数,它将从url字符串中将当前URLConf匹配的部分砍掉,然后将剩余url字符串传递到include
所指定的URLConf作进一步处理
path()的四个参数
1. route
route是一个包含了url模式的字符串,当处理一个request的时候,Django从urlpattern中的第一个模式开始,顺序向下匹配url模式,直到遇到第一个匹配的项。匹配的内容只包含域名后面的部分,不包含get或者post参数,比如
http://www.example.com/myapp/
将只拿/myapp/
与urlpattern中的模式进行匹配,http://www.example.com/myapp/?page=3
同样也只拿/myapp/
进行匹配2. view
当遇到一个匹配的url模式,Django就会调用指定的view函数,并将HttpResponse作为第一个参数传递过去,同时url中捕获的其余关键参数也会传递过去
3. kwargs
当然除了规定好的HttpResponse参数和url中捕获的关键字参数,还有一些关键字参数也可以传递给指定的view,但是本教程不会使用这个功能
4. name
给URL命名可以避免歧义
启用数据库
打开mysite/settings.py
文件,这是个常规的Python模块,其中包含的模块级变量代表着Django的设置
Django默认使用SQLite数据库,这个数据库已经包含在了Python之中,所以不需要安装其他的任何东西
如果想使用其余的数据库,安装合适的数据库插件(database bindings)并更改配置文件中的DATABASE
属性,
- ENGINE:取值的范围
'django.db.backends.sqlite3'
,'django.db.backends.postgresql'
,'django.db.backends.mysql'
或者'django.db.backends.oracle'
等等 - NAME:数据库的名字
编辑mysite/settings.py
文件,设置TIME_ZONE
为自己的时区
另外,注意在INSTALLED_APPS
设置所包含的应用,默认情况下INSTALLED_APPS
包含以下应用:
django.contrib.admin
管理员站点django.contrib.auth
认证系统,用于登录注册的django.contrib.contenttypes
一个content types的框架django.contrib.sessions
一个会话框架django.contrib.message
一个消息框架django.contrib.staticfiles
一个处理静态文件的框架
为了满足常见的需求,这些应用默认情况都加入到了Django项目中,每个应用都会用到若干个数据库表,通过python manage.py migrate
来创建这些表格。
migrate
命令将查看INSTALLED_APPS
设置,然后根据mysite/settings.py
文件中的数据库设置来创建任何必要的数据库。
创建models
创建models就是在定义数据库的布局,包括额外的元数据。
在一个简单的poll应用中,创建两个models:Question和Choice,一个Question包含一个问题和发布日期,一个Choice有两个字段:选项的文本和一个投票计数器,每个选项和一个问题相关联。
这些概念都通过Python的类来实现。编辑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都是django.db.models.Model
的子类,并且都有一些类变量,每个都代表着数据库字段,每个字段都是Field
类的实例,分别表示字段的不同类型.
每个Field
实例的名字都是字段的名字,不过是给机器读的,在创建实例时可以通过参数指定给人阅读的名字,比如上述中的date published
就是以这种方式命名的.
一些Field
类需要一些参数,比如CharField
必须要指定max_length
,又比如有些Field
可以指定默认值default=0
除此之外还可以通过ForeignKey
来指定外键。Django支持常用的数据库关系:一对一,一对多,多对一。
激活models
这些少量代码可以给Django带来很多信息,有了这些信息,Django可以为应用创建一个数据库模式,也可以创建可以访问Question和Choice类的API.
接下来要把polls
这个应用的引用添加到INSTALLED_APPS
中去,PollsConfig
类存在于polls/apps.py
文件中,所以它的点路径为'polls.apps.PollsConfig'
,编辑mysite/settings.py
文件,把这个点路径添加到INSTALLED_APPS
设置中去
INSTALLED_APPS={
'polls.apps.PollsConfig',
...
}
现在Django已经包含了polls
应用,输入python manage.py makemigrations polls
,该命令告诉Django Model代码已经更改,所做的更改将被存储为migrations
migrations
以文件的形式被存储在硬盘上, Django把对models的更改都存储为迁移。可以去polls/migrations/0001_initial.py
查看
接下来使用python manage.py migrate
来运行migrations
,并自动部署数据库模式,这就被称为migrate
将以上所有的操作总结为三个步骤:
- 更改models
- 运行
python manage.py makemigrations
来为这些改变创建migrations
- 运行
python manage.py migrate
来将这些更改应用到数据库
为什么要将两个命令分开?主要是为了做好版本的控制,这些以后再详细讲解
通过API完成数据库操作
本小节进入Python交互式shell来使用Django提供的API
输入python manage.py shell
启动Python shell,进入shell后即可开始使用Django为数据库提供的API:
>> from polls.models import Choice, Question
>> Question.objects.all()
>> from django.utils import timezone
>> q = Question(question_text="What's new?", pub_date=timezone.now())
>> q.save()
>> q.id
>> q.question_text
>> q.pub_date
>> Question.objects.all()
当然,类对象的显式输出不适合人阅读,可以重写Question类进行更改,代码如下:
from django.db import models
class Question(models.Model):
def __str__(self):
return self.question_text
...
class Choice(models.Model):
def __str__(self):
return self.choice_text
...
这样的话,在Python shell中打印出Question的类对象就可以显示出问题文本了。
下面将尝试给Question类添加一个自定义的方法:
import datetime
from django.db import models
from django.db import timezone
class Question(models.Model):
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
保存这些更改,然后重新进入Python shell:
>> from polls.models import Choice, Question
>> Question.objects.all()
>> Question.ojbects.filter(id=1)
>> Question.objects.filter(question_text__startswith='What')
>> from django.utils import timezone
>> current_year = timezone.now().year
>> Question.objects.get(pub_date__year=current_year)
>> Question.objects.get(id=2)
>> Question.objects.get(pk=1)
>> q = Question.objects.get(pk=1)
>> q.was_published_recently()
>> q = Question.objects.get(pk=1)
>> q.choice_set.all()
>> q.choice_set.create(choice_text='Not much', votes=0)
>> q.choice_set.create(choice_text='The sky', votes=0)
>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)
>> c.question
>> q.choice_set.all()
>> q.choice_set.count()
>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
>> c.delete()
介绍Django的Admin
这是一个管理员应用,Django默认支持的
创建一个管理员用户python manage.py createsuperuser
输入用户名、邮箱、密码等信息
然后启动开发服务器,并进入管理员页面http://127.0.0.1:8000/admin/
,然后登陆进入管理员站点
可以看到管理员站点可以对用户组和用户进行可视化编辑
那么如何对Question和Choice也进行可视化编辑呢,需要将poll
应用添加到管理员站点中去
打开polls/admin.py
,输入如下代码:
from django.contrib import admin
from .models import Question
admin.site.register(Question)
然后开始探索管理员的功能,发现可以通过可视化操作Question,比如添加问题。由于在model中已经定义了各个字段的类型,所以管理员页面中的各个字段的输入组件会自动生成。比如字符串字段对应输入框,时间字段对应日期选择控件。
编写更多的view
接下来给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 results of question %s."
return HttpResponse(response % question_id)
def vote(request, question_id):
return HttpResponse("You're voting on question %s." % question_id)
然后将这些新的view函数添加到对应的path()
上去:
from django.urls import path
from . import views
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'),
}
这里的<int:question_id>
捕获名为question_id
的整型参数。
记得别加上.html后缀,这样看起来很笨。
这样就完成了对每个问题详情、结果、投票的查看。
编写views来真正做些事情
每一个view主要做两件事情:返回一个HttpResponse
对象,包含了请求页面的内容,或者触发诸如Http404
的异常。
view可以从数据库中读取记录
它可以使用一个Django自带的或者第三方的Python模板系统,它可以生成一个PDF文件,输出XML,创建一个ZIP压缩文件等等,在此过程中,可以使用任何Python模块。
Django需要的只有一个HttpResponse
,或者是一个异常exception
由于很方便,所以使用Django自己的数据库。下面的例子展示最近的5个poll
问题,用冒号分割
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)
这种在views
中定义前端页面的方式过于呆板。Django采用templates
来实现后台和前端页面的分离。
首先在polls
目录下创建templates
目录,Django将在每个应用的templates
目录下寻找前端页面。
项目设置中的TEMPLATES
设置描述了Django如何加载并渲染模板。默认的设置文件配置了DjangoTemplates
属性,其中APP_DIRS
设置为True
,DjangoTemplates
会在每个INSTALLED_APPS
下的templates
中寻找模板文件。
在刚刚创建的templates
目录中再创建一个目录polls
,并在其中创建一个index.html
文件,换句话说,模板文件应该在polls/templates/polls/index.html
由于上面已经介绍了app_direcotries
模板加载原理,在Django中引用模板可以直接使用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 %}
接下来也把index
的view
函数更新下:
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))
代码加载polls/index.html
前端页面,并将context
传递到前台页面。context
是一个字典,它将模板变量映射为Python对象
render()
常见的三部曲:加载模板、填充context和返回HttpResponse对象。Django提供了render()这种快捷方式:
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()
接受request
对象为第一个参数,接受一个模板名字作为第二个参数,然后接受一个字典作为第三个参数。
触发异常
前面说到,view
要么返回一个HttpResponse
对象要么返回异常,这里介绍如何返回异常
以访问问题详情为例:
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的问题,就触发404异常
那么如何在前台页面显示这个question呢,只需要在一个控件中注入question即可:
{{ question }}
get_object_or_404()
由于上面的操作很常见,所以Django也提供了一种快捷方式来减少平时的代码量:
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
在没有获取到数据对象时,会得到一个异常对象,这个异常对象显示到前台页面会自动显示404
除此之外:还有get_list_or_404()
,区别就是get()
和filter()
的区别了
使用模板系统
回到poll
应用的detail()
函数,对其进行显示:
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
模板系统使用点号.来访问变量属性 for choice in question.choice_set.all
会自动解释为Python代码for choice in queston.choice_set.all()
移除模板中硬编码URL
比如<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
中polls/
属于硬编码,将其改为:
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
这里的detail
是一种url的引用,可以在urls.py
中找到该名称对应的url地址:
...
path('<int:question_id>/', views.detail, name='detail')
...
而'detail'
后面的question.id
变量则是会替代该url地址中的<int:question_id>
,从而形成一个完整的url
移除硬编码的URL有什么好处? 如果前端页面多处需要使用某个具体地url地址,则都可以通过名字来引用,当该url地址需要更改时,只需要更改urls.py中对应的url地址即可,避免在前端页面每个地方都去修改
URL名字的命名空间
如果不同的应用中存在同名的url模式,那么前端页面在引用的时候光写url模式的名字会引起歧义,所以在urls.py
中需要指定app
的名字
from django.urls import path
from . import views
app_name = 'polls'
urlpatterns = {
...
}
这样,在对前端页面引用处做相应修改
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
写一个简单的form表单
<h1>{{ question.questioin_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和问题的选项序号有关,
forloop.counter
是进入for
循环体的次数;每个选项的name
都为choice
表单的跳转链接为投票地址,并以post
方式提交,这样非常重要 csrf(Cross Site Request Forgeries)
,是一个数据保护,加上标签即可
接下来编写vote
的响应函数
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_objects_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):
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, )))
第一,后台获取前台post
提交的数据,使用POST[name]
的形式,其中name
指的是前端控件的name
第二,save()
是对数据做即时保存
第三,一旦成功处理post
请求数据,一定要使用HttpResponseRedirect()
函数重新导向页面,它可以在用户点击后退按钮时避免数据提交两次。HttpResponseRedirect()
只接受一个参数,就是要重定向页面的链接
第四,reverse
是Django中的一个函数,它的作用也是避免硬编码
接下来,当用户投票完成后,就会进入投票结果页面,下面编写reuslts
的处理函数
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})
这里的代码和detail()
中几乎一样了,唯一不同的就是模板名字不同,不过没有关系,稍后会对其进行修复
现在创建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>
现在可以去/polls/1/
进行投票了
通用view:减少代码量
很多的view都是在获取数据并显示在前端页面上,为了降低代码的冗余性,Django提供了通用view机制
三部曲:修改urlconf、删除无用的旧的views、基于Django通用views引入新的views
- 修改urlconf
打开polls/urls.py
,修改如下:
from django.urls import path
from . import views
app_name = 'polls'
uslpatterns = {
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'),
}
这里将,第二个和第三个path()
中的question_id
替换为pk
.
- 修改views
删除之前的index
, detail
和results
三个view,取而代之使用Django通用views
,打开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 Question.objects.order_by('-pub_date')[:5]
class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'
class ResultView(generic.DetailView):
model = Question
template_name = 'polls/results.html'
def vote(request, question_id):
...
上述代码使用了两个通用view
:ListView
和DetailView
,二者分别为展示一个对象列表,一个展示特定类型对象的详情页面。每个通用view
必须指定model
属性,而DetailView
要求能从url
中获取到pk
属性,这就是为什么上一步在URLConf中将question_id
改为pk
.
DetailView
默认使用模板<app_name>/<model_name>_detail.html
这个模板,但是这里另做了指定,避免Detail
和Results
进入了同一页面。同理,ListView
默认使用<app_name>/<model_name>_list.html
这个模板,这里做了指定。
另外,还需强调的是,在之前的教程中,通过context
向前台传输数据,包括latest_question_list
和question
两个变量,但是如果使用了DetailView
,就不用指定context
了,它会自动提供给前端页面,因为已经制定了model
为Question
;但是对于ListView
,它默认传递到前端页面的变量是question_list
,为了覆盖,这里特意制定了context_object_name
.
接下来运行服务器,看看如何基于通用view
来使用最新的网站。
静态文件static files
没有样式文件,网站缺乏美观。Django使用django.contrib.staticfiles
来管理图片、CSS、JavaScript等静态文件。首先在polls
目录下创建static
文件夹,Django会像templates
一样去polls/static/
寻找静态文件。
STATICFILES_FINDERS寻找静态文件
项目
settings.py
文件中的STATICFILES_FINDERS
定义了静态文件所有的存放位置,就像templates
一样,django
会去每个INSTALLED_APPS
目录下的static
目录中寻找静态文件。
创建一个样式文件polls/static/polls/style.css
,其中内容为
li a{
color: green;
}
接下来就是如何在前端页面polls/templates/polls/index.html
中引用这个样式文件:
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}">
在上述代码中load static
将生成static
的绝对路径。引用这个样式文件后,polls
主页的文字变为了绿色。
接下来再添加一个背景颜色。将所要添加的背景图片放在polls/static/polls/images/
目录下,然后在css
文件中添加:
body{
background: url("images/backgroud.jpg") no-repeat;
}
定制管理员表单
之前将Question添加到了管理员站点,使得可以界面化编辑Question,现在可以在此基础上继续定制Admin站点,比如更改Question两个字段对应控件在前端页面的顺序,为此新建一个QuestionAdmin类:
from django.contrib import admin
from .models import Question, Choice
class QuestionAdmin(admin.ModelAdmin):
fields = ['pub_date', 'question_text'] # 默认是按照Question类中定义的顺序,现将其更改
admin.site.register(Question, QuestionAdmin)
admin.site.register(Choice)
更改前端页面两个字段的顺序并没什么大的影响,但是如果Question如果有很多属性,这个时候对各个属性进行排序就显得有必要了。比如,下面的代码将把两个属性拆分为两个大类:
from django.contrib import admin
from .models import Question, Choice
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
('Basic', {'fields': ['question_text']}),
('Date information', {'fields': ['pub_date']}),
]
admin.site.register(Question, QuestionAdmin)
然后,为了对Choice进行更好地编辑,考虑将问题和选项都放在问题的页面进行编辑:
from django.contrib import admin
from .models import Question, Choice
class ChoiceInline(admin.StackedInline):
model = Choice
extra = 3
class QuestionAdmin(admin.ModelAdmin):
fieldsites = [
('Basic', {'fields': ['question_text']}),
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
]
inlines = [ChoiceInline]
admin.site.register(Question, QuestionAdmin)
# admin.site.register(Choice)
这样一来就不用admin.site.register(Choice)
来注册Choice
了。
但是还有个问题,就是三个Choice
的输入框太占空间了,怎么办呢?将ChoiceInline
所继承的父类改为admin.TabularInline
即可。
除了定制问题的编辑页面,还可以定制问题列表的展示页面,控制显示哪些字段,隐藏哪些字段。
...
class QuestionAdmin(admin.ModelAdmin):
# ...
list_display = ('question_text', 'pub_date')
来源:oschina
链接:https://my.oschina.net/u/4080514/blog/3010713