import hashlib import re from urllib.parse import urljoin from app.config import settings from app.services.paperless_client import PaperlessClient from app.services.repository import get_repository from app.services.store import JsonStore class PaperlessService: def __init__(self) -> None: self.store = JsonStore() self.repo = get_repository() self.client = PaperlessClient() def assignment_intake_payload(self, submission: dict) -> dict: local_files = submission.get("stored_paths") or [] if not local_files and submission.get("stored_path"): local_files = [submission.get("stored_path")] local_file = submission.get("receipt_pdf_path") or submission.get("bundle_path") or submission.get("stored_path") return { "target_url": urljoin(settings.n8n_base_url, settings.n8n_assignment_webhook), "class_name": submission.get("class_name"), "assignment_name": submission.get("assignment_name"), "submission_kind": submission.get("submission_kind") or "assignment_turn_in", "semester": submission.get("semester"), "course_code": submission.get("course_code"), "paper_kind": submission.get("paper_kind"), "notes": submission.get("notes", ""), "source": submission.get("source") or "doris-schoolhouse", "telegram_chat_id": submission.get("telegram_chat_id"), "telegram_message_id": submission.get("telegram_message_id"), "local_file": local_file, "local_files": local_files, "version": submission.get("version_label"), "batch_id": submission.get("batch_id"), "file_count": submission.get("file_count"), "artifact_kind": submission.get("artifact_kind") or "document", "source_listing_path": submission.get("source_listing_path"), "receipt_pdf_path": submission.get("receipt_pdf_path"), } def recording_intake_payload(self, recording: dict) -> dict: return { "target_url": urljoin(settings.n8n_base_url, settings.n8n_recording_webhook), "class_name": recording.get("class_name"), "lecture_title": f"{recording.get('class_name')} {recording.get('session_date')}", "notes": recording.get("notes", ""), "local_file": recording.get("mp3_path") or recording.get("stored_path"), "source": "doris-schoolhouse", } def upload_source_bundle_receipt(self, submission: dict) -> dict: file_path = submission.get("receipt_pdf_path") or submission.get("bundle_path") or submission.get("stored_path") title = self.build_submission_title(submission) return self.client.upload_document(file_path, title) def _course_short_label(self, submission: dict) -> str: course_code = str(submission.get("course_code") or "").strip() match = re.search(r"([A-Z]{3,4})_([A-Z]{3,4})_(\d{4})", course_code) if match: return f"{match.group(2)}-{match.group(3)}" class_name = str(submission.get("class_name") or "").strip() match = re.search(r"([A-Za-z]+(?:\s+[A-Za-z]+)*)\s+(\d{4})", class_name) if not match: return class_name words = match.group(1).split() prefix = ''.join(word[0] for word in words[:4]).upper() return f"{prefix}-{match.group(2)}" def _course_tag_name(self, submission: dict) -> str | None: label = self._course_short_label(submission) if not label or "-" not in label: return None return label.replace("-", " ").lower() def build_submission_title(self, submission: dict) -> str: return " — ".join( part for part in [ self._course_short_label(submission) or submission.get("class_name"), submission.get("assignment_name"), submission.get("version_label"), "source bundle" if submission.get("artifact_kind") == "source_bundle" else submission.get("submission_kind"), ] if part ) def build_direct_metadata_payload(self, submission: dict, current_document: dict | None = None) -> dict: current_document = current_document or {} known_tags = { str(tag.get("name", "")).strip().casefold(): tag for tag in self.client.list_tags() } merged_tags = { int(tag_id) for tag_id in (current_document.get("tags") or []) if str(tag_id).isdigit() } for tag_name in ("school", "assignments", "spring-2026"): tag = known_tags.get(tag_name) if tag and tag.get("id"): merged_tags.add(int(tag["id"])) course_tag_name = self._course_tag_name(submission) if course_tag_name: tag = known_tags.get(course_tag_name) if tag and tag.get("id"): merged_tags.add(int(tag["id"])) if submission.get("artifact_kind") == "source_bundle": for tag_name in ("programming", "java"): tag = known_tags.get(tag_name) if tag and tag.get("id"): merged_tags.add(int(tag["id"])) payload = { "title": self.build_submission_title(submission), "tags": sorted(merged_tags), } for doc_type in self.client.list_document_types(): if str(doc_type.get("name", "")).strip().casefold() == "school" and doc_type.get("id"): payload["document_type"] = int(doc_type["id"]) break for correspondent in self.client.list_correspondents(): if str(correspondent.get("name", "")).strip().casefold() == "austin peay state university" and correspondent.get("id"): payload["correspondent"] = int(correspondent["id"]) break return payload def enrich_direct_submission(self, submission: dict, upload_response: dict | None = None) -> dict: upload_response = upload_response or {} task_id = upload_response.get("task_id") or upload_response.get("id") or submission.get("paperless_task_id") if not task_id: raise ValueError("Paperless upload did not return a task id") task = self.client.wait_for_task(str(task_id)) status = str(task.get("status") or "").upper() document_id = task.get("related_document") if status != "SUCCESS": duplicate_match = re.search(r"#(\d+)\)", str(task.get("result") or "")) if duplicate_match: document_id = duplicate_match.group(1) else: raise ValueError(f"Paperless task did not complete successfully: {status or 'unknown'}") if not document_id: raise ValueError("Paperless task completed without a related document id") current = self.client.get_document(document_id) payload = self.build_direct_metadata_payload(submission, current) updated = self.client.update_document(document_id, payload) return { "task": task, "document": updated, "document_id": updated.get("id") or current.get("id") or int(document_id), "title": updated.get("title") or payload.get("title"), "metadata_payload": payload, } def build_grade_note(self, assignment_id: str) -> str: submissions = self.repo.list_submissions() assignments = {item.get("id"): item for item in self.repo.list_assignments()} assignment = assignments.get(str(assignment_id), {}) linked = [s for s in submissions if str(s.get("assignment_id")) == str(assignment_id)] note_lines = [ "D2L grade note imported from Doris Schoolhouse.", f"Assignment: {assignment.get('title', 'unknown')}", ] if assignment.get("grade"): note_lines.append(f"Grade: {assignment['grade']}") comments = str(assignment.get("comments") or "").strip() if comments: note_lines.append(f"Comments: {comments}") else: note_lines.append("Comments: No professor comment captured in the D2L scrape.") if linked: note_lines.append(f"Linked submissions: {len(linked)}") return "\n".join(note_lines) def describe_grade_note_plan(self, assignment_id: str) -> dict: submissions = self.repo.list_submissions() linked = [s for s in submissions if str(s.get("assignment_id")) == str(assignment_id)] return { "status": "stub", "backend": self.repo.backend, "assignment_id": assignment_id, "paperless_base_url": settings.paperless_base_url, "linked_submission_count": len(linked), "note_preview": self.build_grade_note(assignment_id), "message": "Resolve linked Paperless submission document, hash latest grade/comment payload, and append a dated Schoolhouse note if changed.", } def append_grade_note(self, assignment_id: str) -> dict: submissions = self.repo.list_submissions() linked = [s for s in submissions if str(s.get("assignment_id")) == str(assignment_id)] if not linked: raise ValueError("No linked submissions for assignment") target = next((s for s in linked if s.get("paperless_document_id")), linked[0]) document_id = target.get("paperless_document_id") if not document_id: raise ValueError("Linked submission has no Paperless document id yet") note = self.build_grade_note(assignment_id) note_hash = hashlib.sha256(note.encode('utf-8')).hexdigest() if target.get("paperless_note_last_hash") == note_hash: return { "status": "skipped", "reason": "note-unchanged", "assignment_id": assignment_id, "document_id": document_id, "note_hash": note_hash, } response = self.client.add_note(document_id, note) self.repo.update_submission( str(target.get("id")), { "paperless_note_last_hash": note_hash, "paperless_title": target.get("paperless_title"), "status": target.get("status", "paperless-linked"), }, ) return { "status": "ok", "assignment_id": assignment_id, "document_id": document_id, "note_hash": note_hash, "paperless_response": response, } def append_grade_note_if_available(self, assignment_id: str) -> dict: assignments = {item.get("id"): item for item in self.repo.list_assignments()} assignment = assignments.get(str(assignment_id), {}) if not assignment: return {"status": "skipped", "reason": "assignment-not-found", "assignment_id": assignment_id} if not (assignment.get("grade") or str(assignment.get("comments") or "").strip()): return {"status": "skipped", "reason": "no-grade-or-comments", "assignment_id": assignment_id} linked = [s for s in self.repo.list_submissions() if str(s.get("assignment_id")) == str(assignment_id)] if not linked: return {"status": "skipped", "reason": "no-linked-submissions", "assignment_id": assignment_id} if not any(item.get("paperless_document_id") for item in linked): return {"status": "skipped", "reason": "no-paperless-document-id", "assignment_id": assignment_id} return self.append_grade_note(assignment_id)