7.2 管理课程标题

 前面对“基于类的视图”有了初步了解,接下来实现课程标题的管理,如下图所示。

7.2.1 判断用户是否登录

当用户登录后,进入“后台管理”界面,现在已经有了“文章管理”和“图片管理”功能,要增加“课程管理”功能。编辑./templates/article/header.html文件,找到导航中的适当位置,增加如下代码。

<li><a href="{% url 'course:manage_course' %}">课程管理</a> </li>

相应的,需要在./course/urls.py文件中增加如下URL配置。

from .views import AboutView,CourseListView,ManageCourseListView

app_name = "course"
urlpatterns = [
 
    path('manage-course/',ManageCourseListView.as_view(),name="manage_course"),
]

在ManageCourseListView类中已经声明了template_name = 'course/manage/manage_course_list.html’,于是创建文件,输入如下代码。

{% extends "article/base.html" %}
{% block title %}manage courses{% endblock %}
{% block content %}
<div>
    <div class='text-right'><button type="button" class="btn btn-primary">添加课程</button> </div>
    <table class="table table-hover" style="margin-top:10px">
        <tr>
            <td>序号</td>
            <td>课程标题</td>
            <td>发布日期</td>
            <td>操作</td>
        </tr>
        {% for course in courses %}
        <tr id={{ forloop.counter }}>
            <td>{{ forloop.counter }}</td>
            <td>{{ course.title }}</td>
            <td>{{ course.created|date:"Y-m-d" }}</td>
            <td>
                <a name="edit" href="#"><span class="glyphicon glyphicon-pencil"></span> </a>
                <a name="delete" href="#"><span class="glyphicon glyphicon-trash" style="margin-left:20px;"></span> </a>
            </td>
        </tr>
        {% endfor %}
    </table>
</div>
{% endblock %}

与上述代码显示效果有关的文件./templates/article/leftslider.html也顺便修改一下,增加与本页面内容相符的左边功能,写法仿照以前的,代码如下。

<hr>
    <div class="text-center" style="margin-top:5px;">
        <p><h4>课程管理</h4></p>
        <p><a href="{% url 'course:manage_course' %}">课程管理</a></p>
    </div>

保存文件之后,在确保Django服务运行的情况下,通过打开登录用户名称的下拉菜单,进入到“后台管理”界面,可以查看“课程管理”导航,效果如下图所示。

貌似感觉用户能够查看自己的课程标题列表了,但是,读者在编写视图文件中的类时,是不是感觉有点奇怪?当初用视图函数实现类似功能时,会在视图函数前面使用一个装饰器函数,用来判断用户是否处于登录状态,如果没有登录,就转到登录页面,而上面所写的基于类的视图函数中,没有实现类似功能的代码。

如果用户未登录,访问http://127.0.0.1:8000/course/manage-course/,会得到如下图所示的错误页面,没有转到登录页面。

要解决上述问题,还要继续对视图文件中的类进行修改。不仅仅是本功能,还有很多其他功能,都要求用户处于登录状态。那么,按照前面的思想,最好写一个Mixin,在每个有关的类中继承。

对于这种常用的东西,往往有人帮我们解决,程序员也可以继承别人的智慧和成果,“站在巨人的肩膀上”,看得更远,开发更快。

Django-braces是第三方的应用,里面包含了一些Django开发中常用的Mixin,其官方网站是https://django-braces.readthedocs.io/en/lates/。

先安装Django-braces,代码如下:

然后编辑./course/views.py视图文件,在文件头部引入LoginRequiredMixin,代码如下。

from braces.views import LoginRequiredMixin

接下来对UserCourseMixin类进行重写,代码如下。

class UserCourseMixin(UserMixin,LoginRequiredMixin):  #①
    model = Course
    login_url = "/account/login"  #②

与原来不同的地方是语句①和语句②,语句①中增加了一个继承的类LoginRequiredMixin,语句②是新增加的,声明了用户登录的URL。

其他代码不变。上述修改之后,重新启动Django服务,在用户未登录状态下访问http://127.0.0.1:8000/course/manage-course/,就会转到登录页面了(注意观察下图中的URL),如下图所示。

 

 7.2.2 创建课程

前面以初步了解“基于类的视图”为目的,顺便完成了显示课程标题列表的功能,现在就要完成“创建课程”的功能了,会涉及到表单类。根据以往经验,创建./course/forms.py文件,其表单类代码如下。

from django import forms
from .models import Course

class CreateCourseForm(forms.ModelForm):
    class Meta:
        model = Course
        fields = ("title","overview")

这些代码都是熟悉的,没有新知识。紧接着就在./course/views.py文件中编写处理GET和POST请求的类,代码如下:

