用Django全栈开发——15. 开发文章管理
你好呀!皮爷爷为我们带来了最新的一期学习Python能干啥?之Django教程!从零开始到入门到精通的学习过程一直到项目的成功部署和上线。这一节里我们来开发文章管理系统,并涵盖文章发布、查看、修改以及删除等功能。

上一节我们实现了Category和Tag的处理与维护工作;这一节我们将着手处理与维护文章的相关管理。
修改SideBar
首先,在 sidebar 引入了两个管理页面:一个是发布文章页面(publish.html),另一个是管理页面(manage.html)。此外,在 cms 目录下还需要创建 post 目录,并在其中包含 manage.html 和 publish.html 文件。

我们采用的方法与之前的开发方案一致,并基于publish.html页面实现。即发布与修改操作集成在同一页面中,并通过传入或不传入文章内容来判断其功能。
Post的新增
关于文章中的模型部分,在第13节中已经进行了深入阐述,请简要介绍Post模块中的模型包含哪些内容;其中涉及的应用路径为:app/post目录下的models.py文件中定义了相关的模型架构和训练逻辑
class Post(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey('peekpauser.User', on_delete=models.SET_NULL, null=True)
description = models.CharField(max_length=200)
thumbnail = models.URLField()
content = models.TextField()
content_html = models.TextField(blank=True, editable=False)
is_md = models.BooleanField(default=True)
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
tag = models.ManyToManyField(Tag)
priority = models.IntegerField(default=-1)
is_hot = models.BooleanField(default=False)
is_top = models.BooleanField(default=False)
is_main_page = models.BooleanField(default=False)
status = models.PositiveIntegerField(default=STATUS_DRAFT, choices=STATUS_ITEMS)
publish_time = models.DateTimeField(auto_now_add=True)
publish_time_show = models.DateTimeField(default=datetime.datetime.now)
time_id = models.CharField(blank=True, max_length=30)
read_num = models.PositiveIntegerField(default=0)
文章类别中的属性众多,在构建文章发布页面时,应当完整地纳入系统设计流程中。
我们采用Bootstrap框架来生成发布页面中的表格内容,并确保其呈现美观清晰的形式。最终呈现的样子如下所示:

通过分析可以看出,在中间某些区域中使用了Droplist技术,在具体实现方面,则主要涉及作者(Author)、类别(Category)以及状态(Status)这三个维度;而Tag部分则是一个位于页面底部的区域化选择框。
这里的publish.html页面会呈现为和其他页面不同,在此页面内我们需要展示以下数据
- Author;
- Category;
- Tag
这些数据源自数据库中的记录。因此,在我们的post_publish_view方法中,就必须要提取这些数据信息,并随后通过特定的途径将这些信息传递到前端页面。
def post_publish_view(request):
context = {
'list_data_category': Category.objects.all(),
'list_data_tag': Tag.objects.all(),
'list_data_user': User.objects.all(),
'list_data_status': Post.STATUS_ITEMS,
'form': PostForm()
}
return render(request, 'cms/post/publish.html', context=context)
同样需要通过发送完整的表单数据来使用Post请求。因此,在处理Tag和Category时也需要创建一个PostView的同时还需要一个PostForm。但这里的PostForm有所不同:
class PostForm(forms.ModelForm, FormMixin):
tag_id = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Tag.objects.all())
class Meta:
model = Post
exclude = ('tag',)
我们有必要单独定义一个tag_id字段的原因在于,在我们的Post模型中采用的是ManyToMany关系模式。为此,在表单设计阶段就需要考虑如何接收前端提交的多个Tag标签,并通过使用ModelMultipleChoiceField字段来接收这些多选值。接着生成Post实例并将其保存到数据库中完成操作流程
所以,这个时候,我们的后端更新文章的代码就是这样:
class PostView(View):
def post(self, request):
# 新建提交
if 'submit' in request.POST:
form = PostForm(request.POST)
if form.is_valid():
title = form.cleaned_data.get('title')
description = form.cleaned_data.get('description')
author = form.cleaned_data.get('author')
thumbnail = form.cleaned_data.get('thumbnail')
status = form.cleaned_data.get('status')
content = form.cleaned_data.get('content')
is_md = form.cleaned_data.get('is_md')
category = form.cleaned_data.get('category')
priority = form.cleaned_data.get('priority')
is_hot = form.cleaned_data.get('is_hot')
is_top = form.cleaned_data.get('is_top')
is_main_page = form.cleaned_data.get('is_main_page')
publish_time_show = form.cleaned_data.get('publish_time_show')
time_id = form.cleaned_data.get('time_id')
read_num = form.cleaned_data.get('read_num')
tags = form.cleaned_data.get('tag_id')
instance = Post.objects.create(title=title, description=description, author=author,
thumbnail=thumbnail, status=status, content=content,
is_md=is_md, category=category, priority=priority,
is_hot=is_hot, is_top=is_top, is_main_page=is_main_page,
publish_time_show=publish_time_show, time_id=time_id,read_num=read_num
)
instance.tag.set(tags)
return redirect(reverse("cms:post_publish_view"))
在最后一行之前的一行中,“instance.tag.set(tags)”这一行为需要特别关注。为什么要采用这种方法呢?因为当我们从PostForm获取数据时得到的是一个QuerySet对象,在Django中,默认情况下普通对象(如Model)无法直接支持ManyToMany关系。然而,在Django中,默认情况下普通对象(如Model)无法直接支持ManyToMany关系。推荐的做法是先创建一个实例对象(如my_instance),然后调用该实例上的设置字段值方法来完成这一过程。
这个时候,别忘了在CMS目录下的urls.py文件里面配置PistView的映射:
urlpatterns = [
path("dashboard/post/add", PostView.as_view(), name="post_add"),
]
当前完成搭建了文章的基本功能模块。 在开发环境中测试一下启动我们的服务器。 访问发布页面 尝试着将所有的设置都填入表单中.

