trading-platform-database-v2/ddl/schemas/education/tables/005_quizzes.sql
rckrdmrd cd6590ec25 [DDL] feat: Sprint 2 - Add education schema with 11 tables
## 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>
2026-01-16 19:48:39 -06:00

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;