This commit is contained in:
shijing 2024-06-18 15:38:35 +08:00
commit acb25ae76e
29 changed files with 666 additions and 160 deletions

File diff suppressed because one or more lines are too long

View File

@ -27,10 +27,12 @@
"nprogress": "0.2.0", "nprogress": "0.2.0",
"path-to-regexp": "2.4.0", "path-to-regexp": "2.4.0",
"pdfobject": "^2.3.0", "pdfobject": "^2.3.0",
"sortablejs": "^1.15.2",
"vod-js-sdk-v6": "^1.4.12", "vod-js-sdk-v6": "^1.4.12",
"vue": "2.6.10", "vue": "2.7",
"vue-pdf": "^4.2.0", "vue-pdf": "^4.2.0",
"vue-router": "3.0.6", "vue-router": "3.0.6",
"vuedraggable": "^2.24.3",
"vuex": "3.1.0", "vuex": "3.1.0",
"xlsx": "^0.15.5" "xlsx": "^0.15.5"
}, },

View File

@ -27,10 +27,11 @@ export function updateQuestioncat(id, data) {
//删除题目类型 //删除题目类型
export function deleteQuestioncat(id) { export function deleteQuestioncat(id) {
return request({ return request({
url: `/exam/questioncat/${id}/`, url: `/exam/question/${id}/`,
method: 'delete' method: 'delete'
}) })
} }
//题目列表 //题目列表
export function getQuestionList(query) { export function getQuestionList(query) {
return request({ return request({
@ -69,6 +70,14 @@ export function deleteQuestion(id) {
method: 'delete' method: 'delete'
}) })
} }
//批量删除题目类型
export function deletesQuestion(data) {
return request({
url: `/exam/question/deletes/`,
method: 'post',
data
})
}
//导入题目 //导入题目
export function importQuestion(data) { export function importQuestion(data) {
return request({ return request({
@ -82,7 +91,7 @@ export function exportQuestion(data) {
return request({ return request({
url: `/exam/question/export/`, url: `/exam/question/export/`,
method: 'get', method: 'get',
params: query params: data
}) })
} }
//启用题目 //启用题目
@ -213,6 +222,15 @@ export function updateExamRecord(id, data) {
data data
}) })
} }
//导出考试记录
export function exportRecord(data) {
return request({
url: `/exam/examrecord/export/`,
method: 'get',
params: data,
})
}
//删除考试记录 //删除考试记录
export function deleteExamRecord(id) { export function deleteExamRecord(id) {
return request({ return request({

View File

@ -0,0 +1 @@
<svg t="1717725053175" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3023" width="200" height="200"><path d="M153.6 58.88v901.632h706.56V58.88H153.6z m641.024 834.56H218.624v-768h576v768z" fill="#ffffff" p-id="3024"></path><path d="M399.36 228.352h-35.84c-2.048 0-4.608 1.536-5.12 3.584l-31.232 186.368c-0.512 4.096 2.56 7.68 6.144 7.68h32.768c2.048 0 4.096-1.536 4.096-3.584l33.28-188.416c1.024-3.072-1.024-5.632-4.096-5.632zM537.088 242.176c-8.704-10.24-23.552-15.36-39.424-15.36-15.872 0-32.256 5.12-44.544 15.36-12.288 9.728-19.968 22.016-23.552 42.496l-15.36 86.016c-3.584 20.48 0 32.256 8.704 42.496 8.704 10.24 23.552 15.36 39.424 15.36 15.872 0 32.256-5.12 44.544-15.36 12.288-9.728 19.968-22.016 23.552-42.496l15.36-86.016c3.584-20.48 0-32.768-8.704-42.496z m-35.328 40.96l-15.872 89.6c-2.048 10.752-8.704 16.384-16.896 16.384s-12.8-5.12-11.264-16.384l15.872-89.6c2.048-10.752 8.704-16.384 16.896-16.384 8.192 0 13.312 5.12 11.264 16.384zM676.352 242.176c-8.704-10.24-23.552-15.36-39.424-15.36s-32.256 5.12-44.544 15.36c-12.288 9.728-19.968 22.016-23.552 42.496l-15.36 86.016c-3.584 20.48 0 32.256 8.704 42.496 8.704 10.24 23.552 15.36 39.424 15.36 15.872 0 32.256-5.12 44.544-15.36 12.288-9.728 19.968-22.016 23.552-42.496l15.36-86.016c3.584-20.48 0-32.768-8.704-42.496z m-35.328 40.96l-15.872 89.6c-2.048 10.752-8.704 16.384-16.896 16.384s-12.8-5.12-11.264-16.384l15.872-89.6c2.048-10.752 8.704-16.384 16.896-16.384 8.192 0 13.312 5.12 11.264 16.384zM313.856 515.584h347.648V573.44H313.856zM313.856 638.976h275.968v57.856H313.856zM313.856 767.488h275.968v57.856H313.856z" fill="#ffffff" p-id="3025"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -55,8 +55,11 @@
@sort-change="changeSort" @sort-change="changeSort"
> >
<el-table-column type="index" width="50"></el-table-column> <el-table-column type="index" width="50"></el-table-column>
<el-table-column align="left" label="类型"> <!-- <el-table-column align="left" label="类型">
<template slot-scope="scope">{{ scope.row.type }}</template> <template slot-scope="scope">{{ scope.row.type }}</template>
</el-table-column> -->
<el-table-column align="left" label="考试名称">
<template slot-scope="scope">{{ scope.row.exam_name }}</template>
</el-table-column> </el-table-column>
<el-table-column align="left" label="用户"> <el-table-column align="left" label="用户">
<template slot-scope="scope">{{ scope.row.create_by_name}}</template> <template slot-scope="scope">{{ scope.row.create_by_name}}</template>
@ -82,20 +85,32 @@
<el-table-column align="left" label="答题时间"> <el-table-column align="left" label="答题时间">
<template slot-scope="scope">{{ scope.row.start_time }}</template> <template slot-scope="scope">{{ scope.row.start_time }}</template>
</el-table-column> </el-table-column>
<el-table-column align="left" label="证书路径" prop="cert_path">
<template slot-scope="scope">
<img v-if="scope.row.cert_path" :src="scope.row.cert_path" @click="handlePictureCardPreview(scope.row.cert_path)" alt="图片" height="30" width="50"/>
</template>
</el-table-column>
<el-table-column align="left" label="单位名称">
<template slot-scope="scope">{{ scope.row.blong_dept_name }}</template>
</el-table-column>
<el-table-column align="left" label="考试时间">
<template slot-scope="scope">{{ scope.row.create_time }}</template>
</el-table-column>
<el-table-column align="center" label="操作" fixed="right"> <el-table-column align="center" label="操作" fixed="right">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button <!-- <el-button
v-if="scope.row.type=='正式考试'" v-if="scope.row.type=='正式考试'"
type="primary" type="primary"
size="small" size="small"
@click="handleExport(scope)" @click="handleExport(scope)"
>生成Word</el-button> >生成Word</el-button> -->
<el-button <!-- <el-button
v-if="scope.row.type=='正式考试'" v-if="scope.row.type=='正式考试'"
type="warning" type="warning"
size="small" size="small"
@click="handleExport2(scope)" @click="handleExport2(scope)"
>重新生成</el-button> >重新生成</el-button> -->
<el-button <el-button
v-if="scope.row.type=='正式考试'" v-if="scope.row.type=='正式考试'"
type="danger" type="danger"
@ -116,14 +131,13 @@
</template> </template>
<script> <script>
import { getExamRecordList, exportTest, exportwTest, deleteExamRecord,issue } from "@/api/exam"; import { getExamRecordList, exportRecord, exportwTest, deleteExamRecord,issue } from "@/api/exam";
import checkPermission from "@/utils/permission"; import checkPermission from "@/utils/permission";
import Pagination from "@/components/Pagination"; import Pagination from "@/components/Pagination";
const listQuery = { const listQuery = {
page: 1, page: 1,
limit: 20, limit: 20,
type:'正式考试',
search:'' search:''
}; };
export default { export default {
@ -193,13 +207,13 @@
methods: { methods: {
checkPermission, checkPermission,
getQuery() { getQuery() {
if(this.$route.params.exam){ if(this.$route.query.exam){
this.listQuery.exam = this.$route.params.exam; this.listQuery.exam = this.$route.query.exam;
this.getList()
}else{
this.getList()
} }
this.getList()
},
handlePictureCardPreview(url){
window.open(url);
}, },
getList() { getList() {
this.listLoading = true; this.listLoading = true;
@ -216,11 +230,10 @@
this.listQuery = { this.listQuery = {
page: 1, page: 1,
limit: 20, limit: 20,
type:'正式考试',
search:'', search:'',
}; };
this.value = [] this.value = []
this.getList(); // this.getList();
}, },
handleExport(scope) { handleExport(scope) {
const loading = this.$loading({text: '正在生成word...',}); const loading = this.$loading({text: '正在生成word...',});
@ -236,7 +249,7 @@
type: "error" type: "error"
}) })
.then(async () => { .then(async () => {
await deleteExamtest(scope.row.id); await deleteExamRecord(scope.row.id);
this.getList() this.getList()
this.$message({ this.$message({
type: "success", type: "success",
@ -256,10 +269,10 @@
}, },
exportTest() { exportTest() {
const loading = this.$loading(); const loading = this.$loading();
exportTest(this.listQuery).then(response => { exportRecord(this.listQuery).then(res=>{
loading.close() loading.close()
window.open(response.data.path, "_blank"); window.open(res.data.path, "_blank");
}); }).catch(e=>{loading.close()})
}, },
setTimeRange(){ setTimeRange(){
this.listQuery.start = this.value[0], this.listQuery.start = this.value[0],

View File

@ -39,9 +39,9 @@
<el-table-column label="考试地点"> <el-table-column label="考试地点">
<template slot-scope="scope">{{ scope.row.place }}</template> <template slot-scope="scope">{{ scope.row.place }}</template>
</el-table-column> </el-table-column>
<el-table-column label="参考机会"> <!-- <el-table-column label="参考机会">
<template slot-scope="scope">{{ scope.row.chance }}</template> <template slot-scope="scope">{{ scope.row.chance }}</template>
</el-table-column> </el-table-column> -->
<el-table-column label="开启时间"> <el-table-column label="开启时间">
<template slot-scope="scope">{{ scope.row.open_time }}</template> <template slot-scope="scope">{{ scope.row.open_time }}</template>
</el-table-column> </el-table-column>
@ -96,9 +96,9 @@
<el-form-item label="考试地点" prop="place"> <el-form-item label="考试地点" prop="place">
<el-input v-model="exam.place" placeholder="考试地点" /> <el-input v-model="exam.place" placeholder="考试地点" />
</el-form-item> </el-form-item>
<el-form-item label="参考机会" prop="chance"> <!-- <el-form-item label="参考机会" prop="chance">
<el-input-number v-model="exam.chance" placeholder="参考机会" :min="1"/> <el-input-number v-model="exam.chance" placeholder="参考机会" :min="1"/>
</el-form-item> </el-form-item> -->
<el-form-item label="开启时间" prop="open_time"> <el-form-item label="开启时间" prop="open_time">
<el-date-picker <el-date-picker
v-model="exam.open_time" v-model="exam.open_time"
@ -154,15 +154,25 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="考试部门" prop="participant_dep" style="width:100%" clearable collapse-tags filterable > <el-form-item label="考试部门" prop="participant_dep" style="width:100%" clearable collapse-tags filterable >
<el-button @click="selectAll">全选部门</el-button> <!-- <el-button @click="selectAll">全选部门</el-button> -->
<el-select v-model="exam.participant_dep" multiple collapse-tags filterable placeholder = "选择部门" > <!-- <el-select v-model="exam.participant_dep" multiple collapse-tags filterable placeholder = "选择部门" >
<el-option <el-option
v-for="item in depOptions" v-for="item in depOptions"
:key = "item.id" :key = "item.id"
:label = "item.name" :label = "item.name"
:value = "item.id"> :value = "item.id">
</el-option> </el-option>
</el-select> </el-select> -->
<div>
<el-transfer
:props="{key: 'id',label: 'name'}"
:data="depOptions"
v-model="exam.participant_dep"
:titles="['未选部门','指定考试部门']"
@change="handleChange"
@on-change="handleOnChange"
></el-transfer>
</div>
</el-form-item> </el-form-item>
<!-- <el-form-item label="考试人员" prop="participant_user" style="width:100%" clearable> <!-- <el-form-item label="考试人员" prop="participant_user" style="width:100%" clearable>
@ -184,6 +194,7 @@
:total="total" :total="total"
></el-pagination> ></el-pagination>
</el-form-item> --> </el-form-item> -->
</el-form> </el-form>
<div style="text-align:right;"> <div style="text-align:right;">
<el-button type="danger" @click="dialogVisible=false">取消</el-button> <el-button type="danger" @click="dialogVisible=false">取消</el-button>
@ -197,7 +208,11 @@
import {getUserList} from "@/api/user"; import {getUserList} from "@/api/user";
import checkPermission from "@/utils/permission"; import checkPermission from "@/utils/permission";
import Pagination from "@/components/Pagination" import Pagination from "@/components/Pagination"
import Vue from 'vue'
// import ElementUI from 'element-ui'
// import 'element-ui/lib/theme-chalk/index.css'
// Vue.use(ElementUI)
const defaultexam = { const defaultexam = {
id: "", id: "",
@ -207,7 +222,7 @@
close_time: null, close_time: null,
proctor_name:'', proctor_name:'',
proctor_phone:'', proctor_phone:'',
chance:3, chance:1,
paper:'', paper:'',
certificate:'', certificate:'',
course_name:[], course_name:[],
@ -270,9 +285,9 @@
this.paperOptions = res.data this.paperOptions = res.data
}) })
}, },
selectAll(){ // selectAll(){
this.exam.participant_dep = this.depOptions.map(option => option.id); // this.exam.participant_dep = this.depOptions.map(option => option.id);
}, // },
getList() { getList() {
this.listLoading = true; this.listLoading = true;
debugger; debugger;
@ -281,6 +296,12 @@
this.listLoading = false; this.listLoading = false;
}); });
}, },
handleChange(value, direction, movedKeys) {
console.log(value, direction, movedKeys)
},
handleOnChange(value, direction, movedKeys) {
console.log(value, direction, movedKeys)
},
handleSizeChange(val) { handleSizeChange(val) {
// 改变每页显示的条数 // 改变每页显示的条数
this.pageSize = val; this.pageSize = val;
@ -353,7 +374,7 @@
}); });
}) })
.catch(err => { .catch(err => {
// console.error(err); console.error(err);
}); });
}, },
handleView(scope){ handleView(scope){

View File

@ -36,7 +36,8 @@
<el-button slot="reference">Excel导入</el-button> <el-button slot="reference">Excel导入</el-button>
</el-popover> </el-popover>
<el-button type="primary" icon="el-icon-download" @click="exportQuestion">导出Excel</el-button> <el-button type="primary" icon="el-icon-download" @click="exportQuestions">导出Excel</el-button>
<el-button type="danger" v-if="checkPermission(['question_batch_delete'])" :disabled="this.selects.length==0" @click="batchDelete()">批量删除</el-button>
</div> </div>
</div> </div>
<el-table :data="tableData" style="width: 100%;margin-top:10px;" border stripe fit v-loading="listLoading" <el-table :data="tableData" style="width: 100%;margin-top:10px;" border stripe fit v-loading="listLoading"
@ -104,6 +105,7 @@
importQuestion, importQuestion,
exportQuestion, exportQuestion,
enableQuestions, enableQuestions,
deletesQuestion,
} from "@/api/exam"; } from "@/api/exam";
import checkPermission from "@/utils/permission"; import checkPermission from "@/utils/permission";
import Pagination from "@/components/Pagination"; import Pagination from "@/components/Pagination";
@ -159,7 +161,6 @@ export default {
handleUploadSuccess(res, file) { handleUploadSuccess(res, file) {
if (res.code == 201) { if (res.code == 201) {
const loading = this.$loading({ text: "正在导入中..." }) const loading = this.$loading({ text: "正在导入中..." })
console.log(res.data);
importQuestion(res.data).then(response => { importQuestion(res.data).then(response => {
loading.close() loading.close()
if (response.code == 200) { if (response.code == 200) {
@ -235,7 +236,7 @@ export default {
// console.error(err); // console.error(err);
}); });
}, },
exportQuestion() { exportQuestions() {
const loading = this.$loading({ const loading = this.$loading({
text: '正在准备..' text: '正在准备..'
}); });
@ -244,6 +245,34 @@ export default {
window.open(response.data.path, "_blank"); window.open(response.data.path, "_blank");
}).catch(e => { loading.close() }); }).catch(e => { loading.close() });
}, },
batchDelete() {
if (this.selects.length === 0) {
this.$message({
message: '请选择要删除的项',
type: 'warning'
});
return;
}else{
// 删除前的提示
this.$confirm("确认删除记录吗?", "提示", {
type: "warning",
}).then(() => {
console.log(this.selects)
this.deleteItems(this.selects);
this.getList()});
}
},
deleteItems(ids){
let item = {ids:ids};
deletesQuestion(item).then(response => {
if (response.code == 200) {
this.$message.success("删除成功");
this.getList();
} else {
this.$message.error(response.msg);
}
});
},
changeSort(val) { changeSort(val) {
if (val.order == "ascending") { if (val.order == "ascending") {
this.listQuery.ordering = val.prop; this.listQuery.ordering = val.prop;

View File

@ -86,7 +86,7 @@
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="submitForm('Form')" :loading="submitLoding">保存</el-button> <el-button type="primary" @click="submitForm('Form')" :loading="submitLoding">保存</el-button>
<el-button type="warning" @click="goBack()">返回</el-button> <el-button type="warning" @click="goBack()" v-if="qid==null">返回</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
@ -97,6 +97,7 @@ import { upUrl } from "@/api/file";
import { getToken } from "@/utils/auth"; import { getToken } from "@/utils/auth";
export default { export default {
components:{ }, components:{ },
props:["qid"],
data() { data() {
return { return {
upHeaders: { Authorization: "JWT " + getToken() }, upHeaders: { Authorization: "JWT " + getToken() },
@ -135,7 +136,12 @@ export default {
watch:{ watch:{
}, },
created() { created() {
if(this.qid) {
this.Form.id = this.qid
}else{
this.Form.id = this.$route.query.id //接收参数 this.Form.id = this.$route.query.id //接收参数
}
this.getQuestion(); this.getQuestion();
this.getQuestioncatAll() this.getQuestioncatAll()
}, },
@ -169,8 +175,13 @@ export default {
type: "success", type: "success",
message: "修改成功!" message: "修改成功!"
}); });
if (this.qid){
this.$emit("updateQuestion", this.Form)
}else{
this.goBack() this.goBack()
} }
}
}); });
} else { } else {
return false; return false;

View File

@ -54,12 +54,12 @@
icon="el-icon-edit" icon="el-icon-edit"
:disabled="!checkPermission(['paper_update'])" :disabled="!checkPermission(['paper_update'])"
></el-button> ></el-button>
<el-button <!-- <el-button
type="warning" type="warning"
size="small" size="small"
@click="handleClone(scope)" @click="handleClone(scope)"
:disabled="!checkPermission(['paper_clone'])" :disabled="!checkPermission(['paper_clone'])"
>克隆</el-button> >克隆</el-button> -->
<el-button <el-button
type="danger" type="danger"
size="small" size="small"

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<el-row> <el-row :gutter="6">
<el-col :span="8"> <el-col :span="8">
<h3>基本信息</h3> <el-card header="试卷信息">
<el-form :model="Form" :rules="rules" ref="Form" label-width="100px" status-icon> <el-form :model="Form" :rules="rules" ref="Form" label-width="100px" status-icon>
<el-form-item label="名称" prop="name"> <el-form-item label="名称" prop="name">
<el-input v-model="Form.name" style="width:80%"></el-input> <el-input v-model="Form.name" style="width:80%"></el-input>
@ -42,9 +42,10 @@
<el-button type="warning" @click="goBack()">返回</el-button> <el-button type="warning" @click="goBack()">返回</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card>
</el-col> </el-col>
<el-col :span="16"> <el-col :span="16">
<h3>选题信息</h3> <el-card header="试题信息">
<!-- <div> --> <!-- <div> -->
<div style="display: flex;"> <div style="display: flex;">
<el-button type="primary" @click="handleChoose" icon="el-icon-plus" style="margin-right: 10px;">选择试题</el-button> <el-button type="primary" @click="handleChoose" icon="el-icon-plus" style="margin-right: 10px;">选择试题</el-button>
@ -86,34 +87,45 @@
</div> </div>
</el-dialog> </el-dialog>
</div> </div>
<div v-for="(item, index) in questions"> <draggable v-model="questions">
<h4> <el-card style="margin-top: 4px" shadow="never" v-for="(item, index) in questions" v-bind:key="index">
<el-button <div slot="header" style="display: flex;">
type="danger" <span>{{ index+1 }}</span>
size="small" <el-link style="margin-left: auto; margin-right: 8px" type="primary" @click="handleEdit(item, index)" icon="el-icon-edit"></el-link>
@click="handleDelete(index)" <el-link style="margin-right: 4px" type="danger" @click="handleDelete(index)" icon="el-icon-remove"></el-link>
icon="el-icon-delete" <el-link style="margin-right: 4px" type="danger" @click="handleDelete2(item, index)" icon="el-icon-delete"></el-link>
></el-button> </div>
{{ index+1 }} - <div style="font-weight: bold;">
<el-tag>{{item.type}}</el-tag> <el-tag>{{item.type}}</el-tag>
{{ item.name }} {{ item.name }}
<span>(正确答案:{{item.right}})</span> <span>(正确答案:{{item.right}})</span>
</h4>
<div v-for="(value, name) in item.options">{{ name }}: {{ value }}</div>
</div> </div>
<div style="font-size: 14px;" v-for="(value, name) in item.options">{{ name }}: {{ value }}</div>
</el-card>
</draggable>
</el-card>
</el-col> </el-col>
</el-row> </el-row>
<Questionchoose v-bind:chooseVisible="chooseVisible" @closeDg="closeDg" @choseQ="choseQ"></Questionchoose> <Questionchoose v-bind:chooseVisible="chooseVisible" @closeDg="closeDg" @choseQ="choseQ"></Questionchoose>
<el-dialog title="编辑题目" :visible.sync="qDialog">
<QuestionUpdate v-if="qDialog" :qid="qid" @updateQuestion="updateQuestion"></QuestionUpdate>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import { getQuestioncatList, createPaper, UploadPaper } from "@/api/exam"; import { getQuestioncatList, createPaper, UploadPaper, deleteQuestion } from "@/api/exam";
import Questionchoose from "@/views/exam/questionChoose"; import Questionchoose from "@/views/exam/questionChoose";
import { upUrl, upHeaders } from "@/api/file"; import { upUrl, upHeaders } from "@/api/file";
import draggable from 'vuedraggable';
import QuestionUpdate from "./questionupdate.vue";
export default { export default {
components: { Questionchoose }, components: { Questionchoose, draggable, QuestionUpdate },
data() { data() {
return { return {
qDialog: false,
qid: null,
currentEditIndex: null,
questions: [], questions: [],
Form: { Form: {
name: "", name: "",
@ -164,6 +176,15 @@
this.getQuestioncat(); this.getQuestioncat();
}, },
methods: { methods: {
updateQuestion(data){
this.qDialog = false;
this.questions[this.currentEditIndex] = data;
},
handleEdit(item, index) {
this.qDialog = true;
this.qid = item.id;
this.currentEditIndex = index;
},
getQuestioncat() { getQuestioncat() {
getQuestioncatList().then(response => { getQuestioncatList().then(response => {
this.workscopeData = response.data.results; this.workscopeData = response.data.results;
@ -180,10 +201,8 @@
let that =this; let that =this;
if (res.code == 201){ if (res.code == 201){
that.impForm.doc_path = res.data.path; that.impForm.doc_path = res.data.path;
console.log(that.impForm)
UploadPaper(that.impForm).then(res => { UploadPaper(that.impForm).then(res => {
if (res.code == 200) { if (res.code == 200) {
console.log(res.data)
that.questions = res.data; that.questions = res.data;
that.$message({ that.$message({
@ -236,6 +255,21 @@
handleDelete(val) { handleDelete(val) {
this.questions.splice(val, 1); this.questions.splice(val, 1);
}, },
handleDelete2(item, val){
deleteQuestion(item.id).then(res=>{
console.log(res)
if (res.code ==204) {
this.$message({
type: "success",
message: "删除成功!"
});
this.questions.splice(val, 1);
}
}).catch(res=>{
console.log(res)
})
},
calScore() { calScore() {
let danxuan_count = 0, let danxuan_count = 0,
duoxuan_count = 0, duoxuan_count = 0,

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<el-row> <el-row :gutter="6">
<el-col :span="8"> <el-col :span="8">
<h3>基本信息</h3> <el-card header="试卷信息">
<el-form :model="Form" :rules="rules" ref="Form" label-width="100px" status-icon> <el-form :model="Form" :rules="rules" ref="Form" label-width="100px" status-icon>
<el-form-item label="名称" prop="name"> <el-form-item label="名称" prop="name">
<el-input v-model="Form.name" style="width:80%"></el-input> <el-input v-model="Form.name" style="width:80%"></el-input>
@ -42,38 +42,48 @@
<el-button type="warning" @click="goBack()">返回</el-button> <el-button type="warning" @click="goBack()">返回</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card>
</el-col> </el-col>
<el-col :span="16"> <el-col :span="16">
<h3>选题信息</h3> <el-card header="试题信息">
<el-button type="primary" @click="handleChoose" icon="el-icon-plus">选择试题</el-button> <el-button type="primary" @click="handleChoose" icon="el-icon-plus">选择试题</el-button>
<div v-for="(item, index) in questions"> <draggable v-model="questions">
<h4> <el-card style="margin-top: 4px" shadow="never" v-for="(item, index) in questions" v-bind:key="index">
<el-button <div slot="header" style="display: flex;">
type="danger" <span>{{ index+1 }}</span>
size="small" <el-link style="margin-left: auto; margin-right: 8px" type="primary" @click="handleEdit(item, index)" icon="el-icon-edit"></el-link>
@click="handleDelete(index)" <el-link style="margin-right: 4px" type="danger" @click="handleDelete(index)" icon="el-icon-delete"></el-link>
icon="el-icon-delete" </div>
></el-button> <div style="font-weight: bold;">
{{ index+1 }} -
<el-tag>{{item.type}}</el-tag> <el-tag>{{item.type}}</el-tag>
{{ item.name }} {{ item.name }}
<span>(正确答案:{{item.right}})</span> <span>(正确答案:{{item.right}})</span>
</h4>
<div v-for="(value, name) in item.options">{{ name }}: {{ value }}</div>
</div> </div>
<div style="font-size: 14px;" v-for="(value, name) in item.options">{{ name }}: {{ value }}</div>
</el-card>
</draggable>
</el-card>
</el-col> </el-col>
</el-row> </el-row>
<Questionchoose v-bind:chooseVisible="chooseVisible" @closeDg="closeDg" @choseQ="choseQ"></Questionchoose> <Questionchoose v-bind:chooseVisible="chooseVisible" @closeDg="closeDg" @choseQ="choseQ"></Questionchoose>
<el-dialog title="编辑题目" :visible.sync="qDialog">
<QuestionUpdate v-if="qDialog" :qid="qid" @updateQuestion="updateQuestion"></QuestionUpdate>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import { getQuestioncatList, updatePaper,getPaperDetail } from "@/api/exam"; import { getQuestioncatList, updatePaper,getPaperDetail } from "@/api/exam";
import { genTree } from "@/utils"; import { genTree } from "@/utils";
import Questionchoose from "@/views/exam/questionChoose"; import Questionchoose from "@/views/exam/questionChoose";
import draggable from 'vuedraggable';
import QuestionUpdate from "./questionupdate.vue";
export default { export default {
components: { Questionchoose }, components: { Questionchoose, draggable, QuestionUpdate },
data() { data() {
return { return {
qDialog: false,
qid: null,
currentEditIndex: null,
questions: [], questions: [],
Form: { Form: {
name: "", name: "",
@ -116,6 +126,16 @@
this.getPaperDetail(); this.getPaperDetail();
}, },
methods: { methods: {
updateQuestion(data){
this.qDialog = false;
data["question"] = data["id"]
this.questions[this.currentEditIndex] = data;
},
handleEdit(item, index) {
this.qDialog = true;
this.qid = item.question;
this.currentEditIndex = index;
},
getQuestioncat() { getQuestioncat() {
getQuestioncatList().then(response => { getQuestioncatList().then(response => {
this.workscopeData = genTree(response.data); this.workscopeData = genTree(response.data);

View File

@ -42,7 +42,7 @@
:value="item.key" :value="item.key"
/> />
</el-select> </el-select>
<el-select <!-- <el-select
v-model="listQuery.roles" v-model="listQuery.roles"
placeholder="角色" placeholder="角色"
style="width: 90px" style="width: 90px"
@ -55,7 +55,7 @@
:label="item.label" :label="item.label"
:value="item.value" :value="item.value"
/> />
</el-select> </el-select> -->
<el-input <el-input
v-model="listQuery.search" v-model="listQuery.search"
placeholder="姓名/邮箱" placeholder="姓名/邮箱"
@ -115,15 +115,15 @@
</el-form> </el-form>
<div style="text-align: right"> <div style="text-align: right">
<el-button type="danger" @click="close_dialog">取消</el-button> <el-button type="danger" @click="close_dialog">取消</el-button>
<el-button type="primary" @click="submitUpload" :disabled="saveLoading">确认</el-button> <el-button type="primary" @click="submitUpload">确认</el-button>
</div> </div>
</el-dialog> </el-dialog>
</el-card> </el-card>
</div> </div>
<div style="margin-top: 10px"> <div style="margin-top: 10px">
<el-button type="primary" icon="el-icon-plus" @click="handleAddUser" <el-button type="primary" icon="el-icon-plus" @click="handleAddUser"
>新增</el-button >新增</el-button>
>
</div> </div>
<el-table <el-table
v-loading="listLoading" v-loading="listLoading"
@ -147,7 +147,7 @@
scope.row.dept_name scope.row.dept_name
}}</template> }}</template>
</el-table-column> </el-table-column>
<el-table-column align="header-center" label="角色"> <!-- <el-table-column align="header-center" label="角色">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tag <el-tag
style="margin: 2px" style="margin: 2px"
@ -158,7 +158,7 @@
{{ item.name }} {{ item.name }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column> -->
<el-table-column label="创建日期"> <el-table-column label="创建日期">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ scope.row.date_joined }}</span> <span>{{ scope.row.date_joined }}</span>
@ -196,6 +196,7 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<pagination <pagination
v-show="userList.count > 0" v-show="userList.count > 0"
:total="userList.count" :total="userList.count"
@ -206,6 +207,66 @@
</el-card> </el-card>
</el-col> </el-col>
</el-row> </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> </div>
</template> </template>
<style> <style>
@ -266,9 +327,12 @@ export default {
upHeaders: upHeaders(), upHeaders: upHeaders(),
upUrl: upUrl(), upUrl: upUrl(),
userList: { count: 0 }, userList: { count: 0 },
roles: [], // roles: [],
saveLoading:false, saveLoading:false,
fileList: [], fileList: [],
impForm:{
file:''
},
impDialogVisible: false, impDialogVisible: false,
listLoading: true, listLoading: true,
downloadUrl: process.env.VUE_APP_BASE_API, downloadUrl: process.env.VUE_APP_BASE_API,
@ -300,7 +364,7 @@ export default {
{ header: '姓名', key: 'name', wpx: 60 }, { header: '姓名', key: 'name', wpx: 60 },
{ header: '账户', key: 'username', wch: 32 }, { header: '账户', key: 'username', wch: 32 },
{ header: '部门', key: 'dept_name', width: 30 }, { header: '部门', key: 'dept_name', width: 30 },
{ header: '角色', key: 'roles', width: 70 } // { header: '角色', key: 'roles', width: 70 }
], ],
}; };
}, },
@ -353,11 +417,9 @@ export default {
this.impForm.file = ''; this.impForm.file = '';
}, },
handleUpSuccess(res, file, filelist) { handleUpSuccess(res, file, filelist) {
this.Content.file = res.data.id;
this.impForm.file = res.data.path; this.impForm.file = res.data.path;
}, },
handleRemove(file, filelist) { handleRemove(file, filelist) {
this.Content.file = null;
this.impForm.file = ''; this.impForm.file = '';
}, },
handlePreview(file) { handlePreview(file) {
@ -438,12 +500,16 @@ export default {
type: "error", type: "error",
}) })
.then(async () => { .then(async () => {
await deleteUserExam(scope.row.id); deleteUserExam(scope.row.id).then((res) => {
this.userList.splice(scope.row.index, 1); console.log(res);
if (res.code >= 200) {
// this.userList.splice(scope.row.index, 1);
this.$message.success("成功"); this.$message.success("成功");
this.getList();
};
});
}) })
.catch((err) => { .catch((err) => {
}); });
}, },
async confirm(form) { async confirm(form) {
@ -454,9 +520,11 @@ export default {
updateUser(this.user.id, this.user).then((res) => { updateUser(this.user.id, this.user).then((res) => {
if (res.code >= 200) { if (res.code >= 200) {
this.getList(); this.getList();
this.dialogVisible = false; this.dialogVisible = true;
this.impForm.file = ''
this.$message.success("成功"); this.$message.success("成功");
} };
this.impForm.file = ''
}); });
} else { } else {
createUser(this.user).then((res) => { createUser(this.user).then((res) => {

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.12 on 2024-06-04 09:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('exam', '0001_initial'),
('edu', '0006_certificate_examrecord'),
]
operations = [
migrations.AlterField(
model_name='certificate',
name='examrecord',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cert_er', to='exam.examrecord'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2024-06-06 01:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('edu', '0007_alter_certificate_examrecord'),
]
operations = [
migrations.AddField(
model_name='certificate',
name='用户ID',
field=models.IntegerField(blank=True, max_length=32, null=True),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.12 on 2024-06-06 02:26
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('edu', '0008_certificate_用户id'),
]
operations = [
migrations.AlterField(
model_name='certificate',
name='用户ID',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -29,4 +29,5 @@ class Certificate(CommonADModel):
培训结束日期 = models.DateField(null=True, blank=True) 培训结束日期 = models.DateField(null=True, blank=True)
发证日期 = models.DateField(null=True, blank=True) 发证日期 = models.DateField(null=True, blank=True)
证书地址 = models.CharField(max_length=100, null=True, blank=True) 证书地址 = models.CharField(max_length=100, null=True, blank=True)
examrecord = models.ForeignKey('exam.ExamRecord', on_delete=models.CASCADE, null=True, blank=True) 用户ID = models.ForeignKey(null=True, blank=True, to='system.User', on_delete=models.CASCADE)
examrecord = models.OneToOneField('exam.ExamRecord', on_delete=models.CASCADE, null=True, blank=True, related_name='cert_er')

View File

@ -5,7 +5,7 @@ from openpyxl.styles import Font, Fill
import json import json
import os import os
def export_question(questions): def export_question(questions:object):
''' '''
params: serializer questions params: serializer questions
return: xlsx path return: xlsx path
@ -25,3 +25,32 @@ def export_question(questions):
os.makedirs(full_path) os.makedirs(full_path)
wb.save(full_path+filename) wb.save(full_path+filename)
return path + filename return path + filename
def export_record(records:object):
'''
params: serializer records
return: xlsx path
'''
wb = Workbook()
ws1 = wb.active
ws1.title = '答题记录表'
ws1.append(['类型', '用户', '是否通过', '得分', '总分', '耗时(时分秒)', '答题时间', '部门'])
row = ws1.row_dimensions[1]
row.font = Font(bold=True)
for i in records:
tookformat = get_took_format(i.took)
ws1.append([i.type, i.create_by.name, i.is_pass, i.score, i.total_score, str(tookformat), str(i.start_time), i.belong_dept.name])
filename = 'records' + datetime.now().strftime("%Y%m%d%H%M%S") +'.xlsx'
path = '/media/temp/'
full_path = settings.BASE_DIR + '/media/temp/'
if not os.path.exists(full_path):
os.makedirs(full_path)
wb.save(full_path+filename)
return path + filename
def get_took_format(took:int):
m, s = divmod(took, 60)
h, m = divmod(m, 60)
return "%02d:%02d:%02d" % (h, m, s)

View File

@ -1,6 +1,9 @@
from django_filters import rest_framework as filters from django_filters import rest_framework as filters
from django.utils import timezone
from .models import ExamRecord, Exam from .models import ExamRecord, Exam
from django.db.models import Q
class ExamRecordFilter(filters.FilterSet): class ExamRecordFilter(filters.FilterSet):
is_my = filters.BooleanFilter(method='filter_is_my')
class Meta: class Meta:
model = ExamRecord model = ExamRecord
fields = { fields = {
@ -8,9 +11,17 @@ class ExamRecordFilter(filters.FilterSet):
'is_pass': ['exact'], 'is_pass': ['exact'],
'type': ['exact'], 'type': ['exact'],
'is_submited': ['exact'], 'is_submited': ['exact'],
'exam': ['exact']
} }
def filter_is_my(self, queryset, name, value):
if value:
user = self.request.user
return queryset.filter(create_by=user)
return queryset
class ExamFilter(filters.FilterSet): class ExamFilter(filters.FilterSet):
can_attend = filters.BooleanFilter(method='filter_can_attend')
is_my = filters.BooleanFilter(method='filter_is_my')
class Meta: class Meta:
model = Exam model = Exam
fields = { fields = {
@ -19,3 +30,16 @@ class ExamFilter(filters.FilterSet):
'code': ['exact'], 'code': ['exact'],
'is_open': ['exact'] 'is_open': ['exact']
} }
def filter_can_attend(self, queryset, name, value):
if value:
now = timezone.now()
return queryset.filter(open_time__lte=now, close_time__gte=now)| queryset.filter(close_time__isnull=True)
return queryset
def filter_is_my(self, queryset, name, value):
if value:
user = self.request.user
return queryset.filter(Q(participant_user=user)|Q(participant_dep=user.dept)|Q(is_open=True))
return queryset

View File

@ -1,5 +1,5 @@
from django.db import models from django.db import models
from apps.system.models import CommonAModel, CommonBModel from apps.system.models import CommonAModel, CommonADModel, CommonBDModel
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from utils.model import BaseModel from utils.model import BaseModel
from apps.edu.models import Course from apps.edu.models import Course
@ -20,7 +20,7 @@ class Questioncat(CommonAModel):
return self.questioncat.count() return self.questioncat.count()
class Question(CommonAModel): class Question(CommonADModel):
type_choices = ( type_choices = (
('单选', '单选'), ('单选', '单选'),
('多选', '多选'), ('多选', '多选'),
@ -99,7 +99,7 @@ class Exam(CommonAModel):
return self.name return self.name
class ExamRecord(CommonBModel): class ExamRecord(CommonBDModel):
''' '''
考试记录表 考试记录表
''' '''

View File

@ -94,6 +94,6 @@ def interpret_text(start:int, excel_path:str, doc_path:str, field=None):
if __name__ == '__main__': if __name__ == '__main__':
doc_path = "C:\code\data\\test.docx" doc_path = "C:\code\data\\判断题.docx"
excel_path = "C:\code\data\question.xlsx" excel_path = "C:\code\data\question.xlsx"
interpret_text(3, excel_path, doc_path) interpret_text(3, excel_path, doc_path)

View File

@ -88,6 +88,7 @@ class ExamListSerializer(ModelSerializer):
course_ = CourseSerializer(source='course_name', read_only=True) course_ = CourseSerializer(source='course_name', read_only=True)
user_count = serializers.SerializerMethodField() user_count = serializers.SerializerMethodField()
submit_count = serializers.SerializerMethodField() submit_count = serializers.SerializerMethodField()
limit = CharField(source='paper.limit', read_only=True)
class Meta: class Meta:
model = Exam model = Exam
fields = '__all__' fields = '__all__'
@ -132,6 +133,8 @@ class ExamRecordListSerializer(serializers.ModelSerializer):
source='exam.name', read_only=True) source='exam.name', read_only=True)
blong_dept_name = serializers.CharField( blong_dept_name = serializers.CharField(
source='belong_dept.name', read_only=True) source='belong_dept.name', read_only=True)
cert_number = serializers.CharField(source='cert_er.证书编号', read_only=True)
cert_path = serializers.CharField(source='cert_er.证书地址', read_only=True)
class Meta: class Meta:
model = ExamRecord model = ExamRecord
exclude = ('detail',) exclude = ('detail',)

View File

@ -1,7 +1,7 @@
from django.shortcuts import render from django.shortcuts import render
from rest_framework.viewsets import ModelViewSet, GenericViewSet from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.mixins import ListModelMixin, DestroyModelMixin, RetrieveModelMixin from rest_framework.mixins import ListModelMixin, DestroyModelMixin, RetrieveModelMixin
from apps.exam.exports import export_question from apps.exam.exports import export_question, export_record
from apps.exam.models import Question, Questioncat, PaperQuestion from apps.exam.models import Question, Questioncat, PaperQuestion
from apps.exam.serializers import (QuestionSerializer, QuestioncatSerializer, PaperSerializer, ExamDetailSerializer, ExamRecordDetailSerializer, ExamListSerializer, from apps.exam.serializers import (QuestionSerializer, QuestioncatSerializer, PaperSerializer, ExamDetailSerializer, ExamRecordDetailSerializer, ExamListSerializer,
ExamCreateUpdateSerializer, ExamListSerializer, ExamRecordSubmitSerializer, PaperDetailSerializer, PaperCreateUpdateSerializer, AnswerDetailOutSerializer, ExamRecordListSerializer) ExamCreateUpdateSerializer, ExamListSerializer, ExamRecordSubmitSerializer, PaperDetailSerializer, PaperCreateUpdateSerializer, AnswerDetailOutSerializer, ExamRecordListSerializer)
@ -11,7 +11,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from openpyxl import Workbook, load_workbook from openpyxl import Workbook, load_workbook
from django.conf import settings from django.conf import settings
from apps.exam.models import Paper, Exam, ExamRecord, AnswerDetail from apps.exam.models import Paper, Exam, ExamRecord, AnswerDetail, PaperQuestion
from django.utils import timezone from django.utils import timezone
from django.db import transaction from django.db import transaction
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
@ -20,11 +20,13 @@ from apps.exam.filters import ExamRecordFilter, ExamFilter
from datetime import timedelta from datetime import timedelta
from apps.system.mixins import CreateUpdateCustomMixin from apps.system.mixins import CreateUpdateCustomMixin
from apps.edu.serializers import CertificateSerializer from apps.edu.serializers import CertificateSerializer
from apps.edu.models import Certificate
from utils.queryset import get_child_queryset2 from utils.queryset import get_child_queryset2
from apps.system.permission import has_permission from apps.system.permission import has_permission
from apps.exam.parse_word import interpret_text from apps.exam.parse_word import interpret_text
import os import os
import shutil import shutil
from django.db.models import Q
# Create your views here. # Create your views here.
EXCEL_PATH = os.path.join(settings.BASE_DIR, "media/default/question.xlsx") EXCEL_PATH = os.path.join(settings.BASE_DIR, "media/default/question.xlsx")
@ -63,21 +65,20 @@ class QuestionViewSet(CreateUpdateCustomMixin, ModelViewSet):
perms_map = {'get': '*', 'post':'question', 'put':'question', 'delete':'question'} perms_map = {'get': '*', 'post':'question', 'put':'question', 'delete':'question'}
queryset = Question.objects.all() queryset = Question.objects.all()
serializer_class = QuestionSerializer serializer_class = QuestionSerializer
filterset_fields = ['level', 'type', 'year'] filterset_fields = ['level', 'type', 'year', 'questioncat']
search_fields = ['name', 'options', 'resolution'] search_fields = ['name', 'options', 'resolution']
def destroy(self, request, *args, **kwargs):
@action(methods=['get'], detail=False,
url_path='export', url_name='export_question', perms_map=[{'get': '*'}], serializer_class=Serializer)
def export_question(self, request):
""" """
导出题目 删除题目
导出题目
""" """
queryset = self.filter_queryset(self.get_queryset()) id = kwargs.get('pk', None)
path = export_question(queryset) # 删除前进行校验,如果存在考试中不允许删除。
return Response({'path': path}) paperquestion = PaperQuestion.objects.filter(question_id=id).exists()
if paperquestion:
raise ParseError('此试题存在考试中不允许删除')
Question.objects.filter(id=id).delete()
return Response("删除成功")
@action(methods=['post'], detail=False, url_name='enable_question', perms_map={'post': 'question'}, serializer_class=Serializer) @action(methods=['post'], detail=False, url_name='enable_question', perms_map={'post': 'question'}, serializer_class=Serializer)
def enable(self, request): def enable(self, request):
@ -91,6 +92,30 @@ class QuestionViewSet(CreateUpdateCustomMixin, ModelViewSet):
Question.objects.filter(pk__in=ids).update(enabled=True) Question.objects.filter(pk__in=ids).update(enabled=True)
return Response(status=200) return Response(status=200)
@action(methods=['post'], detail=False, perms_map={'*':'question_delete'})
def deletes(self, request):
"""
批量删除
"""
ids = request.data.get('ids', [])
if request.user.is_superuser:
# 删除前进行校验,如果存在考试中不允许删除。
paperquestion = PaperQuestion.objects.filter(question__id__in=ids).exists()
if paperquestion:
return Response('此试题存在考试中不允许删除')
Question.objects.filter(id__in=ids).update(is_deleted=True)
return Response()
return Response({'msg':'权限不足'},status=401)
@action(methods=['get'], detail=False, perms_map={'get':'export_question'})
def export(self, request):
"""
导出题目
"""
queryset = self.filter_queryset(self.get_queryset())
path = export_question(queryset)
return Response({'path': path})
@action(methods=['post'], detail=False, @action(methods=['post'], detail=False,
url_path='import', url_name='import_question', perms_map={'post': 'question'}, serializer_class=Serializer) url_path='import', url_name='import_question', perms_map={'post': 'question'}, serializer_class=Serializer)
def import_question(self, request): def import_question(self, request):
@ -270,13 +295,40 @@ class PaperViewSet(ModelViewSet):
PaperQuestion.objects.bulk_create(q_list) PaperQuestion.objects.bulk_create(q_list)
return Response() return Response()
@action(methods=['put'], detail=True, url_path='clone', url_name='clone_paper', perms_map={'put':'clone_paper'})
def clone(self, request, pk=None):
'''
克隆试卷
'''
paper = self.get_object()
obj = Paper()
obj.name = '克隆卷-'+paper.name
obj.workscope = paper.workscope
obj.limit = paper.limit
obj.total_score = paper.total_score
obj.pass_score = paper.pass_score
obj.danxuan_count = paper.danxuan_count
obj.danxuan_score = paper.danxuan_score
obj.duoxuan_count = paper.duoxuan_count
obj.duoxuan_score = paper.duoxuan_score
obj.panduan_count = paper.panduan_count
obj.panduan_score = paper.panduan_score
obj.save()
for i in PaperQuestion.objects.filter(paper=paper):
o = PaperQuestion()
o.paper = obj
o.question = i.question
o.total_score = i.total_score
o.save()
return Response(status=200)
@action(methods=['post'], detail=False, perms_map={'post': 'question'}, serializer_class=Serializer) @action(methods=['post'], detail=False, perms_map={'post': 'question'}, serializer_class=Serializer)
def upload_paper(self, request): def upload_paper(self, request):
doc_path = request.data.get('doc_path') doc_path = request.data.get('doc_path')
question_type = request.data.get('question_type') question_type = request.data.get('question_type')
excel_path = settings.BASE_DIR + "/media/default/question.xlsx" excel_path = settings.BASE_DIR + "/media/default/question.xlsx"
doc_path = settings.BASE_DIR + doc_path doc_path = settings.BASE_DIR + doc_path
# excel_path = "C:\code\data\question.xlsx"
timenow = timezone.now().strftime('%Y%m%d%H%M%S') timenow = timezone.now().strftime('%Y%m%d%H%M%S')
question_excel_name = "question_excel_"+timenow question_excel_name = "question_excel_"+timenow
question_excel = os.path.join(os.path.dirname(excel_path), question_excel_name) question_excel = os.path.join(os.path.dirname(excel_path), question_excel_name)
@ -426,14 +478,22 @@ class ExamViewSet(CreateUpdateCustomMixin, ModelViewSet):
return ExamDetailSerializer return ExamDetailSerializer
return super().get_serializer_class() return super().get_serializer_class()
def get_queryset(self):
qs = super().get_queryset()
user = self.request.user
if has_permission("exam", user):
return qs
return qs.filter(Q(participant_user=user)|Q(participant_dep=user.dept)|Q(is_open=True))
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
if ExamRecord.objects.filter(exam=instance).exists(): if ExamRecord.objects.filter(exam=instance).exists():
raise ParseError('存在考试记录,禁止删除') raise ParseError('存在考试记录,禁止删除')
instance.delete(soft=False) instance.delete()
return Response(status=204) return Response(status=204)
@action(methods=['post'], detail=True, perms_map=[{'post': '*'}], serializer_class=Serializer, permission_classes = [IsAuthenticated]) @action(methods=['post'], detail=True, perms_map={'post': '*'}, serializer_class=Serializer, permission_classes = [IsAuthenticated])
@transaction.atomic @transaction.atomic
def start(self, request, *args, **kwargs): def start(self, request, *args, **kwargs):
""" """
@ -441,7 +501,8 @@ class ExamViewSet(CreateUpdateCustomMixin, ModelViewSet):
开始考试具体题目信息 开始考试具体题目信息
""" """
exam = self.get_object() # exam = self.get_object()
exam = Exam.objects.get(id=kwargs['pk'])
# 查询本次考试对应哪些人 # 查询本次考试对应哪些人
participants = exam.participant_user.all() participants = exam.participant_user.all()
participants_ids = [i.id for i in participants] participants_ids = [i.id for i in participants]
@ -489,7 +550,7 @@ class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, RetrieveModelMixin, G
考试记录列表和详情 考试记录列表和详情
""" """
perms_map = {'get': '*', 'post': '*', 'delete':'examrecord'} perms_map = {'get': '*', 'post': '*', 'delete':'examrecord'}
queryset = ExamRecord.objects.select_related('create_by') queryset = ExamRecord.objects.select_related('create_by', 'cert_er')
serializer_class = ExamRecordListSerializer serializer_class = ExamRecordListSerializer
ordering_fields = ['create_time', 'score', 'took', 'update_time', 'belong_dept'] ordering_fields = ['create_time', 'score', 'took', 'update_time', 'belong_dept']
ordering = ['-update_time'] ordering = ['-update_time']
@ -505,24 +566,25 @@ class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, RetrieveModelMixin, G
if has_permission('ctc_manager', self.request.user): if has_permission('ctc_manager', self.request.user):
return qs return qs
# 如果是部门管理员,只能看到自己部门下的考试记录 # 如果是部门管理员,只能看到自己部门下的考试记录
# return qs.filter(belong_dept__in=get_child_queryset2(self.request.user.dept)) elif has_permission('exam_manager', self.request.user):
return qs.filter(belong_dept__in=get_child_queryset2(self.request.user.dept))
# 如果是普通员工,只能看到自己考试记录 # 如果是普通员工,只能看到自己考试记录
else: else:
return qs.filter(create_by=self.request.user) return qs.filter(create_by=self.request.user)
def get_serializer_class(self): def get_serializer_class(self):
if self.action == 'retrieve': if self.action == 'retrieve':
now = timezone.now()
if now > self.get_object().exam.close_time:
return ExamRecordDetailSerializer return ExamRecordDetailSerializer
return super().get_serializer_class() return super().get_serializer_class()
def perform_destroy(self, instance): # 考试记录物理删除 def perform_destroy(self, instance): # 考试记录物理删除
instance.delete(soft=False) instance.delete()
@action(methods=['post'], detail=False, perms_map=[{'post': '*'}], serializer_class=Serializer, permission_classes = [IsAuthenticated]) @action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=Serializer, permission_classes = [IsAuthenticated])
def clear(self, request, pk=None): def clear(self, request, pk=None):
""" """
清除七日前未提交的考试记录
清除七日前未提交的考试记录 清除七日前未提交的考试记录
""" """
now = timezone.now now = timezone.now
@ -530,19 +592,19 @@ class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, RetrieveModelMixin, G
ExamRecord.objects.filter(create_time__lte=days7_ago, is_submited=False).delete(soft=False) ExamRecord.objects.filter(create_time__lte=days7_ago, is_submited=False).delete(soft=False)
return Response(status=False) return Response(status=False)
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def self(self, request, pk=None):
'''
个人考试记录
个人考试记录 @action(methods=['get'], detail=False,
''' url_path='export', url_name='export_record', perms_map={'*': 'export_ansrecord'}, serializer_class=Serializer)
queryset = ExamRecord.objects.filter(create_by=request.user).order_by('-update_time') def export(self, request):
page = self.paginate_queryset(queryset) """
serializer = self.get_serializer(page, many=True) 导出答题记录
return self.get_paginated_response(serializer.data) """
queryset = self.filter_queryset(self.get_queryset())
path = export_record(queryset)
return Response({'path': path})
@action(methods=['post'], detail=True, perms_map=[{'post': '*'}], serializer_class=ExamRecordSubmitSerializer, permission_classes = [IsAuthenticated])
@action(methods=['post'], detail=True, perms_map={'post': '*'}, serializer_class=ExamRecordSubmitSerializer, permission_classes = [IsAuthenticated])
@transaction.atomic @transaction.atomic
def submit(self, request, pk=None): def submit(self, request, pk=None):
''' '''
@ -595,12 +657,22 @@ class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, RetrieveModelMixin, G
course = exam.course_name.all() course = exam.course_name.all()
courese_ids = [i.id for i in course] courese_ids = [i.id for i in course]
current_date = now_data.strftime('%Y-%m-%d') current_date = now_data.strftime('%Y-%m-%d')
cer_number = now_data.strftime('%Y%m%d')
# 查询证明编号创建时间为最后一个
cer = Certificate.objects.latest('证书编号')
if cer:
cer_number = int(cer.证书编号[5:]) + 1
cer_number = 'CTCZL' + str(cer_number)
data_dict = { data_dict = {
'姓名': request.user.name, '姓名': request.user.name,
'证书编号': 'CTCZL'+ current_date, '用户ID': request.user.id,
'证书编号': cer_number,
'证书方案': '202312',
'单位名称': request.user.dept.name, '单位名称': request.user.dept.name,
'所属单位': '国检测试控股集团'+request.user.dept.name, '所属单位': '国检测试控股集团'+request.user.dept.name,
'发证日期': current_date, '发证日期': current_date,
'培训日期':current_date,
'培训结束日期': current_date,
'课程列表': courese_ids, '课程列表': courese_ids,
'examrecord': er.id, 'examrecord': er.id,
} }
@ -610,7 +682,7 @@ class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, RetrieveModelMixin, G
er.took = (now - er.create_time).total_seconds() er.took = (now - er.create_time).total_seconds()
er.end_time = now er.end_time = now
er.belong_dept=request.user.dept.id er.belong_dept=request.user.dept
er.is_submited = True er.is_submited = True
er.save() er.save()
return Response(ExamRecordListSerializer(instance=er).data) return Response(ExamRecordListSerializer(instance=er).data)

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.12 on 2024-06-04 09:28
import datetime
from django.db import migrations, models
from django.utils.timezone import utc
class Migration(migrations.Migration):
dependencies = [
('information', '0047_alter_qualification_change_date'),
]
operations = [
migrations.AlterField(
model_name='qualification',
name='change_date',
field=models.DateField(blank=True, default=datetime.datetime(2024, 6, 4, 9, 28, 52, 579869, tzinfo=utc), null=True, verbose_name='变更日期'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.12 on 2024-06-06 01:44
import datetime
from django.db import migrations, models
from django.utils.timezone import utc
class Migration(migrations.Migration):
dependencies = [
('information', '0048_alter_qualification_change_date'),
]
operations = [
migrations.AlterField(
model_name='qualification',
name='change_date',
field=models.DateField(blank=True, default=datetime.datetime(2024, 6, 6, 1, 44, 19, 206626, tzinfo=utc), null=True, verbose_name='变更日期'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.12 on 2024-06-06 02:26
import datetime
from django.db import migrations, models
from django.utils.timezone import utc
class Migration(migrations.Migration):
dependencies = [
('information', '0049_alter_qualification_change_date'),
]
operations = [
migrations.AlterField(
model_name='qualification',
name='change_date',
field=models.DateField(blank=True, default=datetime.datetime(2024, 6, 6, 2, 26, 43, 23224, tzinfo=utc), null=True, verbose_name='变更日期'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.12 on 2024-06-14 05:35
import datetime
from django.db import migrations, models
from django.utils.timezone import utc
class Migration(migrations.Migration):
dependencies = [
('information', '0050_alter_qualification_change_date'),
]
operations = [
migrations.AlterField(
model_name='qualification',
name='change_date',
field=models.DateField(blank=True, default=datetime.datetime(2024, 6, 14, 5, 35, 33, 230280, tzinfo=utc), null=True, verbose_name='变更日期'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.12 on 2024-06-14 06:51
import datetime
from django.db import migrations, models
from django.utils.timezone import utc
class Migration(migrations.Migration):
dependencies = [
('information', '0051_alter_qualification_change_date'),
]
operations = [
migrations.AlterField(
model_name='qualification',
name='change_date',
field=models.DateField(blank=True, default=datetime.datetime(2024, 6, 14, 6, 51, 53, 747831, tzinfo=utc), null=True, verbose_name='变更日期'),
),
]

View File

@ -100,8 +100,6 @@ class ImpMixin:
if 'file' not in request.data: if 'file' not in request.data:
raise ParseError('请提供文件') raise ParseError('请提供文件')
path = request.data['file'] path = request.data['file']
print(path, "---------ssss")
if not str(path).endswith('.xlsx'): if not str(path).endswith('.xlsx'):
raise ParseError('请提供xlsx格式文件') raise ParseError('请提供xlsx格式文件')
fullpath = settings.BASE_DIR + str(path) fullpath = settings.BASE_DIR + str(path)
@ -111,7 +109,7 @@ class ImpMixin:
data_list = self.build_data(sheet, start) data_list = self.build_data(sheet, start)
serializer = mySerializer(data=data_list, many=True, context={'request': request}) serializer = mySerializer(data=data_list, many=True, context={'request': request})
if serializer.is_valid(): if serializer.is_valid():
serializer.save(create_by=request.user, belong_dept=request.user.dept) serializer.save()
else: else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -335,6 +333,8 @@ class UserExamViewset(ImpMixin, ModelViewSet):
else: else:
# password = make_password(''.join(random.sample(string.ascii_letters + string.digits, 8))) # password = make_password(''.join(random.sample(string.ascii_letters + string.digits, 8)))
password = make_password('0000') password = make_password('0000')
exam_role = Role.objects.get(name='考试')
request.data['roles'] = [exam_role.id]
serializer = self.get_serializer(data=request.data) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save(password=password) serializer.save(password=password)
@ -347,12 +347,14 @@ class UserExamViewset(ImpMixin, ModelViewSet):
return Response({'msg': '考试角色不存在'}) return Response({'msg': '考试角色不存在'})
for row in sheet.iter_rows(min_row=start, values_only=True): # 假设第一行是表头,从第二行开始读取数据 for row in sheet.iter_rows(min_row=start, values_only=True): # 假设第一行是表头,从第二行开始读取数据
if row[0] is not None: if row[0] is not None:
dept = Organization.objects.get(name=row[3]) dept = Organization.objects.get(name=row[2])
if not dept: user_depts = get_child_queryset2(self.request.user.dept).order_by('sort')
return Response({'msg': '部门不存在'}) depts = any(i.name==dept.name for i in user_depts)
if depts is False:
return Response({'msg': f'本公司下不存在此部门{row[2]}'})
serializer_data = { serializer_data = {
'name': row[1], 'name': row[0],
'username':row[2], 'username':row[1],
'dept':dept.id, 'dept':dept.id,
'roles':[exam_role.id], 'roles':[exam_role.id],
'avatar': "/media/default/avatar.png" 'avatar': "/media/default/avatar.png"
@ -510,6 +512,7 @@ class UserViewSet(PageOrNot, ModelViewSet):
while sheet['b'+str(i)].value: while sheet['b'+str(i)].value:
name = sheet['b'+str(i)].value name = sheet['b'+str(i)].value
email = sheet['e'+str(i)].value email = sheet['e'+str(i)].value
dept = Organization.objects.get(name=sheet['j'+str(i)].value)
if not User.objects.filter(username=email).exists(): if not User.objects.filter(username=email).exists():
user = User.objects.create(name=name, user = User.objects.create(name=name,
username=email, username=email,
@ -517,7 +520,7 @@ class UserViewSet(PageOrNot, ModelViewSet):
dept=dept) dept=dept)
else: else:
user = User.objects.get(username=email) user = User.objects.get(username=email)
dept = Organization.objects.get(name=sheet['j'+str(i)].value)
if sheet['f'+str(i)].value: if sheet['f'+str(i)].value:
user.roles.add(role1) user.roles.add(role1)
if sheet['g'+str(i)].value: if sheet['g'+str(i)].value:

View File

@ -28,6 +28,4 @@ DATABASES = {
# 'HOST':'1.203.161.101', # 'HOST':'1.203.161.101',
'PORT': '5432', 'PORT': '5432',
} }
} }