当用户点击发布时, 页面被自动重定向到Publish页面; 此时, 请查看一下之前发布的那篇文章:

确实存在。另外能够观察到的是,在参数is_md设置为true时,我们的content会生成HTML格式的文件。数据完全正确的情况下,则实现了新增文章的功能。
Post的查询
有关文章查询的问题,在本项目中我们选择在Manage.html页面上放置相关文章信息。其中包含一个表格区域,在这个区域中我们可以填入这些文章信息的具体内容,并将其与分类(Category)以及标签(Tag)的实现方式相仿地进行操作。
- 生成HTML页面结构;
- 创建视图函数;
- 在视图函数中将数据以特定的context格式传递给前端端口;
- 在urls.py文件中进行路径配置;
Manage.html大致结构如下:
<section class="content">
<div class="container-fluid pt-4">
<div class="row">
<div class="col-sm-12">
<div class="card">
<div class="card-body">
<div class="row p-2 d-flex justify-content-between">
<p class="h3">Post</p>
<div class="float-right">
<a class="btn btn-primary text-right" href="{% url 'cms:post_publish_view' %}"><i class="mr-2 fas fa-plus"></i>Add</a>
</div>
</div>
<table class="table table-bordered table-hover">
<thead class="thead-light">
<tr>
<th style="width: 10%;">#</th>
<th>Post_Time_ID</th>
<th>Post_title</th>
<th>Post_show_time</th>
<th class="w-25">actions</th>
</tr>
</thead>
<tbody>
{% for item in list_data %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.time_id }}</td>
<td>{{ item.title }}</td>
<td>{{ item.publish_time_show }}</td>
<td>
<a href="{% url 'cms:tag_edit' %}?tag_id={{ item.id }}" class="btn btn-info btn-xs">Modify</a>
<button class="btn btn-danger btn-xs delete-btn" data-tag-id="{{ item.id}}">
Delete
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</section>
文件的manage视图函数:
def post_manage_view(request):
context = {
"list_data": Post.objects.all()
}
return render(request, 'cms/post/manage.html', context=context)
最后效果图:

Post的修改
Post的修改,和Category还有Tag的思路是一样的:
- 创建一个用于编辑文章的视图功能,并将其配置为modify按钮。
- 将编辑好的文章传输至publisht.html页面进行保存与更新。
- 优化底部提交按钮的功能逻辑以确保数据正确提交。
所以,视图函数很简单,通过id来取文章:
class PostEditView(View):
def get(self, request):
post_id = request.GET.get('post_id')
post = Post.objects.get(pk=post_id)
tag_list = list()
for item in post.tag.all():
tag_list.append(item.id)
context = {
'item_data': post,
'list_data_category': Category.objects.all(),
'list_data_tag': Tag.objects.all(),
'list_data_user': User.objects.all(),
'list_data_status': Post.STATUS_ITEMS,
'item_data_tag_list': tag_list
}
return render(request, 'cms/post/publish.html', context=context)
为了更好地说明问题,请您注意以下几点:我们需要将Post、Category、Tag、User以及Status这些数据发送到前端系统;同时,在Tag List部分的处理中,请您注意以下几点:我们需要将已选定的Tag信息发送到前端界面。
如同之前Category与Tag的做法,在publish.html页面我们需要进行相应调整。这是因为我们已经引入了item_data数据:
<form class="form-horizontal" action="{% url 'cms:post_add' %}" method="post">
{% if item_data %}
<input type="text" class="form-control" id="id" name="id" value="{{ item_data.id }}" hidden>
{% endif %}
....
<div class="form-group">
<label for="title" class="">Title</label>
{% if item_data %}
<input type="text" class="form-control" id="title" name="title" value="{{ item_data.title }}">
{% else %}
<input type="text" class="form-control" id="title" name="title">
{% endif %}
</div>
....
<div class="form-group">
<label for="is_md" class="">Is MarkDown</label>
<select name="is_md" id="is_md" class="custom-select">
{% if item_data %}
<option value="True" {% if True == item_data.is_md %} selected {% endif %}>Yes</option>
<option value="False" {% if False == item_data.is_md %} selected {% endif %}>No</option>
{% else %}
<option value="True">Yes</option>
<option value="False">No</option>
{% endif %}
</select>
</div>
....
<div class="form-group">
<label for="tag_id" class="">Tag</label>
<div class="d-flex flex-wrap">
{% if item_data %}
{% for item in list_data_tag %}
<div class="form-check mr-4 mb-2">
<input class="form-check-input" type="checkbox" value="{{ item.id }}" name="tag_id" id="tags_id_{{ item.id }}" {% if item.id in item_data_tag_list %} checked {% endif %}>
<label class="form-check-label">{{ item.name }}</label>
</div>
{% endfor %}
{% else %}
{% for item in list_data_tag %}
<div class="form-check mr-4 mb-2">
<input class="form-check-input" type="checkbox" value="{{ item.id }}" name="tag_id" id="tags_id_{{ item.id }}">
<label class="form-check-label">{{ item.name }}</label>
</div>
{% endfor %}
{% endif %}
</div>
</div>
</form>
可以观察到,在使用Django的DTL进行判断时,通过传入item_data来确定相应的数值是否显示。
此时我们点击点击一下文章的modify按钮,发现页面如下:

