3.3 删除和修改文章
网站上对文章的操作通常简称为“增删改查”:“增”即发布,“删”即删除,“改”即修改,“查”即查看。继上节内容,本节学习“删改”之法,如下图所示。

3.3.1 删除
如同“栏目管理”中的功能,对文章也有删除和修改的操作。还是采用bootstrap默认的两个图标,先在./templates/article/column/article_list.html文件中的相应<td>内将该图标显示出来,代码如下。
1 <tr id={{ article.id }}> 2 <td>{{ forloop.counter }}</td> 3 <td><a href="{{ article.get_absolute_url }}">{{ article.title }}</a> </td> 4 <td>{{ article.column }}</td> 5 <span class="glyphicon glyphicon-pencil"></span> 6 <span class="glyphicon glyphicon-trash" style="margin=left:20px;"></span> 7 <td>--</td> 8 </tr>
这次换一个开发顺序,不再先写视图函数了(当然数据模型和表单类都不用写了),而是“从前向后”先写“删除”的前端功能。因此,还要继续编辑上述模板文件,在“删除”功能的代码外围加上<a>标签。
1 {% extends "article/base.html" %} 2 {% load staticfiles %} 3 {% block title %}articles list{% endblock %} 4 {% block content %} 5 <div> 6 <table class="table table-hover"> 7 <tr> 8 <td>序号</td> 9 <td>标题</td> 10 <td>栏目</td> 11 <td>操作</td> 12 </tr> 13 {% for article in articles %} 14 <tr id={{ article.id }}> 15 <td>{{ forloop.counter }}</td> 16 <td><a href="{{ article.get_absolute_url }}">{{ article.title }}</a> </td> 17 <td>{{ article.column }}</td> 18 <td> 19 <span class="glyphicon glyphicon-pencil"></span> 20 <a name="delete" href="javascript:" onclick="del_article(this,{{ article.id }})"></a><span class="glyphicon glyphicon-trash" style="margin-left:20px;"></span> 21 </td> 22 </tr> 23 {% endfor %} 24 </table> 25 </div> 26 27 <script type="text/javascript" src="{% static 'js/jquery-3.3.1.js' %}"></script> 28 <script type="text/javascript" src="{% static 'js/layer.js' %}"></script> 29 <script type="text/javascript"> 30 function del_article(the, article_id){ 31 var article_name = $(the).parents("tr").children("td").eq(1).text(); 32 layer.open({ 33 type:1, 34 skin:"layui-layer-rim", 35 area:["400px","200px"], 36 title:"删除文章", 37 content:'<div class="text-center" style="margin-top:20px"><p>是否确定删除《'+article_name+'》</p></div>', 38 btn:['确定','取消'], 39 yes:function(){ 40 $.ajax({ 41 url:'{% url "article:del_article" %}' 42 type:"POST", 43 data:{"article_id":article_id}, 44 success:function(e){ 45 if(e=="1"){ 46 parent.location.reload(); 47 layer.msg("has been deleted."); 48 }else{ 49 layer.msg("删除失败"); 50 } 51 }, 52 }) 53 }, 54 }); 55 } 56 57 </script> 58 59 {% endblock %}
前端设置完成后,就要编写删除文章的视图函数了,编辑./article/views.py文件,增加del_article()函数。
1 @login_required(login_url='/account/login/') 2 @require_POST 3 @csrf_exempt 4 def del_article(request): 5 article_id = request.POST['article_id'] 6 try: 7 article = ArticlePost.objects.get(id=article_id) 8 article.delete() 9 return HttpResponse("1") 10 except: 11 return HttpResponse("2")
还要在./article/urls.py文件中对删除操作进行URL设置。
1 path('del-article/',views.del_article,name="del_article"),
现在就可以测试一下删除文章的功能,应该不会出错。

点击“确定”按钮,页面自动刷新,如下图所示。

