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
66 lines
3.1 KiB
Python
66 lines
3.1 KiB
Python
"""Public share-link endpoints: token lifecycle and public landing data."""
|
|
import unittest
|
|
from datetime import datetime
|
|
|
|
import test_quiz_builder as fixtures
|
|
from app.models.quiz import Quiz
|
|
from app.models.quiz_question_link import QuizQuestionLink
|
|
from app.routers import quizzes, share
|
|
|
|
|
|
class ShareLinkTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.bank = fixtures.BuilderTests()
|
|
self.bank.setUp()
|
|
self.client = self.bank.client
|
|
self.client.app.include_router(quizzes.router, prefix='/quizzes')
|
|
self.client.app.include_router(share.router, prefix='/share')
|
|
|
|
def tearDown(self):
|
|
self.bank.tearDown()
|
|
|
|
def test_owner_only_token_lifecycle_and_public_landing(self):
|
|
self.bank.user = self.bank.owner
|
|
self.assertEqual(self.client.post('/quizzes/1/share-link').status_code, 403)
|
|
self.bank.user = self.bank.mod
|
|
created = self.client.post('/quizzes/1/share-link')
|
|
self.assertEqual(created.status_code, 200, created.text)
|
|
token = created.json()['token']
|
|
self.assertEqual(self.client.post('/quizzes/1/share-link').json()['token'], token) # Stable token.
|
|
public = self.client.get(f'/share/{token}')
|
|
self.assertEqual(public.status_code, 200, public.text)
|
|
data = public.json()
|
|
self.assertEqual(data['quiz_id'], 1)
|
|
self.assertEqual(data['title'], 'Origin')
|
|
self.assertNotIn('questions', data)
|
|
self.assertNotIn('correct_answer', str(data))
|
|
self.assertEqual(self.client.get('/share/unknown-token').status_code, 404)
|
|
self.bank.user = self.bank.peer
|
|
detail = self.client.get('/quizzes/1')
|
|
self.assertEqual(detail.json()['share_token'], token)
|
|
self.assertEqual(self.client.delete('/quizzes/1/share-link').status_code, 403)
|
|
self.bank.user = self.bank.mod
|
|
self.assertEqual(self.client.delete('/quizzes/1/share-link').status_code, 204)
|
|
self.assertEqual(self.client.get(f'/share/{token}').status_code, 404)
|
|
self.assertEqual(self.bank.db.get(Quiz, 1).is_shared, 0)
|
|
self.assertEqual(self.client.get('/quizzes/1').json()['share_token'], None)
|
|
|
|
def test_a_quiz_holding_a_removed_question_cannot_share(self):
|
|
self.bank.user = self.bank.mod
|
|
# A session carrying a question nobody can reach cannot be handed on.
|
|
# That used to mean one its author kept back; it means a removed one.
|
|
mixed = Quiz(title='Removed mix', user_id=3, is_published=1, questions_count=1)
|
|
self.bank.db.add(mixed)
|
|
self.bank.db.flush()
|
|
self.bank.db.add(QuizQuestionLink(quiz_id=mixed.id, question_id=4, position=0))
|
|
self.bank.db.get(fixtures.Question, 4).deleted_at = datetime(2026, 1, 1)
|
|
self.bank.db.commit()
|
|
self.assertEqual(self.client.post(f'/quizzes/{mixed.id}/share-link').status_code, 400)
|
|
mixed.is_shared = 1 # Stale share flag alone must not make it public.
|
|
self.bank.db.commit()
|
|
self.assertEqual(self.client.post(f'/quizzes/{mixed.id}/share-link').status_code, 400)
|
|
self.assertEqual(self.bank.db.get(Quiz, mixed.id).share_token, None)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|