Merge branch 'dev' into virusdefender-dev

* dev:
  [前端] 修改announcement模块轻微问题 1. 去掉翻页函数多余的参数 2.去掉新增公告刷新函数调用时多传的参数 3.去掉模板空的style标签 [CI SKIP]
  [功能] 新增公告管理模块,对GET users API轻微修改,筛管理员添加超级管理员包含
  修改了用户编辑密码时,密码是否编辑的两个测试用例
  admin/views里里面的typo
  修复合并后的冲突
  创建account的数据库migration
  修复announcement js中作用域的问题
  并没有改动。。。。     [CI SKIP]
  [后端]修改后端公告GET接口,增加返回公告总条数,总页数
  [前端-公告管理]增加仅显示可见公告功能,统一数据获取方法
  后台页面小bug修复
This commit is contained in:
virusdefender
2015-08-08 22:44:30 +08:00
14 changed files with 484 additions and 96 deletions

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import account.models
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(null=True, verbose_name='last login', blank=True)),
('username', models.CharField(unique=True, max_length=30)),
('real_name', models.CharField(max_length=30, null=True, blank=True)),
('email', models.EmailField(max_length=254, null=True, blank=True)),
('create_time', models.DateTimeField(auto_now_add=True)),
('admin_type', models.IntegerField(default=0)),
],
options={
'db_table': 'user',
},
managers=[
('objects', account.models.UserManager()),
],
),
migrations.CreateModel(
name='AdminGroup',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
],
),
]

View File

View File

@@ -41,6 +41,6 @@ class EditUserSerializer(serializers.Serializer):
id = serializers.IntegerField() id = serializers.IntegerField()
username = serializers.CharField(max_length=30) username = serializers.CharField(max_length=30)
real_name = serializers.CharField(max_length=30) real_name = serializers.CharField(max_length=30)
password = serializers.CharField(max_length=30, min_length=6, required=True) password = serializers.CharField(max_length=30, min_length=6, required=False, default=None)
email = serializers.EmailField(max_length=254) email = serializers.EmailField(max_length=254)
admin_type = serializers.IntegerField(default=0) admin_type = serializers.IntegerField(default=0)

View File

@@ -4,6 +4,7 @@ import json
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase, Client from django.test import TestCase, Client
from django.http import HttpResponse from django.http import HttpResponse
from django.contrib import auth
from rest_framework.test import APITestCase, APIClient from rest_framework.test import APITestCase, APIClient
from rest_framework.views import APIView from rest_framework.views import APIView
@@ -194,11 +195,18 @@ class UserAdminAPITest(APITestCase):
self.assertEqual(response.data, {"code": 1, "data": u"该用户不存在!"}) self.assertEqual(response.data, {"code": 1, "data": u"该用户不存在!"})
def test_success_user_edit_not_password(self): def test_success_user_edit_not_password(self):
data = {"id": 1, "username": "test0", "real_name": "test00", "password": "aaaaaa", data = {"id": 1, "username": "test0", "real_name": "test00",
"email": "60@qq.com", "admin_type": "2"} "email": "60@qq.com", "admin_type": "2"}
response = self.client.put(self.url, data=data) response = self.client.put(self.url, data=data)
self.assertEqual(response.data["code"], 0) self.assertEqual(response.data["code"], 0)
def test_success_user_edit_change_password(self):
data = {"id": 1, "username": "test0", "real_name": "test00", "password": "111111",
"email": "60@qq.com", "admin_type": "2"}
response = self.client.put(self.url, data=data)
self.assertEqual(response.data["code"], 0)
self.assertIsNotNone(auth.authenticate(username="test0", password="111111"))
@login_required @login_required
def login_required_FBV_test_without_args(request): def login_required_FBV_test_without_args(request):

View File

@@ -129,7 +129,7 @@ class UserAPIView(APIView):
admin_type = request.GET.get("admin_type", None) admin_type = request.GET.get("admin_type", None)
if admin_type: if admin_type:
try: try:
user = user.filter(admin_type=int(admin_type)) user = user.filter(admin_type__gte=int(admin_type))
except ValueError: except ValueError:
return error_response(u"参数错误") return error_response(u"参数错误")
keyword = request.GET.get("keyword", None) keyword = request.GET.get("keyword", None)