3.3.2 修改
在编写修改文章的功能之前,先梳理清楚实现这个功能的流程。
- 单击修改文章的图标,将该文章的id传给相应的视图函数。
- 视图函数从数据库中读取该文章id的相关内容。
- 将文章相关内容呈现在页面上,并且处于编辑状态,要求依然使用Markdown编辑器。
- 用户编辑之后,单击“提交”按钮,再次将页面中编辑的内容保存在上述id所对应的数据库记录中。
从上述流程中可以看出,我们必须编写一个视图函数(依然在./article/views.py文件中),这个视图函数能够接受文章的id,而且只是GET和POST两种请求方式。先编写满足GET请求的部分,代码如下。
1 @login_required(login_url='/account/login/') 2 @csrf_exempt 3 def redit_article(request,article_id): #① 4 if request.method == "GET": 5 article_columns = request.user.article_column.all() 6 article = ArticlePost.objects.get(id=article_id) 7 this_article_form = ArticlePostForm(initial={"title":article.title}) 8 this_article_column = article.column 9 return render(request,"article/column/redit_article.html", 10 {"article":article,"article_columns":article_columns,"this_article_column":this_article_column, 11 "this_article_form":this_article_form})
重点关注语句①的参数,除熟知的request外,又增加了article_id,这就是用来接收文章id的。正式通过article_id,才能获得该文章对象。
接着在./templates/article/column/目录中创建名为redit_article.html的文件,这就是上述视图函数所指定的前端模板文件,其代码如下。
1 {% extends "article/base.html" %} 2 {% load staticfiles %} 3 {% block title %}article column{% endblock %} 4 {% block content %} 5 <link rel="stylesheet" href="{% static 'editor/css/style.css' %}"> 6 <link rel="stylesheet" href="{% static 'editor/css/editormd.css' %"> 7 <div class="container"> 8 9 <div class="col-md-10"> 10 <div style="margin-left:10px"> 11 <form class="form-horizontal" action="." method="post">{% csrf_token %} 12 <div class="row" style="margin-top:10px;"> 13 <div class="col-md-2 text-right"><span>标题:</span></div> 14 <div class="col-md-10 text-left">{{this_article_form.title}}</div> 15 </div> 16 <div class="row" style="margin-top:10px;"> 17 <div class="col-md-2 text-right"><span>栏目:</span></div> 18 <div class="col-md-10 text-left"> 19 <select id="which_column"> 20 {% for column in article_columns %} 21 {% if column == this_article_column.column %} #② 22 <option value="{{column.id}}" selected="selected">{{column.column}}</option> #③ 23 {% else %} 24 <option value="{{column.id}}">{{column.column}}</option> 25 {% endif %} 26 {% endfor %} 27 </select> 28 </div> 29 </div> 30 <div class="row" style="margin-top:10px;"> 31 <div class="col-md-2 text-right"><span>内容:</span></div> 32 <div id="editormd" class="col-md-10 text-left"> 33 <!--{{article_post_form.body}}--> 34 <textarea style="display:none;" id="id_body"> 35 {{article.body}} #④ 36 </textarea> 37 </div> 38 </div> 39 <div class="row"> 40 <input type="button" class="btn btn-primary btn-lg" value="发布" onclick="redit_article()"> 41 </div> 42 </form> 43 </div> 44 </div> 45 </div> 46 <script type="text/javascript" src="{% static 'js/jquery-3.3.1.js' %}"></script> 47 <script src="{% static 'editor/editormd.min.js' %}"></script> 48 <script type="text/javascript" src="{% static 'js/layer.js' %}"></script> 49 <script type="text/javascript"> 50 $(function(){ 51 var editor = editormd("editormd",{ 52 width:"100%", 53 height:640, 54 //syncScrolling:"single", 55 path:"{% static 'editor/lib/' %}", 56 }); 57 }); 58 </script> 59 {% endblock %}
因为此文航在发布时已经选定栏目了,所以在循环体内部要通过语句②判断当前文章原有的栏目名称,并将下拉菜单中的该名称确定为选定状态,即语句③。
语句④得到文章的内容,但是要注意,前面还是不要留有空格,原因见前文叙述。
下面配置URL,在./article/urls.py文件中增加如下内容:
1 re_path('redit-article/(?P<article_id>\d+)/$', views.redit_article, name="redit_article"),
检查Django服务是否已经启动,在浏览器的地址栏输入http://127.0.0.1:8000/article/redit-article/10/ ,最后的数字就是某篇文章的id,读者可以根据自己的项目输入其他数值,但不能省略,也不能输入不存在的id值,效果如下图所示。

