3.2 发布和显示文章
对文章的管理主要是发布、删除和编辑。对于每个部分,都有一些详细的要求,本节就围绕这三个功能,开发管理文章的程序,如下图所示。
3.2.1 简单的文章发布
下面从简单的文章发布开始,讲解运行机制。
首先介绍一个预备知识slug,下面举例说明。
假如在数据库中有一篇标题为Learn Python in itdiffer.com的文章,要访问这篇文章,一种方法是使用这篇文章的id,比如http://www.itdiffer.com/article/231,231就是这篇文章在数据库表中的id;还有另外一种方法,是直接在URL中显示文章的标题(http://www.itdiffer.com/article/Learn Python in itdiffer.com),但实际上不能这样,因为在URL里面,空格都是用“%20”来表示的,所以上面那个地址最终表现形式应该是https://www.itdiffer.com/article/Learn%20Python%20in%20itdiffer.com。这种显示不是一个很友好的方案,这时slug就可以发挥作用了,它实现的结果是http://www.itdiffer.com/article/Learn-Python-in-itdiffer.com,这种方式对搜索引擎也是友好的。
Django中就提供了实现上述slug的方法。在本项目的根目录中执行python manage.py shell,进入到交互模式。
表现完美。不过我们有时也会用中文写标题,不总是用英文的。那么中文怎样转换呢?
这就不完美了,Django内置的这个方法不能处理中文,它返回的是空。还好,有Google协助,很容易就搜索到了一个能够解决中文的第三方库。
安装之后,还是在交互模式中,代码如下。
自动将汉字转换为拼音,这样就解决了前面遇到的汉字问题,并且英语依然适用。仔细观察,与Django自带的还稍微有一些区别呢!
这是一个好工具。
下面开始按套路出牌----套路就是规则,对于大的系统,按照规则来做,成本是最低的。
1、创建数据模型类
在./article/models.py文件中建立一个数据模型,命名为ArticlePost,作为文章的数据模型对象。
1 from django.utils import timezone #① 2 from django.core.urlresolvers import reverse 3 from slugify import slugify 4 5 class ArticlePost(models.Model): 6 author = models.ForeignKey(User,related_name="article") 7 title = models.CharField(max_length=200) 8 slug = models.SlugField(max_length=500) 9 column = models.ForeignKey(ArticleColumn,related_name="article_column") 10 body = models.TextField() 11 created = models.DateTimeField(default=timezone.now()) #② 12 updated = models.DateTimeField(auto_now=True) 13 14 class Meta: 15 ordering = ("title",) 16 index_together = (('id','slug'),) #③ 17 18 def __str__(self): 19 return self.title 20 21 def save(self, *args, **kargs): #④ 22 self.slug = slugify(self.title) #⑤ 23 super(ArticlePost,self).save(*args, **kargs) 24 25 def get_absolute_url(self): #⑥ 26 return reverse("article:article_detail",args=[self.id, self.slug])
在./testsite/settings.py文件中,有一个TIME_ZONE项,要确认一下是否使用TIME_ZONE=‘Asia/Shanghai’时区,语句①所引入的timezone将被用于语句②,timezone.now()即得到文章发布时的日期和时间。
语句③的作用是对数据库中这两个字段建立索引,在后面,会通过每篇文章的id和slug获取该文章对象,这样建立了索引之后,能提高读取文章对象的速度。
每个数据模型类都有一个save方法,语句④对此方法进行重写,其目的就是要实现语句⑤。
语句⑥是要获取谋篇文章对象的URL,暂时用不到。前面引入模块时看到的reverse就在这个方法中使用了。
新的数据模型建立后,就要迁移数据,从而建立数据库表。依次执行python manage.py makemigrations和python manage.py migrate两个命令。
2、创建表单类
编辑./article/forms.py文件,增加如下代码。
1 class ArticlePostForm(forms.ModelForm): 2 class Meta: 3 model = ArticlePost 4 fields = ("title","body")
3、创建视图函数
编辑./article/views.py文件,输入如下代码。
1 @login_required(login_url='/account/login') 2 @csrf_exempt 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 cd = article_post_form.cleaned_data 8 try: 9 new_article = article_post_form.save(commit=False) 10 new_article.author = request.user 11 new_article.column = request.user.article_column.get(id=request.POST['column_id']) 12 new_article.save() 13 return HttpResponse("1") 14 except: 15 return HttpResponse("2") 16 else: 17 return HttpResponse("3") 18 else: 19 article_post_form = ArticlePostForm() 20 article_columns = request.user.article_column.all() #⑦ 21 return render(request,"article/column/article_post.html",{"article_post_form":article_post_form,"article_columns":article_columns})
在视图函数中会用到ArticlePost和ArticlePostForm类,不要忘记在本文件头部引入。
语句⑦得到的是该登录用户已经设置的栏目对象,但为什么没有使用比较熟悉的article_columns = ArticleColumn.objects.filter(user=request.user)呢?这么写当然可以,在此使用request.user.article_column.all()的目的在于引起读者关注ArticleColumn数据模型类中的user = models.ForeignKey(User,on_delete=models.CASCADE,related_name='article_column'),这里有一个参数related_name,为了理解它的作用。
当一个页面被请求时,Django创建一个包含请求元数据的HttpRequest对象,然后把HttpRequest作为视图函数的第一个参数传入,每个视图要负责返回一个HttpResponse对象。这个HttpResponse对象有很多属性,其中包括熟知的method、GET和POST。user也是一个属性,当用户登录之后,request.user即为User类的一个实例。
4、设置URL
编辑./article/urls.py文件,添加如下URL配置语句。
1 path('article-post/',views.article_post,name="article_post"),
5、编写模板
在./templates/article/column目录中创建一个名为article_post.html的文件,其代码如下。
1 {% extends "article/base.html" %} 2 {% load staticfiles %} 3 {% block title %}article column{% endblock %} 4 {% block content %} 5 <div style="margin-left:10px"> 6 <form class="form-horizontal" action="." method="post">{% csrf_token %} 7 <div class="row" style="margin-top:10px;"> 8 <div class="col-md-2 text-right"><span>标题:</span></div> 9 <div class="col-md-10 text-left">{{article_post_form.title}}</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 <select id="which_column"> 15 {% for column in article_columns %} 16 <option value="{{column.id}}">{{column.column}}</option> 17 {% endfor %} 18 </select> 19 </div> 20 </div> 21 <div class="row" style="margin-top:10px;"> 22 <div class="col-md-2 text-right"><span>内容:</span></div> 23 <div class="col-md-10 text-left">{{article_post_form.body}}</div> 24 </div> 25 <div class="row"> 26 <input type="button" class="btn btn-primary btn-lg" value="发布" onclick="publish_article()"> 27 </div> 28 </form> 29 </div> 30 <script type="text/javascript" src='{% static "js/jquery-3.3.1.js" %}'></script> 31 <script type="text/javascript" src='{% static "js/layer.js" %}'></script> 32 <script type="text/javascript"> 33 function publish_article(){ 34 var title = $("#id_title").val(); 35 var column_id = $("#which_column").val(); 36 var body = $("#id_body").val(); 37 $.ajax({ 38 url:"{% url 'article:article_post' %}", 39 type:"POST", 40 data:{"title":title, "body":body, "column_id":column_id}, 41 success:function(e){ 42 if(e=="1"){ 43 layer.msg("successful"); 44 }else if(e=="2"){ 45 layer.msg("sorry."); 46 }else{ 47 layer.msg("项目名称必须写,不能空。"); 48 } 49 }, 50 }); 51 } 52 </script> 53 {% endblock %}
前端模板还没有结束,还要修改./templates/article/leftslider.html文件,目的是设置一个发布文章的入口。
1 <p><a href="{% url 'article:article_post' %}">发布文章</a> </p>
将上面这行代码放在leftslider.html文件的合适位置。
6、测试本功能
确保Django服务已运行,在浏览器的地址栏中输入http://127.0.0.1:8000/article/article-post/,然后在打开的页面中输入要发布的内容,如下图所示。
界面的确很朴素,连按钮的位置都没有好好安排,而且在文本框中只能输入文本。不过,重点是看功能是否实现,单击“发布”按钮看一下效果。
弹出一个对话框,上面有"successful”,表示成功了;如果报错,要仔细排查,对照上述代码,必要时“请”出Google,或者根据本书最开始列出的其他方式求助。
经过上面6个步骤,就完成了简单的文章发布功能的开发。
接下来就可以美化一下了。
3.2.2 使用Markdown
Markdown是一种轻量级的标记语言,由Jhon Gruber于2004年创立。索然是一种“标记语言”,但是用起来并不难,因为它语法简单、标记符数量少,更重要的是可以完全可视化操作。
在本项目中,我们使用这样一个Markdown编辑器。
1、部署相关文件
2、修改模板
这个插件主要是基于前端的,所以只需要修改./tenplates/article/column/article_post.html文件。首先,在文件中引入两个CSS文件。
1 <link rel="stylesheet" href="{% static 'editor/css/style.css' %}"> 2 <link rel="stylesheet" href="{% static 'editor/css/editormd.css' %}">
其中,editormd.css文件是所下载的插件自带的,另外一个style.css文件是笔者根据所下载的插件中的例子改写的,代码如下。
1 #layout { 2 text-align: left; 3 } 4 5 #layout > header, .btns { 6 padding: 15px 0; 7 width: 90%; 8 margin: 0 auto; 9 } 10 11 #layout > header > h1 { 12 font-size: 20px; 13 margin-bottom: 10px; 14 }
在此模板文件中找到如下代码。
1 <div class="col-md-10 text-left">{{article_post_form.body}}</div>
用下面的代码替换上面找到的代码.
1 <div id="editormd" class="col-md-10 text-left"> 2 <textarea style="display:none;" id="id_body"></textarea> 3</div>
最后在模板文件尾部引入JavaScript文件和相应的脚本代码。
1 <script type="text/javascript" src="{% static 'editor/editormd.min.js' %}"></script> 2 <script type="text/javascript"> 3 $(function(){ 4 var editor = editormd("editormd",{ 5 width:"100%", 6 height:640, 7 syncScrolling:"single", 8 path:"{% static 'editor/lib/' %}" 9 }); 10 }); 11 </script>
可以查看效果了。确保Django服务在运行并且用户处于登录状态,比如还是使用前面已经出现的账号登录。在浏览器的地址栏中输入http://127.0.0.1:8000/article/article-post/地址(如果用户没有登录,系统会提示登录),就可以看到发布文章的界面,如下图所示。
呈现在显示器上的是一个功能丰富的图文混排的编辑器,当然这个编辑器是我们所使用的Editor插件中最简单的。根据Markdown的语法输入内容,在左边输入后,在右边可以看到显示效果。
通过这种方式发布的文章,就不再是简单的文本了,而是可以包含图片、文字、音频、视频等多媒体文档。特别推荐使用Markdown的标记符号,能够让你编辑文档更快捷。
通过点击“发布”按钮,就能够把文章发布,但是不能浏览,接下来就要显示文章及其标题列表。
3.2.3 文章标题列表
1、简单的标题列表
编辑./article/views.py文件,编写一个简单的视图函数。
1 from .models import ArticleColumn,ArticlePost 2 3 @login_required(login_url='/account/login/') 4 def article_list(request): 5 articles = ArticlePost.objects.filter(author=request.user) #① 6 return render(request,"article/column/article_list.html",{"articles":articles})
用语句①筛选出用户的所有文章对象,并将该对象渲染到模板。
创建模板文件./templates/article/column/article_list.html,输入如下代码。
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>{{ article.title }}</td> 17 <td>{{ article.column }}</td> 18 <td>--</td> 19 </tr> 20 {% endfor %} 21 </table> 22 </div> 23 {% endblock %}
上述的模板代码跟文章栏目名称列表代码类似,实现了简单的标题列表。最后一步就是设置URL,编辑./article/urls.py文件,增加下面的代码。
1 path('article-list/',views.article_list,name="article_list"),
因为增加了文件,所以要重启Django服务,在浏览器的地址栏中输入http://127.0.0.1:8000/article/article-list/,效果如下图所示。
还可以看一下数据库,对数据的存储有更直观的了解。
多发布几篇文章,别满足于看文章标题列表页,可以将数据库中的记录顺序和列表页显示的文章标题顺序进行对比,可以发现,文章标题列表中的文章顺序没有按照数据库表中的顺序排列(也不是倒序),是按照什么排列规则排列的呢?能看出来吗?
请翻阅前面写的ArticlePost类,其中有下面的代码。
1 class Meta: 2 ordering = ("title",) 3 index_together = (('id','slug'),)
如果以前没有体会到ordering = ("title",)的作用,仔细观察文章标题列表的结果,是不是按照标题名称的顺序排列的?的确是。
如果要修改为按照文章编辑/发布的时间倒序排列,就应该修改ordering的值,代码如下。
1 ordering = ("-updated",)
保存之后,再次刷新页面,就看到所期望的结果了。
为了访问方便,左侧的功能栏当然要增加显示文章标题的入口,编辑./templates/article/leftslider.html文件,在已经熟悉的位置增加下面的代码。
1 <p><a href="{% url 'article:article_list' %}">文章列表</a> </p>
再次刷新浏览器页面,效果如下图所示、
2、查看文章
文章标题列出来了,接下来就是为其设置超链接,单击标题后就能查看文章内容。
再次阅读ArticlePost类的代码,里面有如下内容:
1 def get_absolute_url(self): 2 return reverse("article:article_detail",args=[self.id, self.slug])
这段代码中使用了reverse()函数,先进行望文生义,reverse有“颠倒、倒转”之意。
reverse()函数的调用方式是:
1 reverse(viewname,urlconf=None,args=None,kwargs=None,current_app=None)
参数viewname就是在每个应用的urls.py中设置URL时name的值。比如,前面刚刚在./article/urls.py中增加的path('article-list/',views.article_list,name="article_list"),配置中的name="article_list"。
还是以上述为例,如果要向该视图函数发出请求,可以使用如127.0.0.1:8000/article/article-list/的形式,其中127.0.0.1:8000是ip地址和端口部分,而/article/article-list/就是路径部分,严格说是请求资源的URI。
在模板文件中,遇到了需要使用链接地址时,可以使用{% url viewsname %}的形式,比如:
1 <a href="{% url 'article:article_list' %}">文章列表</a>
其实上述代码也可以写成如下形式:
1 <a href= '/article/article_list/' >文章列表</a>
为了避免硬编码问题,所以采用前述方式,只要规定的名称不变化,路径设置的修改不会影响这里的超链接对象的访问。
这是在模板中,如果在视图函数中呢?比如:
1 HttpResponseRedirect('/article/article-list/')
如果这样写,也是“硬编码”的风格,要避免。代替它的就是使用reverse()函数。
1 HttpResponseRedirect(reverse('article:article_list'))
通过reverse('article:article_list')实现了'/article/article-list/',即从name到path,而在URL配置中是从path到name,因此reverse()起到了“逆向、颠倒”的作用。
reverse()参数中除viewsname外,还可以传入其他数值,比如前面的reverse("article:article_detail",args=[self.id, self.slug])
在ArticlePost类中的get_absolute_url()方法,就是要得到相应文章的路径,因此可以将其用于模板文件./templates/article/column/article_list.html的<a>标签中。
1 <td><a href="{{ article.get_absolute_url }}">{{ article.title }}</a> </td>
这样修改模板文件,实现的效果是让每个标题都实现超链接,链接对象是该文章的详细内容。
刷新页面,如果Django服务已经运行,那么就会看到如下图所示的报错信息,这没有什么意外。
NoReverseMatch at /article/article-list/,这意味着URL配置存在问题。要增加article_detail的URL配置,用来显示文章的详情。在./article/urls.py中增加如下URL配置代码。
1 from django.shortcuts import get_object_or_404 2 3 @login_required(login_url='/account/login/') 4 def article_detail(request,id,slug): 5 article = get_object_or_404(ArticlePost,id=id,slug=slug) 6 return render(request, "article/column/article_detail.html", {"article": article})
再次编写./templates/article/column/article_detail.html模板文件,为了快速查看效果,先写一个简单的。
1 {% extends "article/base.html" %} 2 {% block title %}article list{% endblock %} 3 {% block content %} 4 <div> 5 <h1>{{ article.title }}</h1> 6 <p>{{user.username }}</p> 7 <div> 8 {{ article.body }} 9 </div> 10 </div> 11 {% endblock %}
刷新文章列表页面,可以看到每个文章标题都已经做好了超链接,如下图所示。
点击某个标题,可以查看该标题所对应的文章,如下图所示。
虽然文章内容的显示方式没有进行美化,但不要在乎其外表,还是要看本质,已经实现了几本功能。此外,还要观察一下URL的特点,与我们所涉及的模式正好符合。
3、按排版格式显示
尽管已经能够看到所发布的内容了,但是在显示方式上并没有按照发布文章时所设定的格式显示。所以,还需要继续编辑显示文章内容的模板文件./templates/article/column/article-detail.html,主要是将Markdown标记符转换为HTML标记符并显示在网页上,重新编辑之后的代码如下。
1 {% extends "article/base.html" %} 2 {% block title %}article list{% endblock %} 3 {% block content %} 4 <div> 5 <header> 6 <h1>{{ article.title }}</h1> 7 <p>{{user.username }}</p> 8 </header> 9 10 <link rel="stylesheet" href="{% static 'editor/css/editormd.preview.css' %}" /> 11 <div id="editormd-view"> 12 <textarea id="append-test" style="display:none;"> 13 {{ article.body }} 14 </textarea> 15 </div> 16 17 </div> 18 <script src="{% static 'js/jquery-3.3.1.js' %}"></script> 19 <script src="{% static 'editor/lib/marked.min.js' %}"></script> 20 <script src="{% static 'editor/lib/prettify.min.js' %}"></script> 21 <script src="{% static 'editor/lib/raphael.min.js' %}"></script> 22 <script src="{% static 'editor/lib/underscore.js' %}"></script> 23 <script src="{% static 'editor/lib/sequence-diagram.min.js' %}"></script> 24 <script src="{% static 'editor/lib/flowchart.min.js' %}"></script> 25 <script src="{% static 'editor/lib/jquery.flowchart.min.js' %}"></script> 26 <script src="{% static 'editor/editormd.js' %}"></script> 27 28 <script type="text/javascript"> 29 $(function(){ 30 editormd.markdownToHTML("editormd-view",{ 31 htmlDecode:"style,script,iframe", // you can filter decode 32 emoji:true, 33 taskList:true, 34 tex:true, //默认不解析 35 flowChart:true, //默认不解析 36 sequenceDiagram:true, //默认不解析 37 }); 38 }); 39 </script> 40 {% endblock %}
对于上述代码,在测试过程中就遇到一个“坑”,用了好长时间才解决,那就是写{{ article.body }}代码时不要像编写HTML代码时那样缩进,前面不能有空格,因为它带过来的Markdown标记,不能按照HTML的编码格式写。读者可以试试,看缩进几个空格是什么效果。
上述代码引入了大量的JavaScript文件,都是我们所使用的Markdown插件所提供的。
再次刷新页面,所看到的就应该是一篇按照排版要求显示的页面了,如下图所示。
然而,“发布文章”的功能还要优化,目前成功发布一篇文章之后,仅仅是提示成功,页面并没有跳转。
4、发布文章后页面跳转
文章发布之后,页面应该跳转到“文章列表”,这样就可以通过列表中的文章标题查看该文章的内容,当然也可以跳转到文章内容页,下面演示的是跳转到文章标题列表页面,读者可以在学习了下面的内容之后,自己实现跳转到该文章内容页的功能。
需要修改的仅仅是./templates/article/column/article_post.html中的JavaScript部分代码。
1 <script type="text/javascript"> 2 function publish_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:article_post' %}", 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 if(e=="2"){ 15 layer.msg("sorry."); 16 }else{ 17 layer.msg("项目名称必须写,不能空。"); 18 } 19 }, 20 }); 21 } 22 </script>
在视图文件中,文章被保存后向前端反馈,前端则通过Javascript脚本捕获反馈信息并进行判断,语句①就是根据文章已经被正确保存的反馈信息作出的页面跳转操作。
完成上述编码之后,自行测试,看看能否实现预想的功能。
查看已经做好的“栏目管理”、“发布文章”和“文章列表”三个功能,然后查看文章详情。一个不错的具有内容发布和管理功能的系统已经有了雏形------关键这是一个刚刚开始学习Dajngo的你做的。眼前的代码,还有功能没有完成,“文章列表”中的“操作”一项下面还是空的,要补充对文章的删除和编辑功能-----这就是程序员的工作。
3.2.4 知识点
1、关于slug
slug的目的在于为每条记录生成一个URL,并且让这个URL更易读。比如一篇文章的URL,有的是http://www.itdiffer.com/course/38,这里使用文章在数据库中的id(id=38),这中方式虽然能够显示对应内容,但是不容易阅读;如果用类似http://www.itdiffer.com/course/learn-django-with-laoqi的样式,那么这个URL的可读性就很好了。当然,不能仅仅如此,因为文章标题会重复,最好是http://www.itdiffer.com/course/38/learn-django-with-laoqi。这里的“learn-django-with-laoqi”就是要保存在数据库中的slug。为此,在数据模型的字段属性中有专门的一个属性models.SlugField(),这个属性的源码读者可以阅读https://docs.djangoproject.com/en/1.10/_modules/django/db/models/fields/#SlugField,它其实也是CharField的子类。Django这么安排,对发布文章的永久链接是非常友好的。
但是,上述的所有阐述是针对英文标题的,如果在中文环境中,则可以使用本节中的方法转换为汉语拼音。
2、模型:“一对多”
“一对多”这种说法翻译自“one-to-many”,不管翻译还是原文,误以为有方向性,即误以为“一对多”和“多对一”是有区别的,而真实的“一对多”没有方向性。比如用户和文章的关系,一个用户可以发表多篇文章和多篇文章可以对应一个用户,是一样的关系。
在数据库中,为了建立两个表之间的“一对多”的关联,要使用外键。所谓外键(Foreign Key),是用于建立两个数据库表之间的链接的一个或者多个字段。比如有一个数据库表A和用户数据库表User建立了外键关系,在表A中有一个名为user_id的字段,它就是表A的外键。当表A的数据被保存时,会自动在user_id记录中保存User表中用户的id,用这种方式创建了数据库表A和用户表User之间的链接,也就明确了表A中的每条记录是属于哪一个用户的。
Django的数据模型类中的某个字段的属性被设置为某个表的外键后,在数据库表中,以“字段名”+“_id”的方式命名一个记录,并保存关联数据库表中记录的id值(或者理解为关联数据模型类实例的id)。如下图所示显示的就是本节所创建的数据模型类ArticlcePost对应的数据库表结构。
外键有几个常用的参数,能够为数据访问带来很多便利,下面仅列出两个。