4.7 管理和应用文章标签
本节所讨论的是“文章标签”,这不同于前面学习过的“模板标签”。所谓“文章标签”,就是文章作者可以为自己的文章设置标签(tag),也称为文章的关键词,如下图所示。

4.7.1 管理文章标签
有一个名称为django-taggit的应用可以直接管理文章标签,如果读者对此有兴趣,可以查看其官方网站(http://django-taggit.readthedocs.io/latest/index.html),并尝试应用到本项目中。
本节我们要做一个属于自己的标签,哪怕这个标签很难看,也是自己动手做的,只有这样才能学习Django的功能。所以,本节还是自己编写有关数据模型、表单类和视图函数等。在开发实践中,笔者鼓励使用类似django-taggit这样的第三方应用。
在./article/models.py中编写一个关于文章标签的数据模型类。
1 class ArticleTag(models.Model): 2 author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="tag") 3 tag = models.CharField(max_length=500) 4 5 def __str__(self): 6 return self.tag
这个类要写在ArticlePost的前面,然后在ArticlePost类里面增加如下代码。
1 article_tag = models.ManyToManyField(ArticleTag,related_name='article_tag',blank=True)
然后迁移数据。为了满足好奇心,来看一下数据库表结构,如下图所示。



已经在数据库中看到了两个数据库表article_articlepost_article_tag和article_articletag,分别用来保存文章标签和标签名称和相应的用户id,以及标签与文章的对应关系。
按照前面的学习步骤,在./article/forms.py中编写表单类,代码如下。
1 from .models import ArticleTag 2 3 class ArticleTagForm(forms.ModelForm): 4 class Meta: 5 model = ArticleTag 6 fields = ('tag',)
完成上述步骤后,就可以编写视图函数了。在./article/views.py中分别将ArticleTag和ArticleTagForm两个类引入,然后增加如下函数。
1 from .models import ArticleTag 2 from .forms import ArticleTagForm 3 4 @login_required(login_url='/account/login') 5 @csrf_exempt 6 def article_tag(request): 7 if request.method == "GET": 8 article_tags = ArticleTag.objects.filter(author=request.user) 9 article_tag_form = ArticleTagForm() 10 return render(request,"article/tag/tag_list.html",{"article_tags":article_tags,"article_tag_form":article_tag_form}) 11 if request.method == "POST": 12 tag_post_form = ArticleTagForm(data=request.POST) 13 if tag_post_form.is_valid(): 14 try: 15 new_tag = tag_post_form.save(commit=False) 16 new_tag.author = request.user 17 new_tag.save() 18 return HttpResponse("1") 19 except: 20 return HttpResponse("the data cannot be save.") 21 else: 22 return HttpResponse("sorry,the form is not valid.")
接下来要做的是./article/urls.py中增加URL设置,代码如下。
1 path('article-tag/',views.article_tag,name="article_tag"),
至此,管理文章标签的后端就写好了。下面来完成前端代码的编写。
编辑./templates/article/leftslider.html文件,在有关文章的操作中增加如下一项。
1 <p><a href="{% url 'article:article_tag' %}">文章标签</a> </p>
确认Django和Redis服务都启动了,用一个账号登录,然后访问“管理后台”界面(单击用户名下菜单进入),如下图所示。

