## 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>
220 lines
6.8 KiB
PL/PgSQL
220 lines
6.8 KiB
PL/PgSQL
-- ============================================================================
|
|
-- SCHEMA: education
|
|
-- TABLE: quizzes
|
|
-- DESCRIPTION: Cuestionarios y evaluaciones
|
|
-- VERSION: 1.0.0
|
|
-- CREATED: 2026-01-16
|
|
-- SPRINT: Sprint 2 - DDL Implementation Roadmap Q1-2026
|
|
-- ============================================================================
|
|
|
|
-- Enum para tipo de quiz
|
|
DO $$ BEGIN
|
|
CREATE TYPE education.quiz_type AS ENUM (
|
|
'practice', -- Quiz de practica (sin limite)
|
|
'graded', -- Quiz calificado
|
|
'final_exam', -- Examen final
|
|
'diagnostic', -- Evaluacion diagnostica
|
|
'survey' -- Encuesta (sin respuestas correctas)
|
|
);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
|
|
-- Tabla de Quizzes
|
|
CREATE TABLE IF NOT EXISTS education.quizzes (
|
|
-- Identificadores
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
course_id UUID NOT NULL REFERENCES education.courses(id) ON DELETE CASCADE,
|
|
module_id UUID REFERENCES education.modules(id) ON DELETE SET NULL,
|
|
lesson_id UUID REFERENCES education.lessons(id) ON DELETE SET NULL,
|
|
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,
|
|
instructions TEXT,
|
|
|
|
-- Tipo
|
|
type education.quiz_type NOT NULL DEFAULT 'practice',
|
|
|
|
-- Configuracion
|
|
time_limit_minutes INTEGER, -- NULL = sin limite
|
|
passing_score INTEGER NOT NULL DEFAULT 70, -- Porcentaje minimo para aprobar
|
|
max_attempts INTEGER, -- NULL = ilimitado
|
|
shuffle_questions BOOLEAN NOT NULL DEFAULT TRUE,
|
|
shuffle_answers BOOLEAN NOT NULL DEFAULT TRUE,
|
|
show_correct_answers BOOLEAN NOT NULL DEFAULT TRUE,
|
|
show_explanations BOOLEAN NOT NULL DEFAULT TRUE,
|
|
immediate_feedback BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
-- Preguntas
|
|
question_count INTEGER NOT NULL DEFAULT 0,
|
|
questions_to_show INTEGER, -- NULL = mostrar todas
|
|
points_per_question INTEGER DEFAULT 1,
|
|
|
|
-- Estado
|
|
status education.publish_status NOT NULL DEFAULT 'draft',
|
|
|
|
-- Disponibilidad
|
|
available_from TIMESTAMPTZ,
|
|
available_until TIMESTAMPTZ,
|
|
|
|
-- Requisitos
|
|
requires_lesson_completion BOOLEAN NOT NULL DEFAULT FALSE,
|
|
required_lessons UUID[], -- Lecciones que deben completarse antes
|
|
|
|
-- Gamificacion
|
|
xp_reward INTEGER DEFAULT 50,
|
|
badge_on_perfect_score UUID,
|
|
|
|
-- Estadisticas (cache)
|
|
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
average_score DECIMAL(5, 2) DEFAULT 0,
|
|
pass_rate DECIMAL(5, 2) DEFAULT 0,
|
|
|
|
-- Metadata
|
|
metadata JSONB DEFAULT '{}'::JSONB,
|
|
|
|
-- Timestamps
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Constraints
|
|
CONSTRAINT quizzes_unique_slug UNIQUE (course_id, slug),
|
|
CONSTRAINT quizzes_passing_score_check CHECK (passing_score BETWEEN 0 AND 100)
|
|
);
|
|
|
|
COMMENT ON TABLE education.quizzes IS
|
|
'Cuestionarios y evaluaciones del sistema educativo';
|
|
|
|
COMMENT ON COLUMN education.quizzes.questions_to_show IS
|
|
'Si es menor que question_count, se seleccionan aleatoriamente';
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_quizzes_course
|
|
ON education.quizzes(course_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_quizzes_module
|
|
ON education.quizzes(module_id)
|
|
WHERE module_id IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_quizzes_lesson
|
|
ON education.quizzes(lesson_id)
|
|
WHERE lesson_id IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_quizzes_tenant
|
|
ON education.quizzes(tenant_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_quizzes_type
|
|
ON education.quizzes(type);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_quizzes_status
|
|
ON education.quizzes(status);
|
|
|
|
-- Trigger para updated_at
|
|
DROP TRIGGER IF EXISTS quiz_updated_at ON education.quizzes;
|
|
CREATE TRIGGER quiz_updated_at
|
|
BEFORE UPDATE ON education.quizzes
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION education.update_education_timestamp();
|
|
|
|
-- Trigger para actualizar quiz_count en modulo y curso
|
|
CREATE OR REPLACE FUNCTION education.update_quiz_counts()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF TG_OP = 'INSERT' THEN
|
|
IF NEW.module_id IS NOT NULL THEN
|
|
UPDATE education.modules
|
|
SET quiz_count = quiz_count + 1
|
|
WHERE id = NEW.module_id;
|
|
END IF;
|
|
|
|
UPDATE education.courses
|
|
SET total_quizzes = total_quizzes + 1
|
|
WHERE id = NEW.course_id;
|
|
|
|
ELSIF TG_OP = 'DELETE' THEN
|
|
IF OLD.module_id IS NOT NULL THEN
|
|
UPDATE education.modules
|
|
SET quiz_count = quiz_count - 1
|
|
WHERE id = OLD.module_id;
|
|
END IF;
|
|
|
|
UPDATE education.courses
|
|
SET total_quizzes = total_quizzes - 1
|
|
WHERE id = OLD.course_id;
|
|
END IF;
|
|
|
|
RETURN COALESCE(NEW, OLD);
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS quiz_counts ON education.quizzes;
|
|
CREATE TRIGGER quiz_counts
|
|
AFTER INSERT OR DELETE ON education.quizzes
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION education.update_quiz_counts();
|
|
|
|
-- Actualizar lesson.has_quiz cuando se asocia un quiz
|
|
CREATE OR REPLACE FUNCTION education.update_lesson_has_quiz()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
|
|
IF NEW.lesson_id IS NOT NULL THEN
|
|
UPDATE education.lessons
|
|
SET has_quiz = TRUE, quiz_id = NEW.id
|
|
WHERE id = NEW.lesson_id;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF TG_OP = 'DELETE' OR (TG_OP = 'UPDATE' AND OLD.lesson_id != NEW.lesson_id) THEN
|
|
IF OLD.lesson_id IS NOT NULL THEN
|
|
UPDATE education.lessons
|
|
SET has_quiz = FALSE, quiz_id = NULL
|
|
WHERE id = OLD.lesson_id;
|
|
END IF;
|
|
END IF;
|
|
|
|
RETURN COALESCE(NEW, OLD);
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS quiz_lesson_link ON education.quizzes;
|
|
CREATE TRIGGER quiz_lesson_link
|
|
AFTER INSERT OR UPDATE OF lesson_id OR DELETE ON education.quizzes
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION education.update_lesson_has_quiz();
|
|
|
|
-- Vista de quizzes de un curso
|
|
CREATE OR REPLACE VIEW education.v_course_quizzes AS
|
|
SELECT
|
|
q.id,
|
|
q.course_id,
|
|
q.module_id,
|
|
q.lesson_id,
|
|
q.title,
|
|
q.type,
|
|
q.time_limit_minutes,
|
|
q.passing_score,
|
|
q.max_attempts,
|
|
q.question_count,
|
|
q.average_score,
|
|
q.pass_rate,
|
|
q.status
|
|
FROM education.quizzes q
|
|
WHERE q.status = 'published'
|
|
ORDER BY q.course_id, q.created_at;
|
|
|
|
-- RLS Policy para multi-tenancy
|
|
ALTER TABLE education.quizzes ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY quizzes_tenant_isolation ON education.quizzes
|
|
FOR ALL
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
|
|
-- Grants
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON education.quizzes TO trading_app;
|
|
GRANT SELECT ON education.quizzes TO trading_readonly;
|
|
GRANT SELECT ON education.v_course_quizzes TO trading_app;
|