## 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>
186 lines
5.7 KiB
PL/PgSQL
186 lines
5.7 KiB
PL/PgSQL
-- ============================================================================
|
|
-- SCHEMA: education
|
|
-- TABLE: lessons
|
|
-- DESCRIPTION: Lecciones individuales de un modulo
|
|
-- VERSION: 1.0.0
|
|
-- CREATED: 2026-01-16
|
|
-- SPRINT: Sprint 2 - DDL Implementation Roadmap Q1-2026
|
|
-- ============================================================================
|
|
|
|
-- Tabla de Lecciones
|
|
CREATE TABLE IF NOT EXISTS education.lessons (
|
|
-- Identificadores
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
module_id UUID NOT NULL REFERENCES education.modules(id) ON DELETE CASCADE,
|
|
course_id UUID NOT NULL REFERENCES education.courses(id) ON DELETE CASCADE,
|
|
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Informacion basica
|
|
title VARCHAR(200) NOT NULL,
|
|
slug VARCHAR(200) NOT NULL,
|
|
description TEXT,
|
|
|
|
-- Tipo de contenido
|
|
content_type education.content_type NOT NULL DEFAULT 'video',
|
|
|
|
-- Orden
|
|
sequence_number INTEGER NOT NULL DEFAULT 0,
|
|
|
|
-- Contenido principal
|
|
content TEXT, -- Contenido HTML/Markdown
|
|
content_json JSONB, -- Contenido estructurado
|
|
|
|
-- Video
|
|
video_url TEXT,
|
|
video_provider VARCHAR(50), -- 'youtube', 'vimeo', 'bunny', 'self'
|
|
video_id VARCHAR(100),
|
|
video_duration_seconds INTEGER,
|
|
video_thumbnail_url TEXT,
|
|
|
|
-- Recursos
|
|
resources JSONB DEFAULT '[]'::JSONB, -- Array de archivos descargables
|
|
-- [{ "name": "PDF", "url": "...", "type": "pdf", "size": 1024 }]
|
|
|
|
-- Interactividad
|
|
has_quiz BOOLEAN NOT NULL DEFAULT FALSE,
|
|
quiz_id UUID, -- Quiz asociado a esta leccion
|
|
|
|
-- Estado
|
|
status education.publish_status NOT NULL DEFAULT 'draft',
|
|
is_preview BOOLEAN NOT NULL DEFAULT FALSE,
|
|
is_mandatory BOOLEAN NOT NULL DEFAULT TRUE, -- Obligatoria para completar curso
|
|
|
|
-- Duracion
|
|
estimated_duration_minutes INTEGER,
|
|
|
|
-- Requisitos
|
|
requires_previous_completion BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
-- Gamificacion
|
|
xp_reward INTEGER DEFAULT 10,
|
|
|
|
-- AI Generated content
|
|
is_ai_generated BOOLEAN NOT NULL DEFAULT FALSE,
|
|
ai_generation_prompt TEXT,
|
|
|
|
-- Metadata
|
|
metadata JSONB DEFAULT '{}'::JSONB,
|
|
|
|
-- Timestamps
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Constraints
|
|
CONSTRAINT lessons_unique_sequence UNIQUE (module_id, sequence_number),
|
|
CONSTRAINT lessons_unique_slug UNIQUE (module_id, slug)
|
|
);
|
|
|
|
COMMENT ON TABLE education.lessons IS
|
|
'Lecciones individuales que componen un modulo del curso';
|
|
|
|
COMMENT ON COLUMN education.lessons.content_type IS
|
|
'Tipo de contenido: video, article, quiz, interactive, download, live, assignment';
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_lessons_module
|
|
ON education.lessons(module_id, sequence_number);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_lessons_course
|
|
ON education.lessons(course_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_lessons_tenant
|
|
ON education.lessons(tenant_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_lessons_status
|
|
ON education.lessons(status);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_lessons_content_type
|
|
ON education.lessons(content_type);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_lessons_preview
|
|
ON education.lessons(course_id, is_preview)
|
|
WHERE is_preview = TRUE;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_lessons_with_quiz
|
|
ON education.lessons(quiz_id)
|
|
WHERE quiz_id IS NOT NULL;
|
|
|
|
-- Trigger para updated_at
|
|
DROP TRIGGER IF EXISTS lesson_updated_at ON education.lessons;
|
|
CREATE TRIGGER lesson_updated_at
|
|
BEFORE UPDATE ON education.lessons
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION education.update_education_timestamp();
|
|
|
|
-- Trigger para actualizar conteos
|
|
CREATE OR REPLACE FUNCTION education.update_lesson_counts()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF TG_OP = 'INSERT' THEN
|
|
-- Actualizar modulo
|
|
UPDATE education.modules
|
|
SET lesson_count = lesson_count + 1
|
|
WHERE id = NEW.module_id;
|
|
|
|
-- Actualizar curso
|
|
UPDATE education.courses
|
|
SET total_lessons = total_lessons + 1,
|
|
last_content_update = NOW()
|
|
WHERE id = NEW.course_id;
|
|
|
|
ELSIF TG_OP = 'DELETE' THEN
|
|
UPDATE education.modules
|
|
SET lesson_count = lesson_count - 1
|
|
WHERE id = OLD.module_id;
|
|
|
|
UPDATE education.courses
|
|
SET total_lessons = total_lessons - 1,
|
|
last_content_update = NOW()
|
|
WHERE id = OLD.course_id;
|
|
END IF;
|
|
|
|
RETURN COALESCE(NEW, OLD);
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS lesson_counts ON education.lessons;
|
|
CREATE TRIGGER lesson_counts
|
|
AFTER INSERT OR DELETE ON education.lessons
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION education.update_lesson_counts();
|
|
|
|
-- Vista de lecciones de un curso
|
|
CREATE OR REPLACE VIEW education.v_course_lessons AS
|
|
SELECT
|
|
l.id,
|
|
l.module_id,
|
|
l.course_id,
|
|
m.title AS module_title,
|
|
m.sequence_number AS module_sequence,
|
|
l.title,
|
|
l.slug,
|
|
l.content_type,
|
|
l.sequence_number,
|
|
l.video_duration_seconds,
|
|
l.estimated_duration_minutes,
|
|
l.is_preview,
|
|
l.is_mandatory,
|
|
l.has_quiz,
|
|
l.status
|
|
FROM education.lessons l
|
|
JOIN education.modules m ON l.module_id = m.id
|
|
WHERE l.status = 'published'
|
|
ORDER BY m.sequence_number, l.sequence_number;
|
|
|
|
-- RLS Policy para multi-tenancy
|
|
ALTER TABLE education.lessons ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY lessons_tenant_isolation ON education.lessons
|
|
FOR ALL
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
|
|
-- Grants
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON education.lessons TO trading_app;
|
|
GRANT SELECT ON education.lessons TO trading_readonly;
|
|
GRANT SELECT ON education.v_course_lessons TO trading_app;
|