Распределение вариантов
This commit is contained in:
322
src/frontend/lms_web/routers/teacher_pages.py
Normal file
322
src/frontend/lms_web/routers/teacher_pages.py
Normal file
@@ -0,0 +1,322 @@
|
||||
import json as _json
|
||||
from typing import Annotated
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Depends, Form, Query, Request, UploadFile, File
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from .. import ctx
|
||||
from ..client import ApiDown, ApiError
|
||||
|
||||
router = APIRouter(prefix="/t")
|
||||
|
||||
|
||||
def _err_or_die(request, e, tpl=None, user=None, **ctx):
|
||||
if isinstance(e, ApiDown):
|
||||
return ctx.down_page(request, user or {})
|
||||
return None
|
||||
|
||||
|
||||
def _page_or_error(request, tpl, user, e):
|
||||
if isinstance(e, ApiDown):
|
||||
return ctx.render_page(request, tpl, user=user,
|
||||
error="Бэкенд lms-api недоступен: запустите make dev")
|
||||
return ctx.render_page(request, tpl, user=user, error=e.detail)
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def dashboard(request: Request, user=Depends(ctx.need_teacher)):
|
||||
return RedirectResponse("/t/labs", status_code=303)
|
||||
|
||||
|
||||
# ------------------------------------------------------------ лабораторные
|
||||
|
||||
@router.get("/labs")
|
||||
def labs_list(request: Request, user=Depends(ctx.need_teacher)):
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
data = c.get("/labs")
|
||||
except ApiDown:
|
||||
return ctx.down_page(request, user)
|
||||
except ApiError as e:
|
||||
return _page_or_error(request, "t_labs.html", user, e)
|
||||
finally:
|
||||
c.close()
|
||||
return ctx.render_page(request, "t_labs.html", user=user, labs=data["labs"])
|
||||
|
||||
|
||||
@router.get("/labs/new")
|
||||
def lab_new_form(request: Request, user=Depends(ctx.need_teacher)):
|
||||
return ctx.render_page(request, "t_lab_form.html", user=user, parsed=None)
|
||||
|
||||
|
||||
@router.post("/labs/new/import")
|
||||
def lab_new_import(request: Request, user=Depends(ctx.need_teacher),
|
||||
file: Annotated[UploadFile | None, File()] = None):
|
||||
if not file or not file.filename:
|
||||
return RedirectResponse("/t/labs/new?err=Выберите%20JSON-файл%20условий", status_code=303)
|
||||
raw_text = file.file.read().decode("utf-8", errors="replace")
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
try:
|
||||
data = c.upload("/labs/parse-json", file.filename, raw_text.encode("utf-8"), "application/json")
|
||||
except ApiDown:
|
||||
return ctx.down_page(request, user)
|
||||
except ApiError as e:
|
||||
return ctx.render_page(request, "t_lab_form.html", user=user, parsed=None, error=e.detail)
|
||||
finally:
|
||||
c.close()
|
||||
parsed = {"raw": raw_text, "lab": data.get("lab", {}), "variants": data.get("variants", [])}
|
||||
return ctx.render_page(request, "t_lab_form.html", user=user, parsed=parsed)
|
||||
|
||||
|
||||
@router.post("/labs/new")
|
||||
def lab_create(request: Request, user=Depends(ctx.need_teacher),
|
||||
variants_json: Annotated[str, Form()] = ""):
|
||||
if not variants_json.strip():
|
||||
return ctx.render_page(request, "t_lab_form.html", user=user, parsed=None,
|
||||
error="Загрузите JSON-файл условий лабораторной")
|
||||
obj = _json.loads(variants_json)
|
||||
payload = {}
|
||||
for key in ("number", "title", "semester", "deadline", "points_max"):
|
||||
if key in obj and obj[key] is not None:
|
||||
payload[key] = obj[key]
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
r = c.post_json("/labs", payload)
|
||||
except ApiDown:
|
||||
return ctx.down_page(request, user)
|
||||
except ApiError as e:
|
||||
return ctx.render_page(request, "t_lab_form.html", user=user, parsed=None, error=e.detail)
|
||||
finally:
|
||||
c.close()
|
||||
msg = f"Лабораторная создана, вариантов: {len(obj.get('variants') or [])}"
|
||||
c2 = ctx.api_client(request)
|
||||
try:
|
||||
up = c2.upload(f"/labs/{r['id']}/content", "content.json",
|
||||
variants_json.encode("utf-8"), "application/json")
|
||||
msg = f"Лабораторная создана, вариантов: {up.get('count', 0)}"
|
||||
except (ApiDown, ApiError) as e:
|
||||
msg += " (варианты не загрузились: " + getattr(e, "detail", "") + ")"
|
||||
finally:
|
||||
c2.close()
|
||||
return RedirectResponse(f"/t/labs/{r['id']}/?msg=" + quote(msg), status_code=303)
|
||||
|
||||
|
||||
@router.get("/labs/{lid}")
|
||||
def lab_page(request: Request, lid: int, user=Depends(ctx.need_teacher)):
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
data = c.get(f"/labs/{lid}")
|
||||
except ApiDown:
|
||||
return ctx.down_page(request, user)
|
||||
except ApiError as e:
|
||||
return _page_or_error(request, "t_lab.html", user, e)
|
||||
finally:
|
||||
c.close()
|
||||
return ctx.render_page(request, "t_lab.html", user=user, lab=data["lab"],
|
||||
variants_main=data["variants_main"],
|
||||
variants_advanced=data["variants_advanced"],
|
||||
levels=data["levels"])
|
||||
|
||||
|
||||
@router.post("/labs/{lid}/json")
|
||||
def lab_json_upload(request: Request, lid: int, user=Depends(ctx.need_teacher),
|
||||
file: Annotated[UploadFile | None, File()] = None):
|
||||
if not file or not file.filename:
|
||||
return RedirectResponse(f"/t/labs/{lid}?err=Выберите%20JSON-файл", status_code=303)
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
r = c.upload(f"/labs/{lid}/json", file.filename, file.file.read(), "application/json")
|
||||
except ApiDown:
|
||||
return ctx.down_page(request, user)
|
||||
except ApiError as e:
|
||||
return RedirectResponse(f"/t/labs/{lid}/?err=" + quote(e.detail), status_code=303)
|
||||
finally:
|
||||
c.close()
|
||||
return RedirectResponse(f"/t/labs/{lid}/?msg=" + quote(
|
||||
f"Вариантов загружено: {r.get('count', 0)} (осн. {r.get('main', 0)}, повыш. {r.get('advanced', 0)})"),
|
||||
status_code=303)
|
||||
|
||||
|
||||
@router.post("/labs/{lid}/delete")
|
||||
def lab_delete(request: Request, lid: int, user=Depends(ctx.need_teacher)):
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
c.delete(f"/labs/{lid}")
|
||||
except (ApiDown, ApiError):
|
||||
pass
|
||||
finally:
|
||||
c.close()
|
||||
return RedirectResponse("/t/labs/?msg=" + quote("Лабораторная удалена"), status_code=303)
|
||||
|
||||
|
||||
# ------------------------------------------------------------ группы
|
||||
|
||||
@router.get("/groups")
|
||||
def groups_list(request: Request, user=Depends(ctx.need_teacher)):
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
data = c.get("/groups")
|
||||
except ApiDown:
|
||||
return ctx.down_page(request, user)
|
||||
except ApiError as e:
|
||||
return _page_or_error(request, "t_groups.html", user, e)
|
||||
finally:
|
||||
c.close()
|
||||
return ctx.render_page(request, "t_groups.html", user=user, groups=data["groups"])
|
||||
|
||||
|
||||
@router.post("/groups")
|
||||
def create_group(request: Request, name: Annotated[str, Form()], user=Depends(ctx.need_teacher)):
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
r = c.post_json("/groups", {"name": name})
|
||||
except ApiDown:
|
||||
return ctx.down_page(request, user)
|
||||
except ApiError as e:
|
||||
return RedirectResponse("/t/groups?err=" + quote(e.detail), status_code=303)
|
||||
finally:
|
||||
c.close()
|
||||
return RedirectResponse(f"/t/groups/{r['id']}/?msg=" + quote(f"Группа {name} создана"), status_code=303)
|
||||
|
||||
|
||||
@router.post("/groups/import")
|
||||
def import_group(request: Request, user=Depends(ctx.need_teacher),
|
||||
file: Annotated[UploadFile | None, File()] = None):
|
||||
if not file or not file.filename:
|
||||
return RedirectResponse("/t/groups?err=Выберите%20файл%20со%20списком%20группы", status_code=303)
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
r = c.upload("/groups/import", file.filename, file.file.read())
|
||||
except ApiDown:
|
||||
return ctx.down_page(request, user)
|
||||
except ApiError as e:
|
||||
return RedirectResponse("/t/groups/?err=" + quote(e.detail), status_code=303)
|
||||
finally:
|
||||
c.close()
|
||||
msg = f"Группа {r['group']}: добавлено {r['added']}, обновлено {r['updated']}. Пароли — в выгрузке «Логины и пароли»"
|
||||
return RedirectResponse(f"/t/groups/{r['gid']}/?msg=" + quote(msg), status_code=303)
|
||||
|
||||
|
||||
@router.get("/groups/{gid}")
|
||||
def group_page(request: Request, gid: int, lab: Annotated[str, Query()] = "0",
|
||||
user=Depends(ctx.need_teacher)):
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
data = c.get(f"/groups/{gid}")
|
||||
except ApiDown:
|
||||
return ctx.down_page(request, user)
|
||||
except ApiError as e:
|
||||
return _page_or_error(request, "t_group.html", user, e)
|
||||
finally:
|
||||
c.close()
|
||||
labs_list = data.get("labs", [])
|
||||
try:
|
||||
lab_id = int(lab)
|
||||
except ValueError:
|
||||
lab_id = 0
|
||||
if lab_id and not any(l["id"] == lab_id for l in labs_list):
|
||||
lab_id = 0
|
||||
unassigned = None
|
||||
if lab_id:
|
||||
unassigned = sum(1 for s in data["students"]
|
||||
if not (s.get("lab_numbers") or {}).get(lab_id, {}).get("number"))
|
||||
return ctx.render_page(request, "t_group.html", user=user, group=data["group"],
|
||||
students=data["students"], labs=labs_list, lab_id=lab_id,
|
||||
unassigned=unassigned,
|
||||
levels={"main": "Основная группа", "advanced": "Повышенная сложность"})
|
||||
|
||||
|
||||
@router.post("/groups/{gid}/levels")
|
||||
def set_level(request: Request, gid: int, student_id: Annotated[str, Form()],
|
||||
level: Annotated[str, Form()], lab_id: Annotated[str, Form()] = "0",
|
||||
user=Depends(ctx.need_teacher)):
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
c.post_json(f"/groups/{gid}/levels", {"student_id": student_id, "level": level})
|
||||
except ApiDown:
|
||||
return ctx.down_page(request, user)
|
||||
except ApiError as e:
|
||||
return RedirectResponse(f"/t/groups/{gid}/?err=" + quote(e.detail), status_code=303)
|
||||
finally:
|
||||
c.close()
|
||||
q = f"?lab={lab_id}&" if lab_id and lab_id != "0" else "?"
|
||||
return RedirectResponse(f"/t/groups/{gid}/{q}msg=" + quote("Уровень обновлён"), status_code=303)
|
||||
|
||||
|
||||
@router.post("/groups/{gid}/assign")
|
||||
def assign(request: Request, gid: int, lab_id: Annotated[str, Form()] = "0",
|
||||
user=Depends(ctx.need_teacher)):
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
r = c.post_json(f"/groups/{gid}/assign", {"lab_id": int(lab_id or 0)})
|
||||
except ApiDown:
|
||||
return ctx.down_page(request, user)
|
||||
except ApiError as e:
|
||||
return RedirectResponse(f"/t/groups/{gid}/?err=" + quote(e.detail), status_code=303)
|
||||
finally:
|
||||
c.close()
|
||||
return RedirectResponse(f"/t/groups/{gid}/?lab={lab_id or 0}&msg=" + quote(
|
||||
f"Варианты распределены: {r.get('assigned', 0)}"), status_code=303)
|
||||
|
||||
|
||||
@router.post("/groups/{gid}/manual")
|
||||
def set_manual(request: Request, gid: int, student_id: Annotated[str, Form()],
|
||||
lab_id: Annotated[str, Form()] = "0", number: Annotated[str, Form()] = "",
|
||||
user=Depends(ctx.need_teacher)):
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
r = c.post_json(f"/groups/{gid}/manual",
|
||||
{"student_id": student_id, "lab_id": int(lab_id or 0), "number": number})
|
||||
except ApiDown:
|
||||
return ctx.down_page(request, user)
|
||||
except ApiError as e:
|
||||
return RedirectResponse(f"/t/groups/{gid}/?lab={lab_id or 0}&err=" + quote(e.detail), status_code=303)
|
||||
finally:
|
||||
c.close()
|
||||
msg = "Ручное назначение снято" if r.get("cleared") else (
|
||||
f"Вариант назначен вручную: {r.get('number') or number}")
|
||||
q = f"?lab={lab_id}&" if lab_id and lab_id != "0" else "?"
|
||||
return RedirectResponse(f"/t/groups/{gid}/{q}msg=" + quote(msg), status_code=303)
|
||||
|
||||
|
||||
@router.post("/students/{sid}/reset-password")
|
||||
def reset_password(request: Request, sid: str, user=Depends(ctx.need_teacher)):
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
r = c.post_json(f"/students/{sid}/reset-password", {})
|
||||
except ApiDown:
|
||||
return ctx.down_page(request, user)
|
||||
except ApiError as e:
|
||||
return RedirectResponse("/t/groups?err=" + quote(e.detail), status_code=303)
|
||||
finally:
|
||||
c.close()
|
||||
msg = f"Новый пароль для студента №{sid}: {r['password']}. Студент сменит его при входе."
|
||||
return RedirectResponse("/t/groups/?msg=" + quote(msg), status_code=303)
|
||||
|
||||
|
||||
@router.get("/groups/{gid}/credentials.xlsx")
|
||||
def credentials_xlsx(request: Request, gid: int, user=Depends(ctx.need_teacher)):
|
||||
return _credentials(request, gid, user, "xlsx")
|
||||
|
||||
|
||||
@router.get("/groups/{gid}/credentials.csv")
|
||||
def credentials_csv(request: Request, gid: int, user=Depends(ctx.need_teacher)):
|
||||
return _credentials(request, gid, user, "csv")
|
||||
|
||||
|
||||
def _credentials(request, gid, user, fmt):
|
||||
from fastapi.responses import Response
|
||||
|
||||
c = ctx.api_client(request)
|
||||
try:
|
||||
data, name = c.download(f"/groups/{gid}/credentials", {"fmt": fmt})
|
||||
except (ApiDown, ApiError) as e:
|
||||
return _page_or_error(request, "t_group.html", user, e)
|
||||
finally:
|
||||
c.close()
|
||||
media = "text/csv; charset=utf-8" if fmt == "csv" else (
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
return Response(content=data, media_type=media,
|
||||
headers={"content-disposition": f"attachment; filename*=UTF-8''{quote(name or 'file')}"})
|
||||
Reference in New Issue
Block a user