现在就可以在本页面中编辑文章了。
至此,完成了编辑文章的一部分功能。但是,总不能让用户记住自己每篇文章的id,所以要再做一个单击“铅笔”图标就能够跳转到上述界面的功能,需要再次对./templates/article/column/article_list.html文件进行编辑。
前面已经给“删除”图标加上了<a>标签,实现了删除功能。现在再给“编辑”图标加上<a>标签,实现编辑功能,代码如下。
1 <a name="edit" href="{% url 'article:redit_article' article.id %}"><span 2 class="glyphicon glyphicon-pencil"></span></a>
{% url 'article:redit_article' article.id %}这个写法较较以往的URL写法稍有不同,后面多了一个参数article.id,它对应URL中的article_id。
完成之后,刷新http://127.0.0.1:8000/article/article-list/页面,单击“编辑”图标,检查是否跳转到上述编辑文章的界面。

在万丈编辑界面单击“发布”按钮时,将数据提交给视图函数,还是用熟悉的Ajax方法。其实,在模板文件中已经有了伏笔onclick="redit_article()",在./templates/article/column/redit_article.html的尾部编写redit_article()函数,代码如下。
1 <script type="text/javascript"> 2 function redit_article(){ 3 var title = $("#id_title").val(); 4 var column_id = $("#which_column").val(); 5 var body = $("#id_body").val(); 6 $.ajax({ 7 url:"{% url 'article:redit_article' article.id %}", 8 type:"POST", 9 data:{"title":title,"body":body,"column_id":column_id}, 10 success:function(e){ 11 if(e=="1"){ 12 layer.msg("successful"); 13 location.href = "{% url 'article:article_list' %}"; 14 }else{ 15 layer.msg("sorry."); 16 } 17 }, 18 }); 19 } 20 </script>
在POST对象地址中,使用了{% url 'article:redit_article' article.id %},这种形式的URL不仅可以用在HTML代码中,还可以用在JavaScript代码中。
前面./article/views.py中的视图函数redit_article()只编写了响应GET请求的部分,现在编写响应POST请求的部分,对此视图函数增加如下代码。
1 else: 2 redit_article = ArticlePost.objects.get(id=article_id) 3 try: 4 redit_article.column = request.user.article_column.get(id=request.POST['column_id']) 5 redit_article.title = request.POST['title'] 6 redit_article.body = request.POST['body'] 7 redit_article.save() 8 return HttpResponse("1") 9 except: 10 return HttpResponse("2")
这样就将本小节开头所说的修改文章的功能都实现了。测试一下吧!从文章列表界面单击“编辑”图标,进入文章编辑界面,对文章进行修改,再单击“发布”按钮,看看是否实现了修改的目的。
3.3.3 设置分页功能
在文章标题列表中,应该再增加一个分页功能,因为当发布的文章多了之后,在同一页面中显示出所有的标题是要耗费较长时间的,所以分页是必须的。
Django也认为分页是常见的操作,所以提供了内置的分页方法,可以在视图函数./article/views.py的顶部引入分页功能用到的三个方法。
1 from django.core.paginator import Paginator,EmptyPage,PageNotAnInteger
下面重写article_list()视图函数,代码如下。
1 @login_required(login_url='/account/login/') 2 def article_list(request): 3 articles_list = ArticlePost.objects.filter(author=request.user) 4 paginator = Paginator(articles_list,2) #① 5 page = request.GET.get('page') #② 6 try: 7 current_page = paginator.page(page) #③ 8 articles = current_page.object_list #④ 9 except PageNotAnInteger: #⑤ 10 current_page = paginator.page(1) 11 articles = current_page.object_list 12 except EmptyPage: #⑥ 13 current_page = paginator.page(paginator.num_pages) #⑦ 14 articles = current_page.object_list 15 return render(request,"article/column/article_list.html",{"articles":articles,"page":current_page})
语句①根据所查询到的文章对象articles_list创建分页的实例对象,并且规定每页最多2个。Paginator类的初始化的完整参数列表是Paginator(object_list,per_page,orphans=0,allow_empty_first_page=True),详细代码可以参考https://docs.djangoproject.com/en/1.10/_modules/django/core/paginator/#Paginator。
语句②获得当前浏览器GET请求的参数page的值,也就是当前浏览器所请求的页码数值。在后面的前端模板中,我们会看到,通过浏览器发出的URL请求中包含page参数,即URL的样式诸如http://127.0.0.1:8000/article/article-list/?page=1,通过语句②得到URL中的参数page的值1,即当前所请求的页码是1(第1页),并赋值给page变量。
语句③中的page()是Paginator对象的一个方法,其作用在于得到指定页面内容,其参数必须是大于或等于1的整数。
语句④object_list是Page对象的属性,能够得到该页所有的对象列表。类似的属性还有Page.number(返回页码)等。
语句⑤和语句⑥捕获两个异常,一个是请求的页码数值不是整数(PageNotAnInteger),另一个是请求的页码数值为空或者在URL参数中没有page(EmptyPage)。
语句⑦paginator.num_pages返回的页数,num_pages是Paginator对象的一个属性。
重写了视图函数之后,还要再写一个专门显示分页的模板文件,创建./templates/paginator.html文件,并输入如下代码。
1 <div class="pagination"> 2 <span class="step-links"> 3 {% if page.has_previous %} 4 <a href="?page={{ page.previous_page_number }}">Previous</a> 5 {% endif %} 6 <span class="current"> 7 Page{{ page.number }} of {{ page.paginator.num_pages }} 8 </span> 9 {% if page.has_next %} 10 <a href="?page={{ page.next_page_number }}">Next</a> 11 {% endif %} 12 </span> 13 </div>
在上述代码中,使用了Page对象的几个属性。
- has_previous:判断是否有上一页。
- previous_page_number:返回上一页的页码。
- number:返回当前页的页码。
- paginator.num_pages:关联Paginator对象(page.paginator),并得到其总页码数(paginator.num_pages)。
- next_page_number:返回下一页的页码。
完成分页模板的代码编写之后,将它引入到article_list.html文件中,编辑./templates/article/column/article_list.html文件,并在表格代码<table></table>的后面加入如下内容。
1 {% include "paginator.html" %}
检查Django服务是否已经运行,为了 看到效果,请现在文章页面中多发布几篇文章,然后访问http://127.0.0.1:8000/article/article-list/,就会看到一个简明且全面的分页功能,如下图所示。

