"""One general-quiz visibility rule for web, attempts and mobile. Course access is deliberately separate: publication/sharing never grants enrollment. """ from fastapi import HTTPException from sqlalchemy import or_, select from app.models.course import Course, CourseEnrollment from app.models.question import Question from app.models.quiz import Quiz from app.models.quiz_question_link import QuizQuestionLink from app.services.quiz_builder import shareable_question_predicate, bank_question_predicate def quiz_shareable_predicate(user=None): allowed = bank_question_predicate(user) if user is not None else shareable_question_predicate() return ~select(QuizQuestionLink.quiz_id).join(Question, Question.id == QuizQuestionLink.question_id).where( QuizQuestionLink.quiz_id == Quiz.id, allowed.is_not(True), ).exists() def general_quiz_visibility(user): privileged = (Quiz.user_id == user.id) & quiz_shareable_predicate(user) if user.is_moderator: privileged = True return (Quiz.course_id.is_(None) & Quiz.deleted_at.is_(None) & or_( privileged, (or_(Quiz.is_published == 1, Quiz.is_shared == 1) & quiz_shareable_predicate()), )) def can_access_quiz(db, quiz, user): if not quiz or quiz.deleted_at is not None: return False if quiz.course_id is None: return db.query(Quiz.id).filter(Quiz.id == quiz.id, general_quiz_visibility(user)).first() is not None course = db.query(Course).filter(Course.id == quiz.course_id).first() if not course: return False if user.is_moderator or course.user_id == user.id: return True return db.query(CourseEnrollment.id).filter( CourseEnrollment.course_id == course.id, CourseEnrollment.user_id == user.id, ).first() is not None def require_quiz_access(db, quiz, user, review=False): if not can_access_quiz(db, quiz, user): raise HTTPException(403, "This quiz is private or no longer available") if review and quiz.course_id is not None and quiz.allow_review != 1 and not user.is_moderator and quiz.user_id != user.id: raise HTTPException(403, "Review is not allowed for this course quiz") def set_quiz_shared(db, quiz, user, shared): if quiz.user_id != user.id and not user.is_moderator: raise HTTPException(403, "Only the owner or a moderator can change sharing") if quiz.course_id is not None: raise HTTPException(400, "Course quizzes cannot be shared in the general bank") if shared and not db.query(Quiz.id).filter(Quiz.id == quiz.id, quiz_shareable_predicate()).first(): raise HTTPException(400, "This test contains private or course-only questions") quiz.is_shared = int(shared) # Explicit revocation must also revoke legacy publication. if not shared: quiz.is_published = 0 db.commit() return {"id": quiz.id, "is_shared": quiz.is_shared, "is_published": quiz.is_published}