You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
django/doc/开发文档.md

50 KiB

开发文档

整个开发过程包括:搭建环境、注册登录功能、首页功能、详情页功能。

1.搭建环境

带领大家搭建开发环境。我们会依次安装python、pip、django、mysql和其他的一些必要类库。

安装python

安装 Python 非常简单,去 Python 官方网站 找到 Python3 的下载地址根据你的系统选择32位或者64位的安装包下载好后双击安装即可。

安装完毕后,在命令行输入 python -v ,如果输出了 Python 的版本号,说明 Python 已安装成功。

$ python3 -V
Python 3.7.0

安装pip

如果已经安装了python3 那么pip3会自动的被安装。

安装django

安装django非常简单一条命令搞定。

pip3 install django

安装mysql

由于该项目使用的是mysql数据库所以需要安装mysql。

如果你使用的是Windows或macOS系统那么可以去 MySQL官网 直接下载安装包一步步安装即可笔者安装的是MySQL 5.7)。安装过程中会提示创建输账号和密码,一定要记得创建哦~。

如果你使用的是Ubuntu系统可以使用apt-get命令安装

sudo apt-get install mysql-server

安装完毕后,可使用mysql -V查看mysql版本号。

然后创建数据库命名为video

CREATE DATABASE video CHARACTER SET utf8;

安装PyCharm

PyCharm 是一款功能强大的 Python 编辑器,具有跨平台性。 我们项目所有功能的开发都是在pycharm上面完成的。

PyCharm官网下载PyCharm安装包。 选择对应系统Windows/Mac的版本下载。一般学习用直接安装社区版本即可足够用。

下载之后,双击点下一步安装即可。

其他安装

另外下面这些是项目开发过程中会用到的类库放到了requirements.txt里面

django_ratelimit==1.1.0 (限制流量的类库)

PyMySQL==0.9.2 (连接mysql数据库的驱动)

django_chunked_upload==1.1.3 (文件上传库)

sorl-thumbnail==12.5.0 (图片显示工具)

pillow==5.3.0 (图片显示)

可以使用pip3直接安装

pip3 install -r requiredments.txt

创建Django工程

一切就绪我们创建django工程仅需要一行命令

django-admin startproject videoproject

创建之后可使用pycharm打开videoproject文件夹查看文件结构

image

pycharm是很强大的有自带的命令行工具Terminal版本控制工具Version Control

打开Terminal输入

python3 manage.py runserver

来启动项目,在之后的开发中,我们会经常用到该命令行来调试程序。

命令行输出

Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

然后在浏览器地址栏输入http://127.0.0.1:8000/ 即可看到django默认首页了。

image

项目配置

项目的配置文件位于videoproject/videoproject/settings.py

配置编码

首先需要配置的是文字编码格式django默认的编码是英语格式我们把它改成中文格式需要修改下面几个变量的值。

LANGUAGE_CODE = 'zh-hans' # zh-hans为中文编码
TIME_ZONE = 'Asia/Shanghai' # 国际时区改成中国时区
USE_I18N = True
USE_L10N = True
USE_TZ = False

配置static

然后还需要配置资源文件目录用于存储CSS、Javascript、Images等文件。这里我们设置目录为/static/

STATIC_URL = '/static/'
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "static"),
)

配置数据库

然后还需要配置数据库信息django默认使用的是sqlite数据库我们修改为mysql数据库。找到DATABASES节点修改为如下代码。其中NAME为数据库名USER为mysql的用户名PASSWORD为密码HOSY为127.0.0.1PORT为3306

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'video',
        'USER': 'root',
        'PASSWORD': '123456',
        'HOST':'127.0.0.1',
        'PORT':'3306',
    }
}

配置好数据库之后,还需要在videoproject/videoproject/__init__.py安装mysql驱动只需要写入代码

import pymysql
pymysql.install_as_MySQLdb()

上面代码运行的前提是你电脑上已经安装了PyMySQL类库。

最后可再次运行工程,检查配置是否正确。

2.注册登录功能

用户注册登录是一个网站的基本功能django对这部分进行了很好的封装我们只需要在django的基础上做些简单的修改就可以达到我们想要的效果。在本讲中我们会用到user中的用户授权方面的一些函数还会对django中的user进行扩展以及django中的form验证。

效果展示

注册页面 image

登录页面 image

创建users应用

django的设计哲学是一个应用只提供一种功能比如users应用只提供用户相关功能comment应用只提供评论相关功能这能提高代码的重复利用率。

在django中只需要下面一条命令即可建立users应用

python3 manage.py startapp users

建表

我们需要一个用户表用来实现登录注册功能虽然django已经自带来用户登录注册功能也有相应的表但是不符合中国人习惯需要我们对user模型进行自定义。实现自定义User模型最简单的方式就是继承AbstractBaseUserAbstractBaseUser实现了User的核心功能我们只需加一些额外的字段进行补充即可。

User模型原有的字段有

  • username
  • password
  • last_login
  • is_superuser
  • first_name
  • last_name
  • email
  • is_staff
  • is_active
  • date_joined

这些都是最基本的字段,并不能满足我们的需求。

根据网站自身业务,我们又添加了下面的字段

  • nickname(昵称)
  • avatar(头像)
  • mobile(手机号)
  • gender(性别)
  • subscribe(是否订阅)

