There will be no courses. What was there: one draft called "jk" with two empty lessons, and 4,000 lines of code around it — courses, modules, lessons, enrolments, per-lesson progress, SCORM, BigBlueButton, completion certificates, three React pages, a router, two models. Its real cost was everywhere else. Every query that measured practice had to remember `Quiz.course_id.is_(None)`, and forgetting it in one place would have silently mixed course attempts into a learner's analytics; the bank predicate carried a subquery to exclude a course's own questions from every search, recommendation and share; quiz access had a second, parallel rule about enrolment. All of that is gone, so the remaining rules say what they mean. `quizzes.allow_review` goes with it. It was only ever enforced for a course quiz, so it had become a promise nothing keeps — the public session page was still offering "no answer review" about sessions that review fine. The fixtures' question 5 lived in a course quiz and stood for "a question that exists but is not in your bank". There is no such thing now — a question is in the bank unless it is deleted — so the counts it kept out of the numbers are back in, and the tests that turned on it now turn on deletion or on the attempt that actually holds a question. Files the LMS uploaded stay on disk and stay protected: LEGACY_LMS_PREFIXES in app/utils/upload_access.py is what keeps them unreachable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
40 lines
1.5 KiB
Python
40 lines
1.5 KiB
Python
"""Public share links — viewing quiz info does not require a login."""
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.database import get_db
|
|
from app.models.quiz import Quiz
|
|
from app.models.user import User
|
|
from app.services.quiz_builder import category_breadcrumbs
|
|
from app.models.question_category import QuestionCategory
|
|
from app.utils.quiz_access import quiz_shareable_predicate
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/{token}")
|
|
def get_shared_quiz(
|
|
token: str,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Public quiz landing data; never exposes questions or answers."""
|
|
quiz = db.query(Quiz).filter(
|
|
Quiz.share_token == token,
|
|
Quiz.deleted_at.is_(None),
|
|
Quiz.is_shared == 1,
|
|
).first()
|
|
if not quiz or not db.query(Quiz.id).filter(
|
|
Quiz.id == quiz.id, quiz_shareable_predicate()).first():
|
|
raise HTTPException(404, "Shared quiz not found")
|
|
owner = db.get(User, quiz.user_id) if quiz.user_id else None
|
|
categories = db.query(QuestionCategory).all()
|
|
return {
|
|
"quiz_id": quiz.id,
|
|
"title": quiz.title,
|
|
"mode": quiz.mode,
|
|
"questions_count": quiz.questions_count,
|
|
"time_limit_minutes": quiz.time_limit_minutes,
|
|
"owner_name": owner.name if owner else None,
|
|
"category": quiz.category.name if quiz.category else None,
|
|
"category_breadcrumbs": category_breadcrumbs(categories, quiz.category_id) if quiz.category_id else [],
|
|
}
|