respuestas cerradas y multiples por id, limite de respuestas abiertas a 250

This commit is contained in:
Your Name
2025-04-03 14:59:46 -06:00
parent 8208ff0292
commit bc07c307cf
6 changed files with 135 additions and 46 deletions
+22 -2
View File
@@ -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'
@@ -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.'
}
};
}
@@ -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);
}
}
@@ -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
}
]
})
+9
View File
@@ -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),
+10
View File
@@ -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,
},
],
},