Finish Schoolhouse programming submission flow
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Form, HTTPException, UploadFile
|
||||
from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from app.config import settings
|
||||
from app.services.bundles import SubmissionBundleService
|
||||
from app.services.d2l import D2LService
|
||||
from app.services.paperless import PaperlessService
|
||||
from app.services.repository import get_repository
|
||||
@@ -17,6 +20,74 @@ repo = get_repository()
|
||||
d2l = D2LService()
|
||||
paperless = PaperlessService()
|
||||
webhooks = WebhookService()
|
||||
bundles = SubmissionBundleService()
|
||||
|
||||
|
||||
def _confirm_submission_to_paperless(current: dict) -> dict:
|
||||
handoff = paperless.assignment_intake_payload(current)
|
||||
file_path = current.get("stored_path") or current.get("local_file")
|
||||
if not file_path or not Path(file_path).exists():
|
||||
raise HTTPException(status_code=400, detail="Stored upload file is missing.")
|
||||
fields = {
|
||||
"class_name": handoff["class_name"],
|
||||
"assignment_name": handoff["assignment_name"],
|
||||
"submission_kind": handoff["submission_kind"],
|
||||
"notes": handoff.get("notes", ""),
|
||||
"source": handoff.get("source", "doris-schoolhouse"),
|
||||
}
|
||||
for optional_key in ("semester", "course_code", "paper_kind", "telegram_chat_id", "telegram_message_id"):
|
||||
value = handoff.get(optional_key)
|
||||
if value:
|
||||
fields[optional_key] = value
|
||||
artifact_kind = handoff.get("artifact_kind") or "document"
|
||||
if artifact_kind == "source_bundle":
|
||||
paperless_response = paperless.upload_source_bundle_receipt(current)
|
||||
enrichment = paperless.enrich_direct_submission(current, paperless_response)
|
||||
response = {
|
||||
"ok": True,
|
||||
"status_code": 200,
|
||||
"body": paperless_response,
|
||||
"mode": "direct-paperless",
|
||||
"enrichment": enrichment,
|
||||
}
|
||||
task_id = paperless_response.get("task_id") or paperless_response.get("id")
|
||||
body_obj = {
|
||||
"paperless_document_id": enrichment.get("document_id"),
|
||||
"paperless_title": enrichment.get("title") or paperless.build_submission_title(current),
|
||||
"paperless_task_id": task_id,
|
||||
"paperless_metadata": enrichment.get("metadata_payload"),
|
||||
}
|
||||
ok = True
|
||||
else:
|
||||
if handoff.get("artifact_kind"):
|
||||
fields["artifact_kind"] = str(handoff["artifact_kind"])
|
||||
if handoff.get("file_count"):
|
||||
fields["file_count"] = str(handoff["file_count"])
|
||||
response = webhooks.post_multipart(handoff["target_url"], fields, "document", file_path)
|
||||
body = response.get("body", {}) if isinstance(response, dict) else {}
|
||||
body_obj = body[0] if isinstance(body, list) and body and isinstance(body[0], dict) else (body if isinstance(body, dict) else {})
|
||||
ok = bool(response.get("ok", False)) if isinstance(response, dict) else False
|
||||
updates = {
|
||||
"status": "paperless-queued" if ok else "paperless-error",
|
||||
"confirmed_at": store.now(),
|
||||
"webhook_response": response,
|
||||
"paperless_document_id": body_obj.get("paperless_document_id") or body_obj.get("document_id"),
|
||||
"paperless_title": body_obj.get("paperless_title") or body_obj.get("title") or handoff.get("assignment_name"),
|
||||
"paperless_task_id": body_obj.get("paperless_task_id"),
|
||||
}
|
||||
updated = repo.update_submission(str(current.get("id")), updates) or current
|
||||
grade_note = None
|
||||
if ok and updated.get("assignment_id"):
|
||||
grade_note = paperless.append_grade_note_if_available(str(updated.get("assignment_id")))
|
||||
return {
|
||||
"status": "ok" if ok else "error",
|
||||
"submission": updated,
|
||||
"submissions": [updated],
|
||||
"handoff": handoff,
|
||||
"webhook": [response],
|
||||
"grade_note": grade_note,
|
||||
"file_count": handoff.get("file_count") or 1,
|
||||
}
|
||||
|
||||
|
||||
@router.get("")
|
||||
@@ -41,7 +112,9 @@ async def list_submissions():
|
||||
|
||||
@router.post("/intake")
|
||||
async def intake_assignment(
|
||||
file: UploadFile,
|
||||
request: Request,
|
||||
files: list[UploadFile] | None = File(None),
|
||||
file: UploadFile | None = File(None),
|
||||
class_id: str = Form(...),
|
||||
class_name: str = Form(...),
|
||||
assignment_name: str = Form(...),
|
||||
@@ -59,16 +132,43 @@ async def intake_assignment(
|
||||
matching = next((item for item in assignments if item.get("title") == assignment_name), None)
|
||||
if not matching:
|
||||
raise HTTPException(status_code=404, detail="Assignment not found in cached D2L data for selected class.")
|
||||
uploads = list(files or [])
|
||||
if file is not None:
|
||||
uploads.append(file)
|
||||
if not uploads:
|
||||
raise HTTPException(status_code=400, detail="At least one upload is required.")
|
||||
|
||||
upload_root = Path(settings.upload_tmp_dir)
|
||||
upload_root.mkdir(parents=True, exist_ok=True)
|
||||
safe_name = Path(file.filename or "upload.bin").name
|
||||
submission_id = store.new_id("submission")
|
||||
stored_path = upload_root / f"{submission_id}-{safe_name}"
|
||||
payload = await file.read()
|
||||
stored_path.write_bytes(payload)
|
||||
checksum = hashlib.sha256(payload).hexdigest()
|
||||
course = next((item for item in d2l.get_courses() if str(item.get("d2l_course_id") or item.get("id")) == str(class_id)), None)
|
||||
duplicate_names: dict[str, int] = {}
|
||||
prepared_uploads: list[dict[str, Any]] = []
|
||||
|
||||
for index, upload in enumerate(uploads, start=1):
|
||||
safe_name = Path(upload.filename or f"upload-{index}.bin").name
|
||||
duplicate_count = duplicate_names.get(safe_name, 0)
|
||||
duplicate_names[safe_name] = duplicate_count + 1
|
||||
if duplicate_count:
|
||||
stem = Path(safe_name).stem
|
||||
suffix = Path(safe_name).suffix
|
||||
safe_name = f"{stem}-{duplicate_count + 1}{suffix}"
|
||||
payload = await upload.read()
|
||||
prepared_uploads.append({
|
||||
"filename": safe_name,
|
||||
"payload": payload,
|
||||
"mime_type": upload.content_type or "application/octet-stream",
|
||||
})
|
||||
|
||||
submission_id = store.new_id("submission")
|
||||
bundle = bundles.build_bundle(
|
||||
upload_root=upload_root,
|
||||
submission_id=submission_id,
|
||||
class_name=class_name,
|
||||
assignment_name=assignment_name,
|
||||
version_label=version,
|
||||
uploads=prepared_uploads,
|
||||
notes=notes,
|
||||
)
|
||||
record = {
|
||||
"id": submission_id,
|
||||
"assignment_id": matching["id"],
|
||||
@@ -77,6 +177,7 @@ async def intake_assignment(
|
||||
"assignment_name": assignment_name,
|
||||
"version_label": version,
|
||||
"submission_kind": submission_kind,
|
||||
"artifact_kind": bundle["artifact_kind"],
|
||||
"semester": semester or None,
|
||||
"course_code": course_code or (course.get("code") if course else None),
|
||||
"paper_kind": paper_kind or None,
|
||||
@@ -84,17 +185,44 @@ async def intake_assignment(
|
||||
"telegram_chat_id": telegram_chat_id or None,
|
||||
"telegram_message_id": telegram_message_id or None,
|
||||
"notes": notes,
|
||||
"original_filename": safe_name,
|
||||
"stored_path": str(stored_path),
|
||||
"mime_type": file.content_type or "application/octet-stream",
|
||||
"file_size_bytes": len(payload),
|
||||
"checksum_sha256": checksum,
|
||||
"original_filename": bundle["bundle_filename"],
|
||||
"stored_path": bundle["bundle_path"],
|
||||
"stored_paths": [item["stored_path"] for item in bundle["file_manifest_json"]],
|
||||
"bundle_path": bundle["bundle_path"],
|
||||
"bundle_filename": bundle["bundle_filename"],
|
||||
"source_listing_path": bundle["source_listing_path"],
|
||||
"receipt_pdf_path": bundle["receipt_pdf_path"],
|
||||
"file_manifest_json": bundle["file_manifest_json"],
|
||||
"mime_type": bundle["bundle_mime_type"],
|
||||
"file_size_bytes": bundle["bundle_size_bytes"],
|
||||
"checksum_sha256": bundle["bundle_checksum_sha256"],
|
||||
"status": "uploaded-local",
|
||||
"received_at": store.now(),
|
||||
"batch_id": None,
|
||||
"file_count": bundle["file_count"],
|
||||
"file_index": None,
|
||||
"combined_source_checksum_sha256": bundle["combined_source_checksum_sha256"],
|
||||
}
|
||||
record["paperless_handoff"] = paperless.assignment_intake_payload(record)
|
||||
created = repo.create_submission(record)
|
||||
return {"status": "ok", "submission": created, "backend": repo.backend}
|
||||
accept = request.headers.get("accept", "")
|
||||
auto_confirmed = None
|
||||
if "text/html" in accept:
|
||||
auto_confirmed = _confirm_submission_to_paperless(created)
|
||||
created = auto_confirmed["submission"]
|
||||
response_payload = {
|
||||
"status": "ok",
|
||||
"submission": created,
|
||||
"submissions": [created],
|
||||
"batch_id": None,
|
||||
"file_count": bundle["file_count"],
|
||||
"paperless": auto_confirmed,
|
||||
"backend": repo.backend,
|
||||
}
|
||||
if "text/html" in accept:
|
||||
redirect_url = "/assignments/upload?submitted=" + str(created["id"]) + "&file_count=" + str(bundle["file_count"]) + "&artifact_kind=" + str(created.get("artifact_kind", "document")) + "&paperless_status=" + str(created.get("status", "uploaded-local"))
|
||||
return RedirectResponse(url=redirect_url, status_code=303)
|
||||
return response_payload
|
||||
|
||||
|
||||
@router.post("/{submission_id}/confirm-paperless")
|
||||
@@ -103,33 +231,7 @@ async def confirm_assignment_paperless(submission_id: str):
|
||||
current = next((item for item in submissions if str(item.get("id")) == str(submission_id)), None)
|
||||
if not current:
|
||||
raise HTTPException(status_code=404, detail="Submission not found.")
|
||||
handoff = paperless.assignment_intake_payload(current)
|
||||
file_path = current.get("stored_path") or current.get("local_file")
|
||||
if not file_path or not Path(file_path).exists():
|
||||
raise HTTPException(status_code=400, detail="Stored upload file is missing.")
|
||||
fields = {
|
||||
"class_name": handoff["class_name"],
|
||||
"assignment_name": handoff["assignment_name"],
|
||||
"submission_kind": handoff["submission_kind"],
|
||||
"notes": handoff.get("notes", ""),
|
||||
"source": handoff.get("source", "doris-schoolhouse"),
|
||||
}
|
||||
for optional_key in ("semester", "course_code", "paper_kind", "telegram_chat_id", "telegram_message_id"):
|
||||
value = handoff.get(optional_key)
|
||||
if value:
|
||||
fields[optional_key] = value
|
||||
response = webhooks.post_multipart(handoff["target_url"], fields, "document", file_path)
|
||||
body = response.get("body", {}) if isinstance(response, dict) else {}
|
||||
body_obj = body[0] if isinstance(body, list) and body and isinstance(body[0], dict) else (body if isinstance(body, dict) else {})
|
||||
ok = bool(response.get("ok", False)) if isinstance(response, dict) else False
|
||||
updated = repo.update_submission(submission_id, {
|
||||
"status": "paperless-queued" if ok else "paperless-error",
|
||||
"confirmed_at": store.now(),
|
||||
"webhook_response": response,
|
||||
"paperless_document_id": body_obj.get("paperless_document_id") or body_obj.get("document_id"),
|
||||
"paperless_title": body_obj.get("paperless_title") or body_obj.get("title") or handoff.get("assignment_name"),
|
||||
})
|
||||
return {"status": "ok" if ok else "error", "submission": updated or current, "handoff": handoff, "webhook": response}
|
||||
return _confirm_submission_to_paperless(current)
|
||||
|
||||
|
||||
@router.post("/{assignment_id}/status")
|
||||
|
||||
Reference in New Issue
Block a user