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()的参数中可以写多个字段名称,即按照字段前后顺序分别作为排序的“主关键词”和“次关键词”。

posted @ 2019-04-30 11:54  taoziya  阅读(221)  评论(0)    收藏  举报