我们只需在users/models.py中写入代码

from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    GENDER_CHOICES = (
        ('M', '男'),
        ('F', '女'),
    )
    nickname = models.CharField(blank=True, null=True, max_length=20)
    avatar = models.FileField(upload_to='avatar/')
    mobile = models.CharField(blank=True, null=True, max_length=13)
    gender = models.CharField(max_length=1, choices=GENDER_CHOICES,blank=True, null=True)
    subscribe = models.BooleanField(default=False)

    class Meta:
        db_table = "v_user"

gender是性别字段其中用到了choices=GENDER_CHOICES。这种方式常常用在下拉框或单多选框例如 M对应男 F对应女。

url配置

在user文件夹下面新建url.py文件写入登录、注册和退出的url信息。app_name是命名空间我们命名为'users'。

from django.urls import path
from . import views

app_name = 'users'
urlpatterns = [
    path('login/', views.login, name='login'),
    path('signup/', views.signup, name='signup'),
    path('logout/', views.logout, name='logout'),
]

url路由配置好了我们下面就开始写视图函数代码了

注册函数

我们先来写注册函数写注册当然得有注册表单了幸运的是在django中可以用代码来生成表单。我们只需在users下新建forms.py文件然后写入注册表单的代码。

class SignUpForm(UserCreationForm):
    username = forms.CharField(min_length=4,max_length=30,
                               error_messages={
                                   'min_length': '用户名不少于4个字符',
                                   'max_length': '用户名不能多于30个字符',
                                   'required': '用户名不能为空',
                               },
                               widget=forms.TextInput(attrs={'placeholder': '请输入用户名'}))
    password1 = forms.CharField(min_length=8, max_length=30,
                                error_messages={
                                    'min_length': '密码不少于8个字符',
                                    'max_length': '密码不能多于30个字符',
                                    'required': '密码不能为空',
                                },
                                widget=forms.PasswordInput(attrs={'placeholder': '请输入密码'}))
    password2 = forms.CharField(min_length=8,max_length=30,
                                error_messages={
                                    'min_length': '密码不少于8个字符',
                                    'max_length': '密码不能多于30个字符',
                                    'required': '密码不能为空',
                                },
                                widget=forms.PasswordInput(attrs={'placeholder': '请确认密码'}))

    class Meta:
        model = User
        fields = ('username', 'password1', 'password2',)

    error_messages = {'password_mismatch': '两次密码不一致', }

我们的表单一共有三个字段username、password1、password2它们都是CharField类型widget分别是TextInput和PasswordInput。而且django是自带验证的只需要我们配置好error_messages字典当form验证的时候就会显示我们自定义的错误信息。 有了注册表单后,就可以在前端模板和视图函数中使用它。

下面是注册视图函数。

...
from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout

def signup(request):
    if request.method == 'POST':
        form = SignUpForm(request.POST)
        if form.is_valid():
            form.save()
            username = form.cleaned_data.get('username')
            raw_password1 = form.cleaned_data.get('password1')
            user = authenticate(username=username, password=raw_password1)
            auth_login(request, user)
            return redirect('home')
        else:
            print(form.errors)
    else:
        form = SignUpForm()
    return render(request, 'registration/signup.html', {'form': form})

在signup函数中我们通过form = SignUpForm初始化一个表单并在render函数中传递给模板。

注册模板文件写在了templates/registration/signup.html

关键代码是

<form class="ui large form" novalidate method="post" action="{% url 'users:signup' %}" enctype="multipart/form-data" >
            {% csrf_token %}
            <div class="ui stacked segment">
                <div class="field"> 
                    {{form.username}} 
                </div>
                <div class="field"> 
                    {{form.password1}} 
                </div>
                <div class="field"> 
                    {{form.password2}} 
                </div>
                <button class="ui submit button" type="submit">注册</button>
            </div>
            {% include "base/form_errors.html" %}
        </form>

form的action为

{% url 'users:signup' %}

即在url.py中定义的signup函数。 通过post请求传递给signup在signup中通过如下四行代码来实现注册并自动登录的。

username = form.cleaned_data.get('username')
raw_password1 = form.cleaned_data.get('password1')
user = authenticate(username=username, password=raw_password1)
auth_login(request, user)

登录函数

登录函数与注册函数的模式是一样的都是先写form写模板最后写视图函数。 由于form和模板的代码和注册功能类似这里就不贴了大家可以上github查看。

重点讲一下login视图函数

def login(request):
    if request.method == 'POST':
        next = request.POST.get('next', '/')
        form = UserLoginForm(request=request, data=request.POST)
        if form.is_valid():
            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password')
            user = authenticate(username=username, password=password)
            if user is not None:
                auth_login(request, user)
                return redirect(next)
        else:
            print(form.errors)
    else:
        next = request.GET.get('next', '/')
        form = UserLoginForm()
    print(next)
    return render(request, 'registration/login.html', {'form': form, 'next':next})

在login函数中我们多了一个next变量next对应的是登录后要跳转的url其实这是一种场景假如你在购物网站买东西最后付款的时候会跳转到付款页假如你没有登录网站会提示你登录登录后会再次跳转到付款页。

当然了跳转到登录页的时候需要你在url后追加next参数如 aaa. com/login/?next=bbb. com 这样用户登录后就会跳到bbb. com

