本文仅作笔记使用,取自:https://docs.djangoproject.com/zh-hans/2.1/intro/
# 安装 pip install django==version
# 查看版本号 python -m django --version
1. 创建网站:admin-startproject mysite
2. 创建应用:admin-startapp polls
3. 环境设置:settings.py
1) INSTALLED_APPS中 polls.apps.PollsConfig
2) 本置数据库(待补)
3) 设置字符及时区:zh-hans, "Asia/shanghai"
4. python manage.py makemigrations
5. python manage.py migrate
6. 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 Meta:
db_table = 'Question'
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
7. python manage.py makemigrations polls
8. python manage.py sqlmigrate polls 0001 #返回对应的sql语句
9. python manage.py migrate
#脚本交互命令行
10. python manage.py shell
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
q.question_text="What's up?"
q.save()
Question.objects.all()
11. polls/models.py
from django.db import models
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
#命令行
12. python manage.py shell
from polls.models import Choice, Question
Question.objects.all()
Question.objects.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()
Choice.objects.filter(question__pub_date__year=current_year)
c = q.choice_set.filter(choice_text__startswith='Just hacking')
c.delete()
13. 创建管理员账号
python manage.py createsuperuser
#浏览器打开 http://127.0.0.1:8000/admin
14. python run manage.py runserver
15. 向管理页面中加入应用
polls/admin.py
from django.contrib import admin
from .models import Question
admin.site.register(Question)
16. python manage.py runserver 0:8000
17.编写视图文件
polls/views.py
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello World, You're at the polls index.")
18.polls/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]
19. mysite/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('polls/', include('polls.urls')),
path('admin/', admin.site.urls),
]
20. path的四个参数, route/view/kwargs/name
21. polls/views.py
def detail(request, question_id):
return HttpResponse("Your'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)
22. 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'),
#ex 也可以为每个url加上不必要的.html
path('latest.html', views.index),
]
23. 每个视图必须要做的只有两件事,一个是返回HttpResponse, 一个是抛出异常,比如:Http404
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)
24. 应用文件夹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>#}
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available</p>
{% endif %}
25. polls/views.py
from django.http import HttpResponse
from .models import Question
from django.template import loader
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))
26. 用快捷函数render()重写index()视图函数
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)
27. 抛出Http404错误: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})
28. 快捷函数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})
polls/detail.html
{{ question }}
29. 快捷函数 get_list_or_404()函数的工作原理同get_object_or_404()一样,除了get()函数被换成了filter()函数。
30. polls/detail.html模板
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
31. 去除模板中的硬编码url
硬编码链接<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>,
硬编码和强耦合的链接,对于一个包含很多应用的项目来说,修改起来是十分困难的。
在polls.urls的url()函数中通过name参数为url定义了名字,可以使用{% url %}标签来代替它:
<li> <a href="{% url 'detail' question.id %}">{{ question.question_text }} </a> </li>
这个标签的工作方式是在polls.urls模块的url定义中寻找具有指定名字的条目。
path('<int: question_id>/', views.detail, name='detail')
32. 为url名称添加命名空间
当很多应用,不清楚名称'detail'对应的是哪个应用的url时,
在polls/urls.py中加上app_name设置命名空间:
polls/urls.py
from django.urls import path
from . import views
app_name = 'polls'
url_patterns = [
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'),
]
再编辑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>#}
{# <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>#}
{# <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>
{% endfor %}
</ul>
{% else %}
<p>No polls are available</p>
{% endif %}
33. 编写一个简单的表单
polls/templates/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>
34. 创建post表单,具有修改数据的作用,需要小心跨站点请求伪造。
Django拥有一个用来防御它的非常容易使用的系统
简而言之,所有针对内部url的Post表单,都应该使用{% csrf_token %}模板标签
35. polls/urls.py
path('<int: question_id>/vote/', views.vote, name='vote')
36. polls/views
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)
print("question:", question)
try:
selected_choice = question.choice_set.get(pk=request.POST['choice'])
print("choice:", selected_choice)
except(KeyError, Choice.DoesNotExist):
return render(request, 'polls/detail.html', {'question': question, 'error_message': "You didn't select a choice."}, )
else:
print("else:")
selected_choice.votes += 1
print("votes:", selected_choice.votes)
selected_choice.save()
print("saved")
print("reverse polls path:", reverse('polls:results', args=(question.id,)))
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
#注 reverse函数中需传入url名,以及所需参数
'''
question: what's up?
choice: not much
else:
votes: 5
saved
reverse polls path: /polls/1/results/
[22/Sep/2019 14:57:35] "POST /polls/1/vote/ HTTP/1.1" 302 0
[22/Sep/2019 14:57:35] "GET /polls/1/results/ HTTP/1.1" 200 44
'''
37. polls/views.py
vote()视图将请求重定向到Question的结果界面
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})
38. 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>
39. 使用通用视图,减少代码量
ListView和DetailView, 这两个视图分别抽象”显示一个对象列表“和”显示一个特定类型对象的详细信息页面"
每个通用视图,需要知道它将作用于哪个模型,这是由model属性提供的
DetailView期望从url中捕获名为'pk'的主键值,所以我们为通用视图把question_id改为了pk
默认情况下,通用视图 DetailView 使用一个叫做 <app name>/<model name>_detail.html 的模板。在我们的例子中,它将使用 "polls/question_detail.html" 模板。template_name 属性是用来告诉 Django 使用一个指定的模板名字,而不是自动生成的默认名字。 我们也为 results 列表视图指定了 template_name —— 这确保 results 视图和 detail 视图在渲染时具有不同的外观,即使它们在后台都是同一个 DetailView 。
类似地,ListView 使用一个叫做 <app name>/<model name>_list.html 的默认模板;我们使用 template_name 来告诉 ListView 使用我们创建的已经存在的 "polls/index.html" 模板。
在之前的教程中,提供模板文件时都带有一个包含 question 和 latest_question_list 变量的 context。对于 DetailView , question 变量会自动提供—— 因为我们使用 Django 的模型 (Question), Django 能够为 context 变量决定一个合适的名字。然而对于 ListView, 自动生成的 context 变量是 question_list。为了覆盖这个行为,我们提供 context_object_name 属性,表示我们想使用 latest_question_list。作为一种替换方案,你可以改变你的模板来匹配新的 context 变量 —— 这是一种更便捷的方法,告诉 Django 使用你想使用的变量名。
改良视图 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 ResultsView(generic.DetailView):
model = Question
template_name = 'polls/results.html'
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,)))
改良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'),
]
40. 自动化测试
polls/tests.py
from django.test import TestCase
import datetime
from django.utils import timezone
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)
41. 运行测试
终端运行: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。
测试系统通知我们哪些测试样例失败了,和造成测试失败的代码所在的行号。
'''
42. 修复bug
polls/models.py
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
43. 更全面的测试
polls/tests.py
def test_was_published_recently_with_old_question(self):
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):
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)
44. 测试视图
$ python manage.py shell
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = Client()
>>> # get a response from '/'
>>> response = client.get('/')
Not Found: /
>>> # we should expect a 404 from that address; if you instead see an
>>> # "Invalid HTTP_HOST header" error and a 400 response, you probably
>>> # omitted the setup_test_environment() call described earlier.
>>> response.status_code
404
>>> # on the other hand we should expect to find something at '/polls/'
>>> # we'll use 'reverse()' rather than a hardcoded URL
>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
200
>>> response.content
b'\n <ul>\n \n <li><a href="/polls/1/">What's up?</a></li>\n \n </ul>\n\n'
>>> response.context['latest_question_list']
<QuerySet [<Question: What's up?>]>
45. 改善视图代码
from django.utils import timezone
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
return Question.objects.filter(pub_date__lte=timezone.now()).order_by('-pub_date')[:5]
# return Question.objects.order_by('-pub_date')[:5]
46. 测试新视图
from django.test import TestCase
import datetime
from django.utils import timezone
from .models import Question
from django.urls import reverse
def create_question(question_text, days):
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time)
class QuestionIndexViewTests(TestCase):
def test_no_questions(self):
response = self.client.get(reverse('polls:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available.")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_past_question(self):
create_question(question_text='Past question', days=-30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(response.context['latest_question_list'], ['<Question: Past question>'])
def test_future_question(self):
create_question(question_text='Future question.', days=30)
response = self.client.get(reverse('polls:index'))
self.assertContains(response, "No polls are available.")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_future_question_and_past_question(self):
create_question(question_text="Past question.", days=-30)
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(response.context['latest_question_list'], ['<Question: Past question.>'])
def test_two_past_questions(self):
create_question(question_text='Past question 1.', days=-30)
create_question(question_text='Past question 2.', days=-5)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past question 2.>', '<Question: Past question 1.>']
)
47. 就算在发布日期未来的那些投票不会在目录页index里出现,但如果用户知道或者猜到正确的url,还是可以访问到它们。
在DetailView中增加约束
polls/views.py
class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'
def get_queryset(self):
return Question.objects.filter(pub_date__lte=timezone.now())
48. 增加测试来检验pub_date在过去的Question可以显示出来,而在未来的不可以。
polls/tests.py
class QuestionDetailViewTests(TestCase):
def test_future_question(self):
future_question = create_question(question_text='Future question.', days=5)
url = reverse('polls:detail', args=(future_question.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_past_question(self):
past_question = create_question(question_text='Past Question.', days=-5)
url = reverse('polls:detail', args=(past_question.id,))
response = self.client.get(url)
self.assertContains(response, past_question.question_text)
49. 规划测试的建议:
如果你对测试有个整体规划,那么它们就几乎不会变得混乱。下面有几条好的建议:
对于每个模型和视图都建立单独的 TestClass
每个测试方法只测试一个功能
给每个测试方法起个能描述其功能的名字
50. 深入代码测试
在本教程中,我们仅仅是了解了测试的基础知识。你能做的还有很多,而且世界上有很多有用的工具来帮你完成这些有意义的事。
举个例子,在上述的测试中,我们已经从代码逻辑和视图响应的角度检查了应用的输出,现在你可以从一个更加 "in-browser" 的角度来检查最终渲染出的 HTML 是否符合预期,使用 Selenium 可以很轻松的完成这件事。这个工具不仅可以测试 Django 框架里的代码,还可以检查其他部分,比如说你的 JavaScript。它假装成是一个正在和你站点进行交互的浏览器,就好像有个真人在访问网站一样!Django 它提供了 LiveServerTestCase 来和 Selenium 这样的工具进行交互。
如果你在开发一个很复杂的应用的话,你也许想在每次提交代码时自动运行测试,也就是我们所说的持续集成 continuous integration ,这样就能实现质量控制的自动化,起码是部分自动化。
一个找出代码中未被测试部分的方法是检查代码覆盖率。它有助于找出代码中的薄弱部分和无用部分。如果你无法测试一段代码,通常说明这段代码需要被重构或者删除。想知道代码覆盖率和无用代码的详细信息,查看文档 Integration with coverage.py 获取详细信息。
文档 Django 中的测试 里有关于测试的更多信息。