fix: improve import, evidence indexing and material-scoped grading
Preserve document revisions and evidence, harden indexing and export behavior, add explicit material selection and direct-folder import, validate model JSON schemas with safe error messages, and retain student selection while loading scoring data.
This commit is contained in:
+29
-13
@@ -8,6 +8,7 @@ from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from . import services
|
||||
from .services import found, audit
|
||||
from .config import get_settings
|
||||
from .db import get_db
|
||||
from .evidence import CONTENT_ALLOWED
|
||||
@@ -26,17 +27,6 @@ from .storage import CATEGORIES, directories, scan
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
def found(db, cls, entity_id):
|
||||
item = db.get(cls, entity_id)
|
||||
if not item:
|
||||
raise HTTPException(404, f"{cls.__name__}不存在")
|
||||
return item
|
||||
|
||||
|
||||
def audit(db, action, entity_type, entity_id, details=None):
|
||||
db.add(AuditLog(action=action, entity_type=entity_type, entity_id=entity_id, details=details or {}))
|
||||
|
||||
|
||||
def version_dict(item):
|
||||
return {key: getattr(item, key) for key in ("id", "course_id", "version", "title", "objectives", "indicators", "directions", "prompts", "templates", "published", "created_at")}
|
||||
|
||||
@@ -92,6 +82,7 @@ class ImportConfirm(BaseModel):
|
||||
class BatchInput(BaseModel):
|
||||
course_version_id: int
|
||||
rubric_version_id: int
|
||||
project_ids: list[int] | None = Field(default=None, min_length=1)
|
||||
|
||||
|
||||
class ReviewInput(BaseModel):
|
||||
@@ -320,6 +311,10 @@ def list_projects(course_version_id: int | None = None, db: Session = Depends(ge
|
||||
result.append({"id": project.id, "student_no": student.student_no, "name": student.name, "topic": project.topic,
|
||||
"course_version_id": project.course_version_id, "status": project.status,
|
||||
"missing": [category for category in CATEGORIES if not any(file.category == category and file.recognition_status == "recognized" for file in files)],
|
||||
"materials": [{"id": file.id, "file_name": Path(file.relative_path).name,
|
||||
"category": file.category, "parse_status": file.parse_status,
|
||||
"parse_error": file.parse_error}
|
||||
for file in files if file.recognition_status == "recognized"],
|
||||
"parse_errors": [file.parse_error for file in files if file.parse_error]})
|
||||
return result
|
||||
|
||||
@@ -331,15 +326,27 @@ def get_project(project_id: int, db: Session = Depends(get_db)):
|
||||
files = db.scalars(select(SubmissionFile).where(SubmissionFile.project_id == project.id)).all()
|
||||
return {"id": project.id, "student_no": student.student_no, "name": student.name, "topic": project.topic,
|
||||
"course_version_id": project.course_version_id, "direction": project.direction, "facts": project.facts,
|
||||
"files": [{key: getattr(file, key) for key in ("id", "category", "relative_path", "sha256", "parse_status", "parse_error", "recognition_status")} for file in files]}
|
||||
"files": [{**{key: getattr(file, key) for key in ("id", "category", "relative_path", "sha256", "parse_status", "parse_error", "recognition_status")},
|
||||
"warnings": [{"page": page["page_number"], "message": message}
|
||||
for page in (file.parsed or {}).get("pages", [])
|
||||
for message in ([page.get("text_quality", {}).get("warning")] +
|
||||
[table.get("warning") for table in page.get("tables", [])])
|
||||
if message]}
|
||||
for file in files]}
|
||||
|
||||
|
||||
@router.get("/knowledge")
|
||||
def knowledge(course_version_id: int, project_id: int | None = None, db: Session = Depends(get_db)):
|
||||
query = select(KnowledgeDocument).where(KnowledgeDocument.course_version_id == course_version_id,
|
||||
KnowledgeDocument.project_id == project_id)
|
||||
documents = db.scalars(query).all()
|
||||
counts = dict(db.execute(select(KnowledgeChunk.document_id, func.count(KnowledgeChunk.id))
|
||||
.where(KnowledgeChunk.document_id.in_(query.with_only_columns(KnowledgeDocument.id)))
|
||||
.group_by(KnowledgeChunk.document_id)).all()) if documents else {}
|
||||
return [{"id": doc.id, "title": doc.title, "type": doc.document_type, "status": doc.status,
|
||||
"chunks": db.scalar(select(func.count(KnowledgeChunk.id)).where(KnowledgeChunk.document_id == doc.id))} for doc in db.scalars(query)]
|
||||
"index_version": doc.index_version, "revision": doc.revision,
|
||||
"requires_reindex": doc.status != "superseded" and (doc.index_version != services.INDEX_VERSION or doc.status != "ready"),
|
||||
"chunks": counts.get(doc.id, 0)} for doc in documents]
|
||||
|
||||
|
||||
@router.get("/knowledge/documents/{document_id}/pages/{page}")
|
||||
@@ -447,12 +454,20 @@ def batch_status_counts(db, batch_id: int) -> dict:
|
||||
return {"status_counts": counts, "needs_review": counts.get("needs_review", 0),
|
||||
"unscorable": counts.get("unscorable", 0), "failed": counts.get("failed", 0)}
|
||||
|
||||
def batch_version_metadata(batch: ScoringBatch) -> dict:
|
||||
"""Display the immutable rule/course versions, not their database IDs."""
|
||||
return {"course_version": batch.course_snapshot.get("version"),
|
||||
"course_title": batch.course_snapshot.get("title", ""),
|
||||
"rubric_version": batch.rubric_snapshot.get("version"),
|
||||
"criterion_count": len(batch.rubric_snapshot.get("criteria", []))}
|
||||
|
||||
|
||||
@router.get("/scoring-batches")
|
||||
def list_batches(db: Session = Depends(get_db)):
|
||||
return [{**{key: getattr(batch, key) for key in ("id", "course_version_id", "rubric_version_id", "status", "progress_done", "progress_total", "error", "created_at", "finished_at")},
|
||||
"status_label": BATCH_STATUS_LABELS.get(batch.status, batch.status),
|
||||
"project_ids": batch.project_ids, "legacy_membership": batch.project_ids is None,
|
||||
**batch_version_metadata(batch),
|
||||
**batch_status_counts(db, batch.id)}
|
||||
for batch in db.scalars(select(ScoringBatch).order_by(ScoringBatch.id.desc()))]
|
||||
|
||||
@@ -463,6 +478,7 @@ def batch_progress(batch_id: int, db: Session = Depends(get_db)):
|
||||
return {**{key: getattr(batch, key) for key in ("id", "status", "progress_done", "progress_total", "error", "created_at", "finished_at")},
|
||||
"status_label": BATCH_STATUS_LABELS.get(batch.status, batch.status),
|
||||
"project_ids": batch.project_ids, "legacy_membership": batch.project_ids is None,
|
||||
**batch_version_metadata(batch),
|
||||
**batch_status_counts(db, batch_id)}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user