From bc07c307cfae293e0170198d46701f640d476652 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 3 Apr 2025 14:59:46 -0600 Subject: [PATCH] respuestas cerradas y multiples por id, limite de respuestas abiertas a 250 --- src/cuestionario/dto/formulario.schema.ts | 24 +++++++- .../cuestionario_respondido.documentation.ts | 36 ++++++++---- .../cuestionario_respondido.service.ts | 57 ++++++++++--------- .../dto/submit-respuestas.dto.ts | 45 ++++++++++++--- src/pregunta/dto/create-pregunta.dto.ts | 9 +++ utils/crear_formulario_feria.ts | 10 ++++ 6 files changed, 135 insertions(+), 46 deletions(-) diff --git a/src/cuestionario/dto/formulario.schema.ts b/src/cuestionario/dto/formulario.schema.ts index 2191d01..d1378dc 100644 --- a/src/cuestionario/dto/formulario.schema.ts +++ b/src/cuestionario/dto/formulario.schema.ts @@ -23,7 +23,7 @@ export const FormularioFeriaSchema = { posicion: 1, seccion: { id_seccion: 100, - contador_pregunta: 2, + contador_pregunta: 3, descripcion: 'Información básica', titulo: 'Datos personales' }, @@ -65,8 +65,26 @@ export const FormularioFeriaSchema = { } }, { - id_seccion_pregunta: 1001, + id_seccion_pregunta: 1005, posicion: 2, + pregunta: { + id_pregunta: 109, + pregunta: 'Correo electrónico', + contador_opcion: 0, + obligatoria: true, + id_tipo_pregunta: 2, + id_opcion_dependiente: null, + limite: 250, // Límite de caracteres para respuestas de texto + tipo_pregunta: { + id_tipo: 2, + tipo_pregunta: 'Abierta' + }, + opciones: [] + } + }, + { + id_seccion_pregunta: 1001, + posicion: 3, pregunta: { id_pregunta: 102, pregunta: 'Número de cuenta (solo para estudiantes de la FES Acatlán)', @@ -74,6 +92,7 @@ export const FormularioFeriaSchema = { obligatoria: false, id_tipo_pregunta: 2, id_opcion_dependiente: 90001, // Dependiente de la respuesta "Sí" en la pregunta anterior + limite: 250, // Límite de caracteres para respuestas de texto tipo_pregunta: { id_tipo: 2, tipo_pregunta: 'Abierta' @@ -166,6 +185,7 @@ export const FormularioFeriaSchema = { obligatoria: false, id_tipo_pregunta: 2, id_opcion_dependiente: null, + limite: 250, // Límite de caracteres para respuestas de texto tipo_pregunta: { id_tipo: 2, tipo_pregunta: 'Abierta' diff --git a/src/cuestionario_respondido/cuestionario_respondido.documentation.ts b/src/cuestionario_respondido/cuestionario_respondido.documentation.ts index 5cbc74d..982d89f 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.documentation.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.documentation.ts @@ -5,7 +5,20 @@ export class CuestionarioRespondidoApiDocumentation { static ApiSubmitRespuestas = ApiOperation({ summary: 'Enviar respuestas a un cuestionario', - description: 'Registra las respuestas de un participante a un cuestionario. Crea un registro de participante si no existe uno con ese correo.' + description: `Registra las respuestas de un participante a un cuestionario. Crea un registro de participante si no existe uno con ese correo. + + ## Tipos de respuestas: + + - **Preguntas abiertas**: Enviar el texto de la respuesta como string (máximo 250 caracteres) + - **Preguntas cerradas**: Enviar el ID de la opción como número + - **Preguntas de selección múltiple**: Enviar un array de IDs de opciones como [número, número, ...] + + ### Importante + + Para preguntas cerradas y múltiples, se deben enviar los IDs de las opciones, NO los textos. + Esto evita conflictos cuando existen opciones con el mismo texto pero diferentes IDs. + + Todas las respuestas de texto y el correo electrónico están limitados a 250 caracteres.` }); static ApiSubmitResponse201 = ApiResponse({ @@ -26,7 +39,7 @@ export class CuestionarioRespondidoApiDocumentation { schema: { properties: { statusCode: { type: 'number', example: 400 }, - message: { type: 'string', example: 'No se encontró la opción \'Opción inválida\' para la pregunta 101' }, + message: { type: 'string', example: 'La opción con ID 999 no está asociada a la pregunta 101' }, error: { type: 'string', example: 'Bad Request' } } } @@ -51,29 +64,32 @@ export class CuestionarioRespondidoApiDocumentation { id_formulario: 1, correo: 'estudiante@acatlan.unam.mx', respuestas: [ - { id_pregunta: 101, valor: 'Si' }, // ¿Eres parte de la comunidad de la FES Acatlán? - { id_pregunta: 102, valor: '123456789' }, // Numero de cuenta - { id_pregunta: 106, valor: 'Prefiero no decirlo' } // Género + { id_pregunta: 101, valor: 3 }, // Opción "Si" (usando el ID de la opción) + { id_pregunta: 102, valor: 'estudiante@acatlan.unam.mx' }, // Correo electrónico + { id_pregunta: 103, valor: '123456789' }, // Numero de cuenta + { id_pregunta: 106, valor: 7 } // Opción "Prefiero no decirlo" (usando el ID de la opción) ], fecha_envio: '2025-04-02T13:45:00' }, - description: 'Datos de un estudiante de la FES Acatlán que proporciona su número de cuenta.' + description: 'Datos de un estudiante de la FES Acatlán con respuestas abiertas y opciones por ID.' }, externalExample: { - summary: 'Ejemplo de respuesta para un participante externo', + summary: 'Ejemplo de respuesta para un participante externo con selección múltiple', value: { id_formulario: 1, correo: 'externo@ejemplo.com', respuestas: [ - { id_pregunta: 101, valor: 'No' }, // ¿Eres parte de la comunidad de la FES Acatlán? + { id_pregunta: 101, valor: 4 }, // Opción "No" (usando el ID de la opción) + { id_pregunta: 102, valor: 'externo@ejemplo.com' }, // Correo electrónico { id_pregunta: 103, valor: 'Juan Carlos' }, // Nombre(s) { id_pregunta: 104, valor: 'Ramírez López' }, // Apellidos - { id_pregunta: 106, valor: 'Prefiero no decirlo' }, // Género + { id_pregunta: 105, valor: [2, 5, 8] }, // Selección múltiple (array de IDs de opciones) + { id_pregunta: 106, valor: 7 }, // Opción "Prefiero no decirlo" (usando el ID de la opción) { id_pregunta: 107, valor: 'UAM Azcapotzalco' } // Institución de procedencia ], fecha_envio: '2025-04-02T13:45:00' }, - description: 'Datos de un participante externo que proporciona su nombre e institución de procedencia.' + description: 'Datos de un participante externo con respuestas abiertas, opciones por ID y selección múltiple.' } }; } \ No newline at end of file diff --git a/src/cuestionario_respondido/cuestionario_respondido.service.ts b/src/cuestionario_respondido/cuestionario_respondido.service.ts index 2d4e56e..e1848c0 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.service.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.service.ts @@ -100,41 +100,46 @@ export class CuestionarioRespondidoService { // Determinar tipo de pregunta y guardar respuesta adecuadamente if (esPreguntaAbierta || esPreguntaTipoAbierta) { // Respuesta abierta (texto) + // Convertir a string y truncar a 250 caracteres si es necesario + let respuestaTexto = String(respuestaDto.valor); + if (respuestaTexto.length > 250) { + respuestaTexto = respuestaTexto.substring(0, 250); + } + const respuestaAbierta = new RespuestaParticipanteAbierta(); respuestaAbierta.pregunta = pregunta; - respuestaAbierta.respuesta = respuestaDto.valor; + respuestaAbierta.respuesta = respuestaTexto; respuestaAbierta.cuestionarioRespondido = savedCuestionarioRespondido; await queryRunner.manager.save(respuestaAbierta); } else { // Respuesta cerrada (opción) - // Buscar la opción correspondiente - const opciones = await this.opcionRepository.find({ - where: { opcion: respuestaDto.valor } - }); + // Verificar si es una respuesta múltiple (array de IDs) + const opcionIds = Array.isArray(respuestaDto.valor) + ? respuestaDto.valor + : [respuestaDto.valor]; - if (opciones.length === 0) { - throw new BadRequestException(`No se encontró la opción '${respuestaDto.valor}' para la pregunta ${respuestaDto.id_pregunta}`); + // Procesar cada ID de opción + for (const opcionId of opcionIds) { + // Buscar la relación entre pregunta y opción usando el ID de opción + const preguntaOpciones = await this.preguntaOpcionRepository + .createQueryBuilder('po') + .innerJoinAndSelect('po.opcion', 'opcion') + .where('po.id_pregunta = :preguntaId', { preguntaId: pregunta.id_pregunta }) + .andWhere('opcion.id_opcion = :opcionId', { opcionId: opcionId }) + .getMany(); + + if (preguntaOpciones.length === 0) { + throw new BadRequestException(`La opción con ID ${opcionId} no está asociada a la pregunta ${respuestaDto.id_pregunta}`); + } + + // Guardar respuesta cerrada + const respuestaCerrada = new RespuestaParticipanteCerrada(); + respuestaCerrada.preguntaOpcion = preguntaOpciones[0]; + respuestaCerrada.cuestionarioRespondido = savedCuestionarioRespondido; + + await queryRunner.manager.save(respuestaCerrada); } - - // Buscar la relación entre pregunta y opción - const preguntaOpciones = await this.preguntaOpcionRepository - .createQueryBuilder('po') - .innerJoinAndSelect('po.opcion', 'opcion') - .where('po.id_pregunta = :preguntaId', { preguntaId: pregunta.id_pregunta }) - .andWhere('opcion.id_opcion = :opcionId', { opcionId: opciones[0].id_opcion }) - .getMany(); - - if (preguntaOpciones.length === 0) { - throw new BadRequestException(`La opción '${respuestaDto.valor}' no está asociada a la pregunta ${respuestaDto.id_pregunta}`); - } - - // Guardar respuesta cerrada - const respuestaCerrada = new RespuestaParticipanteCerrada(); - respuestaCerrada.preguntaOpcion = preguntaOpciones[0]; - respuestaCerrada.cuestionarioRespondido = savedCuestionarioRespondido; - - await queryRunner.manager.save(respuestaCerrada); } } diff --git a/src/cuestionario_respondido/dto/submit-respuestas.dto.ts b/src/cuestionario_respondido/dto/submit-respuestas.dto.ts index 305ee7a..4781f76 100644 --- a/src/cuestionario_respondido/dto/submit-respuestas.dto.ts +++ b/src/cuestionario_respondido/dto/submit-respuestas.dto.ts @@ -1,6 +1,16 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsDateString, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; -import { Type } from 'class-transformer'; +import { + IsArray, + IsDateString, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + ValidateNested, + ValidateIf, + MaxLength +} from 'class-validator'; +import { Type, Transform } from 'class-transformer'; export class RespuestaDto { @ApiProperty({ @@ -11,11 +21,25 @@ export class RespuestaDto { id_pregunta: number; @ApiProperty({ - description: 'Valor de la respuesta', - example: 'Si' + description: 'Valor de la respuesta según tipo de pregunta', + examples: [ + 'Texto para pregunta abierta', // String para preguntas abiertas + 5, // Número para preguntas cerradas (ID de opción) + [2, 7, 9] // Array para preguntas múltiples (IDs de opciones) + ], + oneOf: [ + { type: 'string', description: 'Texto para pregunta abierta (máximo 250 caracteres)' }, + { type: 'number', description: 'ID de opción para pregunta cerrada' }, + { + type: 'array', + items: { type: 'number' }, + description: 'Array de IDs de opciones para pregunta multiple' + } + ] }) - @IsString() - valor: string; + @ValidateIf(o => typeof o.valor === 'string') + @MaxLength(250, { message: 'Las respuestas de texto no pueden exceder 250 caracteres' }) + valor: string | number | number[]; } export class SubmitRespuestasDto { @@ -32,6 +56,7 @@ export class SubmitRespuestasDto { }) @IsString() @IsNotEmpty() + @MaxLength(250, { message: 'El correo electrónico no puede exceder 250 caracteres' }) correo: string; @ApiProperty({ @@ -40,11 +65,15 @@ export class SubmitRespuestasDto { examples: [ { id_pregunta: 101, - valor: 'Si' + valor: 'Texto para pregunta abierta' }, { id_pregunta: 102, - valor: '123456789' + valor: 5 // ID de la opción para pregunta cerrada simple + }, + { + id_pregunta: 103, + valor: [2, 7, 9] // Array de IDs de opciones para pregunta de selección múltiple } ] }) diff --git a/src/pregunta/dto/create-pregunta.dto.ts b/src/pregunta/dto/create-pregunta.dto.ts index ddfad5d..dad95d2 100644 --- a/src/pregunta/dto/create-pregunta.dto.ts +++ b/src/pregunta/dto/create-pregunta.dto.ts @@ -36,6 +36,15 @@ export class CreatePreguntaDto { @IsBoolean() obligatoria?: boolean; + @ApiProperty({ + description: 'Límite de caracteres para respuestas de texto (solo aplica a preguntas tipo Abierta)', + example: 250, + required: false + }) + @IsOptional() + @IsNumber() + limite?: number; + @ApiProperty({ description: 'Tipo de pregunta', enum: Object.values(TipoPreguntaEnum), diff --git a/utils/crear_formulario_feria.ts b/utils/crear_formulario_feria.ts index 5f9e9d8..95cd10e 100644 --- a/utils/crear_formulario_feria.ts +++ b/utils/crear_formulario_feria.ts @@ -18,20 +18,29 @@ export const feriaSexualidad = { opciones: [{ valor: 'Si' }, { valor: 'No' }], obligatoria: true, }, + { + titulo: 'Correo electrónico', + obligatoria: true, + tipo: 'Abierta', + limite: 250, + }, { titulo: 'Numero de cuenta', obligatoria: true, tipo: 'Abierta', + limite: 250, }, { titulo: 'Nombre(s)', obligatoria: true, tipo: 'Abierta', + limite: 250, }, { titulo: 'Apellidos', obligatoria: true, tipo: 'Abierta', + limite: 250, }, { titulo: 'Género', @@ -47,6 +56,7 @@ export const feriaSexualidad = { titulo: 'Institución de procedencia', tipo: 'Abierta', obligatoria: true, + limite: 250, }, ], },