View File

@@ -25,3 +25,7 @@ img {
label { label {
font-size: 16px; font-size: 16px;
} }
[ms-controller] {
display: none
}

View File

@@ -1,34 +1,40 @@
require(["jquery", "avalon", "csrf", "bs_alert", "editor", "validation"], function ($, avalon, csrfHeader, bs_alert, editor) { require(["jquery", "avalon", "csrf", "bs_alert", "editor", "validation"], function ($, avalon, csrfHeader, bs_alert, editor) {
announcementEditor = editor("#editor"); //创建新建公告的内容编辑器 announcementEditor = editor("#editor"); //创建新建公告的内容编辑器
editAnnouncementEditor = null; editAnnouncementEditor = editor("#editAnnouncementEditor");
if (!avalon.vmodels.announcement) // 防止模式重新定义 avalon.vmodels.announcement = null;
{
// avalon:定义模式 announcement // avalon:定义模式 announcement
vm = avalon.define({ avalon.ready(function () {
var vm = avalon.define({
$id: "announcement", $id: "announcement",
announcement: [], // 公告列表数据项 //通用变量
previous_page: 0, // 之前的页数 announcement: [], // 公告列表数据项
next_page: 0, // 之后的页数 previous_page: 0, // 之前的页数
page: 1, // 当前页数 next_page: 0, // 之后的页数
isEditing: 0, // 正在编辑的公告的ID 为零说明未在编辑 page: 1, // 当前页数
isEditing: 0, // 正在编辑的公告的ID 为零说明未在编辑
page_count: 1, // 总页数
visableOnly: false, //仅显示可见公告
// 编辑
announcementVisible: 0,
getState: function (el) { //获取公告当前状态,显示 getState: function (el) { //获取公告当前状态,显示
if (el.visible) if (el.visible)
return "可见"; return "可见";
else else
return "隐藏"; return "隐藏";
}, },
getNext: function (el) { getNext: function () {
if (!vm.next_page) if (!vm.next_page)
return; return;
getPageData(++(vm.page)); getPageData(vm.page + 1);
}, },
getPrevious: function (el) { getPrevious: function () {
if (!vm.previous_page) if (!vm.previous_page)
return; return;
getPageData(--(vm.page)); getPageData(vm.page - 1);
}, },
getBtnClass: function (btn) { getBtnClass: function (btn) { //上一页/下一页按钮启用禁用逻辑
if (btn) { if (btn) {
return vm.next_page ? "btn btn-primary" : "btn btn-primary disabled"; return vm.next_page ? "btn btn-primary" : "btn btn-primary disabled";
} }
@@ -37,13 +43,10 @@ require(["jquery", "avalon", "csrf", "bs_alert", "editor", "validation"], functi
} }
}, },
enEdit: function (el) { //点击编辑按钮的事件,显示/隐藏编辑区 enEdit: function (el) { //点击编辑按钮的事件,显示/隐藏编辑区
$("#newTitle").val(el.title); $("#newTitle").val(el.title);
if (!editAnnouncementEditor) //初始化编辑器
editAnnouncementEditor = editor("#editAnnouncementEditor");
editAnnouncementEditor.setValue(el.content); editAnnouncementEditor.setValue(el.content);
if (el.visible == false) vm.announcementVisible = el.visible;
$("#hidden").attr("checked", true);
if (vm.isEditing == el.id) if (vm.isEditing == el.id)
vm.isEditing = 0; vm.isEditing = 0;
else else
@@ -54,21 +57,25 @@ require(["jquery", "avalon", "csrf", "bs_alert", "editor", "validation"], functi
vm.isEditing = 0; vm.isEditing = 0;
}, },
submitChange: function () { // 处理编辑公告提交事件,顺便验证字段为空 submitChange: function () { // 处理编辑公告提交事件,顺便验证字段为空
var title = $("#newTitle").val(), content = editAnnouncementEditor.getValue(), visible = true; var title = $("#newTitle").val(), content = editAnnouncementEditor.getValue();
if ($("#hidden").attr("checked") == true)
visible = false;
if (title != "") { if (title != "") {
if (content != "") { if (content != "") {
$.ajax({ //发送修改公告请求 $.ajax({ //发送修改公告请求
beforeSend: csrfHeader, beforeSend: csrfHeader,
url: "/api/edit_announcements/", url: "/api/admin/announcement/",
dataType: "json", dataType: "json",
method: "post", method: "put",
data: {id: vm.isEditing, title: title, content: content, visible: visible}, data: {
id: vm.isEditing,
title: title,
content: content,
visible: vm.announcementVisible
},
success: function (data) { success: function (data) {
if (!data.code) { if (!data.code) {
bs_alert("修改成功"); bs_alert("修改成功");
vm.isEditing = 0; vm.isEditing = 0;
getPageData(1);
} }
else { else {
bs_alert(data.data); bs_alert(data.data);
@@ -83,72 +90,80 @@ require(["jquery", "avalon", "csrf", "bs_alert", "editor", "validation"], functi
bs_alert("公告标题不能为空"); bs_alert("公告标题不能为空");
} }
}); });
} vm.$watch("visableOnly", function () {
getPageData(1);
avalon.scan();
getPageData(1); //公告列表初始化
vm.page = 1;
vm.isEditing = 0;
//Ajax get数据
function getPageData(page) {
$.ajax({
beforeSend: csrfHeader,
url: "/api/announcements/?paging=true&page=" + page + "&page_size=10",
dataType: "json",
method: "get",
success: function (data) {
if (!data.code) {
vm.announcement = data.data.results;
vm.previous_page = data.data.previous_page;
vm.next_page = data.data.next_page;
}
else {
bs_alert(data.data);
}
}
}); });
}
//新建公告表单验证与数据提交 avalon.scan();
$("#announcement-form")
.formValidation({ getPageData(1); //公告列表初始化
framework: "bootstrap",
fields: { //Ajax get数据
title: { function getPageData(page) {
validators: { var visible = '';
notEmpty: { if (vm.visableOnly == true)
message: "请填写公告标题" visible = "&visible=true";
$.ajax({
beforeSend: csrfHeader,
url: "/api/announcements/?paging=true&page=" + page + "&page_size=10" + visible,
dataType: "json",
method: "get",
success: function (data) {
if (!data.code) {
vm.announcement = data.data.results;
vm.page_count = data.data.total_page;
vm.previous_page = data.data.previous_page;
vm.next_page = data.data.next_page;
vm.page = page;
}
else {
bs_alert(data.data);
}
}
});
}
//新建公告表单验证与数据提交
$("#announcement-form")
.formValidation({
framework: "bootstrap",
fields: {
title: {
validators: {
notEmpty: {
message: "请填写公告标题"
}
} }
} }
} }
} }
} ).on('success.form.fv', function (e) {
).on('success.form.fv', function (e) { e.preventDefault();
e.preventDefault(); var title = $("#title").val();
var title = $("#title").val(); var content = announcementEditor.getValue();
var content = announcementEditor.getValue(); if (content == "") {
if (content == "") { bs_alert("请填写公告内容");
bs_alert("请填写公告内容"); return;
return;
}
$.ajax({
beforeSend: csrfHeader,
url: "/api/admin/announcement/",
data: {title: title, content: content},
dataType: "json",
method: "post",
success: function (data) {
if (!data.code) {
bs_alert("提交成功!");
$("#title").val("");
announcementEditor.setValue("");
getPageData(1, function (data) {
});
} else {
bs_alert(data.data);
}
} }
}) $.ajax({
}); beforeSend: csrfHeader,
url: "/api/admin/announcement/",
data: {title: title, content: content},
dataType: "json",
method: "post",
success: function (data) {
if (!data.code) {
bs_alert("提交成功!");
$("#title").val("");
announcementEditor.setValue("");
getPageData(1);
} else {
bs_alert(data.data);
}
}
})
});
});
}); });

View File

@@ -0,0 +1,165 @@
require(["jquery", "avalon", "csrf", "bs_alert", "validation"], function ($, avalon, csrfHeader, bs_alert) {
avalon.vmodels.user_list = null;
// avalon:定义模式 user_list
avalon.ready(function () {
var vm = avalon.define({
$id: "user_list",
//通用变量
user_list: [], // 用户列表数据项
previous_page: 0, // 之前的页数
next_page: 0, // 之后的页数
page: 1, // 当前页数
isEditing: 0, // 正在编辑的公告的ID 为零说明未在编辑
page_count: 1, // 总页数
user_type: ["一般用户", "管理员", "超级管理员"],
key_word: "",
showAdminOnly: false,
//编辑区域同步变量
username: "",
real_name: "",
email: "",
admin_type: 0,
id: 0,
last_login: "",
create_time: "",
getNext: function () {
if (!vm.next_page)
return;
getPageData(vm.page + 1);
},
getPrevious: function () {
if (!vm.previous_page)
return;
getPageData(vm.page - 1);
},
getBtnClass: function (btn) { //上一页/下一页按钮启用禁用逻辑
if (btn) {
return vm.next_page ? "btn btn-primary" : "btn btn-primary disabled";
}
else {
return vm.previous_page ? "btn btn-primary" : "btn btn-primary disabled";
}
},
enEdit: function (el) { //点击编辑按钮的事件,显示/隐藏编辑区
vm.username = el.username;
vm.real_name = el.real_name;
vm.admin_type = el.admin_type;
vm.email = el.email;
vm.id = el.id;
if (vm.isEditing == el.id)
vm.isEditing = 0;
else
vm.isEditing = el.id;
},
getPage: function (page_index) {
getPageData(page_index);
}
});
vm.$watch("showAdminOnly", function () {
getPageData(1);
});
avalon.scan();
getPageData(1); //用户列表初始化
//Ajax get数据
function getPageData(page) {
var url = "/api/users/?paging=true&page=" + page + "&page_size=10";
if (vm.showAdminOnly == true)
url += "&admin_type=1";
if (vm.key_word != "")
url += "&keyword=" + vm.key_word;
$.ajax({
beforeSend: csrfHeader,
url: url,
dataType: "json",
method: "get",
success: function (data) {
if (!data.code) {
vm.user_list = data.data.results;
vm.page_count = data.data.total_page;
vm.previous_page = data.data.previous_page;
vm.next_page = data.data.next_page;
vm.page = page;
}
else {
bs_alert(data.data);
}
}
});
}
$("#edit_user-form")
.formValidation({
framework: "bootstrap",
fields: {
username: {
validators: {
notEmpty: {
message: "请填写用户名"
},
stringLength: {
min: 3,
max: 30,
message: '用户名长度必须在3到30位之间'
}
}
},
real_name: {
validators: {
notEmpty: {
message: "请填写真实姓名"
}
}
},
email: {
validators: {
notEmpty: {
message: "请填写电子邮箱邮箱地址"
},
emailAddress: {
message: "请填写有效的邮箱地址"
}
}
},
password: {
validators: {
stringLength: {
min: 6,
max: 30,
message: '密码长度必须在6到30位之间'
}
}
}
}
}
).on('success.form.fv', function (e) {
e.preventDefault();
var data = {
username: vm.username,
real_name: vm.real_name,
email: vm.email,
id: vm.id,
admin_type: vm.admin_type
};
if ($("#password").val() !== "")
data.password = $("#password").val();
$.ajax({
beforeSend: csrfHeader,
url: "/api/admin/user/",
data: data,
dataType: "json",
method: "put",
success: function (data) {
if (!data.code) {
bs_alert("提交成功!");
getPageData(1);
$("#password").val("");
} else {
bs_alert(data.data);
}
}
})
});
});
})

View File

@@ -97,13 +97,20 @@
<li class="list-group-item" id="li-contest-add_contest"> <li class="list-group-item" id="li-contest-add_contest">
<a href="#contest/add_contest">创建比赛</a> <a href="#contest/add_contest">创建比赛</a>
</li> </li>
<li class="list-group-header">用户管理</li>
<li class="list-group-item" id="li-user-user_list">
<a href="#user/user_list">用户列表</a>
</li>
<li class="list-group-item" id="li-user-user_group">
<a href="#user/user_group">用户分组</a>
</li>
</ul> </ul>
</div> </div>
<!-- admin left end --> <!-- admin left end -->
<img src="/static/img/loading.gif" id="loading-gif"> <img src="/static/img/loading.gif" id="loading-gif">
<!-- custom body begin --> <!-- custom body begin -->
<div class='col-md-8' ms-include-src="template_url" data-include-rendered="hide_loading"></div> <div ms-include-src="template_url" data-include-rendered="hide_loading"></div>
<!-- custom body end --> <!-- custom body end -->
</div> </div>

View File

@@ -0,0 +1,65 @@
<div ms-controller="announcement" class="col-md-9">
<h1>Announcement</h1>
<table class="table table-striped">
<tr>
<th>编号</th>
<th>标题</th>
<th>创建时间</th>
<th>更新时间</th>
<th>创建者</th>
<th>状态</th>
<th>操作</th>
</tr>
<tr ms-repeat="announcement">
<td>{{el.id}}</td>
<td>{{el.title}}</td>
<td>{{el.create_time|date("yyyy-MM-dd HH:mm:ss")}}</td>
<td>{{el.last_update_time|date("yyyy-MM-dd HH:mm:ss")}}</td>
<td>{{el.created_by.username}}</td>
<td>{{getState(el)}}</td>
<td>
<button class="btn-sm btn-info" ms-click="enEdit(el)">编辑</button>
</td>
</tr>
</table>
<div class="form-group">
<label>仅显示可见 <input ms-duplex-checked="visableOnly" type="checkbox"/></label>
</div>
<div class="text-right">
页数:{{page}}/{{page_count}}&nbsp;&nbsp;
<botton ms-attr-class="getBtnClass(0)" ms-click="getPrevious">上一页</botton>
<botton ms-attr-class="getBtnClass(1)" ms-click="getNext">下一页</botton>
</div>
<div ms-visible="isEditing">
<h3>编辑公告</h3>
<div class="form-group"><label for="title">标题</label>
<input name="title" type="text" class="form-control" id="newTitle" placeholder="公告标题" value=""></div>
<div class="form-group">
<label>内容</label>
<textarea id="editAnnouncementEditor"></textarea>
</div>
<div class="form-group">
<label>可见 <input ms-duplex-checked="announcementVisible" type="checkbox"/></label>
</div>
<div class="form-group">
<button ms-click="submitChange()" class="btn btn-primary">提交</button>
&nbsp;&nbsp;
<button ms-click="disEdit()" class="btn btn-danger">取消</button>
</div>
</div>
<h3>添加公告</h3>
<form id="announcement-form">
<div class="form-group"><label for="title">标题</label>
<input name="title" type="text" class="form-control" id="title" placeholder="公告标题"></div>
<div class="form-group">
<label>内容</label>
<textarea id="editor" placeholder="公告内容"></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">提交</button>
</div>
</form>
</div>
<script src="/static/js/app/admin/announcement/announcement.js"></script>

View File

@@ -1,6 +1,4 @@
<div ms-controller="add_contest"> <div ms-controller="add_contest">
<form id="add-contest-form"> <form id="add-contest-form">
<div class="col-md-9"> <div class="col-md-9">
<div class="col-md-12"> <div class="col-md-12">

View File

@@ -0,0 +1,79 @@
<div ms-controller="user_list" class="col-md-9">
<h1>User</h1>
<div class="text-right">
<form class="form-inline" onsubmit="return false;">
<div class="form-group-sm">
<label>搜索</label>
<input name="keyWord" class="form-control" placeholder="请输入关键词" ms-duplex="key_word">
<input type="submit" value="搜索" class="btn btn-primary" ms-click="getPage(1)">
</div>
</form>
<br>
</div>
<table class="table table-striped">
<tr>
<th>ID</th>
<th>用户名</th>
<th>注册时间</th>
<th>最近登陆</th>
<th>真实姓名</th>
<th>电子邮箱</th>
<th>用户类型</th>
<th>修改</th>
</tr>
<tr ms-repeat="user_list">
<td>{{el.id}}</td>
<td>{{el.username}}</td>
<td>{{el.create_time|date("yyyy-MM-dd HH:mm:ss")}}</td>
<td>{{el.last_login|date("yyyy-MM-dd HH:mm:ss")}}</td>
<td>{{el.real_name}}</td>
<td>{{el.email}}</td>
<td>{{user_type[el.admin_type]}}</td>
<td>
<button class="btn-sm btn-info" ms-click="enEdit(el)">编辑</button>
</td>
</tr>
</table>
<div class="form-group">
<label>仅显示管理员 <input ms-duplex-checked="showAdminOnly" type="checkbox"/></label>
</div>
<div class="text-right">
页数:{{page}}/{{page_count}}&nbsp;&nbsp;
<botton ms-attr-class="getBtnClass(0)" ms-click="getPrevious">上一页</botton>
<botton ms-attr-class="getBtnClass(1)" ms-click="getNext">下一页</botton>
</div>
<div ms-visible="isEditing">
<h3>修改用户信息</h3>
<form id="edit_user-form">
<div class="row">
<div class="form-group col-md-4"><label>ID</label>
<input name="id" type="number" class="form-control" readonly ms-duplex="id">
</div>
<div class="form-group col-md-4"><label>用户名</label>
<input name="username" type="text" class="form-control" ms-duplex="username">
</div>
<div class="form-group col-md-4"><label>真实姓名</label>
<input name="real_name" type="text" class="form-control" ms-duplex="real_name">
</div>
</div>
<div class="row">
<div class="form-group col-md-4"><label>新密码(留空则保留原密码)</label>
<input name="password" type="password" class="form-control" id="password" ms-duplex="password"
placeholder="此项留空则保留原密码">
</div>
<div class="form-group col-md-4"><label>电子邮箱</label>
<input name="email" type="email" class="form-control" ms-duplex="email">
</div>
<div class="form-group col-md-4"><label>用户类型</label>
<select name="admin_type" class="form-control" ms-duplex="admin_type">
<option ms-repeat="user_type" ms-attr-value="$index">{{el}}</option>
</select>
</div>
</div>
<div class="form-group">
<button type="sbumit" class="btn btn-primary">提交</button>
</div>
</form>
</div>
</div>
<script src="/static/js/app/admin/user/user_list.js"></script>

View File

@@ -70,7 +70,11 @@ def paginate(request, query_set, object_serializer):
except Exception: except Exception:
return error_response(u"参数错误") return error_response(u"参数错误")
data = {"results": object_serializer(current_page, many=True).data, "previous_page": None, "next_page": None} data = {"results": object_serializer(current_page, many=True).data,
"previous_page": None,
"next_page": None,
"count": paginator.count,
"total_page": paginator.num_pages}
try: try:
data["previous_page"] = current_page.previous_page_number() data["previous_page"] = current_page.previous_page_number()

View File

@@ -51,14 +51,18 @@ class PaginatorTest(APITestCase):
self.assertEqual(response.data, {"code": 1, "data": u"参数错误"}) self.assertEqual(response.data, {"code": 1, "data": u"参数错误"})
def test_correct_paginate(self): def test_correct_paginate(self):
response = self.client.get(self.url + "?paging=true&limit=1&page_size=1&page=1") response = self.client.get(self.url + "?paging=true&page_size=1&page=1")
self.assertEqual(response.data["code"], 0) self.assertEqual(response.data["code"], 0)
self.assertEqual(response.data["data"]["previous_page"], None) self.assertEqual(response.data["data"]["previous_page"], None)
self.assertEqual(response.data["data"]["next_page"], 2) self.assertEqual(response.data["data"]["next_page"], 2)
self.assertEqual(len(response.data["data"]["results"]), 1) self.assertEqual(len(response.data["data"]["results"]), 1)
self.assertEqual(response.data["data"]["count"], 2)
self.assertEqual(response.data["data"]["total_page"], 2)
response = self.client.get(self.url + "?paging=true&limit=1&page_size=2&page=1") response = self.client.get(self.url + "?paging=true&page_size=2&page=1")
self.assertEqual(response.data["code"], 0) self.assertEqual(response.data["code"], 0)
self.assertEqual(response.data["data"]["previous_page"], None) self.assertEqual(response.data["data"]["previous_page"], None)
self.assertEqual(response.data["data"]["next_page"], None) self.assertEqual(response.data["data"]["next_page"], None)
self.assertEqual(len(response.data["data"]["results"]), 2) self.assertEqual(len(response.data["data"]["results"]), 2)
self.assertEqual(response.data["data"]["count"], 2)
self.assertEqual(response.data["data"]["total_page"], 1)