task.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  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 text
  7. from sqlalchemy.ext.asyncio import AsyncSession
  8. from starlette.background import BackgroundTasks
  9. from starlette.responses import FileResponse
  10. from bgtask.tasks import update_mark_task
  11. from core.config import settings
  12. from crud.mark import crud_mark, crud_answer, crud_studentmark
  13. from crud.paper import crud_question
  14. from models.mark import StudentAnswer
  15. from models.paper import WorkPaperQuestion
  16. from models.user import Teacher
  17. from schemas.app.task import UpdateMarkTaskQuestion
  18. from schemas.base import OrderByField
  19. from utils.depends import get_async_db, get_current_user
  20. # 阅卷任务列表
  21. async def get_mark_tasks(
  22. page: int = 1,
  23. size: int = 10,
  24. cid: int = Query(0, description="班级ID"),
  25. mtype: str = Query(..., description="阅卷任务类型,work/exam"),
  26. ctgid: int = Query(0, description="分类ID"),
  27. year: int = Query(0, description="年份"),
  28. status: int = Query(None, description="状态"),
  29. name: str = Query(None, description="任务名称"),
  30. order: OrderByField = Query("-created_at",
  31. description="排序字段,用逗号分隔,升降序以-判断,默认-created_at"),
  32. db: AsyncSession = Depends(get_async_db),
  33. current_user: Teacher = Depends(get_current_user)):
  34. filters = [text(f"mtype = '{mtype}'")]
  35. if ctgid:
  36. filters.append(text(f"category_id = {ctgid}"))
  37. if year:
  38. filters.append(text(f"year = {year}"))
  39. if cid:
  40. filters.append(text(f"class_id = {cid}"))
  41. if name:
  42. filters.append(text(f"name LIKE '%{name}%'"))
  43. if status:
  44. filters.append(text(f"status = {status}"))
  45. offset = (page - 1) * size
  46. if isinstance(order, str):
  47. order = [text(order)]
  48. total, marks = await crud_mark.find_all(db,
  49. filters=filters,
  50. limit=size,
  51. offset=offset,
  52. order_by=order)
  53. for item in marks:
  54. if item.marked_amount:
  55. item.pass_rate = round(item.pass_amount / item.marked_amount, 2)
  56. return {"data": marks, "total": total}
  57. # 阅卷任务详情
  58. async def get_mark_task(tid: int = Path(..., description="批阅任务ID"),
  59. db: AsyncSession = Depends(get_async_db),
  60. current_user: Teacher = Depends(get_current_user)):
  61. # 判断task是否存在
  62. db_obj = await crud_mark.find_one(db, filters={"id": tid})
  63. if not db_obj:
  64. return {"errcode": 400, "mess": "阅卷任务不存在!"}
  65. # 按成绩分段统计
  66. stmt = f"""SELECT
  67. sum( CASE WHEN score < 50 THEN 1 ELSE 0 END ) AS '0-50',
  68. sum( CASE WHEN score >= 50 AND score < 60 THEN 1 ELSE 0 END ) AS '50-60',
  69. sum( CASE WHEN score >= 60 AND score < 70 THEN 1 ELSE 0 END ) AS '60-70',
  70. sum( CASE WHEN score >= 70 AND score < 80 THEN 1 ELSE 0 END ) AS '70-80',
  71. sum( CASE WHEN score >= 80 AND score < 90 THEN 1 ELSE 0 END ) AS '80-90',
  72. sum( CASE WHEN score >= 90 THEN 1 ELSE 0 END ) AS '90-100'
  73. FROM
  74. student_marktask
  75. WHERE
  76. task_id = {tid};
  77. """
  78. cursor = await db.execute(stmt)
  79. scores = [x if x else 0 for x in cursor.fetchall()[0]]
  80. score_table = [{
  81. "key": "0-50",
  82. "val": scores[0]
  83. }, {
  84. "key": "50-70",
  85. "val": scores[1] + scores[2]
  86. }, {
  87. "key": "70-100",
  88. "val": sum(scores[3:])
  89. }]
  90. score_gaps = ["0-50", "50-60", "60-70", "70-80", "80-90", "90-100"]
  91. score_chart = [{"key": x[0], "val": x[1]} for x in zip(score_gaps, scores)]
  92. return {
  93. "data": db_obj,
  94. "score_table": score_table,
  95. "score_chart": score_chart
  96. }
  97. # 阅卷任务试题列表
  98. async def get_task_questions(page: int = 1,
  99. size: int = 10,
  100. tid: int = Path(..., description="阅卷任务ID"),
  101. stid: int = Query(None, description="学生ID"),
  102. qno: int = Query(None, description="题号"),
  103. db: AsyncSession = Depends(get_async_db),
  104. current_user: Teacher = Depends(get_current_user)):
  105. # 按学生批阅,数据流:查询StudentMarkTask获取ID,使用ID去查询改学生的所有试题
  106. if stid:
  107. student_task = await crud_studentmark.find_one(db,
  108. filters={
  109. "task_id": tid,
  110. "student_id": stid
  111. },
  112. return_fields=["id"])
  113. filters = {"student_task_id": student_task.id}
  114. elif qno: # 按试题批阅,数据流:通过 task_id 和 qno 查询试题
  115. filters = {"task_id": tid, "qno": qno}
  116. else:
  117. filters = {"task_id": tid}
  118. # 查询试题列表
  119. offset = (page - 1) * size
  120. total, items = await crud_answer.find_all(db,
  121. filters=filters,
  122. offset=offset,
  123. limit=size)
  124. for item in items:
  125. if item.marked_img:
  126. item.qimg = item.marked_img
  127. return {"data": items, "total": total}
  128. # 批阅
  129. async def mark_question(info: UpdateMarkTaskQuestion,
  130. bg_task: BackgroundTasks,
  131. qid: int = Path(..., description="试题ID"),
  132. db: AsyncSession = Depends(get_async_db),
  133. current_user: Teacher = Depends(get_current_user)):
  134. # 判断试题是否存在
  135. filters = {"id": qid}
  136. db_obj = await crud_answer.find_one(db, filters=filters)
  137. if not db_obj:
  138. return {"errcode": 400, "mess": "试题不存在!"}
  139. # 更新流程:更新StudentAnswer -> 根据StudentAnswer.task_id更新StudentMarkTask -> 根据StudentMarkTask.task_id更新MarkTask
  140. db_obj = await crud_answer.update(db, db_obj, info)
  141. bg_task.add_task(update_mark_task,
  142. db,
  143. bg_task,
  144. task_id=db_obj.student_task_id,
  145. qtype=db_obj.qtype,
  146. score=db_obj.marked_score,
  147. editor_id=current_user.id,
  148. editor_name=current_user.username)
  149. return {"data": None}
  150. # 作业中心 - 学生答题统计
  151. async def get_student_tasks(page: int = 1,
  152. size: int = 10,
  153. tid: int = Query(..., description="阅卷任务ID"),
  154. db: AsyncSession = Depends(get_async_db),
  155. current_user: Teacher = Depends(get_current_user)):
  156. # 查询task列表
  157. offset = (page - 1) * size
  158. total, db_tasks = await crud_studentmark.find_all(db,
  159. filters={"task_id": tid},
  160. offset=offset,
  161. limit=size)
  162. # 查询每个学生的错题
  163. for item in db_tasks:
  164. _, db_questions = await crud_answer.find_all(
  165. db,
  166. filters=[
  167. text(f"student_task_id = {item.id} AND marked_score != score")
  168. ],
  169. return_fields=["id", "qno", "sqno"])
  170. questions = [f"{x.id},{x.qno},{x.sqno}" for x in db_questions]
  171. item.wrong_questions = questions
  172. return {"data": db_tasks, "total": total}
  173. # 作业中心 - 学生阅卷任务详情
  174. async def get_student_task(tid: int = Path(..., description="学生阅卷任务ID"),
  175. db: AsyncSession = Depends(get_async_db),
  176. current_user: Teacher = Depends(get_current_user)):
  177. db_task = await crud_studentmark.find_one(db,
  178. filters={"id": tid},
  179. return_fields=[
  180. "id", "student_id",
  181. "student_name", "student_sno",
  182. "score", "objective_score",
  183. "subjective_score",
  184. "question_amount"
  185. ])
  186. return {"data": db_task}
  187. # 作业中心 - 学生答题列表
  188. async def get_student_answers(
  189. page: int = 1,
  190. size: int = 10,
  191. tid: int = Path(..., description="学生阅卷任务ID"),
  192. db: AsyncSession = Depends(get_async_db),
  193. current_user: Teacher = Depends(get_current_user)):
  194. # 查询学生阅卷任务
  195. student_task = await crud_studentmark.find_one(
  196. db, filters={"id": tid}, return_fields=["task_id", "question_amount"])
  197. offset = (page - 1) * size
  198. # 查询学生答题列表
  199. stmt = f"""SELECT
  200. sa.qno,
  201. sa.sqno,
  202. wsq.answer,
  203. wsq.analysis,
  204. sa.marked_img
  205. FROM
  206. {StudentAnswer.__tablename__} AS sa
  207. INNER JOIN {WorkPaperQuestion.__tablename__} AS wsq ON sa.pid = wsq.pid AND sa.qno = wsq.qno
  208. WHERE
  209. sa.student_task_id = {tid}
  210. ORDER BY
  211. sa.qno, sa.sqno
  212. LIMIT {offset},{size};"""
  213. answers = [{
  214. "qno": x[0],
  215. "sqno": x[1],
  216. "answer": x[2],
  217. "analysis": x[3],
  218. "marked_img": x[4]
  219. } for x in await db.execute(stmt)]
  220. # 统计每道题的答案分布
  221. for item in answers:
  222. stmt = f"""SELECT
  223. answer,
  224. count(id) AS amount
  225. FROM
  226. {StudentAnswer.__tablename__}
  227. WHERE
  228. task_id={student_task.task_id}
  229. AND qno={item["qno"]}
  230. AND sqno={item["sqno"]}
  231. GROUP BY
  232. qno, sqno, answer;"""
  233. item["dist"] = [{
  234. "key": x[0],
  235. "val": int(x[1])
  236. } for x in await db.execute(stmt)]
  237. return {"data": answers, "total": student_task.question_amount}
  238. # 作业中心 - 学生阅卷任务列表下载
  239. async def download_student_task(
  240. tid: int = Query(..., description="阅卷任务ID"),
  241. db: AsyncSession = Depends(get_async_db),
  242. current_user: Teacher = Depends(get_current_user)):
  243. # 查询task列表
  244. _, db_tasks = await crud_studentmark.find_all(db, filters={"task_id": tid})
  245. # 查询每个学生的错题
  246. for item in db_tasks:
  247. _, db_questions = await crud_answer.find_all(
  248. db,
  249. filters=[
  250. text(f"student_task_id = {item.id} AND marked_score != score")
  251. ],
  252. return_fields=["qno", "sqno"])
  253. item.questions = ",".join([f"第{x.qno}-{x.sqno}题" for x in db_questions])
  254. # 写入excel表
  255. wb = openpyxl.Workbook()
  256. sh = wb.active
  257. title = ["姓名", "学号", "得分", "客观题", "主观题", "名次", "失分题"]
  258. for ridx, ritem in enumerate([title, db_tasks]):
  259. for cidx, citem in enumerate(ritem):
  260. if ridx == 0:
  261. sh.cell(row=ridx + 1, column=cidx + 1, value=citem)
  262. else:
  263. values = [
  264. citem.student_name, citem.student_sno, citem.score,
  265. citem.objective_score, citem.subjective_score, citem.rank,
  266. citem.questions
  267. ]
  268. sh.append(values)
  269. outfile = f"{settings.UPLOADER_PATH}/{tid}.xlsx"
  270. wb.save(outfile)
  271. return FileResponse(outfile, filename=f"{tid}.xlsx")
  272. async def task_mark_process(tid: int = Path(..., description="任务ID"),
  273. db: AsyncSession = Depends(get_async_db),
  274. current_user: Teacher = Depends(get_current_user)):
  275. # 查询MarkTask
  276. db_task = await crud_mark.find_one(db,
  277. filters={"id": tid},
  278. return_fields=["uploaded_amount", "pid"])
  279. if not db_task:
  280. return {"errcode": 400, "mess": "阅卷任务不存在!"}
  281. # 根据试卷ID查询试卷题目列表
  282. _, db_question = await crud_question.find_all(
  283. db, filters={"pid": db_task["pid"]}, return_fields=["qno"])
  284. questions = OrderedDict()
  285. for q in db_question:
  286. questions[q[0]] = {
  287. "qno": q[0],
  288. "question_amount": db_task.uploaded_amount,
  289. "marked_amount": 0
  290. }
  291. # 按题统计已批阅试题数量
  292. stmt = f"""SELECT
  293. qno,
  294. count( id ) AS marked_amount
  295. FROM
  296. student_answer
  297. WHERE
  298. task_id = {tid}
  299. AND marked_img != ""
  300. GROUP BY
  301. qno;"""
  302. for x in await db.execute(stmt):
  303. questions[x[0]]["marked_amount"] += x[1]
  304. return {"data": list(questions.values())}
  305. # 作业中心 - 阅卷任务详情 - 答题分析
  306. async def get_task_answers(tid: int = Path(..., description="阅卷任务ID"),
  307. db: AsyncSession = Depends(get_async_db),
  308. current_user: Teacher = Depends(get_current_user)):
  309. # 查询阅卷任务
  310. task = await crud_mark.find_one(db, filters={"id": tid})
  311. if not task:
  312. return {"errcode": 404, "mess": "阅卷任务不存在!"}
  313. # 查询试题列表
  314. total, questions = await crud_question.find_all(db,
  315. filters={"pid": task.pid})
  316. # 统计每道题的答案分布
  317. data = []
  318. for item in questions:
  319. temp = {
  320. "qno": item.qno,
  321. "sqno": item.sqno,
  322. "img": item.imgs,
  323. "answer": item.answer,
  324. "analysis": item.analysis
  325. }
  326. stmt = f"""SELECT
  327. answer,
  328. count(id) AS amount
  329. FROM
  330. {StudentAnswer.__tablename__}
  331. WHERE
  332. task_id={tid}
  333. AND qno={item.qno}
  334. AND sqno={item.sqno}
  335. GROUP BY
  336. qno, sqno, answer;"""
  337. temp["dist"] = [{
  338. "key": x[0],
  339. "val": int(x[1])
  340. } for x in await db.execute(stmt)]
  341. data.append(temp)
  342. return {"data": data, "total": total}