退出函数

from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout

def logout(request):
    auth_logout(request)
    return redirect('home')

退出功能,仅需要一行代码 auth_logout(request) 就ok了。

3.首页功能

在本讲中我们开始首页功能的开发在开发过程中大家将会学习到Django中的通用视图类、分页对象paginator以及foreignKey外键的使用。

效果演示 image

整体功能

大家可先通过 网站演示地址 浏览一下首页的效果。我们首页呢比较简洁大方让人一目了然。我这样设计的目的呢是让大家把精力放到学习django上面来不必过度关注花哨的页面效果。

我们把首页拆解为4个小的业务模块来开发分别是列表显示、分页功能、搜索功能、分类功能。下面我们分别对这四个功能模块进行开发讲解。

开发思路

开发一个功能的基本思路是先新建应用然后分析功能涉及到哪些业务从而分析出需要的数据库字段然后编写模型之后就是展示阶段通过url路由配置视图函数来将模型里面的数据显示出来。

ok我们通过命令建立应用命名为video。执行后django将为我们新建video文件夹。

python3 manage.py startapp video

下面的功能模块开发都在该应用(video)下进行。

建模型

此处,我们需要建立两个模型,分别是分类表(classification)和视频表(video)。他们是多对一的关系(一个分类对应多个视频,一个视频对应一个分类)。

首先编写Classification表在model.py下面我们键入如下代码。 字段有title(分类名称)和status(是否启用)

class Classification(models.Model):
    list_display = ("title",)
    title = models.CharField(max_length=100,blank=True, null=True)
    status = models.BooleanField(default=True)

    class Meta:
        db_table = "v_classification"

字段说明

  • title 分类名称。数据类型是CharField最大长度为max_length=100允许为空null=True
  • status 是否启用。数据类型是BooleanField,默认为default=True
  • db_table 表名

然后编写Video模型根据网站业务我们设置了title(标题)、 desc(描述)、 classification(分类)、file(视频文件)、cover(封面)、status(发布状态)等字段。其中classification是一个ForeignKey外键字段表示一个分类对应多个视频一个视频对应一个分类(多对一)

class Video(models.Model):
    STATUS_CHOICES = (
        ('0', '发布中'),
        ('1', '未发布'),
    )
    title = models.CharField(max_length=100,blank=True, null=True)
    desc = models.CharField(max_length=255,blank=True, null=True)
    classification = models.ForeignKey(Classification, on_delete=models.CASCADE, null=True)
    file = models.FileField(max_length=255)
    cover = models.ImageField(upload_to='cover/',blank=True, null=True)
    status = models.CharField(max_length=1 ,choices=STATUS_CHOICES, blank=True, null=True)
    create_time = models.DateTimeField(auto_now_add=True, blank=True, max_length=20)

字段说明

  • title 视频标题。数据类型是charField最大长度为max_length=100允许为空null=True
  • desc 视频描述。数据类型是charField最大长度为max_length=255允许为空null=True
  • file 视频文件地址。数据类型是fileField。其中存的是视频文件的地址在之后的视频管理中我们将会对视频的上传进行具体的讲解。
  • cover 视频封面。数据类型是ImageField。存储目录为upload_to='cover/'允许为空null=True
  • status 视频状态。是一个选择状态用choices设置多选元祖。
  • create_time 创建时间。数据类型是DateTimeField 。设置自动生成时间auto_now_add=True

ForeignKey 表明一种一对多的关联关系。比如这里我们的视频和分类的关系,一个视频只能对应一个分类,而一个分类下可以有多个视频。 更多关于ForeinkKey的说明可以参看 ForeignKey官方介绍

列表显示

要想访问到首页必须先配置好路由。在video下建立urls.py文件写入如下代码

from django.urls import path
from . import views

app_name = 'video'
urlpatterns = [
    path('index', views.IndexView.as_view(), name='index'),
]

一条path语句就代表一条路由信息。这样我们就可以在浏览器输入127.0.0.1:8000/video/index来访问首页了。

显示列表数据非常简单我们使用django中内置的视图模版类ListView来显示首先在view.py中编写IndexView类用它来显示列表数据。键入如下代码

class IndexView(generic.ListView):
    model = Video
    template_name = 'video/index.html'
    context_object_name = 'video_list'  

此处我们使用了django提供的通用视图类ListView, ListView使用很简单只需要我们简单的配置几行代码即可将数据库里面的数据渲染到前端。比如上述代码中我们配置了

  • model = Video, 作用于Video模型
  • template_name = 'video/index.html' 告诉ListView要使用我们已经创建的模版文件。
  • context_object_name = 'video_list' 上下文变量名告诉ListView在前端模版文件中可以使用该变量名来展现数据。

之后我们在templates文件夹下建立video目录用来存放视频相关的模板文件首先我们创建首页文件index.html。并将刚才获取到的数据显示出来。

<div class="ui grid">
    {% for item in video_list %}
    <div class="four wide column">
        <div class="ui card">
            <a class="image">
                {% thumbnail item.cover "300x200" crop="center" as im %}
                <img class="ui image" src="{{ im.url }}">
                {% empty %}
                {% endthumbnail %}
                <i class="large play icon v-play-icon"></i>
            </a>
            <div class="content">
                <a class="header">{{ item.title }}</a>
                <div class="meta">
                    <span class="date">发布于{{ item.create_time|time_since}}</span>
                </div>
                <div class="description">
                    {{ item.view_count}}次观看
                </div>
            </div>
        </div>
    </div>
    {% empty %}
    <h3>暂无数据</h3>
    {% endfor %}
