3.1 管理文章栏目
网站中必须有内容,如果按照内容产生的方式来分类,就目前来讲可以分为两类,一类是“用户生成内容(UGC)”,比如“YouTube”、“Twitter”;另一类是“专业生产内容(PGC/PPC)”,比如“跟老齐学itdiffer.com”。
本章要学习的项目是做“用户生成内容”的网站。这种网站有前后两部分,“前面”是让访客浏览网站上的文档,“后面”是用户管理自己的文章。
每个发布文章的用户都希望能用“栏目”来对自己的文章进行归类,不至于让自己的页面显得凌乱,如下图所示。

3.1.1 设置栏目
在实现对文章的管理功能之前,要创建“文章管理”的应用,这是必须的,以区别前面的各种应用。

创建一个名为article的应用,并且要在./testsite/settings.py中进行设置.

然后就是创建数据模型、表单、视图函数、前端模板和配置URL。
1、栏目的数据模型
对于文章栏目,这里不做多级栏目设置,只设置一级栏目。编辑./article/models.py文件,输入如下代码。
1 from django.db import models 2 from django.contrib.auth.models import User 3 4 # Create your models here. 5 class ArticleColumn(models.Model): 6 user = models.ForeignKey(User,on_delete=models.CASCADE,related_name='article_column') #① 7 column = models.CharField(max_length=200) 8 created = models.DateField(auto_now_add=True) 9 10 def __str__(self): 11 return self.column
用户和文章栏目之间是“一对多”的关系,即一个用户可以设置多个文章栏目,通过语句①实现ArticleColumn和User之间的这种关系。在Django中,模型对象之间的关系可以概括为“一对一”、“一对多”和“多对多”三种关系,分别对应OneToOneField、ForeignKey、ManyToManyField。
数据模型建好后就要迁移数据了,生成数据库表。

