LMS / Course System: - Course → Module → Lesson hierarchy (any user can create) - Lesson types: text, video (Vimeo/YouTube/local), document, quiz, live_session - Video provider auto-detection from URL - BBB API integration (create/join meetings) with env config - Course enrollment with per-lesson progress tracking - AI content generation/refinement for text lessons - Draft/published/archived status workflow - Subscription gate placeholder (requires_subscription flag) - Thumbnail upload support - Module/lesson reorder with up/down controls User Quiz Creation: - Any user can create quizzes from question bank (was moderator-only) - User quizzes: is_published=0, is_shared=0 (private by default) - Fixed section_id fallback: None instead of hardcoded 1 Manual Question Creation: - POST /questions/create endpoint for manual MCQ entry - CreateQuestionModal on QuestionBankPage with options, radio for correct answer - Auto-generates embedding on creation Frontend: - CoursesPage: Browse/My Courses/Created tabs with search and pagination - CourseDetailPage: Student view with module accordion, lesson viewer, progress - CourseEditorPage: Full course builder with AI generate, question bank browser - Courses link in Navbar - Create Question button on Question Bank (available to all users) Backend: - 5 new tables: courses, course_modules, course_lessons, course_enrollments, course_lesson_progress - Course model + schemas + router (22 endpoints) - BBB_SERVER_URL + BBB_SECRET config - Updated CLAUDE.md with LMS documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
86 lines
4.4 KiB
Python
86 lines
4.4 KiB
Python
from datetime import datetime
|
|
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Float
|
|
from sqlalchemy.orm import relationship
|
|
from app.database import Base
|
|
|
|
|
|
class Course(Base):
|
|
__tablename__ = "courses"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
title = Column(String, nullable=False)
|
|
description = Column(Text, nullable=True)
|
|
thumbnail_path = Column(String, nullable=True)
|
|
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
status = Column(String, default="draft") # draft, published, archived
|
|
is_featured = Column(Integer, default=0)
|
|
requires_subscription = Column(Integer, default=0) # 0=free, 1=paid (gate ready)
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
modules = relationship("CourseModule", back_populates="course", cascade="all, delete-orphan", order_by="CourseModule.position")
|
|
enrollments = relationship("CourseEnrollment", back_populates="course", cascade="all, delete-orphan")
|
|
creator = relationship("User")
|
|
|
|
|
|
class CourseModule(Base):
|
|
__tablename__ = "course_modules"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
course_id = Column(Integer, ForeignKey("courses.id", ondelete="CASCADE"), nullable=False)
|
|
title = Column(String, nullable=False)
|
|
description = Column(Text, nullable=True)
|
|
position = Column(Integer, default=0)
|
|
|
|
course = relationship("Course", back_populates="modules")
|
|
lessons = relationship("CourseLesson", back_populates="module", cascade="all, delete-orphan", order_by="CourseLesson.position")
|
|
|
|
|
|
class CourseLesson(Base):
|
|
__tablename__ = "course_lessons"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
module_id = Column(Integer, ForeignKey("course_modules.id", ondelete="CASCADE"), nullable=False)
|
|
title = Column(String, nullable=False)
|
|
description = Column(Text, nullable=True)
|
|
position = Column(Integer, default=0)
|
|
lesson_type = Column(String, nullable=False) # text, video, document, quiz, live_session, scorm_package
|
|
content_text = Column(Text, nullable=True) # markdown for text lessons
|
|
video_url = Column(String, nullable=True) # Vimeo/YouTube URL
|
|
video_provider = Column(String, nullable=True) # vimeo, youtube, local
|
|
local_file_path = Column(String, nullable=True) # uploaded files
|
|
quiz_id = Column(Integer, ForeignKey("quizzes.id", ondelete="SET NULL"), nullable=True)
|
|
live_session_url = Column(String, nullable=True) # BBB/Zoom/Meet join link
|
|
live_session_start = Column(DateTime, nullable=True)
|
|
live_session_end = Column(DateTime, nullable=True)
|
|
bbb_meeting_id = Column(String, nullable=True) # for BBB API integration
|
|
duration_minutes = Column(Integer, nullable=True)
|
|
is_required = Column(Integer, default=1) # 1=must complete to progress
|
|
|
|
module = relationship("CourseModule", back_populates="lessons")
|
|
|
|
|
|
class CourseEnrollment(Base):
|
|
__tablename__ = "course_enrollments"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
course_id = Column(Integer, ForeignKey("courses.id", ondelete="CASCADE"), nullable=False)
|
|
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
enrolled_at = Column(DateTime, default=datetime.utcnow)
|
|
completed_at = Column(DateTime, nullable=True)
|
|
progress_pct = Column(Float, default=0.0)
|
|
status = Column(String, default="enrolled") # enrolled, in_progress, completed
|
|
|
|
course = relationship("Course", back_populates="enrollments")
|
|
user = relationship("User")
|
|
lesson_progress = relationship("CourseLessonProgress", back_populates="enrollment", cascade="all, delete-orphan")
|
|
|
|
|
|
class CourseLessonProgress(Base):
|
|
__tablename__ = "course_lesson_progress"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
enrollment_id = Column(Integer, ForeignKey("course_enrollments.id", ondelete="CASCADE"), nullable=False)
|
|
lesson_id = Column(Integer, ForeignKey("course_lessons.id", ondelete="CASCADE"), nullable=False)
|
|
status = Column(String, default="not_started") # not_started, in_progress, completed
|
|
started_at = Column(DateTime, nullable=True)
|
|
completed_at = Column(DateTime, nullable=True)
|
|
score = Column(Float, nullable=True) # for quiz lessons
|
|
time_spent_seconds = Column(Integer, default=0)
|
|
|
|
enrollment = relationship("CourseEnrollment", back_populates="lesson_progress")
|