</div>

通过for循环将video_list渲染到前端。这里我们使用到了django中的内置标签比如for语句、empty语句。这些都是django中非常常用的语句。在之后的教程中我们会经常遇到。

另外还使用了thumbnail标签来显示图片thumbnail是一个很常用的python库常常被用来做图片显示。

显示结果如下 首页展示

分类功能

在写分类功能之前,我们先学习一个回调函数 get_context_data() 这是ListView视图类中的一个函数在 get_context_data() 函数中,可以传一些额外内容到模板。因此我们可以使用该函数来传递分类数据。

要使用它,很简单。

只需要在IndexView类下面追加get_context_data()的实现即可。

class IndexView(generic.ListView):
    model = Video
    template_name = 'video/index.html'
    context_object_name = 'video_list' 

    def get_context_data(self, *, object_list=None, **kwargs):
        context = super(IndexView, self).get_context_data(**kwargs)
        classification_list = Classification.objects.filter(status=True).values()
        context['classification_list'] = classification_list
        return context

在上述代码中我们将分类数据通过Classification.objects.filter(status=True).values()从数据库里面过滤出来然后赋给classification_list最后放到context字典里面。

在前端模板templates/video/index.html就可以通过classification_list来取数据。添加代码

<div class="classification">
    <a class="ui red label" href="">全部</a>
    {% for item in classification_list %}
    <a class="ui label" href="">{{ item.title }}</a>
    {% endfor %}
</div>

显示效果如下 image

当然现在只是实现了分类展示效果,我们还需要继续实现点击效果,即点击不同的分类,显示不同的视频列表。

我们先给每个分类按钮加上href链接

<div class="classification">
    <a class="ui red label" href="{% url 'home' %}">全部</a>
    {% for item in classification_list %}
    <a class="ui label" href="?c={{ item.id }}">{{ item.title }}</a>
    {% endfor %}
</div>

通过添加?c={{ item.id }} 这里用c代表分类的id点击后会传到视图类中在视图类中我们使用 get_queryset() 函数将get数据取出来。通过self.request.GET.get("c", None) 赋给c判断c是否为None如果为None就响应全部如果有值就通过get_object_or_404(Classification, pk=self.c)先获取当前类然后classification.video_set获取外键数据。

    def get_queryset(self):
        self.c = self.request.GET.get("c", None)
        if self.c:
            classification = get_object_or_404(Classification, pk=self.c)
            return classification.video_set.all().order_by('-create_time')
        else:
            return Video.objects.filter(status=0).order_by('-create_time')

更多关于ForeignKey的使用方法可参考 这里

分页功能

在Django中有现成的分页解决方案我们开发者省了不少事情。如果是简单的分页只需要配置一下paginate_by即可实现。

class IndexView(generic.ListView):
    model = Video
    template_name = 'video/index.html'
    context_object_name = 'video_list'
    paginate_by = 12
    c = None
  • painate_by = 12每页显示12条

这样每页的分页数据就能正确的显示出来来,现在来完善底部的页码条。

image

页码列表需要视图类和模板共同来完成我们先来写视图类。在前面我们已经写过get_context_data了该函数的主要功能就是传递额外的数据给模板。这里我们就利用get_context_data来传递页码数据。

我们先定义一个工具函数叫get_page_list。 在项目根目录下新建一个文件helpers.py该文件当作一个全局的工具类用来存放各种工具函数。把get_page_list放到helpers.py里面 该函数用来生产页码列表,不但这里可以使用,以后在其他地方也可以调用该函数。

def get_page_list(paginator, page):

    page_list = []

    if paginator.num_pages > 10:
        if page.number <= 5:
            start_page = 1
        elif page.number > paginator.num_pages - 5:
            start_page = paginator.num_pages - 9
        else:
            start_page = page.number - 5

        for i in range(start_page, start_page + 10):
            page_list.append(i)
    else:
        for i in range(1, paginator.num_pages + 1):
            page_list.append(i)

    return page_list

分页逻辑:

if 页数>=10:
    当前页<=5时起始页为1
    当前页>(总页数-5)时,起始页为(总页数-9)
    其他情况 起始页为(当前页-5)

举例:

假设一共16页
情况1: 当前页==5  则页码列表为[1,2,3,4,5,6,7,8,9,10]
情况2: 当前页==8  则页码列表为[3,4,5,6,7,8,9,10,11,12]
情况3: 当前页==15 则页码列表为[7,8,9,10,11,12,13,14,15,16]

当然你看到这个逻辑会有点乱,建议大家读着代码,多试验几遍。

当拿到页码列表我们继续改写get_context_data()函数。 将获取到的classification_list追加到context字典中。

    def get_context_data(self, *, object_list=None, **kwargs):
        context = super(IndexView, self).get_context_data(**kwargs)
        paginator = context.get('paginator')
        page = context.get('page_obj')
        page_list = get_page_list(paginator, page)
        classification_list = Classification.objects.filter(status=True).values()
        context['c'] = self.c
        context['classification_list'] = classification_list
        context['page_list'] = page_list
        return context