有了数据模型,自然少不了表单类,因为要通过表单填写栏目名称,即为column字段赋值。所以,要创建./article/forms.py文件,并编写如下代码。
1 from django import forms 2 from .models import ArticleColumn 3 4 class ArticleColumnForm(forms.ModelForm): 5 class Meta: 6 model = ArticleColumn 7 fields = ("column",)
基本机构的内容暂告一段落,下面开始做应用部分。
2、简易视图函数
为了对即将做的东西有一个直观的感受,暂时不写表单类,而是写一个简单的视图函数,先看看要达到的效果。
编辑./article/views.py文件,输入如下代码。
1 from django.shortcuts import render 2 from django.contrib.auth.decorators import login_required 3 from .models import ArticleColumn 4 # Create your views here. 5 @login_required(login_url='/account/login') 6 def article_column(request): 7 columns = ArticleColumn.objects.filter(user=request.user) #② 8 return render(request,"article/column/article_column.html",{"columns":columns})
在article_column()函数中,主要是通过语句②将数据库表中该用户所属的栏目都读取出来。语句②本质上是两条语句合并起来的,一条是ArticleColumn.objects.all(),然后根据user=request.user的条件进行筛选,即ArticleColumn.objects.all().filter(user=request.user),这两个查询行为可以用语句②这样的一条指令表达。
接下来配置./testsite/urls.py中的URL.
1 path('article/',include('article.urls',namespace='article')),
再创建./article/urls.py文件,设置本应用的URL。
1 from django.urls import path 2 from . import views 3 4 app_name="article" 5 urlpatterns = [ 6 path('article-column/', views.article_column,name="article_column"), 7 ]
下面就要编写前端模板了。
3、前端模板
现在要实现用户管理自己的文章栏目,对于这种管理功能,我把它视为所谓的“后台”,即不是显示给所有用户看到的,只有本用户才能使用。所以,显示样式上也有所变化。
在./templates目录中建立article子目录,然后创建./templates/article/header.html文件,其代码如下。
1 {% load staticfiles %} 2 <div class="container"> 3 <nav class="navbar navbar-default" role="navigation"> 4 <div class="navbar-header"> 5 <a class="navbar-brand" href="http://www.itdiffer.com"><img width="100px" src=" 6 {% static 'images/backlogo.png' %"} </a> 7 </div> 8 <div> 9 <ul class="nav navbar-nav" role="tablist"> 10 <li><a href="{% url 'article:article_column' %}">文章管理</a> </li> 11 </ul> 12 <ul class="nav navbar-nav navbar-right" style="margin-right:10px"> 13 <li><a href="{% url 'blog:blog_title' %}">网站首页</a> </li> 14 <li><span>{{ user.username }}</span></li> 15 <li><a href="{% url 'account:user_logout' %}">Logout</a> </li> 16 </ul> 17 </div> 18 </nav> 19 </div>
仔细观察,发现与./templates/header.html源码相差不大。
当用户登录后,要管理自己的文章栏目,就要进入所谓的“后台”,要为登录用户设置入口,入口位置与前面设置的“修改密码”和“个人信息”的位置一样,所以顺便修改./templates/header.html文件设置入口。注意,这里通过入口进入后台的页面,选择了现在正准备做的这个页面,当然也可以修改为其他页面。
<li><a href="{% url 'article:article_column' %}">后台管理</a> </li>
后台部分的footer.html文件,可以继续使用./templates/footer.html。
一般的管理后台,左侧都有一个功能栏目,我们也来做一个。创建./templates/article/leftslider.html文件,代码如下。
1 <div class="bg-info"> 2 <div class="text-center" style="margin-top:5px;"> 3 <p><a href="{% url 'article:article_column' %}">栏目管理</a> </p> 4 </div> 5 </div>
下面就组装./templates/article/base.html文件,代码如下。
1 <!DOCTYPE html> 2 {% load staticfiles %} 3 <html lang="zh-cn"> 4 <head> 5 <meta http-equiv="X-UA-COMPATIBLE" content="IE=Edge"> 6 <meta charset="UTF-8"> 7 <meta name="viewport" content="width=device-width,initial-scale=1"> 8 <title>{% block title %}{% endblock %}</title> 9 <link rel="stylesheet" href="{% static 'css/bootstrap.css' %}"> 10 </head> 11 <body> 12 <div class="container"> 13 {% include 'article/header.html' %} 14 <div class="col-md-2"> 15 {% include 'article/leftslider.html' %} 16 </div> 17 <div class="col-md-10"> 18 {% block content %} 19 {% endblock %} 20 </div> 21 {% include 'footer.html' %} 22 </div> 23 24 </body> 25 </html>
上面做的都是准备工作,还没编写视图函数所要求的模板呢!下面建立./templates/article/column/article_column.html文件,并且输入如下代码。
1 {% extends "article/base.html" %} 2 {% load staticfiles %} 3 {% block title %}article column{% endblock %} 4 {% block content %} 5 <div> 6 <p class="text-right"><button class="btn btn-primary">add column</button> </p> 7 <table class="table table-hover"> 8 <tr> 9 <td>序号</td> 10 <td>栏目名称</td> 11 <td>操作</td> 12 </tr> 13 {% for column in columns %} 14 <tr> 15 <td>{{ forloop.counter }}</td> 16 <td>{{ column.column }}</td> 17 <td>--</td> 18 </tr> 19 {% empty %} 20 <p>还没有设置栏目,太懒了。</p> 21 {% endfor %} 22 </table> 23 </div> 24 {% endblock %}
以{% for column in columns %}实现表格中每行数据的输出。{{ forloop.counter }}中的forloop只在循环内部起作用,它是一个模板变量,具有提示循环进度的属性,不如这里使用的forloop.counter的效果是得到每个循环的顺序号,其本质是显示当前循环次数的计数器(所以从1开始了)。如果变量columns引用的对象为空,则通过{% empyt %}执行后面的内容。循环语句中的{% empty %}省略了通过if来判断。
模板做好了,运行Django服务,在浏览器的地址栏中输入http://127.0.0.1:8000/article/article-column/

目前这个用户还没有设置任何栏目,所以是上面的显示效果。如果想看栏目的效果,可以先用火狐的SQLite Manager工具向数据库中添加几条数据,因为页面右上角的"add column"功能还没有做呢,如下图所示

