Add Doris Schoolhouse and clean pending homelab changes
This commit is contained in:
0
home/doris-schoolhouse/app/routes/__init__.py
Normal file
0
home/doris-schoolhouse/app/routes/__init__.py
Normal file
148
home/doris-schoolhouse/app/routes/api_assignments.py
Normal file
148
home/doris-schoolhouse/app/routes/api_assignments.py
Normal file
@@ -0,0 +1,148 @@
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Form, HTTPException, UploadFile
|
||||
|
||||
from app.config import settings
|
||||
from app.services.d2l import D2LService
|
||||
from app.services.paperless import PaperlessService
|
||||
from app.services.repository import get_repository
|
||||
from app.services.store import JsonStore
|
||||
from app.services.webhooks import WebhookService
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/assignments", tags=["assignments"])
|
||||
store = JsonStore()
|
||||
repo = get_repository()
|
||||
d2l = D2LService()
|
||||
paperless = PaperlessService()
|
||||
webhooks = WebhookService()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_assignments():
|
||||
courses = {item["id"]: item for item in d2l.get_courses()}
|
||||
items = repo.list_assignments()
|
||||
submissions = repo.list_submissions()
|
||||
submission_counts = {}
|
||||
for submission in submissions:
|
||||
submission_counts[submission.get("assignment_id")] = submission_counts.get(submission.get("assignment_id"), 0) + 1
|
||||
for item in items:
|
||||
item["submission_count"] = submission_counts.get(item.get("id"), 0)
|
||||
item["course"] = courses.get(item.get("course_id"), {})
|
||||
return {"items": items, "backend": repo.backend}
|
||||
|
||||
|
||||
|
||||
|
||||
@router.get('/submissions')
|
||||
async def list_submissions():
|
||||
return {"items": repo.list_submissions(), "backend": repo.backend}
|
||||
|
||||
@router.post("/intake")
|
||||
async def intake_assignment(
|
||||
file: UploadFile,
|
||||
class_id: str = Form(...),
|
||||
class_name: str = Form(...),
|
||||
assignment_name: str = Form(...),
|
||||
version: str = Form("v1"),
|
||||
submission_kind: str = Form("assignment_turn_in"),
|
||||
semester: str = Form(""),
|
||||
course_code: str = Form(""),
|
||||
paper_kind: str = Form(""),
|
||||
source: str = Form("doris-schoolhouse"),
|
||||
telegram_chat_id: str = Form(""),
|
||||
telegram_message_id: str = Form(""),
|
||||
notes: str = Form(""),
|
||||
):
|
||||
assignments = d2l.get_assignments(class_id)
|
||||
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.")
|
||||
|
||||
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()
|
||||
|
||||
record = {
|
||||
"id": submission_id,
|
||||
"assignment_id": matching["id"],
|
||||
"class_id": class_id,
|
||||
"class_name": class_name,
|
||||
"assignment_name": assignment_name,
|
||||
"version_label": version,
|
||||
"submission_kind": submission_kind,
|
||||
"semester": semester or None,
|
||||
"course_code": course_code or (course.get("code") if course else None),
|
||||
"paper_kind": paper_kind or None,
|
||||
"source": source or "doris-schoolhouse",
|
||||
"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,
|
||||
"status": "uploaded-local",
|
||||
"received_at": store.now(),
|
||||
}
|
||||
record["paperless_handoff"] = paperless.assignment_intake_payload(record)
|
||||
created = repo.create_submission(record)
|
||||
return {"status": "ok", "submission": created, "backend": repo.backend}
|
||||
|
||||
|
||||
@router.post("/{submission_id}/confirm-paperless")
|
||||
async def confirm_assignment_paperless(submission_id: str):
|
||||
submissions = repo.list_submissions()
|
||||
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}
|
||||
|
||||
|
||||
@router.post("/{assignment_id}/status")
|
||||
async def update_assignment_status(assignment_id: str, status: str = Form(...)):
|
||||
assignments = repo.list_assignments()
|
||||
updated = None
|
||||
for item in assignments:
|
||||
if str(item.get("id")) == str(assignment_id):
|
||||
item["status"] = status
|
||||
item["updated_at"] = store.now()
|
||||
updated = item
|
||||
break
|
||||
if not updated:
|
||||
raise HTTPException(status_code=404, detail="Assignment not found.")
|
||||
repo.replace_assignments(assignments)
|
||||
return {"status": "ok", "assignment": updated}
|
||||
31
home/doris-schoolhouse/app/routes/api_dashboard.py
Normal file
31
home/doris-schoolhouse/app/routes/api_dashboard.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.services.d2l import D2LService
|
||||
from app.services.repository import get_repository
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
|
||||
repo = get_repository()
|
||||
d2l = D2LService()
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
async def dashboard_summary():
|
||||
courses = d2l.get_courses()
|
||||
assignments = repo.list_assignments()
|
||||
submissions = repo.list_submissions()
|
||||
recordings = repo.list_recordings()
|
||||
sync_runs = repo.list_sync_runs()
|
||||
return {
|
||||
"backend": repo.backend,
|
||||
"counts": {
|
||||
"courses": len(courses),
|
||||
"assignments": len(assignments),
|
||||
"submissions": len(submissions),
|
||||
"recordings": len(recordings),
|
||||
},
|
||||
"latest_sync": sync_runs[-1] if sync_runs else None,
|
||||
"recent_assignments": assignments[:8],
|
||||
"recent_submissions": submissions[:8],
|
||||
"recent_recordings": recordings[:8],
|
||||
}
|
||||
69
home/doris-schoolhouse/app/routes/api_paperless.py
Normal file
69
home/doris-schoolhouse/app/routes/api_paperless.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from app.config import settings
|
||||
from app.services.diagnostics import DiagnosticsService
|
||||
from app.services.paperless import PaperlessService
|
||||
from app.services.repository import get_repository
|
||||
from app.services.store import JsonStore
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["paperless", "health"])
|
||||
service = PaperlessService()
|
||||
repo = get_repository()
|
||||
store = JsonStore()
|
||||
diagnostics = DiagnosticsService()
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health():
|
||||
return {"ok": True, "service": "doris-schoolhouse", "state_dir": settings.state_dir, "backend": repo.backend}
|
||||
|
||||
|
||||
@router.get("/health/details")
|
||||
async def health_details():
|
||||
return diagnostics.collect()
|
||||
|
||||
|
||||
@router.post("/paperless/webhook/intake-enriched")
|
||||
async def paperless_intake_enriched(submission_id: str, document_id: int, paperless_title: str | None = None):
|
||||
updated = repo.update_submission(
|
||||
submission_id,
|
||||
{
|
||||
"status": "paperless-linked",
|
||||
"paperless_document_id": document_id,
|
||||
"paperless_title": paperless_title,
|
||||
"paperless_linked_at": store.now(),
|
||||
},
|
||||
)
|
||||
if not updated:
|
||||
raise HTTPException(status_code=404, detail="Submission not found.")
|
||||
return {"status": "ok", "submission": updated}
|
||||
|
||||
|
||||
@router.post("/paperless/webhook/recording-complete")
|
||||
async def paperless_recording_complete(recording_id: str, transcript_document_id: int | None = None, summary_document_id: int | None = None, recording_document_id: int | None = None):
|
||||
updated = repo.update_recording(
|
||||
recording_id,
|
||||
{
|
||||
"status": "paperless-linked",
|
||||
"transcript_paperless_document_id": transcript_document_id,
|
||||
"summary_paperless_document_id": summary_document_id,
|
||||
"recording_paperless_document_id": recording_document_id,
|
||||
"transcription_status": "ready" if transcript_document_id else None,
|
||||
"summary_status": "ready" if summary_document_id else None,
|
||||
"paperless_linked_at": store.now(),
|
||||
},
|
||||
)
|
||||
if not updated:
|
||||
raise HTTPException(status_code=404, detail="Recording not found.")
|
||||
return {"status": "ok", "recording": updated}
|
||||
|
||||
|
||||
@router.post("/paperless/assignments/{assignment_id}/append-grade-note")
|
||||
async def append_grade_note(assignment_id: str, execute: bool = False):
|
||||
if not execute:
|
||||
return service.describe_grade_note_plan(assignment_id)
|
||||
try:
|
||||
return service.append_grade_note(assignment_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
130
home/doris-schoolhouse/app/routes/api_recordings.py
Normal file
130
home/doris-schoolhouse/app/routes/api_recordings.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Form, HTTPException, UploadFile
|
||||
|
||||
from app.config import settings
|
||||
from app.services.d2l import D2LService
|
||||
from app.services.media import MediaService
|
||||
from app.services.paperless import PaperlessService
|
||||
from app.services.repository import get_repository
|
||||
from app.services.store import JsonStore
|
||||
from app.services.webhooks import WebhookService
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/recordings", tags=["recordings"])
|
||||
store = JsonStore()
|
||||
repo = get_repository()
|
||||
d2l = D2LService()
|
||||
paperless = PaperlessService()
|
||||
media = MediaService()
|
||||
webhooks = WebhookService()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_recordings():
|
||||
return {"items": repo.list_recordings(), "backend": repo.backend}
|
||||
|
||||
|
||||
@router.post("/intake")
|
||||
async def intake_recording(
|
||||
file: UploadFile,
|
||||
class_id: str = Form(...),
|
||||
class_name: str = Form(...),
|
||||
session_date: str = Form(...),
|
||||
assignment_id: str | None = Form(None),
|
||||
notes: str = Form(""),
|
||||
):
|
||||
course = next((item for item in d2l.get_courses() if str(item.get("d2l_course_id") or item.get("id")) == str(class_id)), None)
|
||||
if not course:
|
||||
raise HTTPException(status_code=404, detail="Class not found in cached D2L data.")
|
||||
|
||||
upload_root = Path(settings.upload_tmp_dir)
|
||||
upload_root.mkdir(parents=True, exist_ok=True)
|
||||
recording_id = store.new_id("recording")
|
||||
safe_name = Path(file.filename or "recording.wav").name
|
||||
stored_path = upload_root / f"{recording_id}-{safe_name}"
|
||||
payload = await file.read()
|
||||
stored_path.write_bytes(payload)
|
||||
checksum = hashlib.sha256(payload).hexdigest()
|
||||
mp3_path = media.planned_mp3_path(str(stored_path))
|
||||
|
||||
record = {
|
||||
"id": recording_id,
|
||||
"class_id": str(course.get("d2l_course_id") or class_id),
|
||||
"class_name": class_name,
|
||||
"assignment_id": assignment_id,
|
||||
"session_date": session_date,
|
||||
"notes": notes,
|
||||
"original_filename": safe_name,
|
||||
"stored_path": str(stored_path),
|
||||
"mp3_path": mp3_path,
|
||||
"conversion_command": media.conversion_command(str(stored_path), mp3_path),
|
||||
"delete_wav_after_success": True,
|
||||
"mime_type": file.content_type or "audio/wav",
|
||||
"file_size_bytes": len(payload),
|
||||
"checksum_sha256": checksum,
|
||||
"status": "uploaded-local",
|
||||
"conversion_status": "queued",
|
||||
"transcription_status": "queued",
|
||||
"summary_status": "queued",
|
||||
"received_at": store.now(),
|
||||
}
|
||||
record["paperless_handoff"] = paperless.recording_intake_payload(record)
|
||||
created = repo.create_recording(record)
|
||||
return {"status": "ok", "recording": created, "backend": repo.backend}
|
||||
|
||||
|
||||
@router.post("/{recording_id}/reprocess")
|
||||
async def reprocess_recording(recording_id: str):
|
||||
recordings = repo.list_recordings()
|
||||
current = next((item for item in recordings if str(item.get("id")) == str(recording_id)), None)
|
||||
if not current:
|
||||
raise HTTPException(status_code=404, detail="Recording not found.")
|
||||
handoff = paperless.recording_intake_payload(current)
|
||||
file_path = current.get("mp3_path") or 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="Local recording file is missing.")
|
||||
fields = {
|
||||
"class_name": handoff["class_name"],
|
||||
"lecture_title": handoff["lecture_title"],
|
||||
"notes": handoff.get("notes", ""),
|
||||
"source": handoff.get("source", "doris-schoolhouse"),
|
||||
}
|
||||
response = webhooks.post_multipart(handoff["target_url"], fields, "data", 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_recording(recording_id, {
|
||||
"status": "requeued" if ok else "requeue-error",
|
||||
"requeued_at": store.now(),
|
||||
"webhook_response": response,
|
||||
"transcription_status": "processing" if ok else "error",
|
||||
"summary_status": "processing" if ok else "error",
|
||||
"recording_paperless_document_id": body_obj.get("recording_paperless_document_id") or body_obj.get("document_id"),
|
||||
})
|
||||
return {"status": "ok" if ok else "error", "recording": updated or current, "handoff": handoff, "webhook": response}
|
||||
|
||||
|
||||
@router.post("/{recording_id}/convert")
|
||||
async def convert_recording(recording_id: str):
|
||||
recordings = repo.list_recordings()
|
||||
current = next((item for item in recordings if str(item.get("id")) == str(recording_id)), None)
|
||||
if not current:
|
||||
raise HTTPException(status_code=404, detail="Recording not found.")
|
||||
wav_path = current.get("stored_path")
|
||||
if not wav_path or not Path(wav_path).exists():
|
||||
raise HTTPException(status_code=400, detail="Source WAV file is missing.")
|
||||
mp3_path = current.get("mp3_path") or media.planned_mp3_path(wav_path)
|
||||
result = media.convert_wav_to_mp3(wav_path, mp3_path)
|
||||
updates = {
|
||||
"conversion_status": result["status"],
|
||||
"conversion_result": result,
|
||||
"updated_at": store.now(),
|
||||
}
|
||||
if result["status"] == "ok":
|
||||
updates["mp3_path"] = mp3_path
|
||||
updates["delete_source_result"] = media.delete_source_if_requested(wav_path, bool(current.get("delete_wav_after_success")))
|
||||
updates["status"] = "converted"
|
||||
updated = repo.update_recording(recording_id, updates)
|
||||
return {"status": result["status"], "recording": updated or current, "conversion": result}
|
||||
27
home/doris-schoolhouse/app/routes/api_sync.py
Normal file
27
home/doris-schoolhouse/app/routes/api_sync.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.services.d2l import D2LService
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/d2l", tags=["d2l"])
|
||||
service = D2LService()
|
||||
|
||||
|
||||
@router.get("/courses")
|
||||
async def get_courses():
|
||||
return {"items": service.get_courses()}
|
||||
|
||||
|
||||
@router.get("/assignments")
|
||||
async def get_course_assignments(course_id: str):
|
||||
return {"items": service.get_assignments(course_id), "course_id": course_id}
|
||||
|
||||
|
||||
@router.get("/grades")
|
||||
async def get_course_grades(course_id: str):
|
||||
return service.get_grades(course_id)
|
||||
|
||||
|
||||
@router.post("/sync")
|
||||
async def sync_d2l():
|
||||
return service.sync_to_store()
|
||||
24
home/doris-schoolhouse/app/routes/ui.py
Normal file
24
home/doris-schoolhouse/app/routes/ui.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def dashboard(request: Request):
|
||||
return templates.TemplateResponse("dashboard.html", {"request": request, "title": "Doris Schoolhouse"})
|
||||
|
||||
|
||||
@router.get("/assignments/upload", response_class=HTMLResponse)
|
||||
async def assignment_upload(request: Request):
|
||||
return templates.TemplateResponse("assignment_upload.html", {"request": request, "title": "Assignment Upload"})
|
||||
|
||||
|
||||
@router.get("/recordings/upload", response_class=HTMLResponse)
|
||||
async def recording_upload(request: Request):
|
||||
return templates.TemplateResponse("recording_upload.html", {"request": request, "title": "Recording Upload"})
|
||||
Reference in New Issue
Block a user