:feat:新增 考试人员导出功能

This commit is contained in:
zty 2024-05-20 17:45:44 +08:00
parent 9222e2ecdf
commit 8e6e0d4c93
11 changed files with 720 additions and 25 deletions

File diff suppressed because one or more lines are too long

View File

@ -27,7 +27,7 @@ export function getQi(id) {
export function updateQi(id, data) {
return request({
url: `/info/faq/${id}/`,
url: `/info/faqch/${id}/`,
method: 'put',
data
})

View File

@ -14,6 +14,14 @@ export function getRoleAll() {
})
}
export function getRoleList(params) {
return request({
url: '/system/role/',
method: 'get',
params
})
}
export function createRole(data) {
return request({
url: '/system/role/',

View File

@ -0,0 +1,51 @@
import request from '@/utils/request'
export function getUserList(query) {
return request({
url: '/system/userexam/',
method: 'get',
params: query
})
}
export function createUser(data) {
return request({
url: '/system/userexam/',
method: 'post',
data
})
}
export function updateUser(id, data) {
return request({
url: `/system/userexam/${id}/`,
method: 'put',
data
})
}
export function deleteUserExam(id, data) {
return request({
url: `/system/userexam/${id}/`,
method: 'delete',
data
})
}
export function resetUserpw(id) {
return request({
url: `/system/user/${id}/resetpw/`,
method: 'put',
})
}
export function changePassword(data) {
return request({
url: '/system/user/password/',
method: 'put',
data
})
}

View File

@ -559,20 +559,20 @@ export const asyncRoutes = [
component: Layout,
redirect: '/exam/questions',
name: 'exam',
meta: { title: '考试', icon: 'PT', perms: ['pt_view'] },
meta: { title: '考试', icon: 'Exam', perms: ['Exam'] },
alwaysShow: true,
children: [
{
path: 'classify',
name: '题目分类',
component: () => import('@/views/exam/classify.vue'),
meta: { title: '题目分类', perms: ['pt_view'] }
meta: { title: '题目分类', perms: ['CateQues'] }
},
{
path: 'questions',
name: '题目列表',
component: () => import('@/views/exam/questions.vue'),
meta: { title: '题目列表', perms: ['pt_view'] }
meta: { title: '题目列表', perms: ['CateQues'] }
},
{
path: 'questionCreate',
@ -592,7 +592,7 @@ export const asyncRoutes = [
path: 'testPaper',
name: '考试试卷',
component: () => import('@/views/exam/testPaper.vue'),
meta: { title: '考试试卷', perms: ['pt_view'] }
meta: { title: '考试试卷', perms: ['Paper'] }
},
{
path: 'paperCreate',
@ -612,13 +612,19 @@ export const asyncRoutes = [
path: 'index',
name: '考试',
component: () => import('@/views/exam/index.vue'),
meta: { title: '考试', perms: ['pt_view'] }
meta: { title: '考试', perms: ['Paper'] }
},
{
path: 'record',
name: '考试记录',
component: () => import('@/views/exam/examRecord.vue'),
meta: { title: '考试记录', perms: ['pt_view'] }
meta: { title: '考试记录', perms: ['RecordExam'] }
},
{
path: 'userExam',
name: '用户考试管理',
component: () => import('@/views/system/userExam.vue'),
meta: { title: '用户考试管理', perms: ['UserExam'] }
},
]
},

View File

@ -25,11 +25,11 @@
<el-table-column label="变更日期" prop="change_date"></el-table-column>
<el-table-column align="center" label="操作" width="120px" fixed="right">
<template slot-scope="scope">
<el-link :disabled="!checkPermission(['infoCollect_QIN'])" type="primary" size="small"
<el-link type="primary" size="small"
@click="handleEdit(scope)">编辑</el-link>
<el-divider direction="vertical"
v-if="checkPermission(['infoCollect_QIN']) && checkPermission(['infoCollect_QIN'])"></el-divider>
<el-link :disabled="!checkPermission(['infoCollect_QIN'])" type="danger" size="small"
></el-divider>
<el-link type="danger" size="small"
@click="handleDelete(scope)">删除</el-link>
<!-- <el-link type="primary" @click="handleChange(scope)">变更记录</el-link> -->
</template>

View File

@ -0,0 +1,500 @@
<template>
<div class="app-container">
<el-row :gutter="6">
<el-col :xs="24" :md="6">
<el-card >
<div slot="header" class="clearfix">
<span>部门</span>
</div>
<el-input v-model="filterOrgText" placeholder="输入部门名进行过滤" />
<el-tree
ref="tree"
v-loading="treeLoding"
class="filter-tree"
:data="orgData"
default-expand-all
highlight-current
:expand-on-click-node="false"
:filter-node-method="filterNode"
style="margin-top: 10px;max-height:650px;overflow-y: auto;"
@node-click="handleOrgClick"
/>
</el-card>
</el-col>
<el-col :xs="24" :md="18">
<el-card>
<div slot="header" class="clearfix">
<span>用户</span>
</div>
<div>
<el-select
v-model="listQuery.is_active"
placeholder="状态"
clearable
style="width: 90px"
class="filter-item"
@change="handleFilter"
>
<el-option
v-for="item in enabledOptions"
:key="item.key"
:label="item.display_name"
:value="item.key"
/>
</el-select>
<el-select
v-model="listQuery.roles"
placeholder="角色"
style="width: 90px"
class="filter-item"
@change="handleFilter"
>
<el-option
v-for="item in roles"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input
v-model="listQuery.search"
placeholder="姓名/邮箱"
style="width: 200px"
class="filter-item"
@keyup.enter.native="handleFilter"
/>
<el-button
class="filter-item"
type="primary"
icon="el-icon-search"
@click="handleFilter"
>搜索</el-button>
<el-button
class="filter-item"
style="margin-left: 10px"
type="primary"
icon="el-icon-refresh-left"
@click="resetFilter"
>重置</el-button>
<!-- <el-button
class="filter-item"
style="margin-left: 10px"
type="primary"
icon="el-icon-download"
@click="exportExcel"
:loading="listLoading"
>导出</el-button> -->
</div>
<div style="margin-top: 10px">
<el-button type="primary" icon="el-icon-plus" @click="handleAddUser"
>新增</el-button
>
</div>
<el-table
v-loading="listLoading"
:data="userList.results"
style="width: 100%; margin-top: 10px"
border
fit
stripe
highlight-current-row
max-height="600"
>
<el-table-column type="index" width="50" />
<el-table-column align="center" label="姓名">
<template slot-scope="scope">{{ scope.row.name }}</template>
</el-table-column>
<el-table-column align="header-center" label="账户">
<template slot-scope="scope">{{ scope.row.username }}</template>
</el-table-column>
<el-table-column align="header-center" label="部门">
<template v-if="scope.row.dept_name != null" slot-scope="scope">{{
scope.row.dept_name
}}</template>
</el-table-column>
<el-table-column align="header-center" label="角色">
<template slot-scope="scope">
<el-tag
style="margin: 2px"
effect="plain"
v-for="(item, index) in scope.row.roles_"
:key="index"
>
{{ item.name }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建日期">
<template slot-scope="scope">
<span>{{ scope.row.date_joined }}</span>
</template>
</el-table-column>
<el-table-column
align="center"
label="操作"
width="200px"
fixed="right"
>
<template slot-scope="scope">
<el-button
v-if="checkPermission(['admin'])"
type="primary"
size="small"
@click="handeReset(scope)"
>重置密码</el-button
>
<el-button
:disabled="!checkPermission(['user_exam_create'])"
type="primary"
size="small"
icon="el-icon-edit"
@click="handleEdit(scope)"
/>
<el-button
v-if="!scope.row.is_superuser"
:disabled="!checkPermission(['user_exam_delete'])"
type="danger"
size="small"
icon="el-icon-delete"
@click="handleDelete(scope)"
/>
</template>
</el-table-column>
</el-table>
<pagination
v-show="userList.count > 0"
:total="userList.count"
:page.sync="listQuery.page"
:limit.sync="listQuery.page_size"
@pagination="getList"
/>
</el-card>
</el-col>
</el-row>
<el-dialog
:visible.sync="dialogVisible"
:title="dialogType === 'edit' ? '编辑用户' : '新增用户'"
>
<el-form
ref="Form"
:model="user"
label-width="80px"
label-position="right"
:rules="rule1"
>
<el-form-item label="姓名" prop="name">
<el-input v-model="user.name" placeholder="姓名" />
</el-form-item>
<el-form-item label="账户" prop="username">
<el-input v-model="user.username" placeholder="账户" />
</el-form-item>
<el-form-item label="所属部门" prop="dept">
<treeselect
v-model="user.dept"
:multiple="false"
:options="orgData"
placeholder="所属部门"
/>
</el-form-item>
<el-form-item label="角色" prop="roles">
<el-select
v-model="user.roles"
multiple
placeholder="请选择"
style="width: 100%"
>
<el-option
v-for="item in roles"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="头像" prop="dept">
<el-upload
class="avatar-uploader"
:action="upUrl"
accept="image/jpeg, image/gif, image/png, image/bmp"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
:headers="upHeaders"
>
<img v-if="user.avatar" :src="user.avatar" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon" />
</el-upload>
</el-form-item>
</el-form>
<div style="text-align: right">
<el-button type="danger" @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirm('Form')">确认</el-button>
</div>
</el-dialog>
</div>
</template>
<style>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
line-height: 100px;
text-align: center;
}
.avatar {
width: 100px;
height: 100px;
display: block;
}
</style>
<script>
import {
getUserList,
createUser,
deleteUserExam,
updateUser,
resetUserpw,
} from "@/api/userexam";
import { getSubOrgList } from "@/api/org";
import { getRoleList } from "@/api/role";
import { genTree } from "@/utils";
import checkPermission from "@/utils/permission";
import { upUrl, upHeaders } from "@/api/file";
import Pagination from "@/components/Pagination"; // secondary package based on el-pagination
import Treeselect from "@riophae/vue-treeselect";
import {generateExcel} from "@/utils/exportExcel.js";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
const defaultUser = {
id: "",
name: "",
username: "",
dept: null,
avatar: "/media/default/avatar.png",
};
export default {
components: { Pagination, Treeselect },
data() {
return {
user: defaultUser,
upHeaders: upHeaders(),
upUrl: upUrl(),
userList: { count: 0 },
roles: [],
listLoading: true,
listQuery: {
page: 1,
page_size: 20,
},
enabledOptions: [
{ key: "true", display_name: "激活" },
{ key: "false", display_name: "禁用" },
],
dialogVisible: false,
dialogType: "new",
rule1: {
name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
username: [
{ required: true, message: "请输入账号", trigger: "change" },
],
roles: [{ required: true, message: "请选择角色", trigger: "change" }],
// password: [
// { required: true, message: '请输入密码', trigger: 'change' }
// ],
},
filterOrgText: "",
treeLoding: false,
orgData: [],
params : {name:'考试'},
columns : [
{ header: '姓名', key: 'name', wpx: 60 },
{ header: '账户', key: 'username', wch: 32 },
{ header: '部门', key: 'dept_name', width: 30 },
{ header: '角色', key: 'roles', width: 70 }
],
};
},
computed: {},
watch: {
filterOrgText(val) {
this.$refs.tree.filter(val);
},
},
created() {
this.getList();
this.getSubOrgList();
this.getRoleAll();
},
methods: {
checkPermission,
handleAvatarSuccess(res, file) {
if (res.code >= 200) {
this.user.avatar = res.data.path;
} else {
this.$message.error("头像上传失败!");
}
},
beforeAvatarUpload(file) {
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
this.$message.error("上传头像图片大小不能超过 2MB!");
}
return isLt2M;
},
filterNode(value, data) {
if (!value) return true;
return data.label.indexOf(value) !== -1;
},
handleOrgClick(obj, node, vue) {
this.listQuery.page = 1;
this.listQuery.dept = obj.id;
this.getList();
},
getList() {
this.listLoading = true;
getUserList(this.listQuery).then((response) => {
if (response.data) {
this.userList = response.data;
}
this.listLoading = false;
});
},
getSubOrgList() {
this.treeLoding = true;
getSubOrgList().then((response) => {
this.orgData = genTree(response.data);
this.treeLoding = false;
});
},
getRoleAll() {
getRoleList(this.params).then((response) => {
this.roles = genTree(response.data);
});
},
resetFilter() {
this.listQuery = {
page: 1,
page_size: 20,
};
this.getList();
},
handleFilter() {
this.listQuery.page = 1;
this.getList();
},
handleAddUser() {
this.user = Object.assign({}, defaultUser);
this.dialogType = "new";
this.dialogVisible = true;
this.$nextTick(() => {
this.$refs["Form"].clearValidate();
});
},
handleEdit(scope) {
this.user = Object.assign({}, scope.row); // copy obj
this.dialogType = "edit";
this.dialogVisible = true;
this.$nextTick(() => {
this.$refs["Form"].clearValidate();
});
},
handleDelete(scope) {
this.$confirm("确认删除?", "警告", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "error",
})
.then(async () => {
await deleteUserExam(scope.row.id);
this.userList.splice(scope.row.index, 1);
this.$message.success("成功");
})
.catch((err) => {
});
},
async confirm(form) {
this.$refs[form].validate((valid) => {
if (valid) {
const isEdit = this.dialogType === "edit";
if (isEdit) {
updateUser(this.user.id, this.user).then((res) => {
if (res.code >= 200) {
this.getList();
this.dialogVisible = false;
this.$message.success("成功");
}
});
} else {
createUser(this.user).then((res) => {
if (res.code >= 200) {
this.getList();
this.dialogVisible = false;
this.$message.success("成功");
}
});
}
} else {
return false;
}
});
},
handeReset(scope) {
this.$confirm("确认重置密码为0000吗?", "警告", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
resetUserpw(scope.row.id)
.then((res) => {
this.$message.success('成功');
})
.catch((e) => {});
});
},
//导出
exportExcel(){
let that = this;
this.listLoading = true;
//获取全部人员数据
let promises = [1,2, 3, 4].map(function (page) {
return getUserList({page:page,page_size:500}).then((res)=>{return res.data.results})
});
Promise.all(promises).then(function (posts) {
let data = JSON.parse(JSON.stringify(posts))
let list = [...data[0], ...data[1],...data[2], ...data[3]];
let exportData = [];
for(let i=0;i<list.length;i++){
let obj = {};
obj.name=list[i].name;
obj.username=list[i].username;
obj.dept_name=list[i].dept_name;
let rolesList = list[i].roles_;
let roles = rolesList.map((item)=>{
return item.name;
})
obj.roles=roles.toString();
exportData.push(obj)
}
generateExcel(that.columns,exportData,'账号清单');
that.listLoading = false;
}).catch(function(reason){
console.log('出错了',reason)
that.listLoading = false;
});
},
},
};
</script>

View File

@ -20,6 +20,8 @@ from apps.exam.filters import ExamRecordFilter, ExamFilter
from datetime import timedelta
from apps.system.mixins import CreateUpdateCustomMixin
from apps.edu.serializers import CertificateSerializer
from utils.queryset import get_child_queryset2
from apps.system.permission import has_permission
from datetime import datetime
# Create your views here.
@ -354,7 +356,15 @@ class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, RetrieveModelMixin, G
search_fields = ('create_by__name', 'create_by__username', 'exam__name', 'belong_dept__name')
filterset_class = ExamRecordFilter
# 排序
def get_queryset(self):
qs = super().get_queryset()
# if self.request.method == 'GET':
# return qs
# else:
# return qs.filter(belong_dept__in=get_child_queryset2(self.request.user.dept))
if has_permission('ability_review_jygl', self.request.user):
return qs
return qs.filter(belong_dept__in=get_child_queryset2(self.request.user.dept))
def get_serializer_class(self):
if self.action == 'retrieve':
@ -397,7 +407,6 @@ class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, RetrieveModelMixin, G
提交答卷
'''
er = self.get_object()
print('er----------------', er)
now = timezone.now()
if er.create_by != request.user:
raise ParseError('提交人有误')

View File

@ -95,6 +95,7 @@ class UserListSerializer(serializers.ModelSerializer):
queryset = queryset.prefetch_related('roles',)
return queryset
class UserModifySerializer(serializers.ModelSerializer):
"""
用户编辑序列化

View File

@ -1,11 +1,12 @@
from django.urls import path, include
from .views import CityViewSet, ProviceViewSet, UserViewSet, OrganizationViewSet, PermissionViewSet, RoleViewSet, PositionViewSet, TestView, DictTypeViewSet, DictViewSet, InitAreaView, sendMsg
from .views import CityViewSet, ProviceViewSet, UserExamViewset, UserViewSet, OrganizationViewSet, PermissionViewSet, RoleViewSet, PositionViewSet, TestView, DictTypeViewSet, DictViewSet, InitAreaView, sendMsg
from rest_framework import routers
router = routers.DefaultRouter()
router.register('user', UserViewSet, basename="user")
router.register('userexam', UserExamViewset, basename="userexam")
router.register('organization', OrganizationViewSet, basename="organization")
router.register('permission', PermissionViewSet, basename="permission")
router.register('role', RoleViewSet, basename="role")

View File

@ -45,26 +45,84 @@ import requests
import json
from rest_framework.exceptions import AuthenticationFailed
from django.db import transaction
logger = logging.getLogger('log')
# logger.info('请求成功! response_code:{}response_headers:{}response_body:{}'.format(response_code, response_headers, response_body[:251]))
# logger.error('请求出错:{}'.format(error))
class LogoutView(APIView):
permission_classes = []
def get(self, request, *args, **kwargs): # 可将token加入黑名单
return Response(status=status.HTTP_200_OK)
from rest_framework.exceptions import ParseError
from apps.system.permission import has_permission
from openpyxl import load_workbook
import random
import smtplib
import string
from email.header import Header
from email.mime.text import MIMEText
from email.utils import formataddr
from rest_framework_simplejwt.tokens import RefreshToken
logger = logging.getLogger('log')
# logger.info('请求成功! response_code:{}response_headers:{}response_body:{}'.format(response_code, response_headers, response_body[:251]))
# logger.error('请求出错:{}'.format(error))
class ImpMixin:
def get_queryset(self):
mydept = self.request.user.dept
qs = super().get_queryset()
if has_permission('task2', self.request.user):
return qs
return qs.filter(belong_dept=mydept)
def format_date(self, ind, val):
new_val = val
if isinstance(val, datetime.datetime):
new_val = val.date()
elif isinstance(val, datetime.date):
new_val = val
elif isinstance(val, str):
try:
new_val = datetime.datetime.strptime(val, '%Y-%m-%d').date()
except ValueError:
raise ParseError(f'{ind}行, 日期时间格式错误')
elif val is None:
pass
else:
raise ParseError(f'{ind}行, 日期时间格式错误')
return new_val
def get_enum(self, val, atuple, ind):
for i in atuple:
if i[1] == val:
return i[0]
raise ParseError('{}: 请选择固定选项值'.format(ind))
def F(self, data, sheet, i, etype):
raise NotImplementedError()
def gen_imp_view(self, request, start: int, mySerializer):
if 'file' not in request.data:
raise ParseError('请提供文件')
path = request.data['file']
print(path, "---------ssss")
if not str(path).endswith('.xlsx'):
raise ParseError('请提供xlsx格式文件')
fullpath = settings.BASE_DIR + str(path)
wb = load_workbook(fullpath,data_only=True)
sheet = wb.active
# 遍历Excel文件中的数据
data_list = self.build_data(sheet, start)
serializer = mySerializer(data=data_list, many=True, context={'request': request})
if serializer.is_valid():
serializer.save(create_by=request.user, belong_dept=request.user.dept)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response({'uploaded': 'File uploaded successfully'}, status=status.HTTP_201_CREATED)
class LogoutView(APIView):
permission_classes = []
def get(self, request, *args, **kwargs): # 可将token加入黑名单
return Response(status=status.HTTP_200_OK)
def get_tokens_for_user(user):
refresh = RefreshToken.for_user(user)
@ -246,9 +304,69 @@ class RoleViewSet(ModelViewSet):
serializer_class = RoleSerializer
pagination_class = None
search_fields = ['name']
filterset_fields = []
ordering_fields = ['id']
ordering = 'id'
class UserExamViewset(ImpMixin, ModelViewSet):
"""
用户考试增删改查
"""
perms_map = {'get': '*', 'post': 'user_exam_create',
'put': 'user_exam_create', 'delete': 'user_exam_delete'}
queryset = User.objects.all().order_by('-id')
serializer_class = UserListSerializer
filterset_class = UserFilter
search_fields = ['username', 'name', 'phone', 'email']
ordering_fields = ['-id']
def get_queryset(self):
queryset = self.queryset
dept = self.request.user.dept.id
deptqueryset = get_child_queryset2(Organization.objects.get(pk=dept))
queryset = queryset.filter(dept__in=deptqueryset)
return queryset
def create(self, request, *args, **kwargs):
# 创建用户默认添加密码
password = request.data['password'] if 'password' in request.data else None
if password:
password = make_password(password)
else:
# password = make_password(''.join(random.sample(string.ascii_letters + string.digits, 8)))
password = make_password('0000')
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(password=password)
return Response(serializer.data)
def build_data(self, sheet, start):
data_list = []
exam_role = Role.objects.get(name='考试')
if not exam_role:
return Response({'msg': '考试角色不存在'})
for row in sheet.iter_rows(min_row=start, values_only=True): # 假设第一行是表头,从第二行开始读取数据
if row[0] is not None:
dept = Organization.objects.get(name=row[3])
if not dept:
return Response({'msg': '部门不存在'})
serializer_data = {
'name': row[1],
'username':row[2],
'dept':dept.id,
'roles':[exam_role.id],
'avatar': "/media/default/avatar.png"
}
data_list.append(serializer_data)
return data_list
@action(detail=False, methods=['post'])
@transaction.atomic
def imp(self, request, *args, **kwargs):
"""
导入数据
"""
return self.gen_imp_view(request, 2, UserListSerializer)
class UserViewSet(PageOrNot, ModelViewSet):
"""
@ -532,5 +650,6 @@ class InitAreaView(APIView):
for i in cs:
City.objects.create(id=i['code'], name=i['name'], parent=Province.objects.get(id=i['provinceCode']))
return Response()