|
|
|
|
# 开发文档
|
|
|
|
|
|
|
|
|
|
整个开发过程包括:搭建环境、注册登录功能、首页功能、详情页功能。
|
|
|
|
|
|
|
|
|
|
## 1.搭建环境
|
|
|
|
|
|
|
|
|
|
带领大家搭建开发环境。我们会依次安装python、pip、django、mysql和其他的一些必要类库。
|
|
|
|
|
|
|
|
|
|
<!--more-->
|
|
|
|
|
|
|
|
|
|
### 安装python
|
|
|
|
|
安装 Python 非常简单,去 [Python 官方网站](https://www.python.org/downloads/) 找到 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官网](https://dev.mysql.com/downloads/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官网](https://www.jetbrains.com/pycharm/download/)下载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](http://upload-images.jianshu.io/upload_images/3360192-13253f49899bf944?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
|
|
|
|
|
|
|
|
|
|
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](http://upload-images.jianshu.io/upload_images/3360192-0cc24ffc4cdb2606?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 项目配置
|
|
|
|
|
|
|
|
|
|
项目的配置文件位于`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.1,PORT为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验证。
|
|
|
|
|
|
|
|
|
|
<!--more-->
|
|
|
|
|
|
|
|
|
|
### 效果展示
|
|
|
|
|
|
|
|
|
|
**注册页面**
|
|
|
|
|
![image](http://upload-images.jianshu.io/upload_images/3360192-b8cfde3ae772c17c?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
|
|
|
|
|
|
|
|
|
|
**登录页面**
|
|
|
|
|
![image](http://upload-images.jianshu.io/upload_images/3360192-7ec611aa382b2713?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
|
|
|
|
|
|
|
|
|
|
### 创建users应用
|
|
|
|
|
|
|
|
|
|
django的设计哲学是,一个应用只提供一种功能,比如users应用只提供用户相关功能,comment应用只提供评论相关功能,这能提高代码的重复利用率。
|
|
|
|
|
|
|
|
|
|
在django中,只需要下面一条命令,即可建立users应用
|
|
|
|
|
```
|
|
|
|
|
python3 manage.py startapp users
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 建表
|
|
|
|
|
|
|
|
|
|
我们需要一个用户表,用来实现登录注册功能,虽然django已经自带来用户登录注册功能,也有相应的表,但是不符合中国人习惯,需要我们对user模型进行自定义。实现自定义User模型最简单的方式就是继承AbstractBaseUser,AbstractBaseUser实现了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](https://github.com/geeeeeeeek/videoproject)查看。
|
|
|
|
|
|
|
|
|
|
重点讲一下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外键的使用。
|
|
|
|
|
|
|
|
|
|
<!--more-->
|
|
|
|
|
|
|
|
|
|
**效果演示**
|
|
|
|
|
![image](http://upload-images.jianshu.io/upload_images/3360192-bc61c5d610a923b0?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
|
|
|
|
|
|
|
|
|
|
### 整体功能
|
|
|
|
|
大家可先通过 [网站演示地址](https://v.mypython.me/) 浏览一下首页的效果。我们首页呢,比较简洁大方,让人一目了然。我这样设计的目的呢,是让大家把精力放到学习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官方介绍](https://docs.djangoproject.com/zh-hans/2.1/topics/db/examples/many_to_one/)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 列表显示
|
|
|
|
|
|
|
|
|
|
要想访问到首页,必须先配置好路由。在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中的[内置标签](https://docs.djangoproject.com/zh-hans/2.1/ref/templates/builtins/),比如for语句、empty语句。这些都是django中非常常用的语句。在之后的教程中我们会经常遇到。
|
|
|
|
|
|
|
|
|
|
另外,还使用了thumbnail标签来显示图片,[thumbnail](https://sorl-thumbnail.readthedocs.io/en/latest/installation.html)是一个很常用的python库,常常被用来做图片显示。
|
|
|
|
|
|
|
|
|
|
**显示结果如下**
|
|
|
|
|
![首页展示](http://upload-images.jianshu.io/upload_images/3360192-f296532afe723940?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
|
|
|
|
|
|
|
|
|
|
### 分类功能
|
|
|
|
|
|
|
|
|
|
在写分类功能之前,我们先学习一个回调函数 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](http://upload-images.jianshu.io/upload_images/3360192-caacecc28f209ab4?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
|
|
|
|
|
|
|
|
|
|
当然现在只是实现了分类展示效果,我们还需要继续实现点击效果,即点击不同的分类,显示不同的视频列表。
|
|
|
|
|
|
|
|
|
|
我们先给每个分类按钮加上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的使用方法,可参考 [这里](https://docs.djangoproject.com/zh-hans/2.1/topics/db/examples/many_to_one/)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 分页功能
|
|
|
|
|
在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](http://upload-images.jianshu.io/upload_images/3360192-ef837b01ee6a1526?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
|
|
|
|
|
|
|
|
|
|
页码列表需要视图类和模板共同来完成,我们先来写视图类。在前面我们已经写过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')返回的是分页对象,就够了。更加详细的介绍,可参考[官方](https://docs.djangoproject.com/zh-hans/2.1/topics/pagination/)。
|
|
|
|
|
|
|
|
|
|
当数据传递给模板之后,模板就负责显示出来就行了。
|
|
|
|
|
|
|
|
|
|
因为分页功能比较常用,所以需要把它单独拿出来封装到一个单独的文件中,我们新建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 %}"><</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 %}">></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](http://upload-images.jianshu.io/upload_images/3360192-1659e75746461a39?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 4.详情页功能
|
|
|
|
|
|
|
|
|
|
在本讲中,我们开始详情页功能的开发,详情页就是对单个视频进行播放并展示视频的相关信息,比如视频标题、描述、评论信息、相关推荐等。我们将会学习到通用视图类DetailView的使用、评论动态加载、以及如何通过ajax实现喜欢和收藏功能,并通过一段段很酷的代码来说明这些功能。
|
|
|
|
|
|
|
|
|
|
<!--more-->
|
|
|
|
|
|
|
|
|
|
**效果展示**
|
|
|
|
|
|
|
|
|
|
![image](http://upload-images.jianshu.io/upload_images/3360192-5d233e9baaca5209?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 整体功能
|
|
|
|
|
大家可先通过 [网站演示地址](https://v.mypython.me/) 浏览一下网站效果。点击某个视频即可浏览详情页。详情页实现了是对单个视频进行展示,用户可看到视频的一些元信息,包括标题、描述、观看次数、喜欢数、收藏数等等。另外,网站还实现了评论功能,通过上拉网页即可分页加载评论列表,用户还能添加评论。网页侧栏是推荐视频列表,这里使用的推荐逻辑比较简单,就是推荐观看次数最多的视频。
|
|
|
|
|
|
|
|
|
|
我们把详情页分为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官网](https://docs.djangoproject.com/zh-hans/2.1/topics/db/examples/many_to_many/)的介绍。
|
|
|
|
|
|
|
|
|
|
下面就是详情展示阶段,我们先配置好详情页的路由信息,在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](http://upload-images.jianshu.io/upload_images/3360192-b1db11ac88c58345?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
|
|
|
|
|
|
|
|
|
|
虽然可以显示收藏人数、喜欢人数。但是目前还没实现点击喜欢/收藏的功能。下面我们来实现。
|
|
|
|
|
|
|
|
|
|
### 收藏和喜欢功能
|
|
|
|
|
|
|
|
|
|
收藏和喜欢是一组动作,因此可以用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_id`和`csrftoken`。其中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](http://upload-images.jianshu.io/upload_images/3360192-8265d9e14409a227?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
|
|
|
|
|
|
|
|
|
|
评论功能是一个独立的模块,该功能通用性较高,在其他很多网站中都有评论功能,为了避免以后开发其他网站时重复造轮子,我们建立一个新的应用,命名为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的[开源加载插件](https://github.com/ximan/dropload)。基于这个插件,可以很容易实现网页的上拉动态加载效果。它使用超级简单,仅需要调用$('.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
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|