Mki's Blog

Django学习(二):视图、自动测试

Django学习(二):视图、自动测试

视图

视图就是处理Http请求的函数,像这样

# vote/views.py
def Go(request):
    return HttpResponse('test')

假如要添加新的视图,那么我们首先要先添加对应的url函数

# vote/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('test', views.Go, name='1111'),
    path('<int:question_id>/', views.detail, name='detail'),    # 新建
    path('<int:question_id>/results/', views.results, name='result'),  # 新建
    path('<int:question_id>/vot/', views.vot, name='vot')   # 新建
]

这里path函数的route参数和之前的不一样,<int:question_id>表示用int类型去匹配变量,并将匹配到的内容命名为变量question_id

之前的视图都很简单,事实上它要么返回HttpResponse对象,要么报错。当然你也可以自己给他增加功能。

不难看出前面的视图其实是比较单调的,如何让它更加美观精致/多功能一点呢?

调用数据库/模板

首先要新建一个存放模板的文件夹 templates,在这个文件夹下新建和应用同名的文件夹,用来存放模板文件。下面是一个模板的例子。(模板的语法这里先不涉及,其实能看得懂,看得懂就行。)

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

视图函数可以调用数据库API来实现读取数据和加载模板,例如

# views.py
from django.shortcuts import render
from django.http import HttpResponse
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, 'vote/index.html', context)

这里我们调用了Question模型,获取了他的一些数据,把他赋值给context。

利用render函数加载模板,返回响应。

返回异常

利用 Http404() 函数。

# vote/views.py
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, Http404
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 exise")   # Http404是一个返回异常的函数
    return render(requests, 'vote/details.html', {'question':question})

或者可以一步到位,利用 get_object_or_404() 函数。

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)  #获取对象 或者 抛出错误
    return render(request, 'vote/details.html', {'question':question})

同样,这里调用了details.html这个模板,不要忘记设计它。

<!-- vote/details.html -->
{{ question }}

去除硬编码

 <li><a href="/vote/{{ question.id }}/">{{ question.question_text }}</a></li>

之前我们在vote.html里面是这么写的,然鹅这样子万一要修改url设计就会很麻烦,所以可以如下改进。

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

这里的'detail'就是前文path('<int:question_id>/', views.detail, name='detail')里面的name属性。

命名空间

上一部分去除硬编码的设计其实有缺陷,因为在有很多应用的时候,也许会出现重名的情况,url 'detail'可能没法正确指代,因此需要添加命名空间。

# vote/urls.py
from django.urls import path
from . import views

app_name = 'vote' # 命名空间
urlpatterns = [
    path('', views.index, name='index'),
    path('test', views.Go, name='1111'),
    path('<int:question_id>/', views.detail, name='detail'),     
    path('<int:question_id>/results/', views.results, name='result'),  
    path('<int:question_id>/vot/', views.vot, name='vot')   
]

再修改url

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

这样就可以了。

表单设计

表单在这些应用里面是很常见的,基本的设计也很简单,如下

# vote/details.html
<h1>{{ question.question_text }}</h1>  # 设定标题
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="{% url 'vote: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>

哇!这里有个小坑,前面说过,数据库模型QuestionChoice是有对应关系的,之前看文档不仔细,没发现它在shell里面给question模型设置了选项(choice_set属性),导致选项半天出不来。

与之对应的我们要修改vote函数

# vote/views.py
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, 'vote/details.html',{
            'question':question,
            'error_message': "You don't ",
        })
    else:            
        selected_choice.votes += 1      # 投票数加一
        selected_choice.save()
        return HttpResponseRedirect(reverse('vote:result', args=(question.id,)))

增加一个最终投票数量的页面

<!-- vote/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 'vote:detail' question.id %}">vote again?</a>

改进视图

不难发现,之前的detail函数和reults函数很相似

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

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

本着那啥的原则,重复的操作尽量避免。

通用视图

django自带了一些常用功能的视图,开始重写

from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, Http404, HttpResponseRedirect
from .models import Choice, Question
from django.urls import reverse
from django.views import generic

class IndexView(generic.ListView):
    template_name = 'vote/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 = 'vote/details.html'

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

例如,generic.ListView就是一个拿来显示列表的通用视图,而generic.DetailView是一个拿来显示特定对象细节的视图。

我们需要告诉这个函数,通用视图作用的数据库模型是什么

model = Question

本来它是有自己的html页面的,但是我们之前有自己写的,就用自己的好了

template_name = 'vote/details.html'

更好的设计

当然是用我们自己的层叠样式表啦!

首先要搞一个专门拿来放css文件的文件夹,类似templates文件夹一样

/* vote/static/vote/style.css */
li a {
    color: red;
}
body {
    background: white url("images/1.jpg") no-repeat;
}
<!-- vote/templates/vote/index.html-->
{% load static %}   <!--  加载文件 -->
<link rel="stylesheet" type="text/css" href="{% static 'vote/style.css' %}">  <!--  加载文件 -->

{% if latest_question_list %}
    <ul>
        {% for question in latest_question_list %}
            <li><a href="{% url 'vote:detail' question.id %}">{{ question.question_text }}</a></li>
        {% endfor %}
    </ul>
{% else %}
    <p>No vote</p>
{% endif %}

自动测试

先来一个例子

# vote/test.py
from django.test import TestCase # 单元测试也是一个类
from django.utils import timezone
import datetime
from .models import Question

class QuestionModelTests(TestCase):
    def test_was_published_recently_with_future_question(self):
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)   # 创建一个模型
        self.assertIs(future_question.was_published_recently(),False)

此处是针对一个发布时间为未来的问题,“是否是最近发布的”这个属性会被认为真。

# vote/models.py
class Question(models.Model):
    def __str__(self):
        return self.question_text
    def was_published_recently(self):
        now = timezone.now()
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)   # 问题就是在这里

时间是越往后越大

原则上,pub_date大于目前时间减一天(即在目前的一天之内),那么为真。

但是,但凡是在未来发布的所有问题,self.pub_date >= timezone.now() - datetime.timedelta(days=1)永远为真。

运行测试之后就可以发现这个问题。

python3 manage.py test

改正bug,添加必须小于现在的条件

    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now # 限制

自定义

自定义后台

# vote/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)