你或许对 paginator = context.get('paginator') page = context.get('page_obj')这两行代码感到陌生我们只需要知道context.get('page_obj')返回的是当前页码context.get('paginator')返回的是分页对象,就够了。更加详细的介绍,可参考官方

当数据传递给模板之后,模板就负责显示出来就行了。

因为分页功能比较常用所以需要把它单独拿出来封装到一个单独的文件中我们新建templates/base/page_nav.html文件。然后在index.html里面我们将该文件include进来。

{% include "base/page_nav.html" %}

打开page_nav.html写入代码

{% if is_paginated %}
<div class="video-page">
    <div class="ui circular labels">
        {% if page_obj.has_previous %}
        <a class="ui circular label" href="?page={{ page_obj.previous_page_number }}{% if c %}&c={{c}}{% endif %}{% if q %}&q={{q}}{% endif %}">&lt;</a>
        {% endif %}
        {% for i in page_list %}
        {% if page_obj.number == i %}
        <a class="ui red circular label">{{ i }}</a>
        {% else %}
        <a class="ui circular label" href="?page={{ i }}{% if c %}&c={{c}}{% endif %}{% if q %}&q={{q}}{% endif %}">{{ i }}</a>
        {% endif %}
        {% endfor %}
        {% if page_obj.has_next %}
        <a class="ui circular label" href="?page={{ page_obj.next_page_number }}{% if c %}&c={{c}}{% endif %}{% if q %}&q={{q}}{% endif %}">&gt;</a>
        {% endif %}
    </div>
</div>
{% endif %}

上面代码中我们用到了page_obj对象的几个属性has_previous、previous_page_number、next_page_number。通过这几个属性即可实现复杂的页码显示效果。其中我们还这href里面加了

{% if c %}&c={{c}}

代表分类的id。

搜索功能

要实现搜索,我们需要一个搜索框

因为搜索框是很多页面都需要的所以我们把代码写到templates/base/header.html文件里面。

<div class="ui small icon input v-video-search">
    <input class="prompt" value="{{ q }}" type="text" placeholder="搜索视频" id="v-search">
    <i id="search" class="search icon" style="cursor:pointer;"></i>
</div>

点击搜索或回车的代码写在了static/js/header.js里面。

我们还需要配置一下路由,添加一行搜索的路由。

app_name = 'video'
urlpatterns = [
    path('index', views.IndexView.as_view(), name='index'),
    path('search/', views.SearchListView.as_view(), name='search'),
]

搜索路由指向的视图类为SearchListView

下面我们来写SearchListView的代码

class SearchListView(generic.ListView):
    model = Video
    template_name = 'video/search.html'
    context_object_name = 'video_list'
    paginate_by = 8
    q = ''

    def get_queryset(self):
        self.q = self.request.GET.get("q","")
        return Video.objects.filter(title__contains=self.q).filter(status=0)

    def get_context_data(self, *, object_list=None, **kwargs):
        context = super(SearchListView, self).get_context_data(**kwargs)
        paginator = context.get('paginator')
        page = context.get('page_obj')
        page_list = get_page_list(paginator, page)
        context['page_list'] = page_list
        context['q'] = self.q
        return context

关键代码就是Video.objects.filter(title__contains=self.q).filter(status=0) title__contains是包含的意思表示查询title包含q的记录。利用filter将数据过滤出来。这里写了两层过滤第一层过滤搜索关键词第二层过滤status已发布的视频。

另外这里也用到了get_context_data来存放额外的数据包括分页数据、q关键词。

配置模板文件是templates/video/search.html

因此模板代码写在search.html里面

<div class="ui unstackable items">

    {% for item in video_list %}
    <div class="item">
        <div class="ui tiny image">
            {% thumbnail item.cover "300x200" crop="center" as im %}
            <img class="ui image" src="{{ im.url }}">
            {% empty %}
            {% endthumbnail %}
        </div>
        <div class="middle aligned content">
          <a class="header" href="{% url 'video:detail' item.pk %}">{{ item.title }}</a>
        </div>
    </div>
    {% empty %}
    <h3>暂无数据</h3>
    {% endfor %}

</div>

{% include "base/page_nav.html" %}

搜索功能效果 image

4.详情页功能

在本讲中我们开始详情页功能的开发详情页就是对单个视频进行播放并展示视频的相关信息比如视频标题、描述、评论信息、相关推荐等。我们将会学习到通用视图类DetailView的使用、评论动态加载、以及如何通过ajax实现喜欢和收藏功能并通过一段段很酷的代码来说明这些功能。

效果展示

image

整体功能

大家可先通过 网站演示地址 浏览一下网站效果。点击某个视频即可浏览详情页。详情页实现了是对单个视频进行展示,用户可看到视频的一些元信息,包括标题、描述、观看次数、喜欢数、收藏数等等。另外,网站还实现了评论功能,通过上拉网页即可分页加载评论列表,用户还能添加评论。网页侧栏是推荐视频列表,这里使用的推荐逻辑比较简单,就是推荐观看次数最多的视频。

我们把详情页分为4个小的业务模块来开发分别是视频详情显示、喜欢和收藏功能、评论功能、推荐功能。下面我们分别对这四个功能模块进行开发讲解。

