Overview
Both exams combine traditional assessment with gamified elements:Examen 1
Focus: DMO and Age relationship basics
- 3 multiple-choice questions
- Sentence ordering game
- Matching pairs game
- Total: ~8 points
Examen 2
Focus: Graph modeling and analysis
- 7 mixed questions (MCQ + open response)
- Matching activity
- No games
- Conceptual depth
Exam 1 Structure
Phase 1: Multiple Choice Quiz (3 questions)
Question Types with Immediate Feedback:// examen1.vue:343-369
const preguntas = ref([
{
texto: "La Densidad Mineral Ósea (DMO) que mide el volumen del esqueleto humano...",
opciones: [
"A) Son diferentes solo en la vejez.",
"B) Son diferentes desde el nacimiento.",
"C) Son iguales en la niñez y se empiezan a diferenciar a partir de la adolescencia.",
"D) Son iguales, no hay diferencia."
],
correcta: "C) Son iguales en la niñez y se empiezan a diferenciar a partir de la adolescencia."
},
{
texto: "¿Cómo sabemos que la DMO tiene un valor máximo a lo largo de la vida?",
opciones: [...],
correcta: "D) Porque aumenta conforme crecemos pero luego empieza a disminuir..."
},
{
texto: "De las siguientes figuras, selecciona la que representa mejor cómo varía la DMO...",
opciones: ["A", "B", "C", "D"],
correcta: "D"
}
]);
// examen1.vue:446-469
function verificarRespuesta() {
if (respuestaUsuario.value === null || respuestaUsuario.value === '') {
alert("Por favor selecciona una respuesta");
return;
}
const pregunta = preguntas.value[preguntaActual.value];
const key = `r${preguntaActual.value + 1}`;
respuestasGuardadas.value[key] = respuestaUsuario.value;
// Check if correct
if (respuestaUsuario.value === pregunta.correcta) {
esRespuestaCorrecta.value = true;
puntajeQuiz.value++;
} else {
esRespuestaCorrecta.value = false;
}
mostrandoFeedback.value = true; // Show feedback before advancing
}
Immediate Feedback Displayed
Green checkmark for correct, red X for incorrect with correct answer shown
<!-- examen1.vue:128-142 -->
<div v-if="mostrandoFeedback" class="mt-6 p-4 rounded-xl animate-fade-in"
:class="esRespuestaCorrecta ? 'bg-green-900/20 border border-green-500/30' : 'bg-red-900/20 border border-red-500/30'">
<div class="flex items-center gap-3 mb-2">
<span class="material-symbols-outlined text-2xl">
{{ esRespuestaCorrecta ? 'check_circle' : 'cancel' }}
</span>
<h3 class="font-bold text-lg">
{{ esRespuestaCorrecta ? '¡Correcto!' : 'Respuesta Incorrecta' }}
</h3>
</div>
<p v-if="!esRespuestaCorrecta">
La respuesta correcta es: <strong class="underline">{{ preguntas[preguntaActual].correcta }}</strong>
</p>
</div>
Phase 2: Sentence Ordering Game
Game Mechanic: Drag words to form a correct sentence about DMO// examen1.vue:382-387
const oracionCorrecta = [
"En", "general", "la DMO", "de la", "cadera",
"es mayor", "en el hombre", "que", "en", "las mujeres"
];
const palabrasDesordenadas = ref([...oracionCorrecta].sort(() => Math.random() - 0.5));
const oracionUsuario = ref([]);
// examen1.vue:487-504
function moverPalabra(palabra, origen) {
if (juego1Completado.value) return;
if (origen === "banco") {
// Move from word bank to sentence area
const index = palabrasDesordenadas.value.indexOf(palabra);
if (index > -1) {
palabrasDesordenadas.value.splice(index, 1);
oracionUsuario.value.push(palabra);
}
} else {
// Move back from sentence to word bank
const index = oracionUsuario.value.indexOf(palabra);
if (index > -1) {
oracionUsuario.value.splice(index, 1);
palabrasDesordenadas.value.push(palabra);
}
}
}
// examen1.vue:506-514
function verificarOracion() {
const oracionStr = oracionUsuario.value.join(" ");
const correctaStr = oracionCorrecta.join(" ");
const esCorrecto = oracionStr === correctaStr;
juego1Correcto.value = esCorrecto;
juego1Completado.value = true;
respuestasGuardadas.value.r4 = esCorrecto ? "Correcto" : "Incorrecto";
}
Phase 3: Matching Pairs Game
Game Data:// examen1.vue:388-394
const parejas = [
{ id: 1, termino: "Adolescencia y los primeros años de adultez",
definicion: "Etapa en la que aumenta mas la DMO" },
{ id: 2, termino: "25 y 35 años",
definicion: "Rango de edad en el que la DMO llega su valor maximo" },
{ id: 3, termino: "1.025",
definicion: "Valor maximo de la DMO en el hombre" },
{ id: 4, termino: ".938",
definicion: "Valor maximo de la DMO en la mujer" }
];
// examen1.vue:525-553
function seleccionarItem(item) {
if (item.matched) return; // Already matched
if (seleccionActual.value?.type === item.type) return; // Same type (both terms or both definitions)
if (!seleccionActual.value) {
// First selection
seleccionActual.value = item;
return;
}
const primerItem = seleccionActual.value;
// Check if IDs match and types are different
if (primerItem.id === item.id && primerItem.type !== item.type) {
// Correct match!
primerItem.matched = true;
item.matched = true;
actualizarEstadoItem(primerItem, true);
actualizarEstadoItem(item, true);
parejasEncontradas.value++;
mostrarFeedbackPositivo();
} else {
// Wrong match
mostrarFeedbackNegativo();
setTimeout(() => {
seleccionActual.value = null;
}, 1000);
return;
}
seleccionActual.value = null;
}
<!-- examen1.vue:239-248 -->
<div class="mb-6">
<div class="flex justify-between items-center mb-2 text-sm text-white/70">
<span>Progreso</span>
<span>{{ parejasEncontradas }} / {{ parejas.length }}</span>
</div>
<div class="w-full bg-white/10 rounded-full h-2">
<div class="bg-purple-500 h-2 rounded-full transition-all duration-500"
:style="{ width: `${(parejasEncontradas / parejas.length) * 100}%` }">
</div>
</div>
</div>
Final Score Summary
// examen1.vue:627-634
const puntajeTotal = computed(() => {
let p = puntajeQuiz.value; // 0-3 points from quiz
if (juego1Correcto.value) p += 1; // +1 for sentence game
p += parejasEncontradas.value; // +4 for matching (1 per pair)
return p;
});
const totalPosible = computed(() => preguntas.value.length + 1 + parejas.length);
// = 3 + 1 + 4 = 8 points
<!-- examen1.vue:296-320 -->
<div v-if="faseActual === 'final'" class="mt-10 animate-in fade-in duration-500">
<div class="bg-[#161d2b] rounded-2xl p-8 shadow-2xl text-center">
<h2 class="text-white text-3xl font-bold mb-2">Resumen de Actividades</h2>
<p class="text-gray-400 mb-8">Has completado todas las secciones del Examen 1.</p>
<div class="flex justify-center items-baseline gap-2 mb-8">
<span class="text-6xl font-black text-blue-400">{{ puntajeTotal }}</span>
<span class="text-2xl text-gray-500">/ {{ totalPosible }} pts</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
<div class="bg-[#0f172a] p-4 rounded-xl">
<div class="text-xs text-gray-400 uppercase">Cuestionario</div>
<div class="text-2xl font-bold text-white">{{ puntajeQuiz }} / {{ preguntas.length }}</div>
</div>
<div class="bg-[#0f172a] p-4 rounded-xl">
<div class="text-xs text-gray-400 uppercase">Frase</div>
<div class="text-2xl font-bold text-white">{{ juego1Correcto ? 1 : 0 }} / 1</div>
</div>
<div class="bg-[#0f172a] p-4 rounded-xl">
<div class="text-xs text-gray-400 uppercase">Parejas</div>
<div class="text-2xl font-bold text-white">{{ parejasEncontradas }} / {{ parejas.length }}</div>
</div>
</div>
</div>
</div>
Exam 2 Structure
Question Types
Exam 2 includes 7 questions with mixed formats:// examen2.vue:392-464
const preguntas = ref([
{
tipo: "standard",
texto: "Al tomar un valor de edad al inicio de la tabla y comenzar a aumentarlo ¿El valor de la DMO siempre aumenta?",
opciones: ["Verdadero", "Falso"],
correcta: "Verdadero"
},
{
tipo: "match",
texto: "Relaciona cómo cambia la densidad mineral ósea con respecto a la edad.",
items: [
{ pregunta: "En la vida adulta", correcta: "La densidad mineral ósea cambia muy poco." },
{ pregunta: "En la vejez", correcta: "La densidad mineral ósea disminuye." },
{ pregunta: "Hasta la adolescencia...", correcta: "La densidad mineral ósea aumenta." }
],
opciones: [
"La densidad mineral ósea aumenta.",
"La densidad mineral ósea cambia muy poco.",
"La densidad mineral ósea disminuye."
]
},
{
tipo: "standard",
texto: "El gráfico que modela la relación entre la DMO y la Edad comienza en un valor por arriba de cero...",
correcta: "Verdadero"
},
{
tipo: "standard", // Open response (no opciones)
texto: "La gráfica que elaboramos está basada en datos de una tabla... ¿podríamos representar estos valores con una gráfica continua? Justifica tu respuesta."
},
// ... 3 more questions
]);
Matching Question Handler
// examen2.vue:545-597
function verificarRespuesta() {
const pregunta = preguntas.value[preguntaActual.value];
if (pregunta.tipo === "match") {
// Validate all dropdowns are filled
if (Object.keys(respuestasMatch.value).length < pregunta.items.length) {
alert("Por favor completa todas las opciones");
return;
}
// Check if all matches are correct
let correctas = 0;
pregunta.items.forEach((item, index) => {
const userRes = respuestasMatch.value[index];
if (userRes === item.correcta) correctas++;
});
if (correctas === pregunta.items.length) {
esCorrecto = true;
puntajeQuiz.value++;
}
esRespuestaCorrecta.value = esCorrecto;
mostrandoFeedback.value = true;
} else if (pregunta.opciones) {
// Standard multiple choice
if (respuestaUsuario.value === pregunta.correcta) {
puntajeQuiz.value++;
}
esRespuestaCorrecta.value = (respuestaUsuario.value === pregunta.correcta);
mostrandoFeedback.value = true;
} else {
// Open response - no feedback, just advance
siguientePregunta();
}
}
<!-- examen2.vue:239-266 -->
<template v-if="preguntas[preguntaActual].tipo === 'match'">
<div v-for="(item, index) in preguntas[preguntaActual].items" :key="index"
class="flex flex-col gap-2 mb-4 bg-white/5 p-4 rounded-xl">
<p class="text-white font-medium">{{ item.pregunta }}</p>
<select v-model="respuestasMatch[index]"
class="bg-[#161d2b] text-white border border-white/20 rounded-lg p-3 w-full">
<option :value="undefined" disabled selected>Elegir...</option>
<option v-for="op in preguntas[preguntaActual].opciones" :key="op" :value="op">
{{ op }}
</option>
</select>
</div>
</template>
<!-- examen2.vue:181-212 -->
<textarea v-if="preguntaActual === 2"
v-model="respuestasGuardadas.r3"
rows="3"
placeholder="Respuesta"
class="input-foro">
</textarea>
<textarea v-if="preguntaActual === 3"
v-model="respuestasGuardadas.r4"
rows="3"
placeholder="Respuesta"
class="input-foro">
</textarea>
Submission Tracking
Both exams enforce one-time submission:Check Submission Status
// examen1.vue:416-435
async function verificarEstado() {
const email = localStorage.getItem("usuario");
if (!email) {
cargandoEstado.value = false;
return;
}
const response = await fetch(`${API_URL}/verificar_examen1/${email}`);
const data = await response.json();
if (data.participo) {
usuarioYaParticipo.value = true;
await cargarRespuestas();
}
}
Backend Verification
# main.py:359-368 (Examen 1)
@app.get("/verificar_examen1/{email}")
async def verificar_examen1(email: str):
cursor.execute(
"SELECT COUNT(*) FROM examen1 WHERE email = %s AND email != '[email protected]'",
(email,)
)
return {"participo": cursor.fetchone()[0] > 0}
The admin account (
[email protected]) is excluded from the participation check, allowing professors to test exams multiple times without triggering the “already submitted” lock.One-Time Submission Enforcement
Frontend Lock
<!-- examen1.vue:27-40 -->
<div v-if="cargandoEstado" class="text-center py-10">
<p class="text-white text-xl animate-pulse">Verificando tu participación...</p>
</div>
<div v-else-if="usuarioYaParticipo" class="mt-8 animate-fade-in">
<div class="bg-green-900/30 border border-green-500/50 p-4 rounded-xl mb-8">
<h3 class="font-bold text-xl">Examen Terminado</h3>
<p class="text-base opacity-80">Ya has enviado tus respuestas.</p>
</div>
<!-- Display all student submissions -->
<div v-for="(item, index) in listaRespuestas" :key="index">
<!-- ... show results ... -->
</div>
</div>
Backend Enforcement
# main.py:335-346 (Examen 1)
@app.post("/guardar_examen1")
async def guardar_examen1(datos: Examen1):
conexion = conectar_bd()
if not conexion: raise HTTPException(500, "Error BD")
try:
cursor = conexion.cursor()
# No duplicate check here - relying on frontend verification
query = "INSERT INTO examen1 (email,r1,r2,r3,r4,r5,r6) VALUES (%s, %s, %s, %s, %s, %s,%s)"
cursor.execute(query, (datos.email, datos.r1, datos.r2, datos.r3, datos.r4, datos.r5, datos.r6))
conexion.commit()
return {"mensaje": "Guardado", "exito": True}
finally:
conexion.close()
Unlike forums, the exam save endpoints don’t perform server-side duplicate checks. The system relies on the frontend verification step to prevent multiple submissions.
Data Storage
Examen 1 Model
# main.py:66-70
class Examen1(BaseModel):
email: str
r1: str = "" # Question 1 answer
r2: str = "" # Question 2 answer
r3: str = "" # Question 3 answer
r4: str = "" # Sentence game result ("Correcto" or "Incorrecto")
r5: str = "" # Matching game result ("4/4" format)
r6: str = "" # Total score
Examen 2 Model
# main.py:71-79
class Examen2(BaseModel):
email: str
r1: str = "" # Question 1
r2: str = "" # Question 2 (matching, stored as joined string)
r3: str = "" # Question 3 (open response with image)
r4: str = "" # Question 4 (open response)
r5: str = "" # Question 5
r6: str = "" # Question 6 (open response)
r7: str = "" # Question 7 (open response)
Submission Payload Example
// examen1.vue:581-611
async function enviarExamen() {
const email = localStorage.getItem("usuario");
if (!email) return alert("Error: Inicia sesión.");
enviando.value = true;
try {
const payload = {
email: email,
...respuestasGuardadas.value // Contains r1-r6
};
const response = await fetch(`${API_URL}/guardar_examen1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.exito) {
usuarioYaParticipo.value = true;
await cargarRespuestas();
}
} catch (error) {
console.error(error);
alert("Error de conexión");
} finally {
enviando.value = false;
}
}
Viewing Results
After submission, students see all class submissions:// examen1.vue:437-444
async function cargarRespuestas() {
try {
const response = await fetch(`${API_URL}/respuestas_examen1`);
listaRespuestas.value = await response.json();
} catch (error) {
console.error("Error cargando respuestas:", error);
}
}
# main.py:348-357
@app.get("/respuestas_examen1")
async def leer_examen1():
conexion = conectar_bd()
if not conexion: raise HTTPException(500, "Error BD")
try:
cursor = conexion.cursor(cursor_factory=RealDictCursor)
cursor.execute("""
SELECT r.*, u.nombre, u.apellidos
FROM examen1 r
LEFT JOIN usuarios u ON r.email = u.email
ORDER BY r.fecha DESC
""")
return cursor.fetchall()
finally:
conexion.close()
<!-- examen1.vue:46-74 -->
<div v-for="(item, index) in listaRespuestas" :key="index"
class="bg-[#1e2736] p-6 rounded-xl border-l-4 border-blue-500 shadow-md">
<div class="flex justify-between items-start mb-4">
<div class="flex items-center gap-3">
<div class="h-10 w-10 rounded-full bg-blue-600 flex items-center justify-center text-white font-bold">
{{ obtenerIniciales(item.nombre, item.apellidos) }}
</div>
<div>
<span class="text-blue-400 font-bold text-base">
{{ item.nombre ? `${item.nombre} ${item.apellidos}` : item.email }}
</span>
<p class="text-gray-500 text-xs">{{ formatearFecha(item.fecha) }}</p>
</div>
</div>
<span class="bg-blue-900/50 text-blue-200 text-sm px-2 py-1 rounded">
Puntaje: {{ item.r6 }}
</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-gray-300 mt-4 text-sm">
<div class="bg-white/5 p-3 rounded"><strong>P1:</strong> {{ item.r1 }}</div>
<div class="bg-white/5 p-3 rounded"><strong>P2:</strong> {{ item.r2 }}</div>
<div class="bg-white/5 p-3 rounded"><strong>P3:</strong> {{ item.r3 }}</div>
<div class="bg-white/5 p-3 rounded"><strong>Juego Oración:</strong> {{ item.r4 }}</div>
<div class="bg-white/5 p-3 rounded"><strong>Juego Parejas:</strong> {{ item.r5 }}</div>
</div>
</div>
API Reference
Examen 1 Endpoints
POST /guardar_examen1
Content-Type: application/json
{
"email": "[email protected]",
"r1": "C) Son iguales en la niñez...",
"r2": "D) Porque aumenta conforme...",
"r3": "D",
"r4": "Correcto",
"r5": "4/4",
"r6": "8"
}
Examen 2 Endpoints
POST /guardar_examen2
Content-Type: application/json
{
"email": "[email protected]",
"r1": "Verdadero",
"r2": "En la vida adulta: La densidad mineral ósea cambia muy poco. | En la vejez: ...",
"r3": "El gráfico comienza arriba de cero porque...",
"r4": "Sí podríamos porque...",
"r5": "f . Vejez (70-90 años)",
"r6": "El gráfico muestra un valor máximo...",
"r7": "Para estar seguros necesitamos..."
}
GET /respuestas_examen2
GET /verificar_examen2/{email}
The gamified elements in Examen 1 (sentence ordering and matching) make abstract calculus concepts more engaging while still assessing understanding through immediate feedback and scoring.