数据添加之后,再刷新页面,效果如下图。
4、增加新栏目
增加新栏目的操作流程是点击add column按钮,,弹出一个对话框,在这个对话框中输入新的栏目名称。
为了实现弹出对话框,还是用第2章中使用的layer.js插件。编辑./templates/article/column/article_column.html文件,将原来的<button>进行适当修改。
1 <p class="text-right"><button id="add_column" onclick="add_column()" class="btn btn-primary">add column</button> </p>
当用户单击这个按钮时,触发add_column()函数(JavaScript函数),这个函数代码如下(下面的代码放在文件尾部{% endblock %}之内),提醒大家,这种将JavaScript函数和HTML绑定到一起写的方法,在真实的项目中不值得提倡,本书中的项目因为主要是学习Django,所以为了阅读和理解方便,就没有将代码分开,请读者注意。
1 <script type="text/javascript" src="{% static 'js/jquery-3.3.1.js' %}"></script> 2 <script type="text/javascript" src="{% static 'js/layer.js' %}"></script> 3 <script type="text/javascript"> 4 function add_column(){ 5 var index = layer.open({ 6 type:1, 7 skin:"layui-layer-rim", 8 area:["400px","200px"], 9 title:"新增栏目", 10 content: '<div class="text-center" style="margin-top:20px"><p>请输入新的栏目名称</p> 11 <p><input type="text"></p></div>', 12 btn:['确定', '取消’], 13 yes: function(index,layero){ 14 column_name = $('#id_column').val(); 15 alert(column_name); 16 }, 17 btn2: function(index,layero){ 18 layer.close(index); 19 } 20 }); 21 } 22 </script>
在增加的代码中,引入jQuery和layer是必须的,重点要观察add_column()视图函数,下面是所有代码。
1 from django.shortcuts import render 2 from django.contrib.auth.decorators import login_required 3 from .models import ArticleColumn 4 from django.views.decorators.csrf import csrf_exempt 5 from django.http import HttpResponse 6 from .forms import ArticleColumnForm 7 # Create your views here. 8 @login_required(login_url='/account/login') 9 @csrf_exempt #⑥ 10 def article_column(request): 11 if request.method == "GET": 12 columns = ArticleColumn.objects.filter(user=request.user) 13 column_form = ArticleColumnForm() 14 return render(request,"article/column/article_column.html",{"columns":columns,'column_form':column_form}) 15 if request.method == "POST": 16 column_name = request.POST['column'] 17 columns = ArticleColumn.objects.filter(user_id=request.user.id,column=column_name) #⑦ 18 if columns: 19 return HttpResponse('2') 20 else: 21 ArticleColumn.objects.create(user=request.user, column=column_name) 22 return HttpResponse("1")
本书中已经讨论过提交表单的CSRF问题,这里使用语句⑥在视图函数前面添加装饰器的方式也是解决提交表单中遇到的CSRF问题的一种方式。
在视图函数中,用条件语句判断请求类型是GET还是POST。如果是POST,即前端提交的栏目名称,就要检验一下该名称是否已经存在,注意语句⑦的条件有两个,
一个是当前用户,另一个是栏目名称。然后判断查询结果,如果数据库中没有该栏目名称,则允许创建。
根据要展示的效果,对./templates/article/column/article_column.html中的JavaScript部分代码进行适当修改,修改后的代码如下。
1 <script type="text/javascript"> 2 function add_column(){ 3 var index = 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>请输入新的栏目名称</p> 9 <p>{{column_form.column}}</p></div>', 10 btn:['确定', '取消’], 11 yes: function(index,layero){ 12 column_name = $('#id_column').val(); 13 <!--alert(column_name);--> 14 $.ajax({ 15 url:'{% url "article:article_column" %} 16 type:'POST', 17 data:{"column":column_name}, 18 success:function(e){ 19 if(e=="1"){ 20 parent.location.reload(); 21 layer.msg("good"); 22 }else{ 23 layer.msg("此栏目已有,请更换名称") 24 } 25 }, 26 }); 27 }, 28 btn2: function(index,layero){ 29 layer.close(index); 30 } 31 }); 32 } 33 </script>
通过这段脚本,前端可以根据来自后端的不同反馈给予不同的显示。特别提醒,在实际的项目中国,上述代码还缺少一些东西,那就是对用户输入的内容进行判断。一般来讲要限制用户输入的字符内容,比如栏目名称只允许是字母和汉字,为此需要在得到栏目名称column_name之后,用正则表达式来判断该名称是否合法。如果合法,就使用Ajax传送数据,否则提示用户重新为栏目命名。读者可以自行修改上述JavaScript代码,实现上述判断。
完成上述代码后,通过测试可以检查是否实现预期功能。
我们得到了如下图所示的结果。

3.1.2 编辑栏目
在栏目列表中,有一项“操作”列,其下面应该列出能够对本行栏目名称实施的操作。在本项目中,笔者设置“删除”和“编辑”两个操作。
首先,通过修改前端./templates/article/column/article_column.html文件,显示两种操作图标。将原来“操作”列下面对应的代码(原来是<td>--</td>)进行修改。
1 <td> 2 <a name="edit" href="javascript:" onclick="edit_column(this,{{ column.id }})"><span 3 class="glyphicon glyphicon-pencil"></span> </a> 4 <a name="delete" href="javascript:" onclick="del_column(this,{{ column.id }})"><span 5 class="glyphicon glyphicon-trash" style="margin-left:20px;"></span> </a> 6 </td>
刷新 http://127.0.0.1:8000/article/article-column/页面,可以看到出现了两个图标,通常“铅笔”图标对应“编辑”功能,“垃圾桶”图标对应“删除“功能,如下图所示。

为了能够实现对栏目名称的修改,要再写一个视图函数(位于./article/views.py),代码如下。
1 from django.views.decorators.http import require_POST 2 3 @login_required(login_url='/account/login') 4 @require_POST 5 @csrf_exempt 6 def rename_article_column(request): 7 column_name = request.POST["column_name"] 8 column_id = request.POST['column_id'] 9 try: 10 line = ArticleColumn.objects.get(id=column_id) #① 11 line.column = column_name #② 12 line.save() 13 return HttpResponse("1") 14 except: 15 return HttpResponse("0")
在上述代码中,多了一个装饰器@require_POST,所以一定要在文件顶部声明from django.views.decorators.http import require_POST,使用这个装饰器的目的就是保证此视图函数只接收通过POST方式提交的数据。
语句①根据所要修改的栏目名称所在记录的id,查询到该数据,并建立实例对象。语句②则实现将该属性重新赋值的功能,不要忘记line.save()函数,否则不能保存到数据库中。
然后在./article/urls.py中配置URL,代码如下。
1 path('rename-column/',views.rename_article_column,name="rename_article_column"),
后端工作完成,继续修改前端模板文件。在./templstes/article/column/article_column.html文件中编写JavaScript代码,如下所示。
1 function edit_column(the, column_id){ 2 var name = $(the).parents("tr").children("td").eq(1).text(); 3 var index = 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>请编辑的栏目名称</p><p><input type="text" 9 id="new_name" value="'+name+'"></p></div>', 10 btn:['确定','取消'], 11 yes: function(index,layero){ 12 new_name = $("#new_name").val(); 13 $.ajax({ 14 url:"{% url 'article:rename_article_column' %}", 15 type:"POST", 16 data:{"column_id": column_id,"column_name":new_name}, 17 success: function(e){ 18 if(e=="1"){ 19 parent.location.reload(); 20 layer.msg("good"); 21 }else{ 22 layer.msg("新的名称没有保存,修改失败。") 23 } 24 }, 25 }); 26 }, 27 }); 28 }
此函数与前面编写的JavaScript函数类似。
检查Django服务是否在运行,在http://127.0.0.1:8000/article/article-column/页面中,单击代表“编辑”的铅笔图标,实现修改栏目名称的功能,如下图所示。

在弹出的对话框中修改栏目名称后,单击“确定”按钮,会有一个提示“good”闪现,意味着修改成功了,之后页面被刷新了,如下图所示。

3.1.3 删除栏目
删除功能较“编辑”功能简单一些,实现方法还是一样的。
先编写视图函数,编辑./article/views.py文件,输入如下代码。
1 @login_required(login_url='/account/login') 2 @require_POST 3 @csrf_exempt 4 def del_article_column(request): 5 column_id = request.POST["column_id"] 6 try: 7 line = ArticleColumn.objects.get(id=column_id) 8 line.delete() #① 9 return HttpResponse("1") 10 except: 11 return HttpResponse("2")
通过语句①删除该数据记录,然后设置URL,在./article/urls.py文件中增加如下代码。
1 path('del-column/',views.del_article_column,name="del_article_column"),
接下来在模板文件./templates/article/column/article_column.html中编写一个名为del_column的JavaScript函数,代码如下。
1 function del_column(the, column_id){ 2 var name = $(the).parents("tr").children("td").eq(1).text(); 3 var index = 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>是否确定删除{'+name+'}栏目</p></div>', 9 btn:['确定','取消'], 10 yes: function(){ 11 $.ajax({ 12 url:"{% url 'article:del_article_column' %}", 13 type:"POST", 14 data:{"column_id": column_id}, 15 success: function(e){ 16 if(e=="1"){ 17 parent.location.reload(); 18 layer.msg("has been deleted."); 19 }else{ 20 layer.msg("删除失败"); 21 } 22 }, 23 }); 24 }, 25 }); 26 }
检查Django服务是否在运行,在http://127.0.0.1:8000/article/article-column/页面中,单击代表“删除”的垃圾桶图标,实现删除栏目名。

在弹出的对话框中,单击“确定”按钮,会有一个提示“has been deleted.”闪现,意味着删除成功了,之后页面被刷新了,如下图所示。

如果需要将“文章栏目”的管理权限赋给超级管理员,就编辑./article/admin.py文件,输入如下代码。
1 from django.contrib import admin 2 from .models import ArticleColumn 3 # Register your models here. 4 class ArticleColumnAdmin(admin.ModelAdmin): 5 list_display = ('column','created','user') 6 list_filter = ("column",) 7 8 admin.site.register(ArticleColumn, ArticleColumnAdmin)
登录管理员后台http://127.0.0.1:8000/admin/,会看到如下图所示的效果。

对栏目的管理设置已经差不多完成了,其实代码可以继续优化,比如前端的JavaScript代码中两次用到了雷同的Ajax方法,就可以写一个函数来完成,但本书不讨论优化代码的问题。
假如读者对JavaScript还不熟悉,就应该立刻行动,学起来。
3.1.4 知识点
1、模板语法:继承和包含
在编写前端模板时,通常会使用模板的继承和包含,这都是为了尽可能避免重复前端代码。
首先要定义一个基础模板,比如本节中用到的base.html。在基础模板中,通常会使用很多的{% block %},其基本格式是:
{% block name %} <!--html--> {% endblock %}
每个块中可以写HTML代码,也可以为空。一般来说,在基础模板中定义{% block %}数量多一些,是绝对有好处的。如果将来在子模板中有相同名称(name)的,就会将base.html中定义的快覆盖。
在子模板中继承的方式是使用了{% extends "base.html" %},并且放在该页面中的第一个模板标签位置,一般放在第一行最保险了。在子模板中如果重写了父模板中的某个块,则按照子模板中的方式显示。
此外,在模板中还可以使用{% include "templatename.html" %}包含其他模板。
为了更灵活地使用JavaScript和CSS,通常不在base.html里面引入相关文件。在真实的项目中,建议在base.html中设定{% block js %}{% endblock %}和{% block css%}{% endblock %},然后在子模板中重写这两个块,最后调入该模板页面所需要的JavaScript和CSS。
2、模型:查询
Django封装了对数据的操作,可以使用更Python的方式实现数据查询,而不是使用SQL语句。从本节开始,将陆续出现常用的数据查询方式。
BlogArticles是一个数据模型类,可以用Python中常用的dir()和help()两个方法来研究这个类(也是一个对象)所具有的属性和方法。
其中有一个onjects属性是经常被使用的,可以继续用dir(BlogArticles.objects)和help(BlogArticles.objects)查看其详细的属性和方法。
这里展示的结果是一个Manage类(对象),每个数据模型类都有这个对象,或者说除非特别操作,每个数据模型类都有一个objects属性,并借着Manager对象中的各种属性和方法实现对数据库的基本查询操作。例如BlogArticles.objects.all()查询得到的结果是一个被称为QuerySet对象组成的列表,列表中的每个元素就是数据库表中的一条记录,也可以理解为BlogArticles类的一个实例。
BlogArticles.objects.get()中的get()是必须要精准匹配的查询方法,如果查询方法内容不存在,则会报错。因为是精准匹配,所以查询到的第一个总是符合条件的记录。下面分别列出几种常用的查询方法。
- 查询所有对象

查询结果users所引用的是一个序列(类列表的QuerySet对象,不可变),可以使用Python中某些对序列的操作方法来操作。

- 根据条件查询对象
在SQL中可以通过where设置查询条件,在Django的QuerySet中也有一个类似的方法filter()。

这里查询出username中以“meimei”这个字符串结尾的对象。filter()还有很多参数可以使用,读者可以参考官方文档。
除filter()能够实现根据条件查询外,还有exclude()和get(),如下所示。

get()和filter()的区别在于,如果查询对象不存在,get()会报错,而filter()返回空。
此外,查询操作还支持链接过滤。

- 查询结果排序
对于查询返回的QuerySet结果,默认是按照数据模型类中定义的字段排序的,如果要重新排序,可以使用order_by()方法。

比较上面两个排序结果。
当然,order_by()的参数中可以写多个字段名称,即按照字段前后顺序分别作为排序的“主关键词”和“次关键词”。
浙公网安备 33010602011771号