-- ============================================================================ -- 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;