2020-10-18 19:42:32 +11:00
# First Django App
Target: create a basic poll application, with 2 parts a **public site** and a **admin site**
* **public site**: let's pp view polls and vote
* **admin site**: let's you(admin) add/change/delete poll
2020-10-20 14:22:46 +11:00
## Part 1
1. Create web project
2. Create 1st app
3. Create 1st view
4. Add view & app into web url
### Creating a project
2020-10-18 20:07:45 +11:00
In the source directory, create project by
```
$ django-admin startproject < project_name >
```
Constraint on `<project_name>` :
* don't use names that conflict with django
* e.g. `django` , `test` (test with built-in test file)
```
mysite/
manage.py
mysite/
__init__ .py
settings.py
urls.py
asgi.py
wsgi.py
```
Explain file structure:
* top-level `mysite/` : container of the project, can be renamed latter w/o affect Django framework
* `manage.py` : cmd-line utility let us to interact (admin) with Django projects.
* inner `mysite/` dir: actual Python package for the project.
* Importing any package from here: e.g. `mysite.urls`
* `mysite/__init__.py` : empty file, tell python start from here.
* `mysite/settings.py` : settings/config for this django project.
* `mysite/urls.py` : URL declarations for this Django pjt; a "table of contents" of this Django website.
* `mysite/asgi.py` : An entry-point for ASGI-compatible webservers
* `mysite/wsgi.py` : Entry-point for WSGI-compatible webservers
2020-10-20 14:22:46 +11:00
### The development server
2020-10-18 20:07:45 +11:00
Start server steps:
1. cd into outer `mysite/` dir
2. Start server via: `python manage.py runserver`
Outcome:
* A development server is started. So no need to config a production server (e.g. Apache)
* Note: don't use it as production environment.
2020-10-20 14:22:46 +11:00
### Creating the Polls app
2020-10-18 20:07:45 +11:00
> Projects vs. apps
>
> What’ s the difference between a project and an app? An app is a Web application that does something – e.g., a Weblog system, a database of public records or a small poll app. A project is a collection of configuration and apps for a particular website. A project can contain multiple apps. An app can be in multiple projects.
Create a poll app along `mysite/` . For what?
```
$ python manage.py startapp polls
```
app hierarchy
```
polls/
__init__ .py
admin.py
apps.py
migrations/
__init__ .py
models.py
tests.py
views.py
```
2020-10-20 14:22:46 +11:00
### Write the first view
2020-10-18 20:45:21 +11:00
2020-10-20 14:22:46 +11:00
#### Step 1: Create a simple view of `polls` app
2020-10-18 20:45:21 +11:00
modify `polls/views.py` as
```python
from django.http import request
from django.shortcuts import render
def index(request):
return HttpResponse("Hello, world. You're at the polls index")
```
* `def index(request):...` is the simplest view
2020-10-20 14:22:46 +11:00
#### Step 2: In `polls` app, map the URL to this view
2020-10-18 20:45:21 +11:00
After creating a view, map it to a URL so we can call it. Create `polls/urls.py` , and setup mapping `urlpatterns`
```python
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]
```
2020-10-20 14:22:46 +11:00
#### Step 3: Point the root URLconf for polls
2020-10-18 20:45:21 +11:00
To point the root URLconf at `polls.urls` module:
1. In `mysite/urls.py`
1. add import for `django.urls.inclue`
2. insert a `include()` in `urlpatterns` list
i.e.
```python
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('polls/', include('polls.urls')),
path('admin/', admin.site.urls),
]
```
2020-10-20 14:22:46 +11:00
##### django.urls.include()
2020-10-18 20:45:21 +11:00
> django.urls.include(): A function that takes a full Python import path to another URLconf module that should be “included” in this place.
* new imported `include()` function allows referencing other URLconfs.
* `'polls.urls'` is the package that we are using, hence `mysite` can connect to `polls` app
2020-10-20 14:22:46 +11:00
##### path()
2020-10-18 20:45:21 +11:00
Here `path()` function is passed **route** and **view** ; two additional option available **kwargs** , and **name**
* `path()` argument **route** : a string that contains a URL pattern. When Django processing a request, it starts at the first pattern in `urlpatterns` and go down the list, comparing requested URL against each pattern
* `path()` argument **view** : when Django finds a matching pattern, it calls specified view function (a `HttpRequest` obj as the 1st argument, and others captured values as keyword argument)
* `path()` argument **kwargs** : passed in a dictionary to the target view
2020-10-20 10:39:20 +11:00
* `path()` argument **name** : naming URL, so we can refer to it elsewhere.
2020-10-20 14:22:46 +11:00
## Part 2
### Database setup
1. Modify `mysite/settings.py` for database binding
2. `python manage.py migrate` create database for each app
### Models Creation
在 Django 里写一个数据库驱动的 Web 应用的第一步: ** 定义模型** - 也就是数据库结构设计和附加的其它元数据 Meta Data
### Activate Models
改变模型需要这三步:
* 编辑 models.py 文件,改变模型
* 运行 python manage.py makemigrations 为模型的改变生成迁移文件。
* 运行 python manage.py migrate 来应用数据库迁移。
在这之前, 确认polls已经在 INSTALLED_APPS的settings中。
详情如何使用 `manage.py` 可check [Django后台文档 ](https://docs.djangoproject.com/zh-hans/3.1/ref/django-admin/ )
### Test API
2020-10-20 10:39:20 +11:00
2020-10-20 15:25:39 +11:00
进入 `python manage.py shell` 可以使用Django创建的各种API, 如[数据库抽象API database API(建议细看)](https://docs.djangoproject.com/zh-hans/3.1/topics/db/queries/)
## Part 3
* In Django, web content & HTML come from Views models. Every view is a Python function/class. Django use customer requested URL to decide which View to generate.
* Django use 'URLconfs' to map btw URL and Views. Details of[pURL manager](https://docs.djangoproject.com/zh-hans/3.1/topics/http/urls/)
* URL's general form: `/newsarchive/<year>/<month>/`
2020-10-20 15:43:26 +11:00
### 3.1 Create more Views
2020-10-20 15:25:39 +11:00
> 当某人请求你网站的某一页面时——比如说, "/polls/34/" , Django 将会载入 mysite.urls 模块,因为这在配置项 ROOT_URLCONF 中设置了。然后 Django 寻找名为 urlpatterns 变量并且按序匹配正则表达式。在找到匹配项 'polls/',它切掉了匹配的文本("polls/"),将剩余文本——"34/",发送至 'polls.urls' URLconf 做进一步处理。在这里剩余文本匹配了 "<int:question_id>/",使得我们 Django 以如下形式调用 polls.urls.detail():
> `detail(request=<HttpRequest object>, question_id=34)`
2020-10-20 15:28:25 +11:00
2020-10-20 15:43:26 +11:00
### 3.2 Create a true useful View
2020-10-20 15:28:25 +11:00
每个视图必须要做的只有两件事:返回一个包含被请求页面内容的 HttpResponse 对象,或者抛出一个异常,比如 Http404 。至于你还想干些什么,随便你。
你的视图可以从数据库里读取记录,可以使用一个模板引擎(比如 Django 自带的,或者其他第三方的),可以生成一个 PDF 文件,可以输出一个 XML, 创建一个 ZIP 文件,你可以做任何你想做的事,使用任何你想用的 Python 库。
2020-10-20 15:36:03 +11:00
`django.shortcuts.render` 可以“载入模板,填充上下文,再返回由它生成的 HttpResponse 对象“
2020-10-20 15:43:26 +11:00
#### shortcut function: `render()`
2020-10-20 15:36:03 +11:00
「载入模板,填充上下文,再返回由它生成的 HttpResponse 对象」是一个非常常用的操作流程。于是 Django 提供了一个快捷函数 render
2020-10-20 15:43:26 +11:00
> The render() function takes the request object as its first argument, a template name as its second argument and a dictionary as its optional third argument. It returns an HttpResponse object of the given template rendered with the given context.
### 3.3 Throw 404 error
如果指定问题 ID 所对应的问题不存在,这个视图就会抛出一个 Http404 异常。
2020-10-20 15:49:06 +11:00
尝试用 get() 函数获取一个对象,如果不存在就抛出 Http404 错误也是一个普遍的流程。Django 也提供了一个快捷函数, 从而取代try/except语句
> 什么我们使用辅助函数 get_object_or_404() 而不是自己捕获 ObjectDoesNotExist 异常呢?还有,为什么模型 API 不直接抛出 ObjectDoesNotExist 而是抛出 Http404 呢?
2020-10-20 16:19:00 +11:00
> 因为这样做会增加模型层和视图层的耦合性。指导 Django 设计的最重要的思想之一就是要保证松散耦合。一些受控的耦合将会被包含在 django.shortcuts 模块中。
### 3.4 Using template system
[Templates in Django ](https://docs.djangoproject.com/en/3.1/topics/templates/ )
`polls/detail.html` 模板里正式的代码:
```html
< h1 > {{ question.question_text }}< / h1 >
< ul >
{% for choice in question.choice_set.all %}
< li > {{ choice.choice_text }}< / li >
{% endfor %}
< / ul >
```
2020-10-20 16:37:38 +11:00
模板系统统一使用点符号来访问变量的属性
### 3.5 Remove hardlink URL in template
In template `polls/index.html` , links are hardcoded as shown
```html
{% for question in latest_question_list %}
< li > < a href = "/polls/{{ question.id }}/" > {{ question.question_text }}< / a > < / li >
{% endfor %}
```
Consider `index.html` will be reused numerous times, 去耦合is necessary.
```html
< li > < a href = "{% url 'detail' question.id %}" > {{ question.question_text }}< / a > < / li >
2020-10-20 16:45:04 +11:00
```
### 3.6 为 URL 名称添加命名空间
在根 URLconf 中添加命名空间 (namespace)
1. Add `app_name = 'polls'` for namespace in `poll.urls.py`
2020-10-20 19:24:12 +11:00
2. Change `detail` in `index.html` to `polls:detail`
## Part 4. Form
### 4.1 编写一个简单的表单
Edit `polls/templates/polls/detail.html` so it contain ** `HTML<form>` ** elements:
```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 >
```
Explain:
* 当有人选择一个单选按钮并提交表单提交时,它将发送一个 POST 数据 choice=# ,其中# 为选择的 Choice 的 ID。这是 HTML 表单的基本概念。
* 我们设置表单的 action 为 {% url 'polls:vote' question.id %} ,并设置 method="post" 。使用 method="post"``(与其相对的是 ``method="get"`)是非常重要的,因为这个提交表单的行为会改变服务器端的数据。 ** 无论何时,当你需要创建一个改变服务器端数据的表单时,请使用 ``method="post"** 。这不是 Django 的特定技巧;这是优秀的网站开发技巧。
* 所有针对内部 URL 的 POST 表单都应该使用 {% csrf_token %} 模板标签。
修改 `view.py` , 将Choice 逻辑灌输入其中。
```python
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
# request.POST 是一个类字典对象 (dictionary-like object),让你可以通过关键字的名字获取提交的数据。
# request.POST['choice'] 以字符串形式返回选择的 Choice 的 ID
selected_choice = question.choice_set.get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
# 如果在 request.POST['choice'] 数据中没有提供 choice , POST 将引发一个 KeyError 。上面的代码检查 KeyError ,如果没有给出 choice 将重新显示 Question 表单和一个错误信息。
# Redisplay the question voting form.
return render(request, 'polls/detail.html', {
'question': question,
'error_message': "You didn't select a choice.",
})
else:
selected_choice.votes += 1
selected_choice.save()
# 在增加 Choice 的得票数之后,代码返回一个 HttpResponseRedirect 而不是常用的 HttpResponse 、 HttpResponseRedirect 只接收一个参数:用户将要被重定向的 URL
# Always return an HttpResponseRedirect after successfully dealing
# with POST data. This prevents data from being posted twice if a
# user hits the Back button.
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
# 我们在 HttpResponseRedirect 的构造函数中使用 reverse() 函数。这个函数避免了我们在视图函数中硬编码 URL。它需要我们给出我们想要跳转的视图的名字和该视图所对应的 URL 模式中需要给该视图提供的参数。在本例中,使用在 教程第 3 部分 中设定的 URLconf, reverse() 调用将返回一个这样的字符串:'/polls/3/results/' 其中 3 是 question.id 的值。重定向的 URL 将调用 'results' 视图来显示最终的页面。
```
当有人对 Question 进行投票后, vote() 视图将请求重定向到 Question 的结果界面 (result view), edit it:
```python
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': question})
```
同时,我们需要创建一个`polls/results.html` template
```python
< 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 >
2020-10-20 20:36:07 +11:00
```
### 4.2 使用通用视图(generic views):代码还是少点好
Web 开发中的一个常见情况:根据 URL 中的参数从数据库中获取数据、载入模板文件然后返回渲染后的模板。 由于这种情况特别常见, Django 提供一种快捷方式,叫做“通用视图”系统。
让我们将我们的投票应用转换成使用通用视图系统,这样我们可以删除许多我们的代码。我们仅仅需要做以下几步来完成转换,我们将:
* 转换 URLconf。
* 删除一些旧的、不再需要的视图。
* 基于 Django 的通用视图引入新的视图。
#### Modify URLconf of polls
Change
```python
# 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'),
```
to
```python
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'),
```
* 注意,第二个和第三个匹配准则中,路径字符串中匹配模式的名称已经由 < question_id > 改为 < pk > (primary key)。
#### Modify Views
Target: Replace `index` , `details` & `results` views using Django's generic views that named in `polls/urls.py`
将
```python
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)
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': question})
```
改为
```python
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
"""Return the last five published questions."""
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'
```
两个通用视图: ListView 和 DetailView 。这两个视图分别抽象“显示一个对象列表”和“显示一个特定类型对象的详细信息页面”这两种概念。
* 每个通用视图需要知道它将作用于哪个模型。 这由 model 属性 (i.e. model members) 提供。
* DetailView 期望从 URL 中捕获名为 "pk" 的主键值,所以我们为通用视图把 question_id 改成 pk 。
解释 DetailView
* 默认情况下,通用视图 DetailView 使用一个叫做 < app name > /< model name > _detail.html 的模板。
* e.g. "polls/question_detail.html" template for us.
* 但是我们可以用 model member `template_name` 来指定模板
解释 ListView
* ListView 也使用一个叫做 < app name > /< model name > _list.html 的默认模板
* 而这里我们使用 `template_name` 来指定模板
* 我们提供 context_object_name 属性,表示我们想使用 latest_question_list
2020-10-20 21:00:31 +11:00
重启服务器后, 就是新的投票应用( 同样的html) , 但是内部用了generic view
## Part 5 Automated Test
TDD with python. Always stick to it.
### 5.1 First test
#### Create/Verify a bug
幸运的是,我们的 polls 应用现在就有一个小 bug 需要被修复:我们的要求是如果 Question 是在一天之内发布的, Question.was_published_recently() 方法将会返回 True ,然而现在这个方法在 Question 的 pub_date 字段比当前时间还晚时也会返回 True( 这是个 Bug) 。
确认bug:
```
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> # create a Question instance with pub_date 30 days in the future
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> # was it published recently?
>>> future_question.was_published_recently()
True
```
#### Create a test to verify this bug
All tests (polls/tests.py) are in app directory
```python
import datetime
from django.test import TestCase
from django.utils import timezone
from .models import Question
class QuestionModelTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
was_published_recently() returns False for questions whose pub_date is in future
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
```
Result
```
python manage.py test polls
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
was_published_recently() returns False for questions whose pub_date is in future
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/jason/HomeWorkstation/SynologyGiteaSpace/djangoproject/src/polls/tests.py", line 17, 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'...
2020-10-20 21:08:19 +11:00
```
解释上述msg:
* 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。
#### Fix the bug
Fix bug in `polls/models.py`
```python
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) < = self.pub_date < = now
```
2020-10-20 21:19:51 +11:00
#### Comprehensive Tests
Write more tests to make sure it's comprehensive
```python
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() returns False for questions whose pub_date is older than 1 day.
"""
time = timezone.now() - datetime.timedelta(days=1, seconds=1)
old_quesion = Question(pub_date=time)
self.assertIs(old_quesion.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently() returns True for questions whose pub_date is within the last day.
"""
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)
```