feat: Translate various strings and comments to Spanish for better localization
- Updated error messages and comments in main.rs, openapi.rs, portfolio.rs, predictive.rs, ai.rs, health.rs, middleware.rs, models.rs, token_limits.rs, and webhooks.rs to Spanish. - Enhanced user experience by providing localized content for Spanish-speaking users.
This commit is contained in:
@@ -92,7 +92,7 @@ async fn connect_mysql_pool(env_var: &str) -> Result<sqlx::MySqlPool, (StatusCod
|
||||
use sqlx::mysql::MySqlPoolOptions;
|
||||
|
||||
let mysql_url = std::env::var(env_var)
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, format!("{} not configured", env_var)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, format!("{} no configurada", env_var)))?;
|
||||
|
||||
let mut last_error = String::new();
|
||||
|
||||
@@ -112,7 +112,7 @@ async fn connect_mysql_pool(env_var: &str) -> Result<sqlx::MySqlPool, (StatusCod
|
||||
Err(e) => {
|
||||
last_error = e.to_string();
|
||||
tracing::warn!(
|
||||
"MySQL connection attempt {}/3 failed for {}: {}",
|
||||
"Intento de conexión a MySQL {}/3 fallido para {}: {}",
|
||||
attempt,
|
||||
env_var,
|
||||
last_error
|
||||
@@ -128,13 +128,13 @@ async fn connect_mysql_pool(env_var: &str) -> Result<sqlx::MySqlPool, (StatusCod
|
||||
Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!(
|
||||
"Failed to connect to MySQL after 3 attempts: {}",
|
||||
"Error al conectar a MySQL tras 3 intentos: {}",
|
||||
last_error
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
// ==================== MySQL Study Plans & Courses ====================
|
||||
// ==================== Planes de Estudio y Cursos de MySQL ====================
|
||||
|
||||
#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)]
|
||||
pub struct MySqlStudyPlan {
|
||||
@@ -163,7 +163,7 @@ pub struct MySqlCourse {
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// Save or update study plans and courses from MySQL during import
|
||||
/// Guardar o actualizar planes de estudio y cursos de MySQL durante la importación
|
||||
pub async fn save_mysql_courses_and_plans(
|
||||
pool: &PgPool,
|
||||
org_id: Uuid,
|
||||
@@ -172,14 +172,14 @@ pub async fn save_mysql_courses_and_plans(
|
||||
) -> Result<(), String> {
|
||||
let plans_count = plans.len();
|
||||
let courses_count = courses.len();
|
||||
tracing::info!("Saving {} study plans and {} courses from MySQL", plans_count, courses_count);
|
||||
tracing::info!("Guardando {} planes de estudio y {} cursos de MySQL", plans_count, courses_count);
|
||||
|
||||
// Save study plans first
|
||||
// Guardar planes de estudio primero
|
||||
for plan in plans {
|
||||
let course_type = calculate_course_type(&plan.nombre_plan);
|
||||
tracing::debug!("Saving study plan: {} (ID: {})", plan.nombre_plan, plan.id_plan_de_estudios);
|
||||
tracing::debug!("Guardando plan de estudios: {} (ID: {})", plan.nombre_plan, plan.id_plan_de_estudios);
|
||||
|
||||
// Mirror SAM structure in PostgreSQL using SAM-native column names.
|
||||
// Reflejar la estructura SAM en PostgreSQL usando nombres de columna nativos de SAM.
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO sam_study_plans (organization_id, idPlanDeEstudios, Nombre, Activo)
|
||||
@@ -195,7 +195,7 @@ pub async fn save_mysql_courses_and_plans(
|
||||
.bind(&plan.nombre_plan)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to save SAM study plan mirror: {}", e))?;
|
||||
.map_err(|e| format!("Error al guardar el reflejo del plan de estudios SAM: {}", e))?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -213,15 +213,15 @@ pub async fn save_mysql_courses_and_plans(
|
||||
.bind(&course_type)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to save study plan: {}", e))?;
|
||||
.map_err(|e| format!("Error al guardar el plan de estudios: {}", e))?;
|
||||
}
|
||||
|
||||
// Save courses
|
||||
// Guardar cursos
|
||||
for course in courses {
|
||||
// Determine course_type from duration (40h = regular, 80h = intensive)
|
||||
// Determinar el course_type a partir de la duración (40h = regular, 80h = intensivo)
|
||||
let course_type = calculate_course_type_from_duration(course.duracion);
|
||||
let level_calculated = calculate_course_level(course.nivel_curso);
|
||||
tracing::debug!("Saving course: {} (ID: {}, Plan ID: {})", course.nombre_curso, course.id_cursos, course.id_plan_de_estudios);
|
||||
tracing::debug!("Guardando curso: {} (ID: {}, ID de Plan: {})", course.nombre_curso, course.id_cursos, course.id_plan_de_estudios);
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -246,9 +246,9 @@ pub async fn save_mysql_courses_and_plans(
|
||||
.bind(course.duracion)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to save SAM course mirror: {}", e))?;
|
||||
.map_err(|e| format!("Error al guardar el reflejo del curso SAM: {}", e))?;
|
||||
|
||||
// Get study_plan_id from mysql_study_plans
|
||||
// Obtener study_plan_id de mysql_study_plans
|
||||
let study_plan_id: i32 = sqlx::query_scalar(
|
||||
"SELECT id FROM mysql_study_plans WHERE mysql_id = $1 AND organization_id = $2"
|
||||
)
|
||||
@@ -256,7 +256,7 @@ pub async fn save_mysql_courses_and_plans(
|
||||
.bind(org_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to find study plan: {}", e))?;
|
||||
.map_err(|e| format!("Error al encontrar el plan de estudios: {}", e))?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -284,10 +284,10 @@ pub async fn save_mysql_courses_and_plans(
|
||||
.bind(&level_calculated)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to save course: {}", e))?;
|
||||
.map_err(|e| format!("Error al guardar el curso: {}", e))?;
|
||||
}
|
||||
|
||||
tracing::info!("Successfully saved {} study plans and {} courses", plans_count, courses_count);
|
||||
tracing::info!("Se guardaron con éxito {} planes de estudio y {} cursos", plans_count, courses_count);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -320,9 +320,9 @@ fn calculate_course_level(nivel: Option<i32>) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Create ====================
|
||||
// ==================== Crear ====================
|
||||
|
||||
/// POST /api/question-bank - Create a new question in the bank
|
||||
/// POST /api/question-bank - Crear una nueva pregunta en el banco
|
||||
pub async fn create_question(
|
||||
Org(org_ctx): Org,
|
||||
claims: Claims,
|
||||
@@ -382,9 +382,9 @@ pub async fn create_question(
|
||||
Ok(Json(question))
|
||||
}
|
||||
|
||||
// ==================== List ====================
|
||||
// ==================== Listar ====================
|
||||
|
||||
/// GET /api/question-bank - List questions with filters
|
||||
/// GET /api/question-bank - Listar preguntas con filtros
|
||||
pub async fn list_questions(
|
||||
Org(org_ctx): Org,
|
||||
State(pool): State<PgPool>,
|
||||
@@ -396,7 +396,7 @@ pub async fn list_questions(
|
||||
&& filters.search.is_none()
|
||||
&& filters.has_audio.is_none()
|
||||
{
|
||||
// No filters - simple query
|
||||
// Sin filtros - consulta simple
|
||||
sqlx::query_as::<_, QuestionBank>(
|
||||
&format!(
|
||||
"SELECT {} FROM question_bank WHERE organization_id = $1 AND is_archived = false ORDER BY created_at DESC",
|
||||
@@ -504,9 +504,9 @@ pub async fn list_questions(
|
||||
Ok(Json(questions))
|
||||
}
|
||||
|
||||
// ==================== Get ====================
|
||||
// ==================== Obtener ====================
|
||||
|
||||
/// GET /api/question-bank/{id} - Get a single question
|
||||
/// GET /api/question-bank/{id} - Obtener una sola pregunta
|
||||
pub async fn get_question(
|
||||
Org(org_ctx): Org,
|
||||
Path(id): Path<Uuid>,
|
||||
@@ -528,16 +528,16 @@ pub async fn get_question(
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, "Question not found".to_string()),
|
||||
sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, "Pregunta no encontrada".to_string()),
|
||||
_ => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
|
||||
})?;
|
||||
|
||||
Ok(Json(question))
|
||||
}
|
||||
|
||||
// ==================== Update ====================
|
||||
// ==================== Actualizar ====================
|
||||
|
||||
/// PUT /api/question-bank/{id} - Update a question
|
||||
/// PUT /api/question-bank/{id} - Actualizar una pregunta
|
||||
pub async fn update_question(
|
||||
Org(org_ctx): Org,
|
||||
Path(id): Path<Uuid>,
|
||||
@@ -599,16 +599,16 @@ pub async fn update_question(
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, "Question not found".to_string()),
|
||||
sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, "Pregunta no encontrada".to_string()),
|
||||
_ => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
|
||||
})?;
|
||||
|
||||
Ok(Json(question))
|
||||
}
|
||||
|
||||
// ==================== Delete ====================
|
||||
// ==================== Eliminar ====================
|
||||
|
||||
/// DELETE /api/question-bank/{id} - Delete (archive) a question
|
||||
/// DELETE /api/question-bank/{id} - Eliminar (archivar) una pregunta
|
||||
pub async fn delete_question(
|
||||
Org(org_ctx): Org,
|
||||
Path(id): Path<Uuid>,
|
||||
@@ -628,15 +628,15 @@ pub async fn delete_question(
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Question not found".to_string()));
|
||||
return Err((StatusCode::NOT_FOUND, "Pregunta no encontrada".to_string()));
|
||||
}
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
// ==================== Import from MySQL ====================
|
||||
// ==================== Importar desde MySQL ====================
|
||||
|
||||
/// POST /api/question-bank/import-mysql - Import questions from MySQL question bank
|
||||
/// POST /api/question-bank/import-mysql - Importar preguntas desde el banco de preguntas de MySQL
|
||||
pub async fn import_from_mysql(
|
||||
Org(org_ctx): Org,
|
||||
claims: Claims,
|
||||
@@ -645,10 +645,10 @@ pub async fn import_from_mysql(
|
||||
) -> Result<Json<Vec<QuestionBank>>, (StatusCode, String)> {
|
||||
use serde_json::json;
|
||||
|
||||
// Connect to MySQL
|
||||
// Conectar a MySQL
|
||||
let mysql_pool = connect_mysql_pool("MYSQL_DATABASE_URL").await?;
|
||||
|
||||
// Fetch all study plans and courses from MySQL to sync them
|
||||
// Obtener todos los planes de estudio y cursos de MySQL para sincronizarlos
|
||||
let mysql_plans: Vec<MySqlPlanInfo> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT DISTINCT
|
||||
@@ -662,13 +662,13 @@ pub async fn import_from_mysql(
|
||||
.fetch_all(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("Failed to fetch: {}", e);
|
||||
tracing::error!("MySQL Error: {}", error_msg);
|
||||
tracing::error!("Check column names in your MySQL database (plandeestudios, curso tables)");
|
||||
let error_msg = format!("Error al obtener: {}", e);
|
||||
tracing::error!("Error de MySQL: {}", error_msg);
|
||||
tracing::error!("Verifique los nombres de las columnas en su base de datos MySQL (tablas plandeestudios, curso)");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, error_msg)
|
||||
})?;
|
||||
|
||||
tracing::info!("Fetched {} study plans from MySQL", mysql_plans.len());
|
||||
tracing::info!("Se obtuvieron {} planes de estudio de MySQL", mysql_plans.len());
|
||||
|
||||
let mysql_courses: Vec<MySqlCourseInfo> = sqlx::query_as(
|
||||
r#"
|
||||
@@ -688,19 +688,19 @@ pub async fn import_from_mysql(
|
||||
)
|
||||
.fetch_all(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch courses: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos: {}", e)))?;
|
||||
|
||||
tracing::info!("Fetched {} courses from MySQL", mysql_courses.len());
|
||||
tracing::info!("Se obtuvieron {} cursos de MySQL", mysql_courses.len());
|
||||
|
||||
// Save plans and courses to PostgreSQL
|
||||
tracing::info!("Saving plans and courses to PostgreSQL...");
|
||||
// Guardar planes y cursos en PostgreSQL
|
||||
tracing::info!("Guardando planes y cursos en PostgreSQL...");
|
||||
save_mysql_courses_and_plans(&pool, org_ctx.id, mysql_plans, mysql_courses)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to save courses/plans: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al guardar cursos/planes: {}", e)))?;
|
||||
|
||||
// Fetch questions from MySQL
|
||||
// Obtener preguntas de MySQL
|
||||
let mysql_questions: Vec<MySqlQuestion> = if payload.import_all.unwrap_or(false) {
|
||||
// Import ALL questions (no limit)
|
||||
// Importar TODAS las preguntas (sin límite)
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT bp.idPregunta, bp.descripcion, bp.idTipoPregunta, bp.activo,
|
||||
@@ -716,9 +716,9 @@ pub async fn import_from_mysql(
|
||||
)
|
||||
.fetch_all(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch questions: {}", e)))?
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener las preguntas: {}", e)))?
|
||||
} else if let Some(course_id) = payload.mysql_course_id {
|
||||
// Import all questions for a specific course (no limit)
|
||||
// Importar todas las preguntas para un curso específico (sin límite)
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT bp.idPregunta, bp.descripcion, bp.idTipoPregunta, bp.activo,
|
||||
@@ -735,9 +735,9 @@ pub async fn import_from_mysql(
|
||||
.bind(course_id)
|
||||
.fetch_all(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch questions: {}", e)))?
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener las preguntas: {}", e)))?
|
||||
} else if let Some(question_ids) = payload.question_ids {
|
||||
// Fetch specific question IDs - use simple approach
|
||||
// Obtener IDs de preguntas específicas - usar enfoque simple
|
||||
let mut imported_questions: Vec<QuestionBank> = vec![];
|
||||
|
||||
for q_id in question_ids {
|
||||
@@ -756,10 +756,10 @@ pub async fn import_from_mysql(
|
||||
.bind(q_id)
|
||||
.fetch_optional(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch question {}: {}", q_id, e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener la pregunta {}: {}", q_id, e)))?;
|
||||
|
||||
if let Some(question) = mq {
|
||||
// Map MySQL question type to platform question type
|
||||
// Mapear el tipo de pregunta de MySQL al tipo de pregunta de la plataforma
|
||||
let question_type = map_mysql_question_type(question.id_tipo_pregunta, None);
|
||||
|
||||
let options = if question_type == QuestionBankType::MultipleChoice {
|
||||
@@ -804,7 +804,7 @@ pub async fn import_from_mysql(
|
||||
.bind(&source_metadata)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to import question {}: {}", question.id_pregunta, e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al importar la pregunta {}: {}", question.id_pregunta, e)))?;
|
||||
|
||||
imported_questions.push(qb);
|
||||
}
|
||||
@@ -813,21 +813,21 @@ pub async fn import_from_mysql(
|
||||
mysql_pool.close().await;
|
||||
return Ok(Json(imported_questions));
|
||||
} else {
|
||||
return Err((StatusCode::BAD_REQUEST, "Must provide course_id, question_ids, or import_all".to_string()));
|
||||
return Err((StatusCode::BAD_REQUEST, "Debe proporcionar course_id, question_ids o import_all".to_string()));
|
||||
};
|
||||
|
||||
mysql_pool.close().await;
|
||||
|
||||
if mysql_questions.is_empty() {
|
||||
return Err((StatusCode::NOT_FOUND, "No questions found in MySQL to import".to_string()));
|
||||
return Err((StatusCode::NOT_FOUND, "No se encontraron preguntas en MySQL para importar".to_string()));
|
||||
}
|
||||
|
||||
// Import questions into PostgreSQL
|
||||
// Importar preguntas a PostgreSQL
|
||||
let mut imported_questions: Vec<QuestionBank> = vec![];
|
||||
let mut skipped_count = 0;
|
||||
|
||||
for mq in mysql_questions {
|
||||
// Check if question already imported
|
||||
// Comprobar si la pregunta ya ha sido importada
|
||||
let exists: (bool,) = sqlx::query_as(
|
||||
"SELECT EXISTS(SELECT 1 FROM question_bank WHERE imported_mysql_id = $1 AND organization_id = $2)"
|
||||
)
|
||||
@@ -835,17 +835,17 @@ pub async fn import_from_mysql(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to check existing: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al comprobar existencia: {}", e)))?;
|
||||
|
||||
if exists.0 {
|
||||
skipped_count += 1;
|
||||
continue; // Skip already imported question
|
||||
continue; // Omitir pregunta ya importada
|
||||
}
|
||||
|
||||
// Map MySQL question type to platform question type
|
||||
// Mapear el tipo de pregunta de MySQL al tipo de pregunta de la plataforma
|
||||
let question_type = map_mysql_question_type(mq.id_tipo_pregunta, None);
|
||||
|
||||
// Create options for multiple choice (if applicable)
|
||||
// Crear opciones para opción múltiple (si corresponde)
|
||||
let options = if question_type == QuestionBankType::MultipleChoice {
|
||||
Some(json!(["Opción A", "Opción B", "Opción C", "Opción D"]))
|
||||
} else if question_type == QuestionBankType::TrueFalse {
|
||||
@@ -885,27 +885,27 @@ pub async fn import_from_mysql(
|
||||
.bind(&mq.descripcion)
|
||||
.bind(&question_type)
|
||||
.bind(&options)
|
||||
.bind(&serde_json::Value::Null) // Correct answer to be filled by user
|
||||
.bind(&serde_json::Value::Null) // Respuesta correcta para que el usuario la complete
|
||||
.bind(&source_metadata)
|
||||
.bind(mq.id_pregunta)
|
||||
.bind(mq.id_cursos)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to import question {}: {}", mq.id_pregunta, e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al importar la pregunta {}: {}", mq.id_pregunta, e)))?;
|
||||
|
||||
imported_questions.push(question);
|
||||
}
|
||||
|
||||
tracing::info!("Imported {} questions from MySQL (skipped {} already imported)", imported_questions.len(), skipped_count);
|
||||
tracing::info!("Se importaron {} preguntas de MySQL (se omitieron {} ya importadas)", imported_questions.len(), skipped_count);
|
||||
|
||||
Ok(Json(imported_questions))
|
||||
}
|
||||
|
||||
// ==================== Helpers ====================
|
||||
// ==================== Auxiliares ====================
|
||||
|
||||
fn map_mysql_question_type(mysql_type: i32, tipo_nombre: Option<&str>) -> QuestionBankType {
|
||||
// Map MySQL question types to platform types
|
||||
// First try by name, then by ID as fallback
|
||||
// Mapear tipos de preguntas de MySQL a tipos de plataforma
|
||||
// Primero intentar por nombre, luego por ID como respaldo
|
||||
if let Some(nombre) = tipo_nombre {
|
||||
let nombre_lower = nombre.to_lowercase();
|
||||
if nombre_lower.contains("selecc") || nombre_lower.contains("múltiple") || nombre_lower.contains("multiple") || nombre_lower.contains("alternativa") {
|
||||
@@ -929,13 +929,13 @@ fn map_mysql_question_type(mysql_type: i32, tipo_nombre: Option<&str>) -> Questi
|
||||
if nombre_lower.contains("corta") || nombre_lower.contains("short") || nombre_lower.contains("texto") {
|
||||
return QuestionBankType::ShortAnswer;
|
||||
}
|
||||
// Audio type in MySQL is typically listening comprehension with multiple choice answers
|
||||
// El tipo audio en MySQL suele ser comprensión auditiva con respuestas de opción múltiple
|
||||
if nombre_lower.contains("audio") || nombre_lower.contains("listening") {
|
||||
return QuestionBankType::MultipleChoice;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to ID mapping
|
||||
// Respaldo al mapeo por ID
|
||||
match mysql_type {
|
||||
1 => QuestionBankType::MultipleChoice, // Alternativa
|
||||
2 => QuestionBankType::MultipleChoice, // Audio (listening comprehension)
|
||||
@@ -948,7 +948,7 @@ fn map_mysql_question_type(mysql_type: i32, tipo_nombre: Option<&str>) -> Questi
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse MySQL answers JSON and extract options and correct answer
|
||||
/// Analizar el JSON de respuestas de MySQL y extraer opciones y respuesta correcta
|
||||
fn parse_mysql_answers(
|
||||
answers_json: Option<&str>,
|
||||
question_type: QuestionBankType,
|
||||
@@ -956,13 +956,13 @@ fn parse_mysql_answers(
|
||||
if let Some(json_str) = answers_json {
|
||||
if let Ok(answers) = serde_json::from_str::<Vec<serde_json::Value>>(json_str) {
|
||||
if !answers.is_empty() {
|
||||
// Extract options (all answer texts)
|
||||
// Extraer opciones (todos los textos de respuesta)
|
||||
let options: Vec<String> = answers
|
||||
.iter()
|
||||
.filter_map(|a| a.get("texto").and_then(|t| t.as_str()).map(String::from))
|
||||
.collect();
|
||||
|
||||
// Extract correct answer(s)
|
||||
// Extraer respuesta(s) correcta(s)
|
||||
let correct_indices: Vec<usize> = answers
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -978,16 +978,16 @@ fn parse_mysql_answers(
|
||||
|
||||
let correct_answer = if question_type == QuestionBankType::TrueFalse ||
|
||||
question_type == QuestionBankType::MultipleChoice {
|
||||
// For multiple choice, store index/indices
|
||||
// Para opción múltiple, guardar índice/índices
|
||||
if correct_indices.len() == 1 {
|
||||
Some(serde_json::json!(correct_indices[0]))
|
||||
} else if correct_indices.len() > 1 {
|
||||
Some(serde_json::json!(correct_indices))
|
||||
} else {
|
||||
Some(serde_json::json!(0)) // Default to first
|
||||
Some(serde_json::json!(0)) // Por defecto a la primera
|
||||
}
|
||||
} else {
|
||||
// For other types, store the text
|
||||
// Para otros tipos, guardar el texto
|
||||
correct_indices.first()
|
||||
.and_then(|&i| answers.get(i))
|
||||
.and_then(|a| a.get("texto").and_then(|t| t.as_str()))
|
||||
@@ -999,7 +999,7 @@ fn parse_mysql_answers(
|
||||
}
|
||||
}
|
||||
|
||||
// Default values if no answers provided
|
||||
// Valores por defecto si no se proporcionan respuestas
|
||||
let default_options = match question_type {
|
||||
QuestionBankType::MultipleChoice => Some(serde_json::json!(["Opción A", "Opción B", "Opción C", "Opción D"])),
|
||||
QuestionBankType::TrueFalse => Some(serde_json::json!(["Verdadero", "Falso"])),
|
||||
@@ -1009,17 +1009,17 @@ fn parse_mysql_answers(
|
||||
(default_options, Some(serde_json::json!(0)))
|
||||
}
|
||||
|
||||
// ==================== MySQL Integration ====================
|
||||
// ==================== Integración con MySQL ====================
|
||||
|
||||
/// GET /api/question-bank/mysql-courses - List courses from MySQL for import
|
||||
/// GET /api/question-bank/mysql-courses - Listar cursos de MySQL para importación
|
||||
pub async fn list_mysql_courses(
|
||||
Org(_org_ctx): Org,
|
||||
State(_pool): State<PgPool>,
|
||||
) -> Result<Json<Vec<MySqlCourseInfo>>, (StatusCode, String)> {
|
||||
// Connect to MySQL
|
||||
// Conectar a MySQL
|
||||
let mysql_pool = connect_mysql_pool("MYSQL_DATABASE_URL").await?;
|
||||
|
||||
// Fetch courses with their plan names
|
||||
// Obtener cursos con sus nombres de plan
|
||||
let courses: Vec<MySqlCourseInfo> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT DISTINCT
|
||||
@@ -1038,19 +1038,19 @@ pub async fn list_mysql_courses(
|
||||
)
|
||||
.fetch_all(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch courses: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos: {}", e)))?;
|
||||
|
||||
mysql_pool.close().await;
|
||||
|
||||
Ok(Json(courses))
|
||||
}
|
||||
|
||||
/// GET /api/question-bank/mysql-plans - Get all study plans from PostgreSQL (imported from MySQL)
|
||||
/// GET /api/question-bank/mysql-plans - Obtener todos los planes de estudio de PostgreSQL (importados de MySQL)
|
||||
pub async fn get_mysql_plans(
|
||||
Org(org_ctx): Org,
|
||||
State(pool): State<PgPool>,
|
||||
) -> Result<Json<Vec<MySqlPlanInfo>>, (StatusCode, String)> {
|
||||
// Read from SAM mirror in PostgreSQL with SAM-native fields.
|
||||
// Leer del reflejo SAM en PostgreSQL con campos nativos de SAM.
|
||||
let mut plans: Vec<MySqlPlanInfo> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -1064,9 +1064,9 @@ pub async fn get_mysql_plans(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch plans: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los planes: {}", e)))?;
|
||||
|
||||
// Backward-compatible fallback: if SAM mirror is empty, use legacy metadata mirror.
|
||||
// Respaldo compatible con versiones anteriores: si el reflejo SAM está vacío, usar el reflejo de metadatos heredado.
|
||||
if plans.is_empty() {
|
||||
plans = sqlx::query_as(
|
||||
r#"
|
||||
@@ -1081,10 +1081,10 @@ pub async fn get_mysql_plans(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch legacy plans: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los planes heredados: {}", e)))?;
|
||||
}
|
||||
|
||||
// Last-resort auto-sync: if still empty, pull metadata from MySQL and persist it.
|
||||
// Auto-sincronización de último recurso: si sigue vacío, obtener metadatos de MySQL y persistirlos.
|
||||
if plans.is_empty() {
|
||||
match connect_mysql_pool("MYSQL_DATABASE_URL").await {
|
||||
Ok(mysql_pool) => {
|
||||
@@ -1123,21 +1123,21 @@ pub async fn get_mysql_plans(
|
||||
match (mysql_plans, mysql_courses) {
|
||||
(Ok(p), Ok(c)) => {
|
||||
if let Err(err) = save_mysql_courses_and_plans(&pool, org_ctx.id, p, c).await {
|
||||
tracing::warn!("Auto-sync MySQL metadata failed: {}", err);
|
||||
tracing::warn!("Fallo la auto-sincronización de metadatos de MySQL: {}", err);
|
||||
}
|
||||
}
|
||||
(Err(e), _) => tracing::warn!("Auto-sync plans query failed: {}", e),
|
||||
(_, Err(e)) => tracing::warn!("Auto-sync courses query failed: {}", e),
|
||||
(Err(e), _) => tracing::warn!("Fallo la consulta de planes de auto-sincronización: {}", e),
|
||||
(_, Err(e)) => tracing::warn!("Fallo la consulta de cursos de auto-sincronización: {}", e),
|
||||
}
|
||||
|
||||
mysql_pool.close().await;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Auto-sync could not connect to MySQL: {:?}", e);
|
||||
tracing::warn!("La auto-sincronización no pudo conectarse a MySQL: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Reload plans after auto-sync attempt.
|
||||
// Recargar planes tras el intento de auto-sincronización.
|
||||
plans = sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -1174,13 +1174,13 @@ pub async fn get_mysql_plans(
|
||||
Ok(Json(plans))
|
||||
}
|
||||
|
||||
/// GET /api/question-bank/mysql-courses - Get courses filtered by plan from PostgreSQL
|
||||
/// GET /api/question-bank/mysql-courses - Obtener cursos filtrados por plan de PostgreSQL
|
||||
pub async fn get_mysql_courses_by_plan(
|
||||
Org(org_ctx): Org,
|
||||
State(pool): State<PgPool>,
|
||||
Query(filters): Query<MySqlCoursesFilters>,
|
||||
) -> Result<Json<Vec<MySqlCourseInfo>>, (StatusCode, String)> {
|
||||
// Read from SAM mirror in PostgreSQL with SAM-native fields.
|
||||
// Leer del reflejo SAM en PostgreSQL con campos nativos de SAM.
|
||||
let mut courses: Vec<MySqlCourseInfo> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -1205,9 +1205,9 @@ pub async fn get_mysql_courses_by_plan(
|
||||
.bind(filters.plan_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch courses: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos: {}", e)))?;
|
||||
|
||||
// Backward-compatible fallback: if SAM mirror is empty, use legacy metadata mirror.
|
||||
// Respaldo compatible con versiones anteriores: si el reflejo SAM está vacío, usar el reflejo de metadatos heredado.
|
||||
if courses.is_empty() {
|
||||
courses = sqlx::query_as(
|
||||
r#"
|
||||
@@ -1231,7 +1231,7 @@ pub async fn get_mysql_courses_by_plan(
|
||||
.bind(filters.plan_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch legacy courses: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos heredados: {}", e)))?;
|
||||
}
|
||||
|
||||
Ok(Json(courses))
|
||||
@@ -1255,15 +1255,15 @@ pub struct MySqlCourseInfo {
|
||||
pub nivel_curso: Option<i32>,
|
||||
pub id_plan_de_estudios: i32,
|
||||
pub nombre_plan: String,
|
||||
pub duracion: Option<f64>, // Duration in hours (40=regular, 80=intensive) - MySQL float type
|
||||
pub duracion: Option<f64>, // Duración en horas (40=regular, 80=intensivo) - Tipo float de MySQL
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ImportAllFromMySQLPayload {
|
||||
pub import_metadata_only: Option<bool>, // Only import metadata (courses/plans), not questions
|
||||
pub import_metadata_only: Option<bool>, // Solo importar metadatos (cursos/planes), no preguntas
|
||||
}
|
||||
|
||||
/// POST /api/question-bank/import-mysql-all - Import ALL questions from MySQL (bulk import)
|
||||
/// POST /api/question-bank/import-mysql-all - Importar TODAS las preguntas de MySQL (importación masiva)
|
||||
pub async fn import_all_from_mysql(
|
||||
Org(org_ctx): Org,
|
||||
claims: Claims,
|
||||
@@ -1272,7 +1272,7 @@ pub async fn import_all_from_mysql(
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
use serde_json::json;
|
||||
|
||||
// Parse optional JSON body
|
||||
// Analizar el cuerpo JSON opcional
|
||||
let import_metadata_only = if body.trim().is_empty() {
|
||||
false
|
||||
} else {
|
||||
@@ -1281,10 +1281,10 @@ pub async fn import_all_from_mysql(
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
// Connect to MySQL
|
||||
// Conectar a MySQL
|
||||
let mysql_pool = connect_mysql_pool("MYSQL_DATABASE_URL").await?;
|
||||
|
||||
// Fetch all study plans and courses from MySQL to sync them
|
||||
// Obtener todos los planes de estudio y cursos de MySQL para sincronizarlos
|
||||
let mysql_plans: Vec<MySqlPlanInfo> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT DISTINCT
|
||||
@@ -1298,13 +1298,13 @@ pub async fn import_all_from_mysql(
|
||||
.fetch_all(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("Failed to fetch: {}", e);
|
||||
tracing::error!("MySQL Error: {}", error_msg);
|
||||
tracing::error!("Check column names in your MySQL database (plandeestudios, curso tables)");
|
||||
let error_msg = format!("Error al obtener: {}", e);
|
||||
tracing::error!("Error de MySQL: {}", error_msg);
|
||||
tracing::error!("Verifique los nombres de las columnas en su base de datos MySQL (tablas plandeestudios, curso)");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, error_msg)
|
||||
})?;
|
||||
|
||||
tracing::info!("Fetched {} study plans from MySQL", mysql_plans.len());
|
||||
tracing::info!("Se obtuvieron {} planes de estudio de MySQL", mysql_plans.len());
|
||||
|
||||
let mysql_courses: Vec<MySqlCourseInfo> = sqlx::query_as(
|
||||
r#"
|
||||
@@ -1324,17 +1324,17 @@ pub async fn import_all_from_mysql(
|
||||
)
|
||||
.fetch_all(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch courses: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos: {}", e)))?;
|
||||
|
||||
tracing::info!("Fetched {} courses from MySQL", mysql_courses.len());
|
||||
tracing::info!("Se obtuvieron {} cursos de MySQL", mysql_courses.len());
|
||||
|
||||
// Save plans and courses to PostgreSQL
|
||||
tracing::info!("Saving plans and courses to PostgreSQL...");
|
||||
// Guardar planes y cursos en PostgreSQL
|
||||
tracing::info!("Guardando planes y cursos en PostgreSQL...");
|
||||
save_mysql_courses_and_plans(&pool, org_ctx.id, mysql_plans.clone(), mysql_courses.clone())
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to save courses/plans: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al guardar cursos/planes: {}", e)))?;
|
||||
|
||||
// If only metadata import is requested, return early
|
||||
// Si solo se solicita la importación de metadatos, salir antes
|
||||
if import_metadata_only {
|
||||
return Ok(Json(json!({
|
||||
"success": true,
|
||||
@@ -1353,7 +1353,7 @@ pub async fn import_all_from_mysql(
|
||||
})));
|
||||
}
|
||||
|
||||
// Fetch ALL questions from MySQL with answers (using JSON aggregation for answers)
|
||||
// Obtener TODAS las preguntas de MySQL con respuestas (usando agregación JSON para las respuestas)
|
||||
let mysql_questions: Vec<MySqlQuestionFull> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -1393,7 +1393,7 @@ pub async fn import_all_from_mysql(
|
||||
)
|
||||
.fetch_all(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch questions: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener las preguntas: {}", e)))?;
|
||||
|
||||
mysql_pool.close().await;
|
||||
|
||||
@@ -1403,17 +1403,17 @@ pub async fn import_all_from_mysql(
|
||||
"imported": 0,
|
||||
"skipped": 0,
|
||||
"updated": 0,
|
||||
"error": "No questions found in MySQL"
|
||||
"error": "No se encontraron preguntas en MySQL"
|
||||
})));
|
||||
}
|
||||
|
||||
// Import questions into PostgreSQL
|
||||
// Importar preguntas a PostgreSQL
|
||||
let mut imported_count = 0;
|
||||
let mut skipped_count = 0;
|
||||
let mut updated_count = 0;
|
||||
|
||||
for mq in mysql_questions {
|
||||
// Check if question already exists
|
||||
// Comprobar si la pregunta ya existe
|
||||
let existing: Option<(Uuid, bool)> = sqlx::query_as(
|
||||
"SELECT id, is_active FROM question_bank WHERE imported_mysql_id = $1 AND organization_id = $2"
|
||||
)
|
||||
@@ -1421,13 +1421,13 @@ pub async fn import_all_from_mysql(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Check failed: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Fallo en la comprobación: {}", e)))?;
|
||||
|
||||
match existing {
|
||||
Some((id, is_active)) => {
|
||||
// Question exists - update if it was inactive or has new data
|
||||
// La pregunta existe - actualizar si estaba inactiva o tiene nuevos datos
|
||||
if !is_active {
|
||||
// Reactivate and update
|
||||
// Reactivar y actualizar
|
||||
let _ = sqlx::query(
|
||||
"UPDATE question_bank SET is_active = true, question_text = $3, updated_at = NOW() WHERE id = $1 AND organization_id = $2"
|
||||
)
|
||||
@@ -1438,14 +1438,14 @@ pub async fn import_all_from_mysql(
|
||||
.await;
|
||||
updated_count += 1;
|
||||
} else {
|
||||
skipped_count += 1; // Already exists and active, skip
|
||||
skipped_count += 1; // Ya existe y está activa, omitir
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// New question - insert with answers from MySQL
|
||||
// Nueva pregunta - insertar con respuestas de MySQL
|
||||
let question_type = map_mysql_question_type(mq.id_tipo_pregunta, mq.tipo_pregunta_nombre.as_deref());
|
||||
|
||||
// Parse answers from MySQL
|
||||
// Analizar respuestas de MySQL
|
||||
let (options, correct_answer) = parse_mysql_answers(mq.respuestas_json.as_deref(), question_type);
|
||||
|
||||
let has_answers = mq.respuestas_json.is_some();
|
||||
@@ -1495,7 +1495,7 @@ pub async fn import_all_from_mysql(
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Bulk import from MySQL: {} imported, {} skipped, {} updated",
|
||||
"Importación masiva desde MySQL: {} importadas, {} omitidas, {} actualizadas",
|
||||
imported_count,
|
||||
skipped_count,
|
||||
updated_count
|
||||
@@ -1544,7 +1544,7 @@ pub struct MySqlQuestionFull {
|
||||
pub nivel_curso: Option<i32>,
|
||||
pub id_plan_de_estudios: i32,
|
||||
pub plan_nombre: String,
|
||||
pub respuestas_json: Option<String>, // JSON array de respuestas
|
||||
pub respuestas_json: Option<String>, // Array JSON de respuestas
|
||||
pub tipo_pregunta_nombre: Option<String>, // Nombre del tipo de pregunta
|
||||
}
|
||||
|
||||
@@ -1560,7 +1560,7 @@ struct MySqlQuestion {
|
||||
plan_nombre: String,
|
||||
}
|
||||
|
||||
// ==================== AI Generation ====================
|
||||
// ==================== Generación por IA ====================
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AIGenerateQuestionPayload {
|
||||
@@ -1578,7 +1578,7 @@ pub struct AIQuestionResponse {
|
||||
pub explanation: String,
|
||||
}
|
||||
|
||||
/// POST /question-bank/ai-generate - Generate question options/answers using AI (Ollama only)
|
||||
/// POST /question-bank/ai-generate - Generar opciones/respuestas de preguntas usando IA (solo Ollama)
|
||||
pub async fn ai_generate_question(
|
||||
_org_ctx: Org,
|
||||
_claims: Claims,
|
||||
@@ -1587,11 +1587,11 @@ pub async fn ai_generate_question(
|
||||
use std::env;
|
||||
use std::time::Duration;
|
||||
|
||||
let question_text = payload.question_text.unwrap_or_else(|| "English grammar question".to_string());
|
||||
let question_text = payload.question_text.unwrap_or_else(|| "Pregunta de gramática inglesa".to_string());
|
||||
let difficulty = payload.difficulty.unwrap_or_else(|| "medium".to_string());
|
||||
let skill = payload.skill.unwrap_or_else(|| "grammar".to_string());
|
||||
|
||||
// Build prompt for AI
|
||||
// Construir prompt para la IA
|
||||
let system_prompt = format!(
|
||||
r#"You are an expert English Teacher creating quiz questions.
|
||||
|
||||
@@ -1610,7 +1610,7 @@ pub async fn ai_generate_question(
|
||||
question_text, difficulty, skill
|
||||
);
|
||||
|
||||
// Call Ollama AI with extended timeout
|
||||
// Llamar a Ollama AI con tiempo de espera extendido
|
||||
let base_url = env::var("LOCAL_OLLAMA_URL").unwrap_or_else(|_| "http://localhost:11434".to_string());
|
||||
let model = env::var("LOCAL_LLM_MODEL").unwrap_or_else(|_| "llama3.2:3b".to_string());
|
||||
let url = format!("{}/api/chat", base_url);
|
||||
@@ -1628,37 +1628,37 @@ pub async fn ai_generate_question(
|
||||
"model": model,
|
||||
"messages": [
|
||||
{ "role": "system", "content": system_prompt },
|
||||
{ "role": "user", "content": "Generate the question in JSON format" }
|
||||
{ "role": "user", "content": "Generar la pregunta en formato JSON" }
|
||||
],
|
||||
"stream": false,
|
||||
"format": "json"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("AI request failed: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("La solicitud de IA falló: {}", e)))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("AI API error: {}", response.status())));
|
||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Error de la API de IA: {}", response.status())));
|
||||
}
|
||||
|
||||
let result: serde_json::Value = response.json().await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to parse AI response: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al analizar la respuesta de la IA: {}", e)))?;
|
||||
|
||||
// Extract content from Ollama response
|
||||
// Extraer contenido de la respuesta de Ollama
|
||||
let content = result
|
||||
.get("message")
|
||||
.and_then(|m| m.get("content"))
|
||||
.and_then(|c| c.as_str())
|
||||
.ok_or((StatusCode::INTERNAL_SERVER_ERROR, "Invalid AI response format".to_string()))?;
|
||||
.ok_or((StatusCode::INTERNAL_SERVER_ERROR, "Formato de respuesta de IA no válido".to_string()))?;
|
||||
|
||||
// Parse AI response as JSON
|
||||
// Analizar la respuesta de la IA como JSON
|
||||
let ai_question: AIQuestionResponse = serde_json::from_str(content)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to parse question JSON: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al analizar el JSON de la pregunta: {}", e)))?;
|
||||
|
||||
Ok(Json(ai_question))
|
||||
}
|
||||
|
||||
// ==================== Import Courses from MySQL ====================
|
||||
// ==================== Importar Cursos de MySQL ====================
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ImportCourseFromMySQLPayload {
|
||||
@@ -1678,7 +1678,7 @@ pub struct ImportCourseResult {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// POST /api/question-bank/import-course-mysql - Import a course from MySQL with basic structure
|
||||
/// POST /api/question-bank/import-course-mysql - Importar un curso de MySQL con estructura básica
|
||||
pub async fn import_course_from_mysql(
|
||||
Org(org_ctx): Org,
|
||||
claims: Claims,
|
||||
@@ -1687,10 +1687,10 @@ pub async fn import_course_from_mysql(
|
||||
) -> Result<Json<ImportCourseResult>, (StatusCode, String)> {
|
||||
use common::models::Course;
|
||||
|
||||
// Connect to MySQL
|
||||
// Conectar a MySQL
|
||||
let mysql_pool = connect_mysql_pool("MYSQL_DATABASE_URL").await?;
|
||||
|
||||
// Fetch course info from MySQL
|
||||
// Obtener información del curso de MySQL
|
||||
let mysql_course: MySqlCourseInfo = sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -1710,25 +1710,25 @@ pub async fn import_course_from_mysql(
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if let sqlx::Error::RowNotFound = e {
|
||||
(StatusCode::NOT_FOUND, format!("Course with ID {} not found in MySQL", payload.mysql_course_id))
|
||||
(StatusCode::NOT_FOUND, format!("Curso con ID {} no encontrado en MySQL", payload.mysql_course_id))
|
||||
} else {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch course from MySQL: {}", e))
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el curso de MySQL: {}", e))
|
||||
}
|
||||
})?;
|
||||
|
||||
tracing::info!("Importing course from MySQL: {} (ID: {})", mysql_course.nombre_curso, mysql_course.id_cursos);
|
||||
tracing::info!("Importando curso de MySQL: {} (ID: {})", mysql_course.nombre_curso, mysql_course.id_cursos);
|
||||
|
||||
// Start transaction
|
||||
// Iniciar transacción
|
||||
let mut tx = pool.begin().await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to start transaction: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al iniciar la transacción: {}", e)))?;
|
||||
|
||||
// Determine course type and level for structure generation
|
||||
// Determinar el tipo y nivel del curso para la generación de la estructura
|
||||
let course_type = calculate_course_type_from_duration(mysql_course.duracion);
|
||||
let level = calculate_course_level(mysql_course.nivel_curso);
|
||||
|
||||
tracing::info!("Course type: {}, Level: {}", course_type, level);
|
||||
|
||||
// Create course in PostgreSQL
|
||||
// Crear el curso en PostgreSQL
|
||||
let course_title = payload.title.unwrap_or_else(|| format!("{} ({})", mysql_course.nombre_curso, mysql_course.nombre_plan));
|
||||
let pacing_mode = payload.pacing_mode.unwrap_or_else(|| "self_paced".to_string());
|
||||
let description = payload.description.unwrap_or_else(|| format!("Curso importado desde MySQL - Plan: {}", mysql_course.nombre_plan));
|
||||
@@ -1756,11 +1756,11 @@ pub async fn import_course_from_mysql(
|
||||
.bind(mysql_course.id_cursos)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create course: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al crear el curso: {}", e)))?;
|
||||
|
||||
tracing::info!("Created course in PostgreSQL: {}", new_course.id);
|
||||
tracing::info!("Curso creado en PostgreSQL: {}", new_course.id);
|
||||
|
||||
// Generate basic course structure based on course type and level
|
||||
// Generar estructura básica del curso basada en su tipo y nivel
|
||||
let (modules_count, lessons_count) = generate_course_structure(
|
||||
&mut tx,
|
||||
new_course.id,
|
||||
@@ -1772,12 +1772,12 @@ pub async fn import_course_from_mysql(
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
||||
|
||||
// Commit transaction
|
||||
// Confirmar transacción
|
||||
tx.commit().await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to commit transaction: {}", e)))?;
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al confirmar la transacción: {}", e)))?;
|
||||
|
||||
tracing::info!(
|
||||
"Successfully imported course {} with {} modules and {} lessons",
|
||||
"Curso {} importado con éxito con {} módulos y {} lecciones",
|
||||
new_course.id,
|
||||
modules_count,
|
||||
lessons_count
|
||||
@@ -1794,7 +1794,7 @@ pub async fn import_course_from_mysql(
|
||||
}))
|
||||
}
|
||||
|
||||
/// Generate basic course structure based on type and level
|
||||
/// Generar estructura básica del curso según tipo y nivel
|
||||
async fn generate_course_structure<'a>(
|
||||
tx: &mut sqlx::Transaction<'a, sqlx::Postgres>,
|
||||
course_id: Uuid,
|
||||
@@ -1803,9 +1803,9 @@ async fn generate_course_structure<'a>(
|
||||
level: &str,
|
||||
course_name: &str,
|
||||
) -> Result<(i32, i32), String> {
|
||||
// Define module structure based on course type
|
||||
// Regular (40h): 4 modules
|
||||
// Intensive (80h): 8 modules
|
||||
// Definir la estructura de módulos basada en el tipo de curso
|
||||
// Regular (40h): 4 módulos
|
||||
// Intensivo (80h): 8 módulos
|
||||
let modules_config = match course_type {
|
||||
"intensive" => vec![
|
||||
("Fundamentos Básicos", 6),
|
||||
@@ -1829,7 +1829,7 @@ async fn generate_course_structure<'a>(
|
||||
let mut total_lessons = 0;
|
||||
|
||||
for (module_idx, (module_name, lessons_count)) in modules_config.iter().enumerate() {
|
||||
// Create module
|
||||
// Crear módulo
|
||||
let module: common::models::Module = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO modules (course_id, organization_id, title, position)
|
||||
@@ -1843,16 +1843,16 @@ async fn generate_course_structure<'a>(
|
||||
.bind(module_idx as i32)
|
||||
.fetch_one(&mut **tx)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create module {}: {}", module_idx + 1, e))?;
|
||||
.map_err(|e| format!("Error al crear el módulo {}: {}", module_idx + 1, e))?;
|
||||
|
||||
total_modules += 1;
|
||||
|
||||
// Create lessons for this module
|
||||
// Crear lecciones para este módulo
|
||||
for lesson_idx in 0..*lessons_count {
|
||||
let lesson_position = (module_idx * lessons_count + lesson_idx) as i32;
|
||||
let lesson_title = format!("Lección {}.{}", module_idx + 1, lesson_idx + 1);
|
||||
|
||||
// Determine content type based on position (rotate through types)
|
||||
// Determinar el tipo de contenido basado en la posición (rotar por tipos)
|
||||
let content_types = ["video", "document", "interactive", "quiz"];
|
||||
let content_type = content_types[lesson_idx % content_types.len()];
|
||||
|
||||
@@ -1868,13 +1868,13 @@ async fn generate_course_structure<'a>(
|
||||
.bind(org_id)
|
||||
.bind(&lesson_title)
|
||||
.bind(content_type)
|
||||
.bind("") // Empty content URL (to be filled by instructor)
|
||||
.bind("") // URL de contenido vacía (para que el instructor la complete)
|
||||
.bind(lesson_position)
|
||||
.bind(lesson_idx % 4 == 3) // Every 4th lesson is graded (quiz)
|
||||
.bind(lesson_idx % 4 == 3) // Cada 4ª lección se califica (cuestionario)
|
||||
.bind(format!("Contenido de la lección: {}", lesson_title))
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create lesson {}: {}", lesson_position, e))?;
|
||||
.map_err(|e| format!("Error al crear la lección {}: {}", lesson_position, e))?;
|
||||
|
||||
total_lessons += 1;
|
||||
}
|
||||
@@ -1883,7 +1883,7 @@ async fn generate_course_structure<'a>(
|
||||
Ok((total_modules, total_lessons))
|
||||
}
|
||||
|
||||
// ==================== Import from SAM Diagnostico ====================
|
||||
// ==================== Importar desde SAM Diagnóstico ====================
|
||||
|
||||
/// Row retornada por GROUP BY sobre las tablas de SAM_diagnostico
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
@@ -2011,7 +2011,7 @@ pub async fn import_from_sam_diagnostico(
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to check duplicate: {}", e),
|
||||
format!("Error al comprobar duplicado: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -2083,7 +2083,7 @@ pub async fn import_from_sam_diagnostico(
|
||||
mysql_pool.close().await;
|
||||
|
||||
tracing::info!(
|
||||
"SAM_diagnostico import done: imported={} skipped={} errors={}",
|
||||
"Importación de SAM_diagnostico finalizada: imported={} skipped={} errors={}",
|
||||
total_imported, total_skipped, errors.len()
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user