task.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. from collections import OrderedDict
  4. import openpyxl
  5. from fastapi import Query, Depends, Path
  6. from sqlalchemy import select, func, case, between, and_, asc, desc
  7. from sqlalchemy.ext.asyncio import AsyncSession
  8. from starlette.background import BackgroundTasks
  9. from starlette.responses import FileResponse
  10. from app.api.endpoints.review._utils import complete_answer_statistic
  11. from bgtask.tasks import (bgtask_update_student_marktask, bgtask_update_class_error_statistic,
  12. update_student_error_statistic)
  13. from common.const import OBJECTIVE_QUESTION_TYPES
  14. from core.config import settings
  15. from crud.marktask import crud_task, crud_student_answer, crud_student_task
  16. from crud.paper import crud_question
  17. from models.marktask import StudentAnswer, StudentMarkTask, MarkTask
  18. from models.paper import PaperQuestion
  19. from models.user import Teacher
  20. from schemas.app.task import UpdateMarkTaskQuestion
  21. from schemas.base import OrderByField
  22. from utils.depends import get_async_db, get_current_user
  23. # 阅卷任务列表
  24. async def get_mark_tasks(page: int = 1,
  25. size: int = 10,
  26. cid: int = Query(0, description="班级ID"),
  27. mtype: str = Query(..., description="阅卷任务类型,work/exam"),
  28. ctgid: int = Query(0, description="分类ID"),
  29. year: int = Query(0, description="年份"),
  30. status: int = Query(None, description="状态"),
  31. name: str = Query(None, description="任务名称"),
  32. order: OrderByField = Query(
  33. "-created_at", description="排序字段,用逗号分隔,升降序以-判断,默认-created_at"),
  34. db: AsyncSession = Depends(get_async_db),
  35. current_user: Teacher = Depends(get_current_user)):
  36. current_teacher_classes = [int(x) for x in current_user.class_id.split(',')]
  37. _q = [MarkTask.mtype == mtype, MarkTask.subject == current_user.subject]
  38. if ctgid:
  39. _q.append(MarkTask.category_id == ctgid)
  40. if year:
  41. _q.append(MarkTask.year == year)
  42. # 班级条件,只查询教师所属班级
  43. if cid:
  44. _q.append(MarkTask.class_id == cid)
  45. else:
  46. _q.append(MarkTask.class_id.in_(current_teacher_classes))
  47. if name:
  48. _q.append(MarkTask.name.like(f"%{name}%"))
  49. if status:
  50. _q.append(MarkTask.status == status)
  51. offset = (page - 1) * size
  52. # 排序字段
  53. order_fields = []
  54. if order:
  55. for x in order.split(","):
  56. field = x.strip()
  57. if field:
  58. if field.startswith("-"):
  59. order_fields.append(desc(getattr(MarkTask, field[1:])))
  60. else:
  61. order_fields.append(asc(getattr(MarkTask, field)))
  62. total, marks = await crud_task.find_all(db,
  63. filters=_q,
  64. limit=size,
  65. offset=offset,
  66. order_by=order_fields)
  67. for item in marks:
  68. item.pass_rate = f"{int(item.pass_rate * 100)}%"
  69. return {"data": marks, "total": total}
  70. # 阅卷任务详情
  71. async def get_mark_task(tid: int = Path(..., description="批阅任务ID"),
  72. db: AsyncSession = Depends(get_async_db),
  73. current_user: Teacher = Depends(get_current_user)):
  74. # 判断task是否存在
  75. db_obj = await crud_task.find_one(db, filters={"id": tid})
  76. if not db_obj:
  77. return {"errcode": 400, "mess": "阅卷任务不存在!"}
  78. db_obj.pass_rate = f"{int(db_obj.pass_rate * 100)}%"
  79. # 按成绩分段统计
  80. score_gaps = ["0-50", "50-60", "60-70", "70-80", "80-90", "90-100"]
  81. if db_obj.status != 0:
  82. stmt = select(
  83. func.sum(case(whens=(between(StudentMarkTask.score, 0, 49), 1), else_=0)).label("0-50"),
  84. func.sum(case(whens=(between(StudentMarkTask.score, 50, 59), 1),
  85. else_=0)).label("50-60"),
  86. func.sum(case(whens=(between(StudentMarkTask.score, 60, 69), 1),
  87. else_=0)).label("60-70"),
  88. func.sum(case(whens=(between(StudentMarkTask.score, 70, 79), 1),
  89. else_=0)).label("70-80"),
  90. func.sum(case(whens=(between(StudentMarkTask.score, 80, 89), 1),
  91. else_=0)).label("80-90"),
  92. func.sum(case(whens=(between(StudentMarkTask.score, 90, 100), 1),
  93. else_=0)).label("90-100"),
  94. ).select_from(StudentMarkTask).where(StudentMarkTask.task_id == tid,
  95. StudentMarkTask.is_completed == True)
  96. scores = [x if x else 0 for x in (await crud_student_task.execute_v2(db, stmt))[0]]
  97. score_table = [{
  98. "key": "0-50",
  99. "val": scores[0]
  100. }, {
  101. "key": "50-70",
  102. "val": scores[1] + scores[2]
  103. }, {
  104. "key": "70-100",
  105. "val": sum(scores[3:])
  106. }]
  107. score_chart = [{"key": x[0], "val": x[1]} for x in zip(score_gaps, scores)]
  108. else:
  109. score_table = [{
  110. "key": "0-50",
  111. "val": 0
  112. }, {
  113. "key": "50-70",
  114. "val": 0
  115. }, {
  116. "key": "70-100",
  117. "val": 0
  118. }]
  119. score_chart = [{"key": x, "val": 0} for x in score_gaps]
  120. return {"data": db_obj, "score_table": score_table, "score_chart": score_chart}
  121. # 阅卷任务试题列表
  122. async def get_task_questions(page: int = 1,
  123. size: int = 10,
  124. tid: int = Path(..., description="阅卷任务ID"),
  125. stid: int = Query(None, description="学生ID"),
  126. qno: int = Query(None, description="题号"),
  127. db: AsyncSession = Depends(get_async_db),
  128. current_user: Teacher = Depends(get_current_user)):
  129. _q = [StudentAnswer.qtype.notin_(OBJECTIVE_QUESTION_TYPES)]
  130. # 按学生批阅,数据流:查询StudentMarkTask获取ID,使用ID去查询改学生的所有试题
  131. if stid:
  132. student_task = await crud_student_task.find_one(
  133. db,
  134. filters=[StudentMarkTask.task_id == tid, StudentMarkTask.student_id == stid],
  135. return_fields=["id"])
  136. _q.append(StudentAnswer.student_task_id == student_task.id)
  137. elif qno: # 按试题批阅,数据流:通过 task_id 和 qno 查询试题
  138. _q.extend([StudentAnswer.task_id == tid, StudentAnswer.qno == qno])
  139. else:
  140. _q.append(StudentAnswer.task_id == tid)
  141. # 查询试题列表
  142. offset = (page - 1) * size
  143. total, items = await crud_student_answer.find_all(db,
  144. filters=_q,
  145. offset=offset,
  146. limit=size,
  147. order_by=[asc("qid")])
  148. for item in items:
  149. if item.marked_img:
  150. item.qimg = item.marked_img
  151. return {"data": items, "total": total}
  152. # 批阅
  153. async def mark_question(info: UpdateMarkTaskQuestion,
  154. bgtask: BackgroundTasks,
  155. tid: int = Path(..., description="阅卷任务ID"),
  156. qid: int = Path(..., description="试题ID"),
  157. db: AsyncSession = Depends(get_async_db),
  158. current_user: Teacher = Depends(get_current_user)):
  159. # 判断试题是否存在
  160. _q = {"id": qid, "task_id": tid}
  161. db_obj = await crud_student_answer.find_one(db, filters=_q)
  162. if not db_obj:
  163. return {"errcode": 400, "mess": "试题不存在!"}
  164. if info.marked_score > db_obj.score:
  165. return {"errcode": 400, "mess": "批阅得分大于试题满分,请修改!"}
  166. old_score = db_obj.marked_score
  167. has_marked = db_obj.status # 是否已被批阅过
  168. # 错题统计业务逻辑:
  169. # 1、必须是客观题
  170. # 2、如果未批阅,当提交得分不等于试题满分时,错题数量加1;
  171. # 3、如果已批阅,当提交得分等于试题满分时,错题数量减1;
  172. error_count = 0
  173. # 是否错误
  174. info.incorrect = False if db_obj.score == info.marked_score else True
  175. if db_obj.status: # 已批阅
  176. if info.incorrect != db_obj.incorrect:
  177. if db_obj.incorrect:
  178. error_count += -1
  179. else:
  180. error_count += 1
  181. else: # 未批阅
  182. if info.incorrect:
  183. error_count += 1
  184. # 更新班级错题统计数据
  185. bgtask.add_task(bgtask_update_class_error_statistic, db_obj.task_id, db_obj.class_id,
  186. db_obj.qid, error_count, old_score, info.marked_score, has_marked)
  187. # 更新学生错题统计数据
  188. if error_count:
  189. bgtask.add_task(update_student_error_statistic, db_obj.student_id, db_obj.mtype.value,
  190. error_count)
  191. else:
  192. info.incorrect = None
  193. info.status = True
  194. db_obj = await crud_student_answer.update(db, db_obj, info)
  195. # 更新流程:更新StudentAnswer -> 根据StudentAnswer.task_id更新StudentMarkTask -> 根据StudentMarkTask.task_id更新MarkTask
  196. bgtask.add_task(
  197. bgtask_update_student_marktask,
  198. task_id=db_obj.student_task_id,
  199. qtype=db_obj.qtype,
  200. score=db_obj.marked_score - old_score, # 多次批阅时的分数差距
  201. has_marked=has_marked,
  202. editor_id=current_user.id,
  203. editor_name=current_user.username)
  204. return {"data": None}
  205. # 作业中心 - 学生答题统计
  206. async def get_student_tasks(page: int = 1,
  207. size: int = 10,
  208. tid: int = Query(..., description="阅卷任务ID"),
  209. db: AsyncSession = Depends(get_async_db),
  210. current_user: Teacher = Depends(get_current_user)):
  211. # 查询task列表
  212. offset = (page - 1) * size
  213. total, db_tasks = await crud_student_task.find_all(db,
  214. filters={"task_id": tid},
  215. offset=offset,
  216. limit=size)
  217. # 查询每个学生的错题
  218. for item in db_tasks:
  219. _, db_questions = await crud_student_answer.find_all(db,
  220. filters={
  221. "student_task_id": item.id,
  222. "incorrect": True
  223. },
  224. return_fields=["id", "qno", "sqno"])
  225. questions = [f"{x.id},{x.qno},{x.sqno}" for x in db_questions]
  226. item.wrong_questions = questions
  227. return {"data": db_tasks, "total": total}
  228. # 作业中心 - 学生阅卷任务详情
  229. async def get_student_task(tid: int = Path(..., description="学生阅卷任务ID"),
  230. db: AsyncSession = Depends(get_async_db),
  231. current_user: Teacher = Depends(get_current_user)):
  232. return_fields = [
  233. "id", "student_id", "student_name", "student_sno", "score", "objective_score",
  234. "subjective_score", "question_amount"
  235. ]
  236. db_task = await crud_student_task.find_one(db, filters={"id": tid}, return_fields=return_fields)
  237. return {"data": db_task}
  238. # 作业中心 - 学生答题列表
  239. async def get_student_answers(page: int = 1,
  240. size: int = 10,
  241. tid: int = Path(..., description="学生阅卷任务ID"),
  242. db: AsyncSession = Depends(get_async_db),
  243. current_user: Teacher = Depends(get_current_user)):
  244. # 查询学生阅卷任务
  245. student_task = await crud_student_task.find_one(db,
  246. filters={"id": tid},
  247. return_fields=["task_id", "question_amount"])
  248. offset = (page - 1) * size
  249. # 查询学生答题列表
  250. stmt = select(StudentAnswer.qno, StudentAnswer.sqno, StudentAnswer.qimg,
  251. StudentAnswer.marked_img, PaperQuestion.answer, PaperQuestion.analysis)\
  252. .join(PaperQuestion, and_(StudentAnswer.pid == PaperQuestion.pid, StudentAnswer.qno == PaperQuestion.qno))\
  253. .where(StudentAnswer.student_task_id == tid).order_by(StudentAnswer.qid)\
  254. .limit(size).offset(offset)
  255. answers = [{
  256. "qno": x[0],
  257. "sqno": x[1],
  258. "answer": x[4],
  259. "analysis": x[5],
  260. "marked_img": x[2] if x[2] else x[3]
  261. } for x in await crud_student_answer.execute_v2(db, stmt)]
  262. # 统计每道题的答案分布
  263. for item in answers:
  264. stmt = select(StudentAnswer.answer, func.count()).select_from(StudentAnswer).where(
  265. StudentAnswer.task_id == student_task.task_id, StudentAnswer.qno == item["qno"],
  266. StudentAnswer.sqno == item["sqno"]).group_by(StudentAnswer.answer)
  267. item["dist"] = [{
  268. "key": x[0],
  269. "val": int(x[1])
  270. } for x in await crud_student_answer.execute_v2(db, stmt)]
  271. item["dist"] = complete_answer_statistic(item["dist"])
  272. return {"data": answers, "total": student_task.question_amount}
  273. # 作业中心 - 学生阅卷任务列表下载
  274. async def download_student_task(tid: int = Query(..., description="阅卷任务ID"),
  275. db: AsyncSession = Depends(get_async_db),
  276. current_user: Teacher = Depends(get_current_user)):
  277. # 查询task列表
  278. _, db_tasks = await crud_student_task.find_all(db, filters={"task_id": tid})
  279. # 查询每个学生的错题
  280. for item in db_tasks:
  281. _, db_questions = await crud_student_answer.find_all(
  282. db,
  283. filters=[StudentAnswer.student_task_id == item.id, StudentAnswer.incorrect == True],
  284. return_fields=["qno", "sqno"])
  285. item.questions = ",".join([f"第{x.qno}-{x.sqno}题" for x in db_questions])
  286. # 写入excel表
  287. wb = openpyxl.Workbook()
  288. sh = wb.active
  289. title = ["姓名", "学号", "得分", "客观题", "主观题", "名次", "失分题"]
  290. for ridx, ritem in enumerate([title, db_tasks]):
  291. for cidx, citem in enumerate(ritem):
  292. if ridx == 0:
  293. sh.cell(row=ridx + 1, column=cidx + 1, value=citem)
  294. else:
  295. values = [
  296. citem.student_name, citem.student_sno, citem.score, citem.objective_score,
  297. citem.subjective_score, citem.rank, citem.questions
  298. ]
  299. sh.append(values)
  300. outfile = f"{settings.UPLOADER_PATH}/{tid}.xlsx"
  301. wb.save(outfile)
  302. return FileResponse(outfile, filename=f"{tid}.xlsx")
  303. async def task_mark_process(tid: int = Path(..., description="任务ID"),
  304. db: AsyncSession = Depends(get_async_db),
  305. current_user: Teacher = Depends(get_current_user)):
  306. # 查询MarkTask
  307. db_task = await crud_task.find_one(db,
  308. filters={"id": tid},
  309. return_fields=["uploaded_amount", "pid"])
  310. if not db_task:
  311. return {"errcode": 400, "mess": "阅卷任务不存在!"}
  312. # 根据试卷ID查询试卷题目列表
  313. _, db_question = await crud_question.find_all(
  314. db,
  315. filters=[PaperQuestion.qtype.notin_(OBJECTIVE_QUESTION_TYPES),PaperQuestion.pid==db_task.pid,PaperQuestion.usage.in_([1,2])],
  316. return_fields=["qno"],
  317. order_by=[asc("id")])
  318. questions = OrderedDict()
  319. for q in db_question:
  320. questions[q[0]] = {
  321. "qno": q[0],
  322. "question_amount": db_task.uploaded_amount,
  323. "marked_amount": 0
  324. }
  325. # 按题统计已批阅试题数量
  326. stmt = select(StudentAnswer.qno, func.count()).select_from(StudentAnswer)\
  327. .where(StudentAnswer.task_id == tid, StudentAnswer.status == True,
  328. StudentAnswer.qtype.notin_(OBJECTIVE_QUESTION_TYPES)).group_by(StudentAnswer.qno)
  329. for x in await crud_student_answer.execute_v2(db, stmt):
  330. questions[x[0]]["marked_amount"] += x[1]
  331. return {"data": list(questions.values())}
  332. async def mark_process_by_student(tid: int = Path(..., description="任务ID"),
  333. db: AsyncSession = Depends(get_async_db),
  334. current_user: Teacher = Depends(get_current_user)):
  335. process = OrderedDict()
  336. # 从试题列表获取总的试题数量
  337. stmt = select(StudentAnswer.student_id, StudentAnswer.status, func.count())\
  338. .select_from(StudentAnswer)\
  339. .where(StudentAnswer.task_id == tid, StudentAnswer.qtype.notin_(OBJECTIVE_QUESTION_TYPES))\
  340. .group_by(StudentAnswer.student_id, StudentAnswer.status)
  341. db_students = await crud_student_answer.execute_v2(db, stmt)
  342. for item in db_students:
  343. # 主观题总数
  344. if item[0] not in process:
  345. process[item[0]] = {"question_amount": item[2]}
  346. else:
  347. process[item[0]]["question_amount"] += item[2]
  348. # 已批阅数量
  349. if item[1]:
  350. process[item[0]]["marked_amount"] += item[2]
  351. else:
  352. process[item[0]]["marked_amount"] = 0
  353. # 按题统计已批阅试题数量
  354. return {"data": [{"student_id": k, **process[k]} for k in process]}
  355. # 作业中心 - 阅卷任务详情 - 答题分析
  356. async def get_task_answers(page: int = 0,
  357. size: int = 10,
  358. tid: int = Path(..., description="阅卷任务ID"),
  359. db: AsyncSession = Depends(get_async_db),
  360. current_user: Teacher = Depends(get_current_user)):
  361. # 查询阅卷任务
  362. task = await crud_task.find_one(db, filters={"id": tid})
  363. if not task:
  364. return {"errcode": 404, "mess": "阅卷任务不存在!"}
  365. # 查询试题列表
  366. total, questions = await crud_question.find_all(db,
  367. filters={"pid": task.pid},
  368. order_by=[asc("id")],
  369. offset=(page - 1) * size,
  370. limit=size)
  371. # 统计每道题的答案分布
  372. data = []
  373. for item in questions:
  374. temp = {
  375. "qno": item.qno,
  376. "sqno": item.sqno,
  377. "stem": item.stem,
  378. "img": item.full_img,
  379. "answer": item.answer,
  380. "analysis": item.analysis
  381. }
  382. stmt = select(StudentAnswer.answer, func.count()).select_from(StudentAnswer)\
  383. .where(StudentAnswer.task_id == tid, StudentAnswer.qno == item.qno, StudentAnswer.sqno == item.sqno)\
  384. .group_by(StudentAnswer.answer)
  385. temp["dist"] = [{
  386. "key": x[0],
  387. "val": int(x[1])
  388. } for x in await crud_student_answer.execute_v2(db, stmt)]
  389. temp["dist"] = complete_answer_statistic(temp["dist"])
  390. data.append(temp)
  391. return {"data": data, "total": total}