## Schema: education (Complete Learning Management System) ### Course Structure (3 tables): - categories: Hierarchical course categories with materialized paths - courses: Full course catalog with pricing, access models, gamification - modules: Course sections/modules with sequencing ### Content (3 tables): - lessons: Individual lessons (video, article, interactive) - quizzes: Assessments with configurable rules - quiz_questions: Question bank with multiple types ### Progress Tracking (3 tables): - enrollments: User enrollments with progress tracking - lesson_progress: Detailed per-lesson progress - quiz_attempts: Quiz attempt history with grading ### Completion (2 tables): - course_reviews: Student reviews with moderation - certificates: Verifiable completion certificates ## Features: - 8 custom ENUMs for education domain - Multi-tenancy with RLS policies - Automatic progress calculation triggers - Quiz grading and statistics - Certificate generation with verification codes - Rating aggregation for courses - Gamification support (XP, badges) Total: 11 tables, ~95KB of DDL Roadmap: orchestration/planes/ROADMAP-IMPLEMENTACION-DDL-2026-Q1.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
258 lines
8.9 KiB
PL/PgSQL
258 lines
8.9 KiB
PL/PgSQL
-- ============================================================================
|
|
-- SCHEMA: education
|
|
-- TABLE: lesson_progress
|
|
-- DESCRIPTION: Progreso de usuarios por leccion
|
|
-- VERSION: 1.0.0
|
|
-- CREATED: 2026-01-16
|
|
-- SPRINT: Sprint 2 - DDL Implementation Roadmap Q1-2026
|
|
-- ============================================================================
|
|
|
|
-- Tabla de Progreso por Leccion
|
|
CREATE TABLE IF NOT EXISTS education.lesson_progress (
|
|
-- Identificadores
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
enrollment_id UUID NOT NULL REFERENCES education.enrollments(id) ON DELETE CASCADE,
|
|
lesson_id UUID NOT NULL REFERENCES education.lessons(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE,
|
|
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Estado
|
|
is_started BOOLEAN NOT NULL DEFAULT FALSE,
|
|
is_completed BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
-- Progreso de video
|
|
video_progress_percent DECIMAL(5, 2) DEFAULT 0,
|
|
video_position_seconds INTEGER DEFAULT 0, -- Ultima posicion del video
|
|
video_watched_seconds INTEGER DEFAULT 0, -- Total segundos vistos
|
|
video_completed BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
-- Tiempo
|
|
time_spent_minutes INTEGER NOT NULL DEFAULT 0,
|
|
visit_count INTEGER NOT NULL DEFAULT 1,
|
|
|
|
-- Notas del usuario
|
|
user_notes TEXT,
|
|
bookmarked BOOLEAN NOT NULL DEFAULT FALSE,
|
|
bookmark_position_seconds INTEGER,
|
|
|
|
-- Quiz asociado (si existe)
|
|
quiz_attempted BOOLEAN NOT NULL DEFAULT FALSE,
|
|
quiz_passed BOOLEAN NOT NULL DEFAULT FALSE,
|
|
quiz_score DECIMAL(5, 2),
|
|
|
|
-- Recursos descargados
|
|
resources_downloaded JSONB DEFAULT '[]'::JSONB,
|
|
|
|
-- XP ganado
|
|
xp_earned INTEGER NOT NULL DEFAULT 0,
|
|
|
|
-- Timestamps
|
|
started_at TIMESTAMPTZ,
|
|
completed_at TIMESTAMPTZ,
|
|
last_accessed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Constraints
|
|
CONSTRAINT lesson_progress_unique UNIQUE (enrollment_id, lesson_id),
|
|
CONSTRAINT lesson_progress_video_check CHECK (video_progress_percent BETWEEN 0 AND 100)
|
|
);
|
|
|
|
COMMENT ON TABLE education.lesson_progress IS
|
|
'Tracking detallado del progreso de cada usuario en cada leccion';
|
|
|
|
COMMENT ON COLUMN education.lesson_progress.video_position_seconds IS
|
|
'Ultima posicion de reproduccion del video (para continuar donde lo dejo)';
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_lesson_progress_enrollment
|
|
ON education.lesson_progress(enrollment_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_lesson_progress_lesson
|
|
ON education.lesson_progress(lesson_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_lesson_progress_user
|
|
ON education.lesson_progress(user_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_lesson_progress_tenant
|
|
ON education.lesson_progress(tenant_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_lesson_progress_completed
|
|
ON education.lesson_progress(enrollment_id, is_completed)
|
|
WHERE is_completed = TRUE;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_lesson_progress_in_progress
|
|
ON education.lesson_progress(enrollment_id, lesson_id)
|
|
WHERE is_started = TRUE AND is_completed = FALSE;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_lesson_progress_bookmarked
|
|
ON education.lesson_progress(user_id, bookmarked)
|
|
WHERE bookmarked = TRUE;
|
|
|
|
-- Trigger para updated_at
|
|
DROP TRIGGER IF EXISTS lesson_progress_updated_at ON education.lesson_progress;
|
|
CREATE TRIGGER lesson_progress_updated_at
|
|
BEFORE UPDATE ON education.lesson_progress
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION education.update_education_timestamp();
|
|
|
|
-- Trigger para actualizar started_at y completed_at
|
|
CREATE OR REPLACE FUNCTION education.update_lesson_progress_timestamps()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- Marcar inicio
|
|
IF NEW.is_started = TRUE AND OLD.is_started = FALSE THEN
|
|
NEW.started_at = NOW();
|
|
END IF;
|
|
|
|
-- Marcar completado
|
|
IF NEW.is_completed = TRUE AND OLD.is_completed = FALSE THEN
|
|
NEW.completed_at = NOW();
|
|
|
|
-- Otorgar XP si no se ha ganado
|
|
IF NEW.xp_earned = 0 THEN
|
|
SELECT xp_reward INTO NEW.xp_earned
|
|
FROM education.lessons
|
|
WHERE id = NEW.lesson_id;
|
|
END IF;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS lesson_progress_timestamps ON education.lesson_progress;
|
|
CREATE TRIGGER lesson_progress_timestamps
|
|
BEFORE UPDATE OF is_started, is_completed ON education.lesson_progress
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION education.update_lesson_progress_timestamps();
|
|
|
|
-- Trigger para recalcular progreso del enrollment
|
|
CREATE OR REPLACE FUNCTION education.recalculate_enrollment_on_lesson()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- Recalcular progreso del enrollment
|
|
PERFORM education.calculate_enrollment_progress(NEW.enrollment_id);
|
|
|
|
-- Actualizar tiempo total
|
|
UPDATE education.enrollments
|
|
SET total_time_spent_minutes = (
|
|
SELECT COALESCE(SUM(time_spent_minutes), 0)
|
|
FROM education.lesson_progress
|
|
WHERE enrollment_id = NEW.enrollment_id
|
|
),
|
|
last_activity_at = NOW(),
|
|
last_lesson_id = NEW.lesson_id,
|
|
last_position_seconds = NEW.video_position_seconds
|
|
WHERE id = NEW.enrollment_id;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS lesson_progress_enrollment_update ON education.lesson_progress;
|
|
CREATE TRIGGER lesson_progress_enrollment_update
|
|
AFTER INSERT OR UPDATE OF is_completed, time_spent_minutes ON education.lesson_progress
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION education.recalculate_enrollment_on_lesson();
|
|
|
|
-- Funcion para registrar actividad en leccion
|
|
CREATE OR REPLACE FUNCTION education.record_lesson_activity(
|
|
p_enrollment_id UUID,
|
|
p_lesson_id UUID,
|
|
p_video_position INTEGER DEFAULT NULL,
|
|
p_time_spent_minutes INTEGER DEFAULT 0
|
|
)
|
|
RETURNS UUID AS $$
|
|
DECLARE
|
|
v_progress_id UUID;
|
|
v_user_id UUID;
|
|
v_tenant_id UUID;
|
|
v_lesson RECORD;
|
|
BEGIN
|
|
-- Obtener datos del enrollment
|
|
SELECT user_id, tenant_id INTO v_user_id, v_tenant_id
|
|
FROM education.enrollments
|
|
WHERE id = p_enrollment_id;
|
|
|
|
-- Obtener datos de la leccion
|
|
SELECT * INTO v_lesson
|
|
FROM education.lessons
|
|
WHERE id = p_lesson_id;
|
|
|
|
-- Insertar o actualizar progreso
|
|
INSERT INTO education.lesson_progress (
|
|
enrollment_id, lesson_id, user_id, tenant_id,
|
|
is_started, video_position_seconds, time_spent_minutes,
|
|
last_accessed_at
|
|
) VALUES (
|
|
p_enrollment_id, p_lesson_id, v_user_id, v_tenant_id,
|
|
TRUE, COALESCE(p_video_position, 0), p_time_spent_minutes,
|
|
NOW()
|
|
)
|
|
ON CONFLICT (enrollment_id, lesson_id)
|
|
DO UPDATE SET
|
|
video_position_seconds = COALESCE(p_video_position, education.lesson_progress.video_position_seconds),
|
|
time_spent_minutes = education.lesson_progress.time_spent_minutes + p_time_spent_minutes,
|
|
visit_count = education.lesson_progress.visit_count + 1,
|
|
last_accessed_at = NOW()
|
|
RETURNING id INTO v_progress_id;
|
|
|
|
-- Verificar si se completo el video
|
|
IF p_video_position IS NOT NULL AND v_lesson.video_duration_seconds IS NOT NULL THEN
|
|
IF p_video_position >= (v_lesson.video_duration_seconds * 0.9) THEN
|
|
UPDATE education.lesson_progress
|
|
SET video_completed = TRUE,
|
|
video_progress_percent = 100,
|
|
is_completed = CASE WHEN NOT v_lesson.has_quiz THEN TRUE ELSE is_completed END
|
|
WHERE id = v_progress_id;
|
|
ELSE
|
|
UPDATE education.lesson_progress
|
|
SET video_progress_percent = (p_video_position::DECIMAL / v_lesson.video_duration_seconds * 100)
|
|
WHERE id = v_progress_id;
|
|
END IF;
|
|
END IF;
|
|
|
|
RETURN v_progress_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Vista de progreso detallado de un enrollment
|
|
CREATE OR REPLACE VIEW education.v_enrollment_progress AS
|
|
SELECT
|
|
lp.enrollment_id,
|
|
lp.lesson_id,
|
|
l.title AS lesson_title,
|
|
l.content_type,
|
|
l.sequence_number,
|
|
m.id AS module_id,
|
|
m.title AS module_title,
|
|
m.sequence_number AS module_sequence,
|
|
lp.is_started,
|
|
lp.is_completed,
|
|
lp.video_progress_percent,
|
|
lp.time_spent_minutes,
|
|
lp.quiz_passed,
|
|
lp.completed_at
|
|
FROM education.lesson_progress lp
|
|
JOIN education.lessons l ON lp.lesson_id = l.id
|
|
JOIN education.modules m ON l.module_id = m.id
|
|
ORDER BY m.sequence_number, l.sequence_number;
|
|
|
|
-- RLS Policy para multi-tenancy
|
|
ALTER TABLE education.lesson_progress ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY lesson_progress_tenant_isolation ON education.lesson_progress
|
|
FOR ALL
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
|
|
CREATE POLICY lesson_progress_user_isolation ON education.lesson_progress
|
|
FOR SELECT
|
|
USING (user_id = current_setting('app.current_user_id', true)::UUID);
|
|
|
|
-- Grants
|
|
GRANT SELECT, INSERT, UPDATE ON education.lesson_progress TO trading_app;
|
|
GRANT SELECT ON education.lesson_progress TO trading_readonly;
|
|
GRANT SELECT ON education.v_enrollment_progress TO trading_app;
|
|
GRANT EXECUTE ON FUNCTION education.record_lesson_activity TO trading_app;
|