当然,现在单击新增加的“文章标签”只能报错,因为我们还没有具体编写针对它的模板。
在./templates/article目录中创建子目录tag,并在里面创建tag_list.html文件,即./templates/article/tag/tag_list.html,编辑这个文件,输入如下代码。
1 {% extends "article/base.html" %} 2 {% load staticfiles %} 3 {% block title %}articles tag{% endblock %} 4 {% block content %} 5 <div> #① 6 <p>添加文章标签</p> 7 <form class="form-horizontal" action="." method="post">{% csrf_token %} 8 <div class="row" style="margin-top:10px;"> 9 <div class="col-md-2 text-right"><span>文章标签</span></div> 10 <div class="col-md-10 text-left">{{article_tag_form.tag}}</div> 11 </div> 12 <div class="row"> 13 <input type="button" class="btn btn-primary btn-lg" style="margin-left:200px;margin-top:10px;" value="添加" onclick="add_tag()"> 14 </div> 15 </form> 16 </div> 17 <div> #② 18 <p>已有标签列表</p> 19 <table class="table table-hover"> 20 <tr> 21 <td>序号</td> 22 <td>文章标签</td> 23 <td>操作</td> 24 </tr> 25 {% for article_tag in article_tags %} 26 <tr id={{ forloop.counter }}> 27 <td>{{ forloop.counter }}</td> 28 <td>{{ article_tag.tag }}</a></td> 29 <td> 30 <a nane="delete" href="javascript:" onclick="del_tag(this, {{ article_tag.id }})"> 31 <span class="glyphicon glyphicon-trash"></span> 32 </a> 33 </td> 34 </tr> 35 {% empty %} 36 <p>You have no article tags. Please add them.</p> 37 {% endfor %} 38 </table> 39 </div> 40 41 <script type="text/javascript" src="{% static 'js/jquery-3.3.1.js' %}"></script> 42 <script type="text/javascript" src="{% static 'js/layer.js' %}"></script> 43 <script type="text/javascript"> 44 45 function add_tag(){ 46 tag = $("#id_tag").val(); 47 $.ajax({ 48 url:'{% url "article:article_tag" %}', 49 type:"POST", 50 data:{"tag":tag}, 51 success:function(e){ 52 if(e=="1"){ 53 layer.msg("You have added a new tag."); 54 window.location.reload(); 55 }else{ 56 layer.msg(e) 57 } 58 } 59 }); 60 } 61 62 function del_tag(the, tag_id){ 63 var article_tag = $(the).parents("tr").children("td").eq(1).text(); 64 layer.open({ 65 type:1, 66 skin:"layui-layer-rim", 67 area:["400px","200px"], 68 title:"删除文章标签", 69 content:'<div class="text-center" style="margin-top:20px"><p>是否确定删除文章标签《'+article_tag+'》</p></div>', 70 btn:['确定','取消'], 71 yes:function(){ 72 $.ajax({ 73 url:'{% url "article:del_article_tag" %}', 74 type:"POST", 75 data:{"tag_id":tag_id}, 76 success:function(e){ 77 if(e=="1"){ 78 parent.location.reload(); 79 layer.msg("The tag has been deleted."); 80 }else{ 81 layer.msg("删除失败"); 82 } 83 }, 84 }) 85 }, 86 }); 87 } 88 </script> 89 {% endblock %}
这部分代码由两部分组成,语句①下面这部分是一个表单,用于添加新的文章标签;语句②下面这部分用于显示已有的文章标签。
语句①这部分的“添加”按钮使用了JavaScript函数add_tag(),通过Ajax方式将新增的文章标签传给后端的视图函数,显示效果如下图所示。

已经实现了添加文章标签的功能,还有一个删除文章标签的功能没有实现------单击文章标签列表中的“删除”符号即可实现。
在./article/views.py中增加一个删除文章标签的视图函数,代码如下。
1 @login_required(login_url='/account/login/') 2 @require_POST 3 @csrf_exempt 4 def del_article_tag(request): 5 tag_id = request.POST['tag_id'] 6 try: 7 tag = ArticleTag.objects.get(id=tag_id) 8 tag.delete() 9 return HttpResponse("1") 10 except: 11 return HttpResponse("2")
读者或许会发现,在这个视图函数上方,还有一个删除文章的视图函数,两个视图函数的基本结构是一致的,所以可以考虑写一个统一的删除某对象的方法-----如果读者学有余力,可以去研究如何解决。
视图函数编写好先后,在./article/urls.py中增加URL的设置,代码如下。
1 path('del-article-tag/',views.del_article_tag,name="del_article_tag"),
然后回到./templates/article/tag/tag_list.html文件,编写单击“删除”图标时所要调用的JavaScript函数del_tag(),相应的代码如下。
1 function del_tag(the, tag_id){ 2 var article_tag = $(the).parents("tr").children("td").eq(1).text(); 3 layer.open({ 4 type:1, 5 skin:"layui-layer-rim", 6 area:["400px","200px"], 7 title:"删除文章标签", 8 content:'<div class="text-center" style="margin-top:20px"><p>是否确定删除文章标签《'+article_tag+'》</p></div>', 9 btn:['确定','取消'], 10 yes:function(){ 11 $.ajax({ 12 url:'{% url "article:del_article_tag" %}', 13 type:"POST", 14 data:{"tag_id":tag_id}, 15 success:function(e){ 16 if(e=="1"){ 17 parent.location.reload(); 18 layer.msg("The tag has been deleted."); 19 }else{ 20 layer.msg("删除失败"); 21 } 22 }, 23 }) 24 }, 25 }); 26 }
至此,就实现了删除功能。
对文章标签的简单管理功能就这样实现了。这里是对原来已经学习过的知识的复习。
4.7.2 发布文章时选择标签
下面要实现的功能是用户发布文章时,能够从已经有的标签中为该文章选定文章标签。
首先在文章发布的页面中,将用户已经添加的标签显示出来。为此要修改./article/views.py文件中的视图函数article_post()。这个函数有两个作用,一是满足GET请求方式的页面显示,二是满足POST请求方式的数据存储。按照开发顺序,先修改GET请求,使得页面显示用户设置的文章标签,并作为发布文章的选项,代码如下。
1 def article_post(request): 2 if request.method =="POST": 3 article_post_form = ArticlePostForm(data=request.POST) 4 if article_post_form.is_valid(): 5 cd = article_post_form.cleaned_data 6 try: 7 new_article = article_post_form.save(commit=False) 8 new_article.author = request.user 9 new_article.column = request.user.article_column.get(id=request.POST['column_id']) 10 new_article.save() 11 return HttpResponse("1") 12 except: 13 return HttpResponse("2") 14 else: 15 return HttpResponse("3") 16 else: 17 article_post_form = ArticlePostForm() 18 article_columns = request.user.article_column.all() 19 article_tags = request.user.tag.all() #① 20 return render(request,"article/column/article_post.html", 21 {"article_post_form":article_post_form,"article_columns":article_columns,"article_tags":article_tags}) #②
语句①是这个视图函数新增的部分,作用是得到当前用户的所有文章标签。语句②增加了“article_tags”:article_tags,将得到的文章标签渲染到模板文件。
编辑./templates/article/column/article_post.html模板文件,在“栏目”代码块下面增加“文章标签”代码块,代码如下。
1 <div class="row" style="margin-top:10px;"> 2 <div class="col-md-2 text-right"><span>栏目:</span></div> 3 <div class="col-md-10 text-left"> 4 <select id="which_column"> 5 {% for column in article_columns %} 6 <option value="{{column.id}}">{{column.column}}</option> 7 {% endfor %} 8 </select> 9 </div> 10 </div> 11 <div class="row" style="margin-top:10px;"> 12 <div class="col-md-2 text-right"><span>文章标签:</span></div> 13 <div class="col-md-10 text-left"> 14 {% for tag in article_tags %} 15 <label class="checkbox-inline"> 16 <input class="tagcheckbox" type="checkbox" id="{{ tag.id }}" name="article_tag" 17 value="{{ tag.tag }}">{{ tag.tag }} 18 </label> 19 {% empty %} 20 <p>You have not type tags for articles. Please <a href="{% url 'article:article_tag' %}">input your tags</a> </p> 21 {% endfor %} 22 </div> 23 </div>
这样就实现了“文章标签”的显示,并且以多选项选择的方式,允许用户在发布文章时设置文章的标签,其效果如下图所示。