发现的内容与上一篇完全相同;接下来需要对后续部分进行调整。也就是需要处理提交相关的逻辑,在之前的PostView中集中完成这一功能;由于我们的button的name不一样;因此可以通过name来判断是进行修改还是创建操作;从而执行相应的处理步骤:
class PostView(View):
def post(self, request):
# 修改Post
elif 'modify' in request.POST:
form = PostEditForm(request.POST)
if form.is_valid():
id = form.cleaned_data.get('id')
title = form.cleaned_data.get('title')
description = form.cleaned_data.get('description')
author = form.cleaned_data.get('author')
thumbnail = form.cleaned_data.get('thumbnail')
status = form.cleaned_data.get('status')
content = form.cleaned_data.get('content')
is_md = form.cleaned_data.get('is_md')
category = form.cleaned_data.get('category')
priority = form.cleaned_data.get('priority')
is_hot = form.cleaned_data.get('is_hot')
is_top = form.cleaned_data.get('is_top')
is_main_page = form.cleaned_data.get('is_main_page')
publish_time_show = form.cleaned_data.get('publish_time_show')
time_id = form.cleaned_data.get('time_id')
read_num = form.cleaned_data.get('read_num')
tags = form.cleaned_data.get('tag_id')
instance = Post.objects.filter(id=id)
instance.update(title=title, description=description, author=author,
thumbnail=thumbnail, status=status, content=content,
is_md=is_md, category=category, priority=priority,
is_hot=is_hot, is_top=is_top, is_main_page=is_main_page,
publish_time_show=publish_time_show, time_id=time_id,read_num=read_num
)
instance.first().tag.set(tags)
return redirect(reverse("cms:post_manage_view"))
else:
return restful.method_error("Form is error", form.get_errors())
当前阶段我们决定前往前端进行测试,并对各个数据进行相应的调整,请问能否顺利完成?

然后点击提交,页面跳回到了管理页面:

发现我们的标题确实改了,我们再去数据库里面核对一下:

看到数据确实是正确的,那么我们的修改就完成了。
Post的删除
删除Post的行为与Category及Tag的行为保持一致,在后端通过编写相应的HTTP客户端代码实现这一目标,并在View函数中完成具体的逻辑处理。为了实现这一目标,在后端编写相应的HTTP客户端代码的同时,请确保在urls.py文件中适当配置View函数以支持该功能的实现
class PostDeleteView(View):
def post(self, request):
post_id = request.POST.get('post_id')
Post.objects.filter(id=post_id).delete()
return restful.ok()
在manag.html页面的按钮,别忘了修改传入post_id:
<button class="btn btn-danger btn-xs delete-btn" data-post-id="{{ item.id}}">
Delete
</button>
完成后,在唯一的一篇文章上点击删除按钮后即可看到页面跳转至manage.html页面,并处于数据为空的状态

说明数据删除成功。
技术总结
最后总结一下,
文章管理开发,基本和Category还有Tag的开发一样:
- 实现视图功能时,请确保发送POST请求数组,并利用form表单接收和处理数据最后将数据存入数据库。
- 在删除操作中,请确保所有JavaScript脚本均需提前关闭并正确执行AJAX提交POST请求数组。
- 建议在原有页面基础上进行复制粘贴操作,并根据是否传递item_data参数决定是更新还是新增内容。
- 系统会自动读取数据库中的数据内容并将其传递给页面。
- 操作完成。
整套教程源码获取,可以关注『皮爷撸码』,回复『peekpa.com』
长按下图二维码关注,如文章对你有启发,欢迎在看与转发。

