Django入门与实践-第13章:表单处理(完结)
http://127.0.0.1:8000/boards/1/
http://127.0.0.1:8000/boards/2/
http://127.0.0.1:8000/boards/3/
http://127.0.0.1:8000/boards/1/new/
http://127.0.0.1:8000/boards/2/new/
http://127.0.0.1:8000/boards/3/new/
这是我们在前一个教程绘制的线框图。我现在意识到这个可能是一个不好的例子,因为这个特殊的表单涉及到处理两个不同模型数据:Topic(subject)和 Post(message)。
GET 可能是最常见的请求类型了。它用于从服务器请求数据。每当你点击了一个链接或者直接在浏览器中输入了一个地址时,你就创建一个 GET 请求。
POST 用于当我们想更改服务器上的数据的时候。一般来说,每次我们发送数据给服务器都会导致资源状态的变化,我们应该使用 POST 请求发送数据。
Django 使用 CSRF Token(Cross-Site Request Forgery Token) 保护所有的POST 请求。
这是一个避免外部站点或者应用程序向我们的应用程序提交数据的安全措施。
应用程序每次接收一个 POST 时,都会先检查 CSRFToken。
如果这个 request 没有 token,或者这个 token是无效的,它就会抛弃提交的数据。
csrf_token 的模板标签:
{% csrf_token %}
下面是示范我们如何检索数据:
subject = request.POST['subject']
message = request.POST['message']
创建表单正确的姿势
自从我们开始使用 Forms,我们已经走了很长一段路。终于,是时候使用Forms API 了。
Forms API 可在模块 django.forms 中得到。
Django 使用两种类型的form: forms.Form 和 forms.ModelForm 。
Form 类是通用的表单实现。我们可以使用它来处理与应用程序 model 没有直接关联的数据。
ModelForm 是 Form 的子类,它与 model 类相关联。
让我们去掉一些多余的部分,只看表单处理的核心部分:
if request.method == 'POST':
form = NewTopicForm(request.POST)
if form.is_valid():
topic = form.save()
return redirect('board_topics', pk=board.pk)
else:
form = NewTopicForm()
return render(request, 'new_topic.html', {'form': form})
首先我们判断请求是 POST 还是 GET。
如果请求是 POST,这意味着用户向服务器提交了一些数据。
所以我们实例化一个将 POST 数据传递给 form 的form 实例: form = NewTopicForm(request.POST) 。
然后,我们让 Django 验证数据,检查 form 是否有效,我们能否将其存入数据库: if form.is_valid(): 。
如果表单有效,我们使用 form.save()将数据存入数据库。
save() 方法返回一个存入数据库的 Model 实例。
所以,因为这是一个 Topic form, 所以它会返回 topic = form.save() 创建的 Topic。
然后,通用的路径是把用户重定向到其他位置,以避免用户通过按 F5 重新提交表单,并且保证应用程序的流程走向。
现在,如果数据是⽆效的,Django 会给 form 添加错误列表。
然后,视图函数不会做任何处理并且返回最后一句: return render(request,'new_topic.html', {'form': form}) 。
这意味着我们需要更新new_topic.html 以显示错误。
如果请求是 GET,我们只需要使用 form = NewTopicForm() 初始化一个新的空表单。
这个 form 有三个渲染选项: form.as_table , form.as_ul 和form.as_p 。
这是一个快速的渲染表单所有字段的方法。
顾名思义, as_table 使用 table 标签来格式化输入, as_ul 使用 li 标签。
当使⽤ Bootstrap 或者其他的前端库时,我比较喜欢使用一个叫做 djangowidget-tweaks 的 Django 库。
它可以让我们更好地控制渲染的处理,在保证默认值的情况下,只需在上面添加额外的自定义设置。
一些 render_field 模板标签的例子:
{% render_field form.subject class="form-control" %}
{% render_field form.message class="form-control" placeholder=form.message.label %}
{% render_field field class="form-control" placeholder="Writea message!" %}
{% render_field field style="font-size: 20px" %}
#myproject/urls.py from django.conf.urls import url from django.contrib import admin from boards import views urlpatterns = [ url(r'^$', views.home, name='home'), url(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topics'), url(r'^boards/(?P<pk>\d+)/new/$', views.new_topic, name='new_topic'), url(r'^admin/', admin.site.urls), ]
#boards/views.py from django.shortcuts import render, get_object_or_404 from .models import Board def new_topic(request, pk): board = get_object_or_404(Board, pk=pk) return render(request, 'new_topic.html', {'board': board})
<!--templates/new_topic.html--> {% extends 'base.html' %} {% block title %}Start a New Topic{% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li> <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li> <li class="breadcrumb-item active">New topic</li> {% endblock %} {% block content %} {% endblock %}
<!--templates/new_topic.html--> {% extends 'base.html' %} {% block title %}Start a New Topic{% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li> <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li> <li class="breadcrumb-item active">New topic</li> {% endblock %} {% block content %} <form method="post"> {% csrf_token %} <div class="form-group"> <label for="id_subject">Subject</label> <input type="text" class="form-control" id="id_subject"name="subject"> </div> <div class="form-group"> <label for="id_message">Message</label> <textarea class="form-control" id="id_message" name="message" rows="5"></textarea> </div> <button type="submit" class="btn btn-success">Post</button> </form> {% endblock %}
#boards/views.py from django.contrib.auth.models import User from django.shortcuts import render, redirect, get_object_or_404 from .models import Board, Topic, Post def new_topic(request, pk): board = get_object_or_404(Board, pk=pk) if request.method == 'POST': subject = request.POST['subject'] message = request.POST['message'] user = User.objects.first() # TODO: 临时使??个账号作为登录?户 topic = Topic.objects.create( subject=subject, board=board, starter=user ) post = Post.objects.create( message=message, topic=topic, created_by=user ) return redirect('board_topics', pk=board.pk) # TODO:redirect to the created topic page return render(request, 'new_topic.html', {'board': board})
<!--templates/topics.html--> {% extends 'base.html' %} {% block title %} {{ board.name }} - {{ block.super }} {% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li> <li class="breadcrumb-item active">{{ board.name }}</li> {% endblock %} {% block content %} <div class="mb-4"> <!--我们已经修改了topics.html模板,让我们创建?个能让我们转到new topic页?的按钮:--> <a href="{% url 'new_topic' board.pk %}" class="btn btn-primary">New topic</a> </div> <table class="table"> <thead class="thead-inverse"> <tr> <th>Topic</th> <th>Starter</th> <th>Replies</th> <th>Views</th> <th>Last Update</th> </tr> </thead> <tbody> {% for topic in board.topics.all %} <tr> <td>{{ topic.subject }}</td> <td>{{ topic.starter.username }}</td> <td>0</td> <td>0</td> <td>{{ topic.last_updated }}</td> </tr> {% endfor %} </tbody> </table> {% endblock %}
# boards/forms.py from django import forms from .models import Topic
class NewTopicForm(forms.ModelForm): message = forms.CharField(widget=forms.Textarea(), max_length=4000)
class Meta: model = Topic fields = ['subject', 'message']
# boards/views.py from django.contrib.auth.models import User from django.shortcuts import render, redirect, get_object_or_404 from .forms import NewTopicForm from .models import Board, Topic, Post def new_topic(request, pk): board = get_object_or_404(Board, pk=pk) user = User.objects.first() # TODO: get the currently logged in user
if request.method == 'POST': form = NewTopicForm(request.POST) if form.is_valid(): topic = form.save(commit=False) topic.board = board topic.starter = user topic.save()
post = Post.objects.create( message=form.cleaned_data.get('message'), topic=topic, created_by=user ) return redirect('board_topics', pk=board.pk) # TODO: redirect to the created topic page else: form = NewTopicForm() return render(request, 'new_topic.html', {'board': board,'form': form})
<!--我们甚至修复了最后两个测试。--> <!--Django Forms API 不仅仅是处理和验证数据。它还为我们生成 HTML。--> <!--templates/new_topic.html--> {% extends 'base.html' %} {% block title %}Start a New Topic{% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li> <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li> <li class="breadcrumb-item active">New topic</li> {% endblock %} {% block content %} <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit" class="btn btn-success">Post</button> </form> {% endblock %}
# boards/forms.py from django import forms from .models import Topic class NewTopicForm(forms.ModelForm): message = forms.CharField( widget=forms.Textarea(), max_length=4000, help_text='The max length of the text is 4000.' ) class Meta: model = Topic fields = ['subject', 'message']
# 我们也可以为表单字段设置额外的属性: # boards/forms.py from django import forms from .models import Topic class NewTopicForm(forms.ModelForm): message = forms.CharField( widget=forms.Textarea( attrs={'rows': 5, 'placeholder': 'What is on your mind?'} ), max_length=4000, help_text='The max length of the text is 4000.' )
class Meta: model = Topic fields = ['subject', 'message']
# 用BootStrap 表单渲染 # pip install django-widget-tweaks # myproject/settings.py
INSTALLED_APPS = [ 'widget_tweaks', ]
# templates/new_topic.html {% extends 'base.html' %} {% load widget_tweaks %} {% block title %}Start a New Topic{% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boar ds</a></li> <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li> <li class="breadcrumb-item active">New topic</li> {% endblock %} {% block content %} <form method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group"> {{ field.label_tag }} {% render_field field class="form-control" %} {% if field.help_text %} <small class="form-text text-muted"> {{ field.help_text }} </small> {% endif %} </div> {% endfor %} <button type="submit" class="btn btn-success">Post</button> </form> {% endblock %}
# 现在要实现 Bootstrap 4 验证标签,我们可以修改 new_topic.html 模板。 # templates/new_topic.html {% extends 'base.html' %} {% load widget_tweaks %} {% block title %}Start a New Topic{% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boar ds</a></li> <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li> <li class="breadcrumb-item active">New topic</li> {% endblock %} {% block content %} <form method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group"> {{ field.label_tag }} {% if form.is_bound %} {% if field.errors %} {% render_field field class="form-control is-invalid" %} {% for error in field.errors %} <div class="invalid-feedback"> {{ error }} </div> {% endfor %} {% else %} {% render_field field class="form-control is-valid" %} {% endif %} {% else %} {% render_field field class="form-control" %} {% endif %} {% if field.help_text %} <small class="form-text text-muted"> {{ field.help_text }} </small> {% endif %} </div> {% endfor %} <button type="submit" class="btn btn-success">Post</button> </form> {% endblock %}
# 复用表单模板 # 模板看起来有点复杂,是吧?有个好消息是我们可以在项目中重复使用它。 # 在 templates 文件夹中,创建一个新的文件夹命名为 includes: # templates/includes/form.html {% load widget_tweaks %} {% for field in form %} <div class="form-group"> {{ field.label_tag }} {% if form.is_bound %} {% if field.errors %} {% render_field field class="form-control is-invalid" %} {% for error in field.errors %} <div class="invalid-feedback"> {{ error }} </div> {% endfor %} {% else %} {% render_field field class="form-control is-valid" % } {% endif %} {% else %} {% render_field field class="form-control" %} {% endif %} {% if field.help_text %} <small class="form-text text-muted"> {{ field.help_text }} </small> {% endif %} </div> {% endfor %}
<!--templates/new_topic.html--> {% extends 'base.html' %} {% block title %}Start a New Topic{% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li> <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li> <li class="breadcrumb-item active">New topic</li> {% endblock %} {% block content %} <form method="post" novalidate> {% csrf_token %} {% include 'includes/form.html' %} <button type="submit" class="btn btn-success">Post</button> </form> {% endblock %}