Django web-poll系统

2021-02-20 10:42发布

通过一个调查问卷的例子来学习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设置为TrueDjangoTemplates会在每个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 %}

接下来也把indexview函数更新下:

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

  1. 修改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.

  1. 修改views

删除之前的index, detailresults三个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):
    ...

上述代码使用了两个通用viewListViewDetailView,二者分别为展示一个对象列表,一个展示特定类型对象的详情页面。每个通用view必须指定model属性,而DetailView要求能从url中获取到pk属性,这就是为什么上一步在URLConf中将question_id改为pk.

DetailView默认使用模板<app_name>/<model_name>_detail.html这个模板,但是这里另做了指定,避免DetailResults进入了同一页面。同理,ListView默认使用<app_name>/<model_name>_list.html这个模板,这里做了指定。

另外,还需强调的是,在之前的教程中,通过context向前台传输数据,包括latest_question_listquestion两个变量,但是如果使用了DetailView,就不用指定context了,它会自动提供给前端页面,因为已经制定了modelQuestion;但是对于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')
标签: