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:服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。一般来说,这个问题会在服务器的程序码出错时出现。

 

posted @ 2019-05-29 15:22  taoziya  阅读(246)  评论(0)    收藏  举报