这仅仅实现了展示,当单击“发布”按钮向后台提交文章时,通过JavaScript函数publish_article()得到各项内容,并以Ajax的方式发给后端的视图函数。所以,要对该函数进行修改,以得到用户选择的文章标签。
1 <script type="text/javascript" src="{% static 'js/json2.js' %}"></script> #③ 2 <script type="text/javascript"> 3 function publish_article(){ 4 var title = $("#id_title").val(); 5 var column_id = $("#which_column").val(); 6 var body = $("#id_body").val(); 7 var article_tags = []; #④ 8 $.each($("input[name='article_tag']:checked"), 9 function(){article_tags.push($(this).val());}); #⑤ 10 $.ajax({ 11 url:"{% url 'article:article_post' %}", 12 type:"POST", 13 data:{"title":title, "body":body, "column_id":column_id,"tags":JSON.stringify(article_tags)}, #⑥ 14 success:function(e){ 15 if(e=="1"){ 16 layer.msg("successful"); 17 location.href = "{% url 'article:article_list' %}"; 18 }else if(e=="2"){ 19 layer.msg("sorry."); 20 }else{ 21 layer.msg("项目名称必须写,不能空。"); 22 } 23 }, 24 }); 25 } 26 </script>
语句③是新引入的一个JavaScript插件,可以到https://github.com/douglascrockford/JSON-js/blob/master/json2.js下载,其作用在语句⑥中体现了出来,JSON.string()函数就来自于语句③引入的插件,将语句④所定义的Array类型的数值转换为JSON对象。
语句④和语句⑤是配合使用的,语句④定义了一个数组,用语句⑤得到所选择的项目(选择的文章标签),并将项目加入到语句④所定义的数组中。
当数据被提交到后端的视图函数(./article/views/py中的article_post()函数)后,视图函数要接收并处理数据,所以相应地还要修改该视图函数(前面修改了响应GET请求的部分,现在要修改响应POST请求的部分)。
1 import json #注意,要在文件顶部引入JSON模块 2 3 def article_post(request): 4 if request.method =="POST": 5 article_post_form = ArticlePostForm(data=request.POST) 6 if article_post_form.is_valid(): 7 try: 8 new_article = article_post_form.save(commit=False) 9 new_article.author = request.user 10 new_article.column = request.user.article_column.get(id=request.POST['column_id']) 11 new_article.save() 12 tags = request.POST['tags'] #⑦ 13 if tags: 14 for atag in json.loads(tags): #⑧ 15 tag = request.user.tag.get(tag=atag) #⑨ 16 new_article.article_tag.add(tag) #⑩ 17 return HttpResponse("1")
语句⑦得到前端所传过来的文章标签,注意是JSON格式的,如果要在Python中使用,就要用语句⑧中的json.loads()将JSON格式的数据转换为列表。不要忘记,要在此文件的顶部引入JSON模块。JSON模块是Python的标准库之一。
语句⑧中的json.loads(tags)得到了以文章标签为元素的列表,循环此列表,得到每个标签,并用语句⑨得到该文章标签对象。因为在简历文章标签数据模型类之后,又在文章的数据模型类(.article/models.py中的ArticlePost类)中增加了article_tag = models.ManyToManyField(ArticleTag,related_name='article_tag',blank=True),即建立了ArticlePost和ArticleTag之间的多对多关系,所以语句⑩就实现了将文章和文章标签之间对应关系记录的功能。
经过上面的几番修改,就实现了在发布文章时作者可以为文章选定标签的功能。
4.7.3 在文章中显示文章标签
已经为文章添加了标签,那么在显示文章时,也要将标签显示出来。
实现这个功能比较简单的,在./templates/article/list/article_detail.html文件的作者名称、点赞计数的那一行代码下面增加如下代码即可。
1 <p> <span style="margin-right: 10px"><strong>Tags:</strong></span> {{ article.article_tag.all | join:", "}}</p>
之所以如此简单,主要得益于Django模板语言的强大。{{ article.article_tag.all | join:","}}中的article.article_tag.all得到所有的文章标签内容,并且用选择器join使结果以“,”连接。
可以查看页面效果(注意检查Django服务和Redis服务是否运行),如下图所示。

再一次展现Django快速开发的特点。
4.7.4 推荐相似文章
在发布文章时,给每篇文章都设置了文章标签,在这个功能的基础上,可以向阅读某篇文章的用户推荐与该文章相似的文章。有同样标签的文章应该是相似的,并且假设用户想阅读相似的文章,于是就有了推荐相似文章的开发需求。
要实现上述功能,需要编辑./article/list_views.py文件中的article_detail()视图函数,在这个函数的return语句前面,增加如下语句。
1 article_tags_ids = article.article_tag.values_list("id",flat=True) #① 2 similar_articles = ArticlePost.objects.filter(article_tag__in=article_tags_ids).exclude(id=article.id) #② 3 similar_articles = similar_articles.annotate(same_tags=Count("article_tag")).order_by('-same_tags','-created')[:4] #③
语句①中values_list()的作用就是得到article对象的属性article_tag的id列表。请读者不要忘记,ArticlePost和ArticleTag之间是多对多关系,./article/models.py中的ArticlePost类的属性article_tag = models.ManyToManyField(ArticleTag,related_name='article_tag',blank=True)声明了这种关系,所以可以通过article.article_tag得到其文章标签的id值。为了进一步理解values_list(),可以在交互模式中做如下尝试。

如果只声明字段值“id”,返回的是由元组组成的列表,也可以声明两个字段,如下所示。