from django.core.urlresolvers import reverse_lazy
from django.views.generic.edit import CreateView
from django.shortcuts import redirect
from .forms import CreateCourseForm

class CreateCourseView(UserCourseMixin,CreateView): #①
    fields = ['title','overview']  #②
    template_name = 'course/manage/create_course.html'

    def post(self,request,*args,**kwargs):  #③
        form = CreateCourseForm(data=request.POST)
        if form.is_valid():
            new_course = form.save(commit=False)
            new_course.user = self.request.user
            new_course.save()
            return redirect("course:manage_course") #④
        return self.render_to_response({"form":form}) #⑤

在文件中完成各种引入,语句①的继承对象列表中包括CreateView(一个通用视图类),当用户以GET方式请求时,即在页面中显示表单,CreateView就是完成这个作用的类,只要继承它,就不需要写get()方法了。语句②是声明在表单中显示的字段。

语句③专门处理以POST方式提交的表单内容,处理方法跟以往的方法一样。要注意语句④,当表单内容被保存后,将页面转向指定位置。

语句⑤是在表单数据检测不通过时,让用户重新填写,注意这里没有使用render(),而是使用了实例的render_to_responase()方法(self.render_to_response({"form":form})),它跟render()在形式上稍有差别,读者是否可以通过某种方式找出其差别?

完成上面的代码后,就可以在./course/urls.py文件中配置URL了(先引入CreateCourseForm类),代码如下。

path('create-course/',CreateCourseView.as_view(),name="create_course"),

最后就剩下创建前端模板了。创建./templates/course/manage/create_course.html文件,代码如下。

