140 lines
5.6 KiB
Python
140 lines
5.6 KiB
Python
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from app.config import settings
|
|
from app.services.repository import get_repository
|
|
from app.services.store import JsonStore
|
|
|
|
|
|
class D2LService:
|
|
def __init__(self) -> None:
|
|
self.store = JsonStore()
|
|
self.repo = get_repository()
|
|
|
|
def _snapshot_path(self) -> Path:
|
|
candidate_dirs = [
|
|
Path(settings.d2l_scraper_workdir),
|
|
Path('/home/fizzlepoof/.openclaw/workspace/school'),
|
|
]
|
|
for workdir in candidate_dirs:
|
|
for name in ('d2l_current_snapshot.json', 'd2l_grades.json', 'd2l_scraper_output.json'):
|
|
candidate = workdir / name
|
|
if candidate.exists():
|
|
return candidate
|
|
return candidate_dirs[0] / 'd2l_current_snapshot.json'
|
|
|
|
def load_snapshot(self) -> dict[str, Any]:
|
|
path = self._snapshot_path()
|
|
if not path.exists():
|
|
return {"courses": [], "source_path": str(path), "missing": True}
|
|
data = json.loads(path.read_text())
|
|
if isinstance(data, dict):
|
|
data["source_path"] = str(path)
|
|
return data
|
|
return {"courses": [], "source_path": str(path), "missing": True}
|
|
|
|
def _normalize_courses(self, snapshot: dict[str, Any]) -> list[dict[str, Any]]:
|
|
items = []
|
|
for course in snapshot.get("courses", []):
|
|
items.append({
|
|
"id": str(course.get("id")),
|
|
"d2l_course_id": str(course.get("id")),
|
|
"name": course.get("name"),
|
|
"code": course.get("code"),
|
|
"finalSummary": course.get("finalSummary"),
|
|
"gradeItemCount": course.get("gradeItemCount"),
|
|
"synced_at": self.store.now(),
|
|
})
|
|
return items
|
|
|
|
def _normalize_assignments(self, snapshot: dict[str, Any]) -> list[dict[str, Any]]:
|
|
items = []
|
|
for course in snapshot.get("courses", []):
|
|
course_id = str(course.get("id"))
|
|
for grade_item in course.get("gradeItems", []):
|
|
if grade_item.get("isCategory"):
|
|
continue
|
|
title = grade_item.get("name") or "Untitled"
|
|
items.append({
|
|
"id": f"{course_id}:{title.lower().strip()}",
|
|
"course_id": course_id,
|
|
"course_name": course.get("name"),
|
|
"title": title,
|
|
"points": grade_item.get("points"),
|
|
"grade": grade_item.get("grade"),
|
|
"comments": grade_item.get("comments"),
|
|
"source": "d2l-grade-item",
|
|
"synced_at": self.store.now(),
|
|
})
|
|
return items
|
|
|
|
def sync_to_store(self) -> dict[str, Any]:
|
|
snapshot = self.load_snapshot()
|
|
courses = self._normalize_courses(snapshot)
|
|
assignments = self._normalize_assignments(snapshot)
|
|
existing_assignments = {item.get('id'): item for item in self.repo.list_assignments()}
|
|
for item in assignments:
|
|
prior = existing_assignments.get(item['id']) or {}
|
|
item['status'] = prior.get('status', 'assigned')
|
|
item['updated_at'] = self.store.now()
|
|
item['created_at'] = prior.get('created_at', self.store.now())
|
|
self.repo.replace_courses(courses)
|
|
self.repo.replace_assignments(assignments)
|
|
sync_runs = self.repo.list_sync_runs()
|
|
sync_runs.append({
|
|
"id": self.store.new_id("sync"),
|
|
"snapshot_path": snapshot.get("source_path"),
|
|
"course_count": len(courses),
|
|
"assignment_count": len(assignments),
|
|
"completed_at": self.store.now(),
|
|
"status": "completed",
|
|
})
|
|
self.repo.save_sync_runs(sync_runs)
|
|
return {
|
|
"status": "ok",
|
|
"snapshot_path": snapshot.get("source_path"),
|
|
"course_count": len(courses),
|
|
"assignment_count": len(assignments),
|
|
"backend": self.repo.backend,
|
|
}
|
|
|
|
def get_courses(self) -> list[dict[str, Any]]:
|
|
items = self.repo.list_courses()
|
|
if items:
|
|
return items
|
|
self.sync_to_store()
|
|
return self.repo.list_courses()
|
|
|
|
def get_assignments(self, course_id: str) -> list[dict[str, Any]]:
|
|
items = self.repo.list_assignments()
|
|
if not items:
|
|
self.sync_to_store()
|
|
items = self.repo.list_assignments()
|
|
return [item for item in items if str(item.get("course_id")) == str(course_id)]
|
|
|
|
def get_grades(self, course_id: str) -> dict[str, Any]:
|
|
snapshot = self.load_snapshot()
|
|
for course in snapshot.get("courses", []):
|
|
if str(course.get("id")) == str(course_id):
|
|
return {
|
|
"course_id": str(course.get("id")),
|
|
"course_name": course.get("name"),
|
|
"final": course.get("final"),
|
|
"finalSummary": course.get("finalSummary"),
|
|
"gradeItems": course.get("gradeItems", []),
|
|
}
|
|
return {"course_id": course_id, "gradeItems": []}
|
|
|
|
def describe_sync_plan(self) -> dict:
|
|
snapshot = self.load_snapshot()
|
|
return {
|
|
"status": "stub",
|
|
"backend": self.repo.backend,
|
|
"scraper_workdir": settings.d2l_scraper_workdir,
|
|
"scraper_command": settings.d2l_scraper_command,
|
|
"snapshot_path": snapshot.get("source_path"),
|
|
"course_count": len(snapshot.get("courses", [])),
|
|
"message": "Run existing D2L scraper, normalize courses/assignments/grades, then store into Schoolhouse persistence.",
|
|
}
|