对内容维护所需要的“增删改查”功能都已经实现了。
3.3.4 知识点
1、Markdown
在本项目中,编辑文章所用的编辑器Markdown,没有用别的编辑器。Markdown编辑器是目前最好的编辑器之一。
感谢这个编辑器的创始人Jhon Gruber。
Markdown是一种可以使用普通文本编辑器编写的标记语言,通过简单的标记语法,可以是普通文本内容具有一定的格式。Markdown具有一系列衍生版本,用于扩展Markdown的功能(如表格、脚注、内嵌HTML等)这些功能原始的Markdown尚不具备。而衍生版本能让Markdown转换成更多的格式,比如LaTeX、Docbook。Markdown增强版中比较有名的有Markdown Extra、MultiMarkdown、Maruku等。这些衍生版本要么基于工具,如Pandoc;要么基于网站,如GitHub和Wikipedia。他们在语法上基本兼容,但在一些渲染效果上有所不他。
关于Markdown的语法,可以查看http://wowubuntu.com/markdown/,或者访问官方网站http://daringfireball.net/projects/markdown/。
2、模型:插入和更新
SQL语句中的INSERT和UPDATE可以实现数据库表中记录的插入和更新。在ORM中,要实现这种操作,可以使用完全类似Python中创建实例或者属性赋值的方式进行。
- 插入数据

上述代码等效于SQL语句的INSERT,最后要使用new_user实例的save()方法才能完成数据的入库操作。除这种方法外,还可以使用如下方式。

create()函数直接让数据入库。
- 修改数据。

注意:save()函数是必须的。
从上述操作可以看出,插入和更新操作不返回QuerySet结果,这与查询操作是有显著差别的。
3、模型:删除
ORM中删除记录是方法非常简单。

语句1创建了User实例之后,语句2直接执行该实例对象的delete()方法即可将实例本身删除,即删除数据库表中的相应记录,删除后返回该记录(实例)的值。
语句3使用filter()方法查询,返回空;语句4使用get()方法,因为已经不存在该记录了,所以报错。
浙公网安备 33010602011771号