category.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import os
  4. from typing import Any, List, Dict
  5. from fastapi import Depends, Query, Path, UploadFile, File
  6. from openpyxl import load_workbook
  7. from sqlalchemy import text
  8. from sqlalchemy.ext.asyncio import AsyncSession
  9. from admin.api.endpoints.school.utils import check_filetype, check_row
  10. from common.const import PERIODS, SUBJECTS, RESOURCE_TYPES, WORK_RESOURCE_TYPES
  11. from core.config import settings
  12. from crud.base import CrudManager
  13. from crud.marktask import crud_task
  14. from crud.paper import crud_paper
  15. from crud.resource import CATEGORY_CRUDS, RESOURCE_CRUDS
  16. from models.resource import CATEGORY_MODES
  17. from models.user import Admin
  18. from schemas.resource import NewCategory, UpdateCategory
  19. from schemas.resource.category import CategoryInDB
  20. from utils.depends import get_async_db, get_current_user
  21. # 学段列表
  22. async def get_periods(current_user: Admin = Depends(get_current_user)):
  23. data = [{"name": item} for item in PERIODS]
  24. return {"data": data}
  25. # 学科列表
  26. async def get_subjects(current_user: Admin = Depends(get_current_user)):
  27. data = [{"name": item} for item in SUBJECTS]
  28. return {"data": data}
  29. async def get_work_types(current_user: Admin = Depends(get_current_user)):
  30. data = [{"name": item} for item in WORK_RESOURCE_TYPES]
  31. return {"data": data}
  32. # 分类
  33. async def get_category_tree(crud: CrudManager,
  34. db: AsyncSession = Depends(get_async_db),
  35. root: List[Any] = None):
  36. for item in root:
  37. children = await crud.fetch_all(db, filters=[text(f"pid={item.id}")])
  38. item.children = children
  39. await get_category_tree(crud, db, item.children)
  40. async def get_categories(ctype: str = Query(..., description="资源类型,exam / work"),
  41. period: str = Query(None, description="学段名称,eg:小学/初中/高中"),
  42. subject: str = Query(None, description="学科名称,eg: 语文/数学..."),
  43. db: AsyncSession = Depends(get_async_db),
  44. current_user: Admin = Depends(get_current_user)):
  45. if ctype not in RESOURCE_TYPES:
  46. return {"errcode": 400, "mess": "资源类型错误!"}
  47. else:
  48. model = CATEGORY_MODES[ctype]
  49. crud = CATEGORY_CRUDS[ctype]
  50. _q = [model.pid == 0]
  51. if period:
  52. _q.append(model.period == period)
  53. if subject:
  54. _q.append(model.subject == subject)
  55. root = await crud.fetch_all(db, filters=_q)
  56. await get_category_tree(crud, db, root)
  57. return {"data": root}
  58. # 创建资源分类
  59. async def create_category(info: NewCategory,
  60. db: AsyncSession = Depends(get_async_db),
  61. current_user: Admin = Depends(get_current_user)):
  62. # 判断提交参数是否为空
  63. info_dict = info.dict(exclude_none=True)
  64. if not info_dict:
  65. return {"errcode": 400, "mess": "缺少分类信息!"}
  66. # 分类名称
  67. if not info_dict["name"]:
  68. return {"errcode": 400, "mess": "缺少分类名称!"}
  69. # 分类类型,work / exam
  70. if info.ctype not in RESOURCE_TYPES:
  71. return {"errcode": 400, "mess": "分类类型错误!"}
  72. else:
  73. crud = CATEGORY_CRUDS[info.ctype]
  74. # 分类
  75. if info.pid: # 判断上级分类是否存在
  76. category = await crud.find_one(db, filters={"id": info.pid})
  77. if not category:
  78. return {"errcode": 404, "mess": "上级分类不存在!"}
  79. else:
  80. info.pname = category.name
  81. # 判断同级分类是否存在重复
  82. _q = {"name": info.name, "pid": info.pid, "period": info.period, "subject": info.subject}
  83. existed = await crud.count(db, filters=_q)
  84. if existed:
  85. return {"errcode": 400, "mess": "存在同名分类!"}
  86. # 创建
  87. delattr(info, "ctype")
  88. db_obj = await crud.insert_one(db, info)
  89. return {"data": db_obj}
  90. # 更新资源分类
  91. async def update_category(info: UpdateCategory,
  92. cid: int = Path(..., description="分类ID"),
  93. db: AsyncSession = Depends(get_async_db),
  94. current_user: Admin = Depends(get_current_user)):
  95. # 上级分类ID不能与自身相同
  96. if info.pid == cid:
  97. return {"errcode": 400, "mess": "上级分类不能等于自身!"}
  98. if info.ctype not in RESOURCE_TYPES:
  99. return {"errcode": 400, "mess": "分类类型错误!"}
  100. # 判断提交参数是否为空
  101. info_dict = info.dict(exclude_none=True)
  102. if not info_dict:
  103. return {"errcode": 400, "mess": "提交参数为空!"}
  104. # 判断分类是否存在
  105. crud = CATEGORY_CRUDS[info_dict["ctype"]]
  106. model = CATEGORY_MODES[info_dict["ctype"]]
  107. db_obj = await crud.find_one(db, filters={"id": cid})
  108. if not db_obj:
  109. return {"errcode": 404, "mess": "分类不存在!"}
  110. # 如果修改的不是一级分类,则判断上级分类是否存在
  111. if info.pid != db_obj.pid:
  112. if bool(info.pid) ^ bool(info.pname):
  113. return {"errcode": 400, "mess": "缺少上级分类ID或名称"}
  114. if info.pid: # 如果不是修改为一级分类
  115. existed = await crud.count(db, filters={"id": info.pid, "name": info.pname})
  116. if not existed:
  117. return {"errcode": 404, "mess": "上级分类不存在!"}
  118. else:
  119. info_dict.pop("pid", None)
  120. info_dict.pop("pname", None)
  121. # 判断同级分类是否存在重复
  122. if ("name" in info_dict) and (info_dict["name"] != db_obj.name):
  123. existed = await crud.count(
  124. db, filters=[model.id != cid, model.name == info.name, model.pid == info.pid])
  125. if existed:
  126. return {"errcode": 400, "mess": "存在同名分类!"}
  127. else:
  128. info_dict.pop("name", None)
  129. # 判断学段是否变更
  130. if ("period" in info_dict) and (info.period == db_obj.period):
  131. info_dict.pop("period", None)
  132. # 判断学科是否变更
  133. if ("subject" in info_dict) and (info.subject == db_obj.subject):
  134. info_dict.pop("subject", None)
  135. # 更新
  136. if info_dict:
  137. info_dict.pop("ctype", None)
  138. db_obj = await crud.update(db, db_obj, info)
  139. return {"data": db_obj}
  140. # 删除资源分类
  141. async def delete_category(cid: int = Path(..., description="资源分类ID"),
  142. ctype: str = Query(..., description="分类类型,exam / work"),
  143. db: AsyncSession = Depends(get_async_db),
  144. current_user: Admin = Depends(get_current_user)):
  145. if ctype not in RESOURCE_TYPES:
  146. return {"errcode": 400, "mess": "分类类型错误!"}
  147. else:
  148. crud = CATEGORY_CRUDS[ctype]
  149. existed = await crud.count(db, filters={"id": cid})
  150. if not existed:
  151. return {"errcode": 404, "mess": "分类不存在!"}
  152. # 判断是否存在关联资源
  153. _q = {"category_id": cid}
  154. total = await RESOURCE_CRUDS[ctype].count(db, filters=_q)
  155. if not total:
  156. total = await crud_paper.count(db, filters=_q) # 判断是否存在关联试卷
  157. if not total:
  158. total = await crud_task.count(db, filters=_q) # 判断是否存在关联阅卷任务
  159. # 执行删除
  160. if total:
  161. return {"errcode": 400, "mess": "不能删除有资源关联的分类"}
  162. else:
  163. await crud.delete(db, obj_id=cid)
  164. return {"data": None}
  165. # 批量导入分类
  166. async def import_category(ctype: str = Path(..., description="资源类型,exam / work"),
  167. datafile: UploadFile = File(..., description="数据文件"),
  168. db: AsyncSession = Depends(get_async_db),
  169. current_user: Admin = Depends(get_current_user)):
  170. # 判断文件格式
  171. if not check_filetype(datafile.filename, ".xlsx"):
  172. return {"errcode": 400, "mess": "文件格式错误!"}
  173. # 判断分类类型
  174. if ctype not in RESOURCE_TYPES:
  175. return {"errcode": 400, "mess": "资源类型错误!"}
  176. else:
  177. crud = CATEGORY_CRUDS[ctype]
  178. # 把文件写入磁盘,再加载回来
  179. disk_file = os.path.join(settings.UPLOADER_PATH, datafile.filename)
  180. content = await datafile.read()
  181. with open(disk_file, "wb") as f:
  182. f.write(content)
  183. # 返回结果
  184. errors = []
  185. success = 0
  186. counter = 2
  187. category_maps = {}
  188. row_length = 6 if ctype == "work" else 5
  189. # 使用openpyxl读取文件,生成分类字典
  190. wb = load_workbook(disk_file)
  191. ws = wb.worksheets[0]
  192. for row in ws.iter_rows(min_row=2, max_col=ws.max_column, max_row=ws.max_row, values_only=True):
  193. row = await check_row(row, row_length)
  194. if row is None: # 空行
  195. continue
  196. elif not row: # 字段不完整
  197. errors.append(f"第{counter}行: 某些单元格为空!")
  198. continue
  199. # 判断学段是否正确
  200. if row[0] not in PERIODS:
  201. errors.append(f"第{counter}行: 分类学段错误!")
  202. continue
  203. # 判断学科是否正确
  204. if row[1] not in SUBJECTS:
  205. errors.append(f"第{counter}行: 分类科目错误!")
  206. continue
  207. # 判断分类名称是否为空
  208. if not row[row_length - 1]:
  209. errors.append(f"第{counter}行: 分类名称错误!")
  210. continue
  211. # 解析模版,获取所有的分类
  212. if row[0] not in category_maps:
  213. category_maps[row[0]] = {}
  214. if row[1] not in category_maps[row[0]]:
  215. category_maps[row[0]][row[1]] = {}
  216. if row[2] not in category_maps[row[0]][row[1]]:
  217. category_maps[row[0]][row[1]][row[2]] = {}
  218. if row[3] not in category_maps[row[0]][row[1]][row[2]]:
  219. category_maps[row[0]][row[1]][row[2]][row[3]] = {}
  220. if row[4] not in category_maps[row[0]][row[1]][row[2]][row[3]]:
  221. category_maps[row[0]][row[1]][row[2]][row[3]][row[4]] = {}
  222. if ctype == "work":
  223. if row[5] not in category_maps[row[0]][row[1]][row[2]][row[3]][row[4]]:
  224. category_maps[row[0]][row[1]][row[2]][row[3]][row[4]][row[5]] = counter
  225. counter += 1
  226. # 解析分类字典,进行分类创建
  227. for period in category_maps: # 学段
  228. for sub in category_maps[period]: # 学科
  229. for c1 in category_maps[period][sub]: # 一级分类
  230. _q = {"period": period, "subject": sub, "name": c1, "pid": 0}
  231. fields = {"name": c1, "pid": 0, "pname": "", "period": period, "subject": sub}
  232. db_c1 = await get_or_create(crud, db, _q, fields)
  233. for c2 in category_maps[period][sub][c1]: # 二级分类
  234. _q.update({"name": c2, "pid": db_c1.id})
  235. fields.update({"name": c2, "pid": db_c1.id, "pname": c1})
  236. db_c2 = await get_or_create(crud, db, _q, fields)
  237. for c3 in category_maps[period][sub][c1][c2]: # 三级分类
  238. _q.update({"name": c3, "pid": db_c2.id})
  239. fields.update({"name": c3, "pid": db_c2.id, "pname": c2})
  240. db_c3 = await get_or_create(crud, db, _q, fields)
  241. if ctype == "work":
  242. for c4 in category_maps[period][sub][c1][c2][c3]: # 四级分类
  243. if not c4:
  244. continue
  245. _q.update({"name": c4, "pid": db_c3.id})
  246. fields.update({"name": c4, "pid": db_c3.id, "pname": c3})
  247. await get_or_create(crud, db, _q, fields)
  248. success += 1
  249. else:
  250. success += 1
  251. # 删除上传文件
  252. os.remove(disk_file)
  253. return {"data": {"success": success, "fail": len(errors), "errors": errors}}
  254. async def get_or_create(crud: CrudManager, db: AsyncSession, filters: Dict[str, Any],
  255. fields: Dict[str, Any]):
  256. db_obj = await crud.find_one(db, filters=filters, return_fields=["id"])
  257. if not db_obj:
  258. obj_in = CategoryInDB(**fields)
  259. db_obj = await crud.insert_one(db, obj_in)
  260. return db_obj