第14章 简单的表单
Django鼓励使用表单类验证用户输入和显示错误信息。
14.1 把验证逻辑移动到表单中
在Django中视图很复杂则说明代码异味。考虑如何把逻辑移动到表单或模型类中。
Django表单的功能:
- 可以处理用户输入并加以验证
- 可以在模板中使用,并且有不同的渲染以及错误消息
- 可以把数据存入数据库
14.1.1 使用单元测试探索表单API
lists/forms.py
from django import forms
class ItemForm(forms.Form):
item_text = forms.CharField()
lists/tests/test_forms.py
from django.test import TestCase
from lists.forms import ItemForm
class ItemFormTest(TestCase):
def test_form_renders_tem_text_input(self):
form = ItemForm()
self.fail(form.as_p())
测试:AssertionError: <p><label for="id_item_text">Item text:</label> <input type="text" name="item_text" required id="id_item_text" /></p>
lists/tests/test_forms.py
class ItemFormTest(TestCase):
def test_form_item_input_has_placeholder_and_css_classes(self):
form = ItemForm()
self.assertIn('placeholder="Enter a to-do item"', form.as_p())
self.assertIn('class="form-control input-lg"', form.as_p())
AssertionError: <p><label for="id_item_text">Item text:</label> <input type="text" name="item_text" required id="id_item_text" /></p>
lists/forms.py
widget = forms.fields.TextInput(attrs={ 'placeholder': 'Enter a to-do item', 'class':'form-control input-lg' })
AssertionError: 'placeholder="Enter a to-do item"' not found in '<p><label for="id_item_text">Item text:</label> <input type="text" name="item_text" required id="id_item_text" /></p>'
14.1.2 换用Django中的ModelForm类
表单可用通过Django提供的ModelForm类来重用已经在模型中定义好的验证规则。
from django import forms
from lists.models import Item
class ItemForm(forms.models.ModelForm):
class Meta:
model = Item #指定表单应用模型
fields = ('text',) #使用字段
AssertionError: 'placeholder="Enter a to-do item"' not found in '<p><label for="id_text">Text:</label> <textarea name="text" cols="40" rows="10" required id="id_text">\n</textarea></p>'
class ItemForm(forms.models.ModelForm):
class Meta:
model = Item # 指定表单应用模型
fields = ('text',) # 使用字段
widgets = {
'text': forms.fields.TextInput(
attrs={
'placeholder': 'Enter a to-do item',
'class': 'form-control input-lg',
}
),
}
通过测试
14.1.3 测试和定制表单验证
lists/tests/test_forms.py增加用一个测试
def test_form_validation_for_blank_items(self): form = ItemForm(data={'text':''}) form.save()
ValueError: The Item could not be created because the data didn't validate.
def test_form_validation_for_blank_items(self): form = ItemForm(data={'text':''}) self.assertFalse(form.is_valid()) self.assertEqual( form.errors['text'], ["You can't have an empty list item"] )
AssertionError: ['This field is required.'] != ["You can't have an empty list item"]
那就修改默认错误提示
lists/forms.py
class Meta: model = Item # 指定表单应用模型 fields = ('text',) # 使用字段 widgets = { 'text': forms.fields.TextInput( attrs={ 'placeholder': 'Enter a to-do item', 'class': 'form-control input-lg', } ), } error_messages = { 'text':{'required':"You can't have an empty list item"} }
通过测试
使用常量避免错误消息搅乱代码
EMPTY_ITEM_ERROR = "You can't have an empty list item" [...] error_messages = { 'text':{'required':EMPTY_ITEM_ERROR} }
修改测试
from lists.forms import ItemForm,EMPTY_ITEM_ERROR [...] self.assertEqual( form.errors['text'],[EMPTY_ITEM_ERROR] )
通过测试
提交: git commit -am 'new form for list items'
14.2 在视图中使用这个表单
14.2.1 在处理GET请求的视图中使用这个表单
lists/tests/test_views.py
from lists.forms import ItemForm class HomePageTest(TestCase): def test_use_home_template(self): response = self.client.get('/') self.assertTemplateUsed(response, 'home.html') def test_home_page_uses_item_form(self): response = self.client.get('/') self.assertIsInstance(response.context['form'],ItemForm)
测试结果:KeyError: 'form'
修改视图:
from lists.forms import ItemForm def home_page(request): return render(request, 'home.html',{'form':ItemForm()})
替换base.html input
<form method="POST" action="{% block form_action %}{% endblock %}"> {% csrf_token %} {{ form.text }} {% if error %}
14.2.2 大量查找和替换
功能测试:selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: [id="id_new_item"]
提交以区分重命名和逻辑变动:git commit -am 'use new form in home_page,simplify tests. NB breaks stuff'
base.py 增加一个新辅助方法
def get_item_input_box(self): return self.browser.find_element_by_id('id_text')
使用get_item_input_box方法替换掉功能测试中id_new_item
grep -Ir item_text lists/ 找到之后替换为text
grep -r id_new_item lists/ 找到之后替换为id_text
通过单元测试
Ran 17 tests in 0.063s
OK
功能测试:selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: [id="id_text"]
14.3 在POST请求的视图中使用这个表单
14.3.1 修改new_list视图的单元测试
lists/tests/test_views.py
分拆 test_validation_errors_are_sent_back_to_home_page_template方法
class NewListTest(TestCase): def test_for_invalid_input_renders_home_template(self): response = self.client.post('/lists/new', data={'text': ''}) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'home.html') def test_validation_errors_are_shown_on_home_page(self): response = self.client.post('/lists/new', data={'text': ''}) self.assertContains(response, escape(EMPTY_ITEM_ERROR)) def test_for_invalid_input_passes_form_to_template(self): response = self.client.post('/lists/new',data={'text':''}) self.assertIsInstance(response.context['form'],ItemForm)
单元测试:
KeyError: 'form'
14.3.2 在视图中使用这个表单
视图:
def new_list(request): form = ItemForm(data=request.POST) if form.is_valid(): list_ = List.objects.create() Item.objects.create(text=request.POST['text'], list=list_) return redirect(list_) else: return render(request, 'home.html', {'form': form})
单元测试:AssertionError: False is not true : Couldn't find 'You can't have an empty list item' in response
14.3.3 使用这个表单在模板中显示错误消息
base.html
<form method="POST" action="{% block form_action %}{% endblock %}"> {% csrf_token %} {{ form.text }} {% if form.errors %} <div class="form-group has-error"> <div class="help-block">{{ form.text.errors }}</div> </div> {% endif %} </form>
单元测试:AssertionError: False is not true : Couldn't find 'You can't have an empty list item' in response
14.4 在其他视图中使用这个表单
lists/tests/test_views.py增加一个新的测试
class ListViewTest(TestCase):
[...]
def test_displays_item_form(self):
list_ = List.objects.create()
response = self.client.get(f'/lists/{list_.id}/')
self.assertIsInstance(response.context['form'], ItemForm)
self.assertContains(response, 'name="text"')
KeyError: 'form' ,解决错误:修改视图
def view_list(request, list_id): [...] form = ItemForm() return render(request, 'list.html', {'list': list_, 'form':form,'error': error})
14.4.1 定义辅助方法,简化测试
lists/tests/test_views.py
分拆test_validation_errors_end_up_on_lists_page方法
def post_invalid_input(self):
list_ = List.objects.create()
return self.client.post(
f'/lists/{list_.id}/',
data={'text': ''}
)
def test_for_inwalid_input_noting_saved_to_db(self):
self.post_invalid_input()
self.assertEqual(Item.objects.count(), 0)
def test_for_invalid_input_renders_list__template(self):
response = self.post_invalid_input()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'list.html')
def test_for_invalid_input_passes_form_template(self):
response = self.post_invalid_input()
self.assertIsInstance(response.context['form'], ItemForm)
def test_for_invalid_input_shows_error_on_page(self):
response = self.post_invalid_input()
self.assertContains(response, escape(EMPTY_ITEM_ERROR))
测试结果:AssertionError: False is not true : Couldn't find 'You can't have an empty list item' in response
视图:
def view_list(request, list_id): list_ = List.objects.get(id=list_id) error = None form = ItemForm(data=request.POST) if request.method == 'POST': form = ItemForm() if form.is_valid(): Item.objects.create(text=request.POST['text'], list=list_) return redirect(list_) return render(request, 'list.html', {'list': list_, 'form': form})
单元测试:
Ran 23 tests in 0.085s
OK
功能测试:selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: .has-error
14.4.2 意想不到的好处:HTML5自带的客户端验证
Django为输入框添加了required属性,HTML5的特性,浏览器会在客户端做验证,输入无效禁止提交表单。
python manage.py test functional_tests
Ran 4 tests in 43.807s
OK
14.5 提交修改
git commit -am 'use form in all views, back to working state'
14.6 自定制错误并非无意义,因为并不是所有的浏览器都自带验证
14.7 使用表单自带的save方法
lists/tests/test_forms.py
def test_form_save_handles_saving_to_a_list(self): list_ = List.objects.create() form = ItemForm(data={'text':'do me'}) new_item = form.save(for_list = list_) self.assertEqual(new_item,Item.objects.first()) self.assertEqual(new_item.text,'do me') self.assertEqual(new_item.list,list_)
TypeError: save() got an unexpected keyword argument 'for_list'
lists/forms.py
def save(self, for_list): self.instance.list = for_list return super().save()
Ran 24 tests in 0.084s
OK
修改视图:
def new_list(request): form = ItemForm(data=request.POST) if form.is_valid(): list_ = List.objects.create() form.save(for_list=list_) return redirect(list_) else: return render(request, 'home.html', {'form': form})
Ran 24 tests in 0.084s
OK
def view_list(request, list_id): list_ = List.objects.get(id=list_id) form = ItemForm() if request.method == 'POST': form = ItemForm(data=request.POST) if form.is_valid(): form.save(for_list=list_) return redirect(list_) return render(request, 'list.html', {'list': list_, 'form': form})
Ran 24 tests in 0.083s
OK
功能测试:
Ran 4 tests in 33.001s
OK