{% extends "article/base.html" %}
{% block title %}create course{% endblock %}
{% block content %}
<div style="margin-left:100px;margin-top:10px;">
    <form action="." method="post">{% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Create Course">
    </form>
</div>
{% endblock %}

这里没有使用JavaScript,读者可以以最朴素的方式快速地练习本节内容,并且代码中没有使用新的知识。

为了让“课程列表”中的“添加课程”按钮有效,还需要编辑./templates/course/manage/manage_course_list.html文件中按钮部分的代码,代码如下。

<div class='text-right'><a href="{% url 'course:create_course' %}"><button type="button" class="btn btn-primary">添加课程</button></a></div>

上述代码中较原来增加了<a>超链接,链接的目标是create_course。

重启Django服务,访问http://127.0.0.1:8000/account/login/?next=/course/manage-course/,如果没有登录,可以先登录,然后单击页面中的“添加课程”按钮,效果如下图所示。

 

填写表单,然后单击“Create Course”按钮,跳转到课程列表页。

不妨测试一下,如果有一个表单没有填写,那么系统就会给出提示。因为在创建Course数据模型类时,没有说明哪一项可以为空,所以都是必须项,如下图所示。

“创建课程”的功能就这样实现了。应该重点理解CreateView类,有了它就不用写get()的有关方法了,并且在类中可以重写post()方法,用于处理得到的表单数据。

对于“课程管理”而言,除创建外,还有删除和编辑操作。

7.2.3 删除课程

细心的读者在“课程列表”中看到了两个操作图标,其中由“垃圾桶”图标表示的就是删除操作。在本书前面的内容中,曾经通过编写前端的JavaScript脚本,并以POST方式向服务器发送请求,从而实现删除功能。在这里,读者当然可以用以前学过的方式实现删除,但是因为现在学习的是基于类的视图,所以应该不同于以往的方式实现。

在./course/views.py文件中,引入DeleteView,类似于CreateView,也是Django内置的基于类的视图,代码如下。

from django.views.generic.edit import CreateView,DeleteView

继续在本文件中编写如下代码。

class DeleteCourseView(UserCourseMixin,DeleteView):  #①
    template_name = 'course/manage/delete_course_confirm.html'
    success_url = reverse_lazy("course:manage_course")

在django.views.generic.edit中,不仅有CreateView类,还有DeleteView类、UpdateView类等。语句①中的类继承DeleteView类后,后续代码就不需要重复删除动作了,只需要声明确认删除的模板template_name和删除完成之后的界面success_url即可。

在./course/urls.py文件中,用下面的方式配置URL。

from .views import DeleteCourseView

urlpatterns = [
  path('delete-course/<int:pk>/', DeleteCourseView.as_view(),name="delete_course"),
 ]

在默认状态下,DeleteView类接收以pk或者slug作为参数传入的值,并且通过GET方式访问一个删除的确认页面,然后以POST方式提交删除表单,才能完成删除(读者或者或许对这个流程有异议,暂且保留)。按照上述流程,以GET方式访问的地址就是上述配置的URL,其模板页面即为template_name所规定的页面,下面的代码就是./templates/course/manage/delete_course_confirm.html。

{% extends "article/base.html" %}
{% block title %}delete course{% endblock %}
{% block content %}
<div>
    <form action="" method="post">{% csrf_token %}
        <p>Are you sure you want to delete "{{ object.title }}"?</p> #②
        <input type="submit" value="Confirm">
    </form>
</div>
{% endblock %}

因为在DeleteCourseView类中没有使用context_object_name声明渲染模板的变量名称,所以在模板中像语句②这样使用默认的变量object是不提倡的。这里如此使用,一是为了告知读者,二是因为此模板简单,如此处理也未尝不可。

最后要做的是在./templates/course/manage/manage_course_list.html文件中为“垃圾桶”图标做好超链接,实现单击它进入到删除确认页面中,代码如下。

<a name="delete" href="{% url 'course:delete_course' course.id %}"><span class="glyphicon glyphicon-trash" style="margin-left:20px;"></span> </a>

确保Django服务已经运行,在浏览器中访问http://127.0.0.1:8000/course/manage-course/,查看该用户已有的课程名称列表,单击“删除”图标,进入删除确认页面。

单击“Confirm”按钮,如下图所示,即可删除当前课程标题,并转到课程标题列表页面。

 至此,完成了删除功能。

但是,细思量,不满意。

在基于函数的视图中,类似的删除功能是这样实现的:①单击删除;②弹出确认框;③单击确认;④提交数据;⑤返回结果并跳转/刷新页面。这样的流程才是理想的删除流程,而非上面那样。

为了实现上述的删除流程,必须重写./course/views.py中的DeleteCourseView类,重写之后的代码如下。

class DeleteCourseView(UserCourseMixin,DeleteView):
    template_name = 'course/manage/delete_course_confirm.html'
    success_url = reverse_lazy("course:manage_course")

    def dispatch(self,*args,**kwargs): #③
        resp = super(DeleteCourseView, self).dispatch(*args,**kwargs) #④
        if self.request.is_ajax(): #⑤
            response_data = {"result":"ok"}
            return HttpResponse(json.dumps(response_data),content_type="application/json")
        else:
            return resp

语句③重写了DeleteView类中的dispatch()方法,在这个方法中首先执行语句④。原本在DeleteView类中执行的dispatch()方法后,会实现URL的转向,但是在此指令发送给前端之前,通过语句⑤进行判断,如果是Ajax方法提交过来的数据,就直接反馈HttpResponse对象给前端,前端的JavaScript函数得到反馈结果,这样就完成了删除和页面的刷新。为此,在前端模板中就要嵌入JavaScript代码。下面是重写了./templates/course/manage/manage_course_list.html之后的代码。

{% extends "article/base.html" %}
{% load staticfiles %}
{% block title %}manage courses{% endblock %}
{% block content %}
<div>
    <div class='text-right'>
        <a href="{% url 'course:create_course' %}">
            <button type="button" class="btn btn-primary">添加课程</button>
        </a>
    </div>

    <table class="table table-hover" style="margin-top:10px">
        <tr>
            <td>序号</td>
            <td>课程标题</td>
            <td>发布日期</td>
            <td>操作</td>
        </tr>
        {% for course in courses %}
        <tr id={{ forloop.counter }}>
            <td>{{ forloop.counter }}</td>
            <td>{{ course.title }}</td>
            <td>{{ course.created|date:"Y-m-d" }}</td>
            <td>
                <a name="edit" href="javascript:"><span class="glyphicon glyphicon-pencil"></span></a>
                <a class="delete" nane="delete" href="{% url 'course:delete_course' course.id %}"><span class="glyphicon glyphicon-trash" style="margin-left:20px;"></span></a>
                <a href="{% url 'course:list_lessons' course.id %}"><span class="glyphicon glyphicon-search" style="margin-left: 20px;"></span></a>
            </td>
        </tr>
        {% endfor %}
    </table>
</div>
<script type="text/javascript" src='{% static "js/jquery-3.3.1.js" %}'></script>
<script type="text/javascript">
function getCookie(name) {
    var cookieValue = null;
    if (document.cookie && document.cookie != '') {
        var cookies = document.cookie.split(';');
        for (var i = 0; i < cookies.length; i++) {
            var cookie = jQuery.trim(cookies[i]);
            if (cookie.substring(0, name.length + 1) == (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}
$(document).ready(function() {
    var csrftoken = getCookie('csrftoken');
    function csrfSafeMethod(method) {
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
    }
    $.ajaxSetup({
        crossDomain: false, // obviates need for sameOrigin test
        beforeSend: function(xhr, settings) {
            if (!csrfSafeMethod(settings.type)) {
                xhr.setRequestHeader("X-CSRFToken", csrftoken);
            }
        }
    });
    var onDelete = function(){
        alert("delete it?");
        $.post(this.href, function(data) {
            if (data.result == "ok"){
                window.location.reload();
            } else {
                alert("sth wrong");
            }
        }).fail(function() {
            alert("error");
        });
        return false;
    }
    $(".delete").click(onDelete);
})
</script>
{% endblock %}

 

在JavaScript部分,用一种新的方式解决 CSRF问题,从而实现用POST方式向后端提交数据,请读者认真阅读上述代码,这是另外一种解决问题的方法。当然,上述代码在实现效果上比较朴素,读者完全可以在上述基础上对某些效果进行优化。

完成之后,访问http://127.0.0.1:8000/course/manage-course/进行测试。

如下图所示,单击OK按钮即可删除一个课程并重新载入本页面。

顺利完成删除功能。

我们已经使用了django.views.generic.edit中的CreateView类和DeleteView类,还有一个UpdateView类,这三个类实现了“增改删”常用功能。因为UpdateView类的使用方法与其他类雷同,所以本书在此不再赘述,请读者自行学习,可单靠官方文档。

7.2.4 知识点

1、模型:继承

“继承”是对象的重要特征,建议读者对Python类的继承进行详细学习,有助于对Django中关于模型继承的理解。

这里所说的Django数据模型的继承,不是指前面我们已经看到的数据模型类继承model.Model,而是在下述情况下所发生的继承。

  1. 在多个数据模型类中都有相同的字段(在本项目的各个应用中就有相同的字段),在这种情况下,可以把多个相同的字段抽取出来定义一个类,然后其他的类继承这个类。
  2. 在某个应用中已经有一个数据模型,新的数据模型要用到这个数据模型的字段(不需要在新的数据模型中建立与已有数据模型重复的字段),此时也可以使用继承。
  3. 如果新的数据模型类相对已有的类只有行为的变化,比如排序方式变化,那么也适合用继承的方法解决。

针对上述三种情况,Django中提供了三种类型的继承方式。

  1. 抽象模型继承

针对第一种情况,可以建立“抽象模型”,即在数据模型类的内部类中声明abstract = True,则该数据模型即为抽象模型。例如:

 

from django.db import models
class BaseInfo(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

    class Meta:
        abstract = True
        ordering = ['age']

class Programmer(BaseInfo):
    lang = models.CharField(max_length=100)

class Boss(BaseInfo):
    ppt = models.BooleanField()

 数据模型类Programmer和Boss通过继承BaseInfo,实现了将BaseInfo中的字段和行为继承过来的目的。当实施数据迁移后,在数据库表中,没有BaseInfo所生成的表,只有Programmer和Boss对应的表,在其数据库表的结构中,包含了name和age字段。

在BaseInfo中,如果name=models.ForeignKey(User,on_delete=models.CASCADE, related_name='user_base'),这里使用Foreignkey,目的是当BaseInfo被子类继承之后,每个子类和User建立“一对多”的关系。然而,按照上面的方式定义name会出问题,Programmer和Boss中都是related_name='user_base',如果使用User反向查询,则不知所踪。对此Django早有设计,专门解决次问题。

class BaseInfo(models.Model):
    name=models.ForeignKey(User,on_delete=models.CASCADE, related_name='%(app_label)s_%(class)s_related')
    #省略其他代码

“%(app_label)s”和“%(class)s”都是占位符,从名称上就能猜到,前者对应的是应用名称,后者对应的是子类的名称。这样子类中的related_name就不冲突了,而且跨应用继承也毫无问题。

         ⑵ 多表继承

多变欧继承是针对第二种情况的。

官方文档有一个非常合适的例子,引用过来,相信读者就能理解了。

from django.db import models

class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.Charfield(max_length=80)

class Restaurant(Place):
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)

这种继承,本质上是建立了Place和Restaurant之间的“一对一”关系,即如同在子类中设置了一个OneToOneField类型的字段。

当完成数据迁移后,两个数据模型都会有对应的数据库表。

  ⑶ 代理模型

代理模型就是子类通过自定义的方法或者重写内部类Meta的方式,仅改变模型的方式。例如:

class Student(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

class Teacher(Student):
    class Meta:
        proxy = True
        ordering = ['name']

    def teacher_age(self):
        return self.age + 17

数据模型类Teacher继承了Student,子类没有声明任何字段。在Meta内部类中,通过proxy=True说明Teacher相对Student的关系,并且Teacher的排序规则为ordering = ['name'],这就是行为的变化。类的行为还体现在方法上,后面定义的teacher_age()方法就是如此。

posted @ 2019-06-12 17:30  taoziya  阅读(172)  评论(0)    收藏  举报