Add materials, exam images, storage quota, and API guide

Upload documents for AI context, exam images for Moodle questions, per-template storage limits, embedded images in XML export, and GUIA_API_Y_FLUJO.md with full endpoint documentation.
This commit is contained in:
Mireya Cueto Garrido
2026-06-01 10:30:40 +02:00
parent ba2507918b
commit 7bc27da33a
29 changed files with 1892 additions and 59 deletions
+32 -2
View File
@@ -6,11 +6,41 @@ from sqlalchemy.orm import Session
from app.core.config import Settings, get_settings
from app.db.session import get_db
from app.services.exam_service import ExamService
from app.services.image_service import ImageService
from app.services.llm import LLMClient
from app.services.material_service import MaterialService
from app.services.storage_quota import StorageQuotaService
def get_exam_service(db: Annotated[Session, Depends(get_db)]) -> ExamService:
return ExamService(db)
def get_storage_quota_service(
db: Annotated[Session, Depends(get_db)],
settings: Annotated[Settings, Depends(get_settings)],
) -> StorageQuotaService:
return StorageQuotaService(db, settings)
def get_material_service(
db: Annotated[Session, Depends(get_db)],
settings: Annotated[Settings, Depends(get_settings)],
storage_quota: Annotated[StorageQuotaService, Depends(get_storage_quota_service)],
) -> MaterialService:
return MaterialService(db, settings, storage_quota)
def get_image_service(
db: Annotated[Session, Depends(get_db)],
settings: Annotated[Settings, Depends(get_settings)],
storage_quota: Annotated[StorageQuotaService, Depends(get_storage_quota_service)],
) -> ImageService:
return ImageService(db, settings, storage_quota)
def get_exam_service(
db: Annotated[Session, Depends(get_db)],
material_service: Annotated[MaterialService, Depends(get_material_service)],
image_service: Annotated[ImageService, Depends(get_image_service)],
) -> ExamService:
return ExamService(db, material_service=material_service, image_service=image_service)
def get_llm_client(settings: Annotated[Settings, Depends(get_settings)]) -> LLMClient:
+7 -1
View File
@@ -26,7 +26,12 @@ def build_prompt(
current_user: Annotated[User, Depends(get_current_user)],
service: Annotated[ExamService, Depends(get_exam_service)],
) -> PromptResponse:
return service.build_prompt(current_user.id, template_id, payload.topic_prompt)
return service.build_prompt(
current_user.id,
template_id,
payload.topic_prompt,
payload.material_ids,
)
@router.post("/generate", response_model=ParsedQuestionsResponse)
@@ -41,6 +46,7 @@ async def generate_exam(
payload.template_id,
payload.topic_prompt,
llm_client,
payload.material_ids,
)
+73
View File
@@ -0,0 +1,73 @@
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, File, Form, UploadFile, status
from fastapi.responses import FileResponse
from app.api.dependencies import get_exam_service, get_image_service
from app.core.auth import get_current_user
from app.models.user import User
from app.schemas.image import ExamImageRead, ExamImageUploadResponse
from app.services.exam_service import ExamService
from app.services.image_service import ImageService
router = APIRouter(tags=["images"])
@router.post(
"/templates/{template_id}/images",
response_model=ExamImageUploadResponse,
status_code=status.HTTP_201_CREATED,
)
def upload_exam_image(
template_id: uuid.UUID,
current_user: Annotated[User, Depends(get_current_user)],
exam_service: Annotated[ExamService, Depends(get_exam_service)],
image_service: Annotated[ImageService, Depends(get_image_service)],
file: UploadFile = File(...),
caption: Annotated[str | None, Form()] = None,
) -> ExamImageUploadResponse:
template = exam_service.get_owned_template(current_user.id, template_id)
image = image_service.upload(template, file, caption=caption)
return ExamImageUploadResponse(
image=ExamImageRead.model_validate(image_service.to_read(image)),
message="Image uploaded successfully",
)
@router.get("/templates/{template_id}/images", response_model=list[ExamImageRead])
def list_exam_images(
template_id: uuid.UUID,
current_user: Annotated[User, Depends(get_current_user)],
exam_service: Annotated[ExamService, Depends(get_exam_service)],
image_service: Annotated[ImageService, Depends(get_image_service)],
) -> list[ExamImageRead]:
exam_service.get_owned_template(current_user.id, template_id)
images = image_service.list_images(template_id)
return [ExamImageRead.model_validate(image_service.to_read(image)) for image in images]
@router.get("/images/{image_id}/content")
def get_exam_image_content(
image_id: uuid.UUID,
current_user: Annotated[User, Depends(get_current_user)],
image_service: Annotated[ImageService, Depends(get_image_service)],
) -> FileResponse:
image = image_service.get_image_for_user(current_user.id, image_id)
return FileResponse(
path=image.storage_path,
media_type=image.mime_type,
filename=image.original_filename,
)
@router.delete("/templates/{template_id}/images/{image_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_exam_image(
template_id: uuid.UUID,
image_id: uuid.UUID,
current_user: Annotated[User, Depends(get_current_user)],
exam_service: Annotated[ExamService, Depends(get_exam_service)],
image_service: Annotated[ImageService, Depends(get_image_service)],
) -> None:
template = exam_service.get_owned_template(current_user.id, template_id)
image_service.delete_image(template, image_id)
+55
View File
@@ -0,0 +1,55 @@
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, File, UploadFile, status
from app.api.dependencies import get_exam_service, get_material_service
from app.core.auth import get_current_user
from app.models.exam import MaterialStatus
from app.models.user import User
from app.schemas.material import ExamMaterialRead, ExamMaterialUploadResponse
from app.services.exam_service import ExamService
from app.services.material_service import MaterialService
router = APIRouter(prefix="/templates/{template_id}/materials", tags=["materials"])
@router.post("", response_model=ExamMaterialUploadResponse, status_code=status.HTTP_201_CREATED)
def upload_material(
template_id: uuid.UUID,
current_user: Annotated[User, Depends(get_current_user)],
exam_service: Annotated[ExamService, Depends(get_exam_service)],
material_service: Annotated[MaterialService, Depends(get_material_service)],
file: UploadFile = File(...),
) -> ExamMaterialUploadResponse:
template = exam_service.get_owned_template(current_user.id, template_id)
material = material_service.upload(template, file)
message = (
"File uploaded and processed successfully"
if material.status == MaterialStatus.PROCESSED
else "File uploaded but text extraction failed"
)
return ExamMaterialUploadResponse(material=material, message=message)
@router.get("", response_model=list[ExamMaterialRead])
def list_materials(
template_id: uuid.UUID,
current_user: Annotated[User, Depends(get_current_user)],
exam_service: Annotated[ExamService, Depends(get_exam_service)],
material_service: Annotated[MaterialService, Depends(get_material_service)],
) -> list[ExamMaterialRead]:
exam_service.get_owned_template(current_user.id, template_id)
return material_service.list_materials(template_id)
@router.delete("/{material_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_material(
template_id: uuid.UUID,
material_id: uuid.UUID,
current_user: Annotated[User, Depends(get_current_user)],
exam_service: Annotated[ExamService, Depends(get_exam_service)],
material_service: Annotated[MaterialService, Depends(get_material_service)],
) -> None:
template = exam_service.get_owned_template(current_user.id, template_id)
material_service.delete_material(template, material_id)
+27
View File
@@ -0,0 +1,27 @@
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends
from app.api.dependencies import get_exam_service, get_image_service
from app.core.auth import get_current_user
from app.models.user import User
from app.schemas.exam import QuestionRead
from app.schemas.image import QuestionImageAttach
from app.services.exam_service import ExamService
from app.services.image_service import ImageService
router = APIRouter(prefix="/questions", tags=["questions"])
@router.patch("/{question_id}/image", response_model=QuestionRead)
def attach_image_to_question(
question_id: uuid.UUID,
payload: QuestionImageAttach,
current_user: Annotated[User, Depends(get_current_user)],
exam_service: Annotated[ExamService, Depends(get_exam_service)],
image_service: Annotated[ImageService, Depends(get_image_service)],
) -> QuestionRead:
question, template = exam_service.get_owned_question(current_user.id, question_id)
updated = image_service.attach_image_to_question(template, question, payload.image_id)
return exam_service.to_question_read(updated)
+14 -1
View File
@@ -3,11 +3,13 @@ from typing import Annotated
from fastapi import APIRouter, Depends, status
from app.api.dependencies import get_exam_service
from app.api.dependencies import get_exam_service, get_storage_quota_service
from app.core.auth import get_current_user
from app.models.user import User
from app.schemas.exam import ExamTemplateCreate, ExamTemplateRead
from app.schemas.storage import TemplateStorageUsage
from app.services.exam_service import ExamService
from app.services.storage_quota import StorageQuotaService
router = APIRouter(prefix="/templates", tags=["templates"])
@@ -36,3 +38,14 @@ def get_template(
service: Annotated[ExamService, Depends(get_exam_service)],
) -> ExamTemplateRead:
return service.get_template(current_user.id, template_id)
@router.get("/{template_id}/storage", response_model=TemplateStorageUsage)
def get_template_storage_usage(
template_id: uuid.UUID,
current_user: Annotated[User, Depends(get_current_user)],
exam_service: Annotated[ExamService, Depends(get_exam_service)],
storage_quota: Annotated[StorageQuotaService, Depends(get_storage_quota_service)],
) -> TemplateStorageUsage:
exam_service.get_owned_template(current_user.id, template_id)
return TemplateStorageUsage.model_validate(storage_quota.get_usage_summary(template_id))