视频详情显示

因为在上一讲中我们已经建立了video模型所以不必再新建模型我们就在video模型的基础上进行扩展。上一讲我们创建的字段有title、desc、classification、file、cover、status、create_time。这些字段目前是不够用的我们再加几个字段需要加观察次数喜欢的用户收藏的用户。video模型扩展后如下

class Video(models.Model):
    STATUS_CHOICES = (
        ('0', '发布中'),
        ('1', '未发布'),
    )
    title = models.CharField(max_length=100,blank=True, null=True)
    desc = models.CharField(max_length=255,blank=True, null=True)
    classification = models.ForeignKey(Classification, on_delete=models.CASCADE, null=True)
    file = models.FileField(max_length=255)
    cover = models.ImageField(upload_to='cover/',blank=True, null=True)
    status = models.CharField(max_length=1 ,choices=STATUS_CHOICES, blank=True, null=True)
    view_count = models.IntegerField(default=0, blank=True)
    liked = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                   blank=True, related_name="liked_videos")
    collected = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                   blank=True, related_name="collected_videos")
    create_time = models.DateTimeField(auto_now_add=True, blank=True, max_length=20)

新增了3个字段

  • view_count 观看次数。数据类型是IntegerField默认是0
  • liked 喜欢的用户。数据类型是ManyToManyField这是一种多对多的关系表示一个视频可以被多个用户喜欢一个用户也可以喜欢多个视频。记得设置用户表为settings.AUTH_USER_MODEL
  • collected 收藏的用户。数据类型是ManyToManyField这是一种多对多的关系表示一个视频可以被多个用户收藏一个用户也可以收藏多个视频。设置用户表为settings.AUTH_USER_MODEL

更多关于ManyToManyField的使用介绍可以查询django官网的介绍。

下面就是详情展示阶段我们先配置好详情页的路由信息在video/urls.py中追加detail的路由信息。


app_name = 'video'
urlpatterns = [
    path('index', views.IndexView.as_view(), name='index'),
    path('search/', views.SearchListView.as_view(), name='search'),
    path('detail/<int:pk>/', views.VideoDetailView.as_view(), name='detail'),
]

path('detail/<int:pk>/', views.VideoDetailView.as_view(), name='detail')即表示详情信息,注意每条视频都是有自己的主键的,所以设置路径匹配为detail/<int:pk>/,其中<int:pk>表示主键这是django中表示主键的一种方法。这样我们就可以在浏览器输入127.0.0.1:8000/video/detail/xxx来访问详情了。

怎么显示详情呢聪明的django为我们提供了DetailView。urls.py中设置的视图类是VideoDetailView我们让VideoDetailView继承DetailView即可。

class VideoDetailView(generic.DetailView):
    model = Video
    template_name = 'video/detail.html' 

看起来超级简单django就是如此的酷只需要我们配置几行代码就能实现很强大的功能。这里我们配置model为Video模型模板为video/detail.html其它的工作都不用管全都交给django去干oh这棒极了。

模板文件位于templates/video/detail.html它的代码比较简单这里就不贴了。

从效果图上我们看到还有个观看次数的展示,这里的观看次数本质上就是数据库里的一个自增字段,每次观看的时候,view_count自动加1。对于这个小需求我们需要做两件事情首先这video模型里面添加一个次数自增函数命名为increase_view_count,这很简单,如下所示:

    def increase_view_count(self):
        self.view_count += 1
        self.save(update_fields=['view_count'])

然后还需要我们在VideoDetailView视图类里面调用到这个函数。这个时候get_object()派上用场了。因为每次调用DetailView的时候django都会回调get_object()这个函数。因此我们可以把increase_view_count()放到get_object()里面执行。完美的代码如下

class VideoDetailView(generic.DetailView):
    model = Video
    template_name = 'video/detail.html'

    def get_object(self, queryset=None):
        obj = super().get_object()
        obj.increase_view_count()  # 调用自增函数
        return obj

目前为止,我们就能在详情页看到标题、描述、观看次数、收藏次数、喜欢次数。预览如下

image

虽然可以显示收藏人数、喜欢人数。但是目前还没实现点击喜欢/收藏的功能。下面我们来实现。

收藏和喜欢功能

收藏和喜欢是一组动作因此可以用ajax来实现用户点击后调用后端接口接口返回json数据前端显示结果。

既然需要接口,那我们先添加喜欢/收藏接口的路由在video/urls.py追加代码如下

path('like/', views.like, name='like'),
path('collect/', views.collect, name='collect'),

由于喜欢和收藏的功能实现非常类似,限于篇幅,我们只实现喜欢功能。

我们先写like函数

@ajax_required
@require_http_methods(["POST"])
def like(request):
    if not request.user.is_authenticated:
        return JsonResponse({"code": 1, "msg": "请先登录"})
    video_id = request.POST['video_id']
    video = Video.objects.get(pk=video_id)
    user = request.user
    video.switch_like(user)
    return JsonResponse({"code": 0, "likes": video.count_likers(), "user_liked": video.user_liked(user)})

首先判断用户是否登录,如果登录了则调用switch_like(user)来实现喜欢或不喜欢功能最后返回json。注意这里添加了两个注解@ajax_required@require_http_methods(["POST"])分别验证request必须是ajax和post请求。