返回值也是元组组成的列表。但是,如果使用了参数flat=True,结果就有所变化了。

在Django中,除了values_list()外,还有类似的values()。它们有相同的地方,即得到当前对象的所有字段或者指定字段的值;同时也有区别,values()返回值的类型是字典,而values_list返回值的类型是列表(如果不声明flat=True,列表是由元组组成的)。
语句①就得到了当前文章的所有标签(article tag)在数据库表中的id值,并且以列表的形式返回结果。
语句②可以看成两个部分,第一部分是ArticlePost.objects.filter(article_tag__in=article_tags_ids),这是一个条件选择指令,找出文章标签的id在article_tags_ids(列表)里面的所有ArticlePost对象(文章);第二部分是exclude(id=article.id),exclude()是一个条件选择函数,其含义是排除参数所规定的值,即从第一部分帅选出来的结果中,将当前文章清除。这样语句②得到了所有与当前文章有共同文章标签的文章对象,这些对象就是相似文章。
语句③使用了本章前面用过的方法,对所有相似文章,根据与当前文章相同的标签数量进行标注,然后以相同标签数量和文章发布时间为关键词排序(倒序)。不要忘记,这里使用了Count,应该在本文件的顶部引入这个类,即增加from django.db.models import Count。
最后就是将return语句进行适当修改,代码如下。
1 return render(request, "article/read_article.html", {"article": article,"total_views": total_views, 2 "most_viewed": most_viewed,"comment_form":comment_form, 3 "similar_articles":similar_articles})
视图函数修改好之后,修改相应的模板文件./templates/article/list/article_detail.html,找到右边的代码编写位置,在“最多评论文章”栏目的代码块下面增加如下代码。
1 <hr> 2 <p class="text-center"><h3>推荐相似文章</h3></p> 3 {% for similar in similar_articles %} 4 <p><a href="{{ similar.get_url_path }}">{{ similar.title }}</a></p> 5 {% empty %} 6 <p>Sorry,没有相似的文章</p> 7 {% endfor %}
请检查Django和Redis服务是否都已经运行起来了,并且建议多发布几篇文章,以便测试刚才创建的功能,如下图所示。

从“推荐相似文章”栏目中可以浏览与本篇文章有相同标签的其他文章。
我们的Django项目发展到现在,已经建立起了一个具有用户管理和文章管理、浏览等功能的多用户系统,并且设计到了诸多Django的知识。当然,在web开发的道路上,现在也只能说刚刚开始,后面还要保持旺盛的经历不断进取,最终才能体会到成功的喜悦。
4.7.5 知识点
1、HTTP 404
在网站开发中,404是著名的数字了,其全称是“HTTP 404”,是HTTP的状态码。当用户提交访问请求时,服务器没有响应且不知原因,就会出现“server not found”提示。代码404的第一个“4”代表客户端的错误,如错误的URL;后两位数字则代表着特定的错误信息。HTTP的三字符代码与早期通信协议FTP和NNTP的代码类似。
在网站开发中,不仅有404,HTTP的状态码还有不少,以下是一些常见的状态码(节选自维基百科的“HTTP状态码”词条)。
- 200 OK:请求已成功,请求所希望的响应头或数据体将岁此响应返回。
- 302 Found:请求的资源现在临时从不同的URI响应请求。这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定,这个响应才是可缓存的。新的临时性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。注意,索然RFC 1945和RFC 2067规范不允许客户端在重定向时改变请求的方法,但是很多现存的浏览器将302响应视为303响应,并且使用GET方式访问在Location中规定的URI,而无视原先请求的方法。状态码303和307被添加了进来,用以明确服务器期待客户端进行何种反应。
- 400 Bad Request:包含语法错误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求。
- 403 Forbidden:服务器已经理解请求,但是拒绝执行它。与401响应不同的是,身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交。如果这不是一个HEAD请求,而且服务器希望能够讲清楚为何请求不能被执行,那么久应该在实体内描述拒绝的原因。当然服务器也可以返回一个404响应,假如它不希望让客户端获得任何信息。
- 408 Request Timeout:请求超时。客户端没有在服务器预备等待的时间内完成一个请求的发送。客户端可以随时再次提交这个请求而无需进行任何更改。
- 500 Internal Server Error:服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。一般来说,这个问题会在服务器的程序码出错时出现。
浙公网安备 33010602011771号