feat:fix data importation
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
# Importación de Cursos desde MySQL
|
||||
|
||||
## Descripción
|
||||
|
||||
OpenCCB ahora soporta la importación completa de cursos desde una base de datos MySQL externa. Esta funcionalidad permite:
|
||||
|
||||
1. **Importar metadatos** de cursos y planes de estudio
|
||||
2. **Importar preguntas** del banco de preguntas MySQL
|
||||
3. **Crear cursos completos** con estructura básica (módulos y lecciones)
|
||||
|
||||
## Endpoints Implementados
|
||||
|
||||
### 1. Importar Metadatos (Cursos y Planes)
|
||||
|
||||
```bash
|
||||
POST /question-bank/import-mysql-all
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <TOKEN>
|
||||
|
||||
{
|
||||
"import_metadata_only": true
|
||||
}
|
||||
```
|
||||
|
||||
**Respuesta:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Metadatos importados exitosamente",
|
||||
"metadata": {
|
||||
"study_plans_imported": 5,
|
||||
"courses_imported": 20,
|
||||
"courses": [
|
||||
{
|
||||
"id_cursos": 1,
|
||||
"nombre_curso": "Inglés Básico",
|
||||
"nombre_plan": "Plan Regular",
|
||||
"duracion": 40,
|
||||
"nivel_curso": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Ver Cursos Disponibles
|
||||
|
||||
```bash
|
||||
GET /question-bank/mysql-courses
|
||||
Authorization: Bearer <TOKEN>
|
||||
```
|
||||
|
||||
**Respuesta:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id_cursos": 1,
|
||||
"nombre_curso": "Inglés Básico",
|
||||
"nombre_plan": "Plan Regular",
|
||||
"duracion": 40,
|
||||
"nivel_curso": 2
|
||||
},
|
||||
{
|
||||
"id_cursos": 2,
|
||||
"nombre_curso": "Inglés Intermedio",
|
||||
"nombre_plan": "Plan Intensivo",
|
||||
"duracion": 80,
|
||||
"nivel_curso": 6
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 3. Importar Curso Completo con Estructura
|
||||
|
||||
```bash
|
||||
POST /question-bank/import-course-mysql
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <TOKEN>
|
||||
|
||||
{
|
||||
"mysql_course_id": 1,
|
||||
"title": "Opcional: Título personalizado",
|
||||
"description": "Opcional: Descripción del curso",
|
||||
"pacing_mode": "self_paced"
|
||||
}
|
||||
```
|
||||
|
||||
**Respuesta:**
|
||||
```json
|
||||
{
|
||||
"course_id": "uuid-del-curso",
|
||||
"course_title": "Inglés Básico (Plan Regular)",
|
||||
"mysql_course_id": 1,
|
||||
"modules_created": 4,
|
||||
"lessons_created": 20,
|
||||
"message": "Curso 'Inglés Básico (Plan Regular)' importado exitosamente con 4 módulos y 20 lecciones"
|
||||
}
|
||||
```
|
||||
|
||||
## Estructura del Curso Importado
|
||||
|
||||
Al importar un curso desde MySQL, se crea automáticamente una estructura básica basada en la duración del curso:
|
||||
|
||||
### Cursos Regulares (40 horas)
|
||||
- **4 módulos** con 5 lecciones cada uno (20 lecciones total)
|
||||
- Módulo 1: Introducción y Fundamentos
|
||||
- Módulo 2: Gramática Básica
|
||||
- Módulo 3: Vocabulario Esencial
|
||||
- Módulo 4: Práctica Integradora
|
||||
|
||||
### Cursos Intensivos (80 horas)
|
||||
- **8 módulos** con 4-6 lecciones cada uno (46 lecciones total)
|
||||
- Módulo 1-2: Fundamentos Básicos y Gramática Esencial
|
||||
- Módulo 3-4: Vocabulario Intermedio y Comprensión Auditiva
|
||||
- Módulo 5-6: Expresión Oral y Lectura/Escritura
|
||||
- Módulo 7-8: Práctica Avanzada y Proyecto Final
|
||||
|
||||
### Tipos de Lecciones
|
||||
Las lecciones se crean rotando entre diferentes tipos de contenido:
|
||||
1. **video** - Contenido de video
|
||||
2. **document** - Documentos de lectura (PDF, DOCX)
|
||||
3. **interactive** - Actividades interactivas
|
||||
4. **quiz** - Evaluaciones (cada 4ta lección es graded)
|
||||
|
||||
## Scripts de Importación
|
||||
|
||||
### Script Completo (Recomendado)
|
||||
|
||||
```bash
|
||||
./scripts/import_courses_mysql.sh
|
||||
```
|
||||
|
||||
Este script:
|
||||
1. Obtiene el token de autenticación
|
||||
2. Importa todos los metadatos de cursos y planes
|
||||
3. Muestra la lista de cursos disponibles
|
||||
4. Permite importar un curso específico o todos
|
||||
|
||||
### Script Manual (Paso a Paso)
|
||||
|
||||
```bash
|
||||
# 1. Obtener token
|
||||
TOKEN=$(curl -s -X POST "http://localhost:3001/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"admin@norteamericano.cl","password":"Admin123!"}' \
|
||||
| jq -r '.token')
|
||||
|
||||
# 2. Importar metadatos
|
||||
curl -X POST "http://localhost:3001/question-bank/import-mysql-all" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"import_metadata_only": true}'
|
||||
|
||||
# 3. Ver cursos disponibles
|
||||
curl -X GET "http://localhost:3001/question-bank/mysql-courses" \
|
||||
-H "Authorization: Bearer $TOKEN" | jq .
|
||||
|
||||
# 4. Importar un curso específico
|
||||
curl -X POST "http://localhost:3001/question-bank/import-course-mysql" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"mysql_course_id": 1}'
|
||||
```
|
||||
|
||||
## Configuración Requerida
|
||||
|
||||
### Variables de Entorno
|
||||
|
||||
Asegúrate de configurar las siguientes variables en tu `.env`:
|
||||
|
||||
```bash
|
||||
# Conexión a MySQL externa
|
||||
MYSQL_DATABASE_URL=mysql://usuario:contraseña@host:3306/base_de_datos
|
||||
|
||||
# Credenciales de admin para importación
|
||||
EMAIL="admin@norteamericano.cl"
|
||||
PASSWORD="Admin123!"
|
||||
```
|
||||
|
||||
### Estructura de la Base de Datos MySQL
|
||||
|
||||
La base de datos MySQL debe tener las siguientes tablas:
|
||||
|
||||
```sql
|
||||
-- Planes de estudio
|
||||
plandeestudios (
|
||||
idPlanDeEstudios INT PRIMARY KEY,
|
||||
Nombre VARCHAR(255),
|
||||
Activo BOOLEAN
|
||||
)
|
||||
|
||||
-- Cursos
|
||||
curso (
|
||||
idCursos INT PRIMARY KEY,
|
||||
idPlanDeEstudios INT,
|
||||
NombreCurso VARCHAR(255),
|
||||
NivelCurso INT,
|
||||
Duracion INT,
|
||||
Activo BOOLEAN,
|
||||
FOREIGN KEY (idPlanDeEstudios) REFERENCES plandeestudios(idPlanDeEstudios)
|
||||
)
|
||||
|
||||
-- Banco de preguntas (opcional)
|
||||
bancopreguntas (
|
||||
idPregunta INT PRIMARY KEY,
|
||||
idCursos INT,
|
||||
idPlanDeEstudios INT,
|
||||
idTipoPregunta INT,
|
||||
descripcion TEXT,
|
||||
activo BOOLEAN,
|
||||
FOREIGN KEY (idCursos) REFERENCES curso(idCursos)
|
||||
)
|
||||
|
||||
-- Respuestas (opcional)
|
||||
bancorespuestas (
|
||||
idRespuesta INT PRIMARY KEY,
|
||||
idPregunta INT,
|
||||
descripcion TEXT,
|
||||
resultado INT,
|
||||
activo BOOLEAN,
|
||||
FOREIGN KEY (idPregunta) REFERENCES bancopreguntas(idPregunta)
|
||||
)
|
||||
```
|
||||
|
||||
## Flujo de Importación Recomendado
|
||||
|
||||
1. **Primera vez:**
|
||||
```bash
|
||||
./scripts/import_courses_mysql.sh
|
||||
```
|
||||
- Selecciona "all" para importar todos los cursos
|
||||
|
||||
2. **Actualizaciones posteriores:**
|
||||
```bash
|
||||
# Solo importar nuevos cursos
|
||||
./scripts/import_courses_mysql.sh
|
||||
# Selecciona los cursos específicos que faltan
|
||||
```
|
||||
|
||||
3. **Importar solo preguntas:**
|
||||
```bash
|
||||
./scripts/import_mysql.sh
|
||||
```
|
||||
|
||||
## Solución de Problemas
|
||||
|
||||
### Error: "MYSQL_DATABASE_URL not configured"
|
||||
**Solución:** Agrega la variable `MYSQL_DATABASE_URL` en tu `.env`
|
||||
|
||||
### Error: "Course with ID X not found in MySQL"
|
||||
**Solución:** Verifica que el curso exista en la base de datos MySQL y que `Activo = 1`
|
||||
|
||||
### Error: "Failed to connect to MySQL"
|
||||
**Solución:**
|
||||
- Verifica que la conexión a MySQL esté disponible
|
||||
- Comprueba las credenciales en `MYSQL_DATABASE_URL`
|
||||
- Asegúrate de que el firewall permita la conexión
|
||||
|
||||
### Los cursos importados no tienen contenido
|
||||
**Solución:** Esto es esperado. Los cursos se importan con una estructura básica (módulos y lecciones vacías). Debes completar el contenido de cada lección desde el Studio.
|
||||
|
||||
## Notas Importantes
|
||||
|
||||
1. **Los cursos importados no sobrescriben existentes** - Cada importación crea un nuevo curso
|
||||
2. **El campo `imported_mysql_course_id`** se usa para rastrear el origen del curso
|
||||
3. **Las preguntas del banco de preguntas** se importan por separado con `import_mysql.sh`
|
||||
4. **La estructura generada es básica** - Debes personalizar el contenido de las lecciones
|
||||
|
||||
## Ejemplo de Uso con curl
|
||||
|
||||
```bash
|
||||
# Login
|
||||
RESPONSE=$(curl -s -X POST "http://localhost:3001/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"admin@norteamericano.cl","password":"Admin123!"}')
|
||||
|
||||
TOKEN=$(echo $RESPONSE | jq -r '.token')
|
||||
|
||||
# Importar curso ID 5
|
||||
curl -X POST "http://localhost:3001/question-bank/import-course-mysql" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"mysql_course_id": 5,
|
||||
"title": "Curso Personalizado",
|
||||
"description": "Descripción personalizada del curso",
|
||||
"pacing_mode": "instructor_led"
|
||||
}' | jq .
|
||||
```
|
||||
|
||||
## Próximas Mejoras
|
||||
|
||||
- [ ] Importación de contenido específico desde MySQL (si existe)
|
||||
- [ ] Mapeo de prerrequisitos entre cursos
|
||||
- [ ] Importación de usuarios inscritos
|
||||
- [ ] Sincronización automática de cambios en MySQL
|
||||
Executable
+149
@@ -0,0 +1,149 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Import complete courses from MySQL with basic structure (modules and lessons)
|
||||
|
||||
CMS_API_URL="http://localhost:3001"
|
||||
EMAIL="admin@norteamericano.cl"
|
||||
PASSWORD="Admin123!"
|
||||
|
||||
echo "📚 Importación de Cursos desde MySQL"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Step 1: Login to get JWT token
|
||||
echo "🔑 Obteniendo token de autenticación..."
|
||||
TOKEN=$(curl -s -X POST "$CMS_API_URL/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}" \
|
||||
| jq -r '.token')
|
||||
|
||||
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
|
||||
echo "❌ Error: No se pudo obtener el token. Verifica las credenciales."
|
||||
echo " Credenciales usadas: $EMAIL / $PASSWORD"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Token obtenido: ${TOKEN:0:30}..."
|
||||
echo ""
|
||||
|
||||
# Step 2: Import metadata first (courses and plans)
|
||||
echo "📋 Importando metadatos de cursos y planes..."
|
||||
METADATA_RESULT=$(curl -s -X POST "$CMS_API_URL/question-bank/import-mysql-all" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"import_metadata_only": true}')
|
||||
|
||||
echo "📊 Resultado:"
|
||||
echo "$METADATA_RESULT" | jq '.metadata' 2>/dev/null || echo "$METADATA_RESULT"
|
||||
|
||||
STUDY_PLANS=$(echo "$METADATA_RESULT" | jq -r '.metadata.study_plans_imported // 0')
|
||||
COURSES=$(echo "$METADATA_RESULT" | jq -r '.metadata.courses_imported // 0')
|
||||
|
||||
echo ""
|
||||
echo "✅ Metadatos importados:"
|
||||
echo " - Planes de estudio: $STUDY_PLANS"
|
||||
echo " - Cursos: $COURSES"
|
||||
echo ""
|
||||
|
||||
# Step 3: Show available courses
|
||||
echo "📚 Cursos disponibles para importar:"
|
||||
echo "-------------------------------------"
|
||||
COURSE_LIST=$(curl -s -X GET "$CMS_API_URL/question-bank/mysql-courses" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
echo "$COURSE_LIST" | jq -r '.[] | " ID: \(.id_cursos) | \(.nombre_curso) | Plan: \(.nombre_plan) | Duración: \(.duracion)h"'
|
||||
|
||||
echo ""
|
||||
|
||||
# Step 4: Ask user which course to import
|
||||
read -p "Ingresa el ID del curso a importar (o 'all' para importar todos, o 'q' para salir): " COURSE_ID
|
||||
|
||||
if [ "$COURSE_ID" = "q" ]; then
|
||||
echo "👋 Operación cancelada"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$COURSE_ID" = "all" ]; then
|
||||
echo ""
|
||||
echo "📦 Importando TODOS los cursos..."
|
||||
|
||||
# Get all course IDs
|
||||
COURSE_IDS=$(echo "$COURSE_LIST" | jq -r '.[].id_cursos')
|
||||
|
||||
TOTAL=0
|
||||
SUCCESS=0
|
||||
FAILED=0
|
||||
|
||||
for CID in $COURSE_IDS; do
|
||||
TOTAL=$((TOTAL + 1))
|
||||
echo ""
|
||||
echo "📦 Importando curso $CID ($TOTAL/$COURSES)..."
|
||||
|
||||
RESULT=$(curl -s -X POST "$CMS_API_URL/question-bank/import-course-mysql" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d "{\"mysql_course_id\": $CID}")
|
||||
|
||||
if echo "$RESULT" | jq -e '.course_id' > /dev/null 2>&1; then
|
||||
MESSAGE=$(echo "$RESULT" | jq -r '.message')
|
||||
echo "✅ $MESSAGE"
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
else
|
||||
ERROR=$(echo "$RESULT" | jq -r '.error // "Error desconocido"')
|
||||
echo "❌ Error al importar curso $CID: $ERROR"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Resumen ==="
|
||||
echo "✅ Cursos importados exitosamente: $SUCCESS"
|
||||
echo "❌ Cursos fallidos: $FAILED"
|
||||
echo "📊 Total procesados: $TOTAL"
|
||||
|
||||
else
|
||||
# Import single course
|
||||
echo ""
|
||||
echo "📦 Importando curso ID: $COURSE_ID..."
|
||||
|
||||
RESULT=$(curl -s -X POST "$CMS_API_URL/question-bank/import-course-mysql" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d "{\"mysql_course_id\": $COURSE_ID}")
|
||||
|
||||
echo "📋 Resultado:"
|
||||
echo "$RESULT" | jq .
|
||||
|
||||
if echo "$RESULT" | jq -e '.course_id' > /dev/null 2>&1; then
|
||||
COURSE_TITLE=$(echo "$RESULT" | jq -r '.course_title')
|
||||
MODULES=$(echo "$RESULT" | jq -r '.modules_created')
|
||||
LESSONS=$(echo "$RESULT" | jq -r '.lessons_created')
|
||||
|
||||
echo ""
|
||||
echo "✅ ¡Curso importado exitosamente!"
|
||||
echo " - Título: $COURSE_TITLE"
|
||||
echo " - Módulos creados: $MODULES"
|
||||
echo " - Lecciones creadas: $LESSONS"
|
||||
echo ""
|
||||
echo "💡 Puedes ver el curso en: http://localhost:3000/courses"
|
||||
else
|
||||
ERROR=$(echo "$RESULT" | jq -r '.error // .message // "Error desconocido"')
|
||||
echo ""
|
||||
echo "❌ Error al importar el curso: $ERROR"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Comandos Útiles ==="
|
||||
echo ""
|
||||
echo "📋 Ver cursos disponibles:"
|
||||
echo " curl -X GET http://localhost:3001/question-bank/mysql-courses \\"
|
||||
echo " -H \"Authorization: Bearer \$TOKEN\""
|
||||
echo ""
|
||||
echo "📦 Importar un curso específico:"
|
||||
echo " curl -X POST http://localhost:3001/question-bank/import-course-mysql \\"
|
||||
echo " -H \"Content-Type: application/json\" \\"
|
||||
echo " -H \"Authorization: Bearer \$TOKEN\" \\"
|
||||
echo " -d '{\"mysql_course_id\": <ID>}'"
|
||||
echo ""
|
||||
+57
-2
@@ -30,8 +30,20 @@ fi
|
||||
|
||||
echo "✅ Token obtenido: ${TOKEN:0:20}..."
|
||||
|
||||
# Step 2: Import all from MySQL
|
||||
echo "📊 Importando cursos, planes y preguntas desde MySQL..."
|
||||
# Step 3: Get list of courses from MySQL
|
||||
echo ""
|
||||
echo "📋 Obteniendo lista de cursos desde MySQL..."
|
||||
COURSES_RESULT=$(curl -s -X POST "$CMS_API_URL/question-bank/import-mysql-all" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"import_metadata_only": true}')
|
||||
|
||||
echo "📊 Metadatos de cursos importados:"
|
||||
echo "$COURSES_RESULT" | jq '.metadata // empty' 2>/dev/null || echo "$COURSES_RESULT"
|
||||
|
||||
# Step 4: Import all questions from MySQL
|
||||
echo ""
|
||||
echo "📊 Importando preguntas desde MySQL..."
|
||||
RESULT=$(curl -s -X POST "$CMS_API_URL/question-bank/import-mysql-all" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
@@ -46,3 +58,46 @@ if [ "$IMPORTED" != "null" ] && [ "$IMPORTED" -gt 0 ]; then
|
||||
else
|
||||
echo "⚠️ Revisa el resultado para más detalles"
|
||||
fi
|
||||
|
||||
# Step 5: Import courses with structure (optional - uncomment to enable)
|
||||
# echo ""
|
||||
# echo "📚 Importando cursos con estructura básica..."
|
||||
#
|
||||
# # Get course list
|
||||
# COURSE_LIST=$(curl -s -X GET "$CMS_API_URL/question-bank/mysql-courses" \
|
||||
# -H "Authorization: Bearer $TOKEN")
|
||||
#
|
||||
# echo "📋 Cursos disponibles:"
|
||||
# echo "$COURSE_LIST" | jq '.[] | {id: .id_cursos, name: .nombre_curso, plan: .nombre_plan}'
|
||||
#
|
||||
# # Import first course as example
|
||||
# FIRST_COURSE_ID=$(echo "$COURSE_LIST" | jq -r '.[0].id_cursos')
|
||||
# if [ "$FIRST_COURSE_ID" != "null" ] && [ -n "$FIRST_COURSE_ID" ]; then
|
||||
# echo ""
|
||||
# echo "📦 Importando curso de ejemplo (ID: $FIRST_COURSE_ID)..."
|
||||
# COURSE_IMPORT=$(curl -s -X POST "$CMS_API_URL/question-bank/import-course-mysql" \
|
||||
# -H "Content-Type: application/json" \
|
||||
# -H "Authorization: Bearer $TOKEN" \
|
||||
# -d "{\"mysql_course_id\": $FIRST_COURSE_ID}")
|
||||
#
|
||||
# echo "📋 Resultado:"
|
||||
# echo "$COURSE_IMPORT" | jq .
|
||||
#
|
||||
# MESSAGE=$(echo "$COURSE_IMPORT" | jq -r '.message // "Error al importar"')
|
||||
# echo "✅ $MESSAGE"
|
||||
# fi
|
||||
|
||||
echo ""
|
||||
echo "=== Resumen ==="
|
||||
echo "✅ Metadatos de cursos y planes importados"
|
||||
echo "✅ Preguntas importadas desde MySQL"
|
||||
echo ""
|
||||
echo "💡 Para importar un curso completo con estructura básica, usa:"
|
||||
echo " curl -X POST http://localhost:3001/question-bank/import-course-mysql \\"
|
||||
echo " -H \"Content-Type: application/json\" \\"
|
||||
echo " -H \"Authorization: Bearer \$TOKEN\" \\"
|
||||
echo " -d '{\"mysql_course_id\": <ID_DEL_CURSO>}'"
|
||||
echo ""
|
||||
echo "📋 Para ver los cursos disponibles, usa:"
|
||||
echo " curl -X GET http://localhost:3001/question-bank/mysql-courses \\"
|
||||
echo " -H \"Authorization: Bearer \$TOKEN\""
|
||||
|
||||
@@ -836,7 +836,7 @@ pub struct MySqlCoursesFilters {
|
||||
pub plan_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, sqlx::FromRow, Serialize)]
|
||||
#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
|
||||
pub struct MySqlPlanInfo {
|
||||
#[sqlx(rename = "idPlanDeEstudios")]
|
||||
#[serde(rename = "idPlanDeEstudios")]
|
||||
@@ -846,12 +846,42 @@ pub struct MySqlPlanInfo {
|
||||
pub nombre_plan: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct MySqlCourseInfo {
|
||||
#[sqlx(rename = "idCursos")]
|
||||
#[serde(rename = "idCursos")]
|
||||
pub id_cursos: i32,
|
||||
#[sqlx(rename = "NombreCurso")]
|
||||
#[serde(rename = "NombreCurso")]
|
||||
pub nombre_curso: String,
|
||||
#[sqlx(rename = "NivelCurso")]
|
||||
#[serde(rename = "NivelCurso", skip_serializing_if = "Option::is_none")]
|
||||
pub nivel_curso: Option<i32>,
|
||||
#[sqlx(rename = "idPlanDeEstudios")]
|
||||
#[serde(rename = "idPlanDeEstudios")]
|
||||
pub id_plan_de_estudios: i32,
|
||||
#[sqlx(rename = "NombrePlan")]
|
||||
#[serde(rename = "NombrePlan")]
|
||||
pub nombre_plan: String,
|
||||
#[sqlx(rename = "Duracion")]
|
||||
#[serde(rename = "Duracion", skip_serializing_if = "Option::is_none")]
|
||||
pub duracion: Option<i32>, // Duration in hours (40=regular, 80=intensive)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ImportAllFromMySQLPayload {
|
||||
pub import_metadata_only: Option<bool>, // Only import metadata (courses/plans), not questions
|
||||
}
|
||||
|
||||
/// POST /api/question-bank/import-mysql-all - Import ALL questions from MySQL (bulk import)
|
||||
pub async fn import_all_from_mysql(
|
||||
Org(org_ctx): Org,
|
||||
claims: Claims,
|
||||
State(pool): State<PgPool>,
|
||||
) -> Result<Json<ImportResult>, (StatusCode, String)> {
|
||||
Json(payload): Json<ImportAllFromMySQLPayload>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
use serde_json::json;
|
||||
|
||||
// Connect to MySQL
|
||||
let mysql_url = std::env::var("MYSQL_DATABASE_URL")
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "MYSQL_DATABASE_URL not configured".to_string()))?;
|
||||
@@ -901,10 +931,29 @@ pub async fn import_all_from_mysql(
|
||||
|
||||
// Save plans and courses to PostgreSQL
|
||||
tracing::info!("Saving plans and courses to PostgreSQL...");
|
||||
save_mysql_courses_and_plans(&pool, org_ctx.id, mysql_plans, mysql_courses)
|
||||
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)))?;
|
||||
|
||||
// If only metadata import is requested, return early
|
||||
if payload.import_metadata_only.unwrap_or(false) {
|
||||
return Ok(Json(json!({
|
||||
"success": true,
|
||||
"message": "Metadatos importados exitosamente",
|
||||
"metadata": {
|
||||
"study_plans_imported": mysql_plans.len(),
|
||||
"courses_imported": mysql_courses.len(),
|
||||
"courses": mysql_courses.iter().map(|c| json!({
|
||||
"id_cursos": c.id_cursos,
|
||||
"nombre_curso": c.nombre_curso,
|
||||
"nombre_plan": c.nombre_plan,
|
||||
"duracion": c.duracion,
|
||||
"nivel_curso": c.nivel_curso
|
||||
})).collect::<Vec<_>>()
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
// Fetch ALL questions from MySQL with answers (using JSON aggregation for answers)
|
||||
let mysql_questions: Vec<MySqlQuestionFull> = sqlx::query_as(
|
||||
r#"
|
||||
@@ -948,14 +997,15 @@ pub async fn import_all_from_mysql(
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch questions: {}", e)))?;
|
||||
|
||||
mysql_pool.close().await;
|
||||
|
||||
|
||||
if mysql_questions.is_empty() {
|
||||
return Ok(Json(ImportResult {
|
||||
imported: 0,
|
||||
skipped: 0,
|
||||
updated: 0,
|
||||
error: Some("No questions found in MySQL".to_string()),
|
||||
}));
|
||||
return Ok(Json(json!({
|
||||
"success": false,
|
||||
"imported": 0,
|
||||
"skipped": 0,
|
||||
"updated": 0,
|
||||
"error": "No questions found in MySQL"
|
||||
})));
|
||||
}
|
||||
|
||||
// Import questions into PostgreSQL
|
||||
@@ -1051,13 +1101,17 @@ pub async fn import_all_from_mysql(
|
||||
skipped_count,
|
||||
updated_count
|
||||
);
|
||||
|
||||
Ok(Json(ImportResult {
|
||||
imported: imported_count,
|
||||
skipped: skipped_count,
|
||||
updated: updated_count,
|
||||
error: None,
|
||||
}))
|
||||
|
||||
Ok(Json(json!({
|
||||
"success": true,
|
||||
"imported": imported_count,
|
||||
"skipped": skipped_count,
|
||||
"updated": updated_count,
|
||||
"metadata": {
|
||||
"study_plans_imported": mysql_plans.len(),
|
||||
"courses_imported": mysql_courses.len()
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -1068,28 +1122,6 @@ pub struct ImportResult {
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)]
|
||||
pub struct MySqlCourseInfo {
|
||||
#[sqlx(rename = "idCursos")]
|
||||
#[serde(rename = "idCursos")]
|
||||
pub id_cursos: i32,
|
||||
#[sqlx(rename = "NombreCurso")]
|
||||
#[serde(rename = "NombreCurso")]
|
||||
pub nombre_curso: String,
|
||||
#[sqlx(rename = "NivelCurso")]
|
||||
#[serde(rename = "NivelCurso", skip_serializing_if = "Option::is_none")]
|
||||
pub nivel_curso: Option<i32>,
|
||||
#[sqlx(rename = "idPlanDeEstudios")]
|
||||
#[serde(rename = "idPlanDeEstudios")]
|
||||
pub id_plan_de_estudios: i32,
|
||||
#[sqlx(rename = "NombrePlan")]
|
||||
#[serde(rename = "NombrePlan")]
|
||||
pub nombre_plan: String,
|
||||
#[sqlx(rename = "Duracion")]
|
||||
#[serde(rename = "Duracion", skip_serializing_if = "Option::is_none")]
|
||||
pub duracion: Option<i32>, // Duration in hours (40=regular, 80=intensive)
|
||||
}
|
||||
|
||||
// Excel import - pendiente de fix
|
||||
// /// POST /api/question-bank/import-excel - Import questions from Excel file
|
||||
// pub async fn import_from_excel(
|
||||
@@ -1226,3 +1258,233 @@ pub async fn ai_generate_question(
|
||||
|
||||
Ok(Json(ai_question))
|
||||
}
|
||||
|
||||
// ==================== Import Courses from MySQL ====================
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ImportCourseFromMySQLPayload {
|
||||
pub mysql_course_id: i32,
|
||||
pub title: Option<String>, // Optional custom title (defaults to MySQL course name)
|
||||
pub description: Option<String>, // Optional description
|
||||
pub pacing_mode: Option<String>, // self_paced or instructor_led
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ImportCourseResult {
|
||||
pub course_id: Uuid,
|
||||
pub course_title: String,
|
||||
pub mysql_course_id: i32,
|
||||
pub modules_created: i32,
|
||||
pub lessons_created: i32,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// POST /api/question-bank/import-course-mysql - Import a course from MySQL with basic structure
|
||||
pub async fn import_course_from_mysql(
|
||||
Org(org_ctx): Org,
|
||||
claims: Claims,
|
||||
State(pool): State<PgPool>,
|
||||
Json(payload): Json<ImportCourseFromMySQLPayload>,
|
||||
) -> Result<Json<ImportCourseResult>, (StatusCode, String)> {
|
||||
use common::models::Course;
|
||||
|
||||
// Connect to MySQL
|
||||
let mysql_url = std::env::var("MYSQL_DATABASE_URL")
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "MYSQL_DATABASE_URL not configured".to_string()))?;
|
||||
|
||||
let mysql_pool = sqlx::MySqlPool::connect(&mysql_url)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to connect to MySQL: {}", e)))?;
|
||||
|
||||
// Fetch course info from MySQL
|
||||
let mysql_course: MySqlCourseInfo = sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
c.idCursos AS id_cursos,
|
||||
c.NombreCurso AS nombre_curso,
|
||||
c.NivelCurso AS nivel_curso,
|
||||
pe.idPlanDeEstudios AS id_plan_de_estudios,
|
||||
pe.Nombre AS nombre_plan,
|
||||
CAST(c.Duracion AS SIGNED INTEGER) AS duracion
|
||||
FROM curso c
|
||||
JOIN plandeestudios pe ON c.idPlanDeEstudios = pe.idPlanDeEstudios
|
||||
WHERE c.idCursos = ? AND c.Activo = 1 AND pe.Activo = 1
|
||||
"#
|
||||
)
|
||||
.bind(payload.mysql_course_id)
|
||||
.fetch_one(&mysql_pool)
|
||||
.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))
|
||||
} else {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch course from MySQL: {}", e))
|
||||
}
|
||||
})?;
|
||||
|
||||
tracing::info!("Importing course from MySQL: {} (ID: {})", mysql_course.nombre_curso, mysql_course.id_cursos);
|
||||
|
||||
// Start transaction
|
||||
let mut tx = pool.begin().await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to start transaction: {}", e)))?;
|
||||
|
||||
// Determine course type and level for structure generation
|
||||
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
|
||||
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));
|
||||
|
||||
let new_course: Course = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO courses (
|
||||
organization_id, instructor_id, title, pacing_mode, description,
|
||||
passing_percentage, certificate_template, imported_mysql_course_id
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING *
|
||||
"#
|
||||
)
|
||||
.bind(org_ctx.id)
|
||||
.bind(claims.sub)
|
||||
.bind(&course_title)
|
||||
.bind(&pacing_mode)
|
||||
.bind(&description)
|
||||
.bind(60.0) // Default passing percentage
|
||||
.bind(serde_json::json!({
|
||||
"template": "default",
|
||||
"show_logo": true,
|
||||
"show_instructor": true
|
||||
}))
|
||||
.bind(mysql_course.id_cursos)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create course: {}", e)))?;
|
||||
|
||||
tracing::info!("Created course in PostgreSQL: {}", new_course.id);
|
||||
|
||||
// Generate basic course structure based on course type and level
|
||||
let (modules_count, lessons_count) = generate_course_structure(
|
||||
&mut tx,
|
||||
new_course.id,
|
||||
org_ctx.id,
|
||||
&course_type,
|
||||
&level,
|
||||
&mysql_course.nombre_curso,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
||||
|
||||
// Commit transaction
|
||||
tx.commit().await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to commit transaction: {}", e)))?;
|
||||
|
||||
tracing::info!(
|
||||
"Successfully imported course {} with {} modules and {} lessons",
|
||||
new_course.id,
|
||||
modules_count,
|
||||
lessons_count
|
||||
);
|
||||
|
||||
Ok(Json(ImportCourseResult {
|
||||
course_id: new_course.id,
|
||||
course_title: new_course.title.clone(),
|
||||
mysql_course_id: mysql_course.id_cursos,
|
||||
modules_created: modules_count,
|
||||
lessons_created: lessons_count,
|
||||
message: format!("Curso '{}' importado exitosamente con {} módulos y {} lecciones",
|
||||
new_course.title, modules_count, lessons_count),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Generate basic course structure based on type and level
|
||||
async fn generate_course_structure<'a>(
|
||||
tx: &mut sqlx::Transaction<'a, sqlx::Postgres>,
|
||||
course_id: Uuid,
|
||||
org_id: Uuid,
|
||||
course_type: &str,
|
||||
level: &str,
|
||||
course_name: &str,
|
||||
) -> Result<(i32, i32), String> {
|
||||
// Define module structure based on course type
|
||||
// Regular (40h): 4 modules
|
||||
// Intensive (80h): 8 modules
|
||||
let modules_config = match course_type {
|
||||
"intensive" => vec![
|
||||
("Fundamentos Básicos", 6),
|
||||
("Gramática Esencial", 6),
|
||||
("Vocabulario Intermedio", 6),
|
||||
("Comprensión Auditiva", 6),
|
||||
("Expresión Oral", 6),
|
||||
("Lectura y Escritura", 6),
|
||||
("Práctica Avanzada", 6),
|
||||
("Proyecto Final", 4),
|
||||
],
|
||||
_ => vec![
|
||||
("Introducción y Fundamentos", 5),
|
||||
("Gramática Básica", 5),
|
||||
("Vocabulario Esencial", 5),
|
||||
("Práctica Integradora", 5),
|
||||
],
|
||||
};
|
||||
|
||||
let mut total_modules = 0;
|
||||
let mut total_lessons = 0;
|
||||
|
||||
for (module_idx, (module_name, lessons_count)) in modules_config.iter().enumerate() {
|
||||
// Create module
|
||||
let module: common::models::Module = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO modules (course_id, organization_id, title, position)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *
|
||||
"#
|
||||
)
|
||||
.bind(course_id)
|
||||
.bind(org_id)
|
||||
.bind(format!("Módulo {}: {}", module_idx + 1, module_name))
|
||||
.bind(module_idx as i32)
|
||||
.fetch_one(&mut **tx)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create module {}: {}", module_idx + 1, e))?;
|
||||
|
||||
total_modules += 1;
|
||||
|
||||
// Create lessons for this module
|
||||
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)
|
||||
let content_types = ["video", "document", "interactive", "quiz"];
|
||||
let content_type = content_types[lesson_idx % content_types.len()];
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO lessons (
|
||||
module_id, organization_id, title, content_type,
|
||||
content_url, position, is_graded, summary
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
"#
|
||||
)
|
||||
.bind(module.id)
|
||||
.bind(org_id)
|
||||
.bind(&lesson_title)
|
||||
.bind(content_type)
|
||||
.bind("") // Empty content URL (to be filled by instructor)
|
||||
.bind(lesson_position)
|
||||
.bind(lesson_idx % 4 == 3) // Every 4th lesson is graded (quiz)
|
||||
.bind(format!("Contenido de la lección: {}", lesson_title))
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create lesson {}: {}", lesson_position, e))?;
|
||||
|
||||
total_lessons += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((total_modules, total_lessons))
|
||||
}
|
||||
|
||||
@@ -356,6 +356,10 @@ async fn main() {
|
||||
"/question-bank/import-mysql-all",
|
||||
post(handlers_question_bank::import_all_from_mysql),
|
||||
)
|
||||
.route(
|
||||
"/question-bank/import-course-mysql",
|
||||
post(handlers_question_bank::import_course_from_mysql),
|
||||
)
|
||||
.route(
|
||||
"/question-bank/ai-generate",
|
||||
post(handlers_question_bank::ai_generate_question),
|
||||
|
||||
Reference in New Issue
Block a user