Merge pull request 'respuestas cerradas y multiples por id, limite de respuestas abiertas a 250' (#13) from eithan into develop
Reviewed-on: #13
This commit was merged in pull request #13.
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user