switch_like()函数则写在了video/model.py里面

    def switch_like(self, user):
        if user in self.liked.all():
            self.liked.remove(user)
        else:
            self.liked.add(user)

所有的后端工作都准备好了我们再把视线转向前端。前端主要是写ajax代码。

由于ajax代码量较大我们封装到一个单独的js文件中 ==> static/js/detail.js

在detail.js中我们先实现喜欢的ajax调用

$(function () {

    // 写入csrf
    $.getScript("/static/js/csrftoken.js");

    // 喜欢
    $("#like").click(function(){
      var video_id = $("#like").attr("video-id");
      $.ajax({
            url: '/video/like/',
            data: {
                video_id: video_id,
                'csrf_token': csrftoken
            },
            type: 'POST',
            dataType: 'json',
            success: function (data) {
                var code = data.code
                if(code == 0){
                    var likes = data.likes
                    var user_liked = data.user_liked
                    $('#like-count').text(likes)
                    if(user_liked == 0){
                        $('#like').removeClass("grey").addClass("red")
                    }else{
                        $('#like').removeClass("red").addClass("grey")
                    }
                }else{
                    var msg = data.msg
                    alert(msg)
                }

            },
            error: function(data){
              alert("点赞失败")
            }
        });
    });

上述代码中,关键代码是$.ajax()函数,我们传入了参数:video_idcsrftoken。其中csrftoken可通过/static/js/csrftoken.js生成。在success回调中通过判断user_liked的值来确定自己是否喜欢过然后改变模板中相应的css。

推荐功能

每个网站都有自己的推荐功能,且都有自己的推荐逻辑。我们视点的推荐逻辑是根据访问次数最高的n个视频来降序排序然后推荐给用户的。

实现起来非常容易我们知道详情页实现用的是VideoDetailView我们可以在get_context_data()中把推荐内容传递给前端模板。只需要我们改写VideoDetailView的get_context_date()函数。

    def get_context_data(self, **kwargs):
        context = super(VideoDetailView, self).get_context_data(**kwargs)
        form = CommentForm()
        recommend_list = Video.objects.get_recommend_list()
        context['form'] = form
        context['recommend_list'] = recommend_list
        return context

改写后,我们添加了一行

recommend_list = Video.objects.get_recommend_list()

我们把获取推荐列表的函数get_recommend_list()封装到了Video模型里面。在Video/models.py里面 我们追加代码:

class VideoQuerySet(models.query.QuerySet):
    def get_recommend_list(self):
        return self.filter(status=0).order_by('-view_count')[:4]

关键是self.filter(status=0).order_by('-view_count')[:4]通过order_by把view_count降序排序并选取前4条数据。

注意此处我们用了VideoQuerySet查询器需要我们在Video下面添加一行依赖。表示用VideoQuerySet作为Video的查询管理器。

objects = VideoQuerySet.as_manager()

当模板拿到数据后即可渲染显示。这里我们将推荐侧栏的代码封装到templates/video/recommend.html里面。

# templates/video/recommend.html
{% load thumbnail %}
<span class="video-side-title">推荐列表</span>
<div class="ui unstackable divided items">
    {% for item in recommend_list %}
    <div class="item">
        <div class="ui tiny image">
            {% thumbnail item.cover "300x200" crop="center" as im %}
            <img class="ui image" src="{{ im.url }}">
            {% empty %}
            {% endthumbnail %}
        </div>
        <div class="middle aligned content">
            <a class=" header-title" href="{% url 'video:detail' item.pk %}">{{ item.title }}</a>
            <div class="meta">
                <span class="description">{{ item.view_count }}次观看</span>
            </div>
        </div>
    </div>
    {% empty %}
    <h3>暂无推荐</h3>
    {% endfor %}

</div>

并在detail.html中将它包含进来

{% include "video/recommend.html" %}

评论功能

评论区位于详情页下侧显示效果如下。共分为两个部分评论form和评论列表。

image

评论功能是一个独立的模块该功能通用性较高在其他很多网站中都有评论功能为了避免以后开发其他网站时重复造轮子我们建立一个新的应用命名为comment

python3 manage.py startapp comment

接下来我们创建comment模型

# 位于comment/models.py

class Comment(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    nickname = models.CharField(max_length=30,blank=True, null=True)
    avatar = models.CharField(max_length=100,blank=True, null=True)
    video = models.ForeignKey(Video, on_delete=models.CASCADE)
    content = models.CharField(max_length=100)
    timestamp = models.DateTimeField(auto_now_add=True) 

    class Meta:
        db_table = "v_comment"
  • user 用户。数据类型是ForeignKey外键是settings.AUTH_USER_MODEL并设置为级联删除on_delete=models.CASCADE
  • nickname 用户昵称。数据类型是CharField。
  • avatar 头像。数据类型是CharField。
  • video 对应的视频。数据类型是ForeignKey对应Video模型级联删除 on_delete=models.CASCADE
  • content 评论内容。 数据类型是CharField。
  • timestamp 评论时间。 数据类型是DateTimeField。

有了模型之后我们就可以专心写业务代码了首先在comment下建立路由文件urls.py。并写入代码:

from django.urls import path
from . import views

app_name = 'comment'
urlpatterns = [
    path('submit_comment/<int:pk>',views.submit_comment, name='submit_comment'),
    path('get_comments/', views.get_comments, name='get_comments'),
]

我们配置了两条路由信息:评论提交 和 获取评论。

提交评论需要一个form我们把form放到video/forms.py

from django import forms
from comment.models import Comment

class CommentForm(forms.ModelForm):
    content = forms.CharField(error_messages={'required': '不能为空',},
        widget=forms.Textarea(attrs = {'placeholder': '请输入评论内容' })
    )

    class Meta:
        model = Comment
        fields = ['content']

然后在video/views.py的VideoDetailView下添加form的相关代码。

class VideoDetailView(generic.DetailView):
    model = Video
    template_name = 'video/detail.html'

    def get_object(self, queryset=None):
        obj = super().get_object()
        obj.increase_view_count()
        return obj

    def get_context_data(self, **kwargs):
        context = super(VideoDetailView, self).get_context_data(**kwargs)
        form = CommentForm() 
        context['form'] = form 
        return context

在get_context_data()函数里面我们把form传递给模板。

同样的提交评论也是异步的我们用ajax实现我们打开static/js/detail.js写入

    // 提交评论
    var frm = $('#comment_form')
    frm.submit(function () {
        $.ajax({
            type: frm.attr('method'),
            url: frm.attr('action'),
            dataType:'json',
            data: frm.serialize(),
            success: function (data) {
                var code = data.code
                var msg = data.msg
                if(code == 0){
                    $('#id_content').val("")
                    $('.comment-list').prepend(data.html);
                    $('#comment-result').text("评论成功")
                    $('.info').show().delay(2000).fadeOut(800)
                }else{
                    $('#comment-result').text(msg)
                    $('.info').show().delay(2000).fadeOut(800);
                }
            },
            error: function(data) {
            }
        });
        return false;
    });

评论通过ajax提交后我们在submit_comment()中就能接收到这个请求。处理如下

def submit_comment(request,pk):
    video = get_object_or_404(Video, pk = pk)
    form = CommentForm(data=request.POST)

    if form.is_valid():
        new_comment = form.save(commit=False)
        new_comment.user = request.user
        new_comment.nickname = request.user.nickname
        new_comment.avatar = request.user.avatar
        new_comment.video = video
        new_comment.save()

        data = dict()
        data['nickname'] = request.user.nickname
        data['avatar'] = request.user.avatar
        data['timestamp'] = datetime.fromtimestamp(datetime.now().timestamp())
        data['content'] = new_comment.content

        comments = list()
        comments.append(data)

        html = render_to_string(
            "comment/comment_single.html", {"comments": comments})

        return JsonResponse({"code":0,"html": html})
    return JsonResponse({"code":1,'msg':'评论失败!'})

在接收函数中通过form自带的验证函数来保存记录然后将这条记录返回到前端模板。

下面我们开始评论列表的开发。

评论列表部分我们使用了的是上拉动态加载的方案即当页面拉到最下侧时js加载代码会自动的获取下一页的数据并显示出来。前端部分我们使用了一种基于js的开源加载插件。基于这个插件,可以很容易实现网页的上拉动态加载效果。它使用超级简单,仅需要调用$('.comments').dropload({})即可。我们把调用的代码封装在static/js/load_comments.js里面。

完整的调用代码如下:

$(function(){
    // 页数
    var page = 0;
    // 每页展示15个
    var page_size = 15;

    // dropload
    $('.comments').dropload({
        scrollArea : window,
        loadDownFn : function(me){
            page++;

            $.ajax({
                type: 'GET',
                url: comments_url,
                data:{
                     video_id: video_id,
                     page: page,
                     page_size: page_size
                },
                dataType: 'json',
                success: function(data){
                    var code = data.code
                    var count = data.comment_count
                    if(code == 0){
                        $('#id_comment_label').text(count + "条评论");
                        $('.comment-list').append(data.html);
                        me.resetload();
                    }else{
                        me.lock();
                        me.noData();
                        me.resetload();
                    }
                },
                error: function(xhr, type){
                    me.resetload();
                }
            });
        }
    });
});

不用过多的解释这段代码已经非常非常清晰了本质还是ajax的接口请求调用调用后返回结果更新前端网页内容。

我们看到ajax调用的接口是get_comments我们继续来实现它它位于comment/views.py中。代码如下所示这段代码也很简单没有什么复杂的技术。当获取到page和page_size后使用paginator对象来实现分页。最后通过render_to_string将html传递给模板。

def get_comments(request):
    if not request.is_ajax():
        return HttpResponseBadRequest()
    page = request.GET.get('page')
    page_size = request.GET.get('page_size')
    video_id = request.GET.get('video_id')
    video = get_object_or_404(Video, pk=video_id)
    comments = video.comment_set.order_by('-timestamp').all()
    comment_count = len(comments)

    paginator = Paginator(comments, page_size)
    try:
        rows = paginator.page(page)
    except PageNotAnInteger:
        rows = paginator.page(1)
    except EmptyPage:
        rows = []

    if len(rows) > 0:
        code = 0
        html = render_to_string(
            "comment/comment_single.html", {"comments": rows})
    else:
        code = 1
        html = ""

    return JsonResponse({
        "code":code,
        "html": html,
        "comment_count": comment_count
    })