本章给博客应用添加表单,以便用户可以创建、编辑或删除博客条目。
表单
表单在现代网络中无处不在,但要正确实现它们非常复杂。任何时候你接受用户输入都会有安全问题(XSS 攻击XSS Attacks),需要适当的错误处理,并且有关于提示的 UI 考虑。
对我们来说幸运的是,Django 的内置表单(Django’s built-in Forms)消除了很多困难。
首先,更新我们的基本模板以显示指向用于输入新博客文章的页面的链接。它将采用<a href="{% url 'post_new' %}"></a> 形式,其中 post_new 是我们 URL 的名称。
templates/base.html
<!-- templates/base.html -->
{% load static %}
<html>
<head>
<title>Django blog</title>
<link href="https://fonts.font.im/css?family=Source+Sans+Pro:400" rel="stylesheet">
<link href="{% static 'css/base.css' %}" rel="stylesheet">
</head>
<body>
<div>
<header>
<div class="nav-left">
<h1><a href="{% url 'home' %}">Django blog</a></h1>
</div>
<div class="nav-right">
<a href="{% url 'post_new' %}">+ New Blog Post</a>
</div>
</header>
{% block content %}
{% endblock content %}
</div>
</body>
</html>
blog/urls.py
# blog/urls.py
from django.urls import path
from .views import BlogListView, BlogDetailView, BlogCreateView # new
urlpatterns = [
path('post/new/', BlogCreateView.as_view(), name='post_new'), # new
path('post/<int:pk>/', BlogDetailView.as_view(), name='post_detail'),
path('', BlogListView.as_view(), name='home'),
]
blog/views.py
# blog/views.py
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView # new
from .models import Post
class BlogListView(ListView):
model = Post
template_name = 'home.html'
class BlogDetailView(DetailView):
model = Post
template_name = 'post_detail.html'
class BlogCreateView(CreateView): # new
model = Post
template_name = 'post_new.html'
fields = ['title', 'author', 'body']
在 BlogCreateView 中,我们指定了我们的数据库模型 Post,即我们的模板 post_new.html 的名称。对于字段,我们明确设置要公开的数据库字段,即标题、作者和正文。
templates/post_new.html
<!-- templates/post_new.html -->
{% extends 'base.html' %}
{% block content %}
<h1>New post</h1>
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Save">
</form>
{% endblock content %}
添加 Django 提供的 {% csrf_token %} 以保护我们的表单免受跨站点请求伪造。
blog/models.py
# blog/models.py
from django.db import models
from django.urls import reverse # new
class Post(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(
'auth.User',
on_delete=models.CASCADE,
)
body = models.TextField()
def __str__(self):
return self.title
def get_absolute_url(self): # new
return reverse('post_detail', args=[str(self.id)])
Reverse 是 Django 提供的的实用函数,我们可以通过其 URL 模板名称来引用对象,在本例中为 post_detail。
path('post/<int:pk>/', BlogDetailView.as_view(), name='post_detail'),
尽管 Django 文档建议使用 self.id 和 get_absolute_url,但 pk 和 id 在 Django 中是可以互换的。
更新表单
我们将再次使用内置的基于 Django 类的通用视图 UpdateView,并创建必要的模板、url 和视图。
templates/post_detail.html
<!-- templates/post_detail.html -->
{% extends 'base.html' %}
{% block content %}
<div class="post-entry">
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
</div>
<a href="{% url 'post_edit' post.pk %}">+ Edit Blog Post</a>
{% endblock content %}
templates/post_edit.html
<!-- templates/post_edit.html -->
{% extends 'base.html' %}
{% block content %}
<h1>Edit post</h1>
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Update">
</form>
{% endblock content %}
blog/views.py
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView # new
from .models import Post
class BlogListView(ListView):
model = Post
template_name = 'home.html'
class BlogDetailView(DetailView):
model = Post
template_name = 'post_detail.html'
class BlogCreateView(CreateView):
model = Post
template_name = 'post_new.html'
fields = ['title', 'author', 'body']
class BlogUpdateView(UpdateView): # new
model = Post
template_name = 'post_edit.html'
fields = ['title', 'body']
请注意,在 BlogUpdateView 中,我们明确列出了要使用的字段 ['title', 'body'] 而不是使用 'all'。这是因为我们假设帖子的作者没有改变;我们只希望标题和文本是可编辑的。
blog/urls.py
# blog/urls.py
from django.urls import path
from .views import (
BlogListView,
BlogDetailView,
BlogCreateView,
BlogUpdateView, # new
)
urlpatterns = [
path('post/<int:pk>/edit/',
BlogUpdateView.as_view(), name='post_edit'), # new
path('post/new/', BlogCreateView.as_view(), name='post_new'),
path('post/<int:pk>/', BlogDetailView.as_view(), name='post_detail'),
path('', BlogListView.as_view(), name='home'),
]
删除博客
创建用于删除博客帖子的表单的过程与更新帖子的过程非常相似。我们将使用另一个基于类的通用视图 DeleteView,创建必要的视图、url 和模板。
templates/post_detail.html
<!-- templates/post_detail.html -->
{% extends 'base.html' %}
{% block content %}
<div class="post-entry">
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
</div>
<p><a href="{% url 'post_edit' post.pk %}">+ Edit Blog Post</a></p>
<p><a href="{% url 'post_delete' post.pk %}">+ Delete Blog Post</a></p>
{% endblock content %}
templates/post_delete.html
<!-- templates/post_delete.html -->
{% extends 'base.html' %}
{% block content %}
<h1>Delete post</h1>
<form action="" method="post">{% csrf_token %}
<p>Are you sure you want to delete "{{ post.title }}"?</p>
<input type="submit" value="Confirm">
</form>
{% endblock content %}
blog/views.py
# blog/views.py
from django.views.generic import ListView, DetailView
from django.views.generic.edit import (
CreateView, UpdateView, DeleteView
) # new
from django.urls import reverse_lazy # new
from .models import Post
class BlogListView(ListView):
model = Post
template_name = 'home.html'
class BlogDetailView(DetailView):
model = Post
template_name = 'post_detail.html'
class BlogCreateView(CreateView):
model = Post
template_name = 'post_new.html'
fields = ['title', 'author', 'body']
class BlogUpdateView(UpdateView):
model = Post
template_name = 'post_edit.html'
fields = ['title', 'body']
class BlogDeleteView(DeleteView): # new
model = Post
template_name = 'post_delete.html'
success_url = reverse_lazy('home')
我们使用 reverse_lazy 而不是仅仅使用 reverse,这样它就不会执行 URL 重定向,直到我们的视图完成删除博客文章。
blog/urls.py
# blog/urls.py
from django.urls import path
from .views import (
BlogListView,
BlogDetailView,
BlogCreateView,
BlogUpdateView,
BlogDeleteView, # new
)
urlpatterns = [
path('post/<int:pk>/delete/', # new
BlogDeleteView.as_view(), name='post_delete'),
path('post/new/', BlogCreateView.as_view(), name='post_new'),
path('post/<int:pk>/', BlogDetailView.as_view(), name='post_detail'),
path('post/<int:pk>/edit/',
BlogUpdateView.as_view(), name='post_edit'),
path('', BlogListView.as_view(), name='home'),
]
测试
blog/tests.py
# blog/tests.py
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from .models import Post
class BlogTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
username='testuser',
email='test@email.com',
password='secret'
)
self.post = Post.objects.create(
title='A good title',
body='Nice body content',
author=self.user,
)
def test_string_representation(self):
post = Post(title='A sample title')
self.assertEqual(str(post), post.title)
def test_get_absolute_url(self): # new
self.assertEqual(self.post.get_absolute_url(), '/post/1/')
def test_post_content(self):
self.assertEqual(f'{self.post.title}', 'A good title')
self.assertEqual(f'{self.post.author}', 'testuser')
self.assertEqual(f'{self.post.body}', 'Nice body content')
def test_post_list_view(self):
response = self.client.get(reverse('home'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Nice body content')
self.assertTemplateUsed(response, 'home.html')
def test_post_detail_view(self):
response = self.client.get('/post/1/')
no_response = self.client.get('/post/100000/')
self.assertEqual(response.status_code, 200)
self.assertEqual(no_response.status_code, 404)
self.assertContains(response, 'A good title')
self.assertTemplateUsed(response, 'post_detail.html')
def test_post_create_view(self): # new
response = self.client.post(reverse('post_new'), {
'title': 'New title',
'body': 'New text',
'author': self.user.id,
})
self.assertEqual(response.status_code, 302)
self.assertEqual(Post.objects.last().title, 'New title')
self.assertEqual(Post.objects.last().body, 'New text')
def test_post_update_view(self): # new
response = self.client.post(reverse('post_edit', args='1'), {
'title': 'Updated title',
'body': 'Updated text',
})
self.assertEqual(response.status_code, 302)
def test_post_delete_view(self): # new
response = self.client.post(
reverse('post_delete', args='1'))
self.assertEqual(response.status_code, 302)
小结
我们用少量代码构建了博客应用程序,允许创建、阅读、更新和删除博客文章。 此核心功能称为首字母缩略词 CRUD:创建-读取-更新-删除。 虽然有多种方法可以实现相同的功能——我们可以使用基于函数的视图或编写我们自己的基于类的视图——但我们已经展示了在 Django 中实现这一点所需的代码是多么少。
但是,请注意潜在的安全问题:目前任何用户都可以更新或删除博客条目,而不仅仅是创建者! 这并不理想,而且确实 Django 带有内置功能来限制基于权限的访问,我们将在第13章深入介绍。
网友评论