feat: Sincronizar cambios desde workspace-old
Backend: - exercise-responses.dto.ts - exercise-responses.service.ts Database DDL: - 00-prerequisites.sql - 04-initialize_user_stats.sql Scripts: - init-database.sh Seeds: - 01-tenants.sql (dev + prod) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5a0a29412c
commit
43441691cc
@ -35,24 +35,6 @@ export enum SortOrder {
|
|||||||
DESC = 'desc',
|
DESC = 'desc',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* G20 FIX: Source of the response (which table it comes from)
|
|
||||||
*/
|
|
||||||
export enum ResponseSource {
|
|
||||||
ATTEMPT = 'attempt',
|
|
||||||
SUBMISSION = 'submission',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* G20 FIX: Status for exercise submissions (only for source='submission')
|
|
||||||
*/
|
|
||||||
export enum ExerciseSubmissionStatus {
|
|
||||||
DRAFT = 'draft',
|
|
||||||
SUBMITTED = 'submitted',
|
|
||||||
GRADED = 'graded',
|
|
||||||
REVIEWED = 'reviewed',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query DTO for filtering exercise attempts
|
* Query DTO for filtering exercise attempts
|
||||||
*/
|
*/
|
||||||
@ -265,31 +247,6 @@ export class AttemptResponseDto {
|
|||||||
example: '2024-11-24T10:30:00Z',
|
example: '2024-11-24T10:30:00Z',
|
||||||
})
|
})
|
||||||
submitted_at!: string;
|
submitted_at!: string;
|
||||||
|
|
||||||
// G20 FIX: New fields to support exercise_submissions table
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
description: 'Source of the response (attempt=auto-graded, submission=manual review)',
|
|
||||||
enum: ResponseSource,
|
|
||||||
example: 'attempt',
|
|
||||||
})
|
|
||||||
source?: ResponseSource;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
description: 'Status (only for submissions - draft, submitted, graded, reviewed)',
|
|
||||||
enum: ExerciseSubmissionStatus,
|
|
||||||
example: 'submitted',
|
|
||||||
})
|
|
||||||
status?: ExerciseSubmissionStatus;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
description: 'Feedback from teacher (only for submissions)',
|
|
||||||
})
|
|
||||||
feedback?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
description: 'Whether the exercise requires manual grading',
|
|
||||||
})
|
|
||||||
requires_manual_grading?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -15,8 +15,6 @@ import {
|
|||||||
AttemptResponseDto,
|
AttemptResponseDto,
|
||||||
AttemptDetailDto,
|
AttemptDetailDto,
|
||||||
AttemptsListResponseDto,
|
AttemptsListResponseDto,
|
||||||
ResponseSource,
|
|
||||||
ExerciseSubmissionStatus,
|
|
||||||
} from '../dto/exercise-responses.dto';
|
} from '../dto/exercise-responses.dto';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,13 +74,6 @@ export class ExerciseResponsesService {
|
|||||||
* is_correct: true,
|
* is_correct: true,
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
/**
|
|
||||||
* G20 FIX: Get paginated list of exercise responses from BOTH tables
|
|
||||||
* - exercise_attempts: Auto-graded exercises (modules 1-3)
|
|
||||||
* - exercise_submissions: Manual review exercises (modules 4-5)
|
|
||||||
*
|
|
||||||
* Uses UNION query to combine both sources.
|
|
||||||
*/
|
|
||||||
async getAttempts(
|
async getAttempts(
|
||||||
userId: string,
|
userId: string,
|
||||||
query: GetAttemptsQueryDto,
|
query: GetAttemptsQueryDto,
|
||||||
@ -98,175 +89,107 @@ export class ExerciseResponsesService {
|
|||||||
const limit = query.limit || 20;
|
const limit = query.limit || 20;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
// Sorting - use alias for UNION compatibility
|
// Sorting
|
||||||
const sortField = query.sort_by === 'score'
|
const sortField = query.sort_by === 'score'
|
||||||
? 'score'
|
? 'attempt.score'
|
||||||
: query.sort_by === 'time'
|
: query.sort_by === 'time'
|
||||||
? 'time_spent_seconds'
|
? 'attempt.time_spent_seconds'
|
||||||
: 'submitted_at';
|
: 'attempt.submitted_at';
|
||||||
const sortOrder = query.sort_order?.toUpperCase() === 'ASC' ? 'ASC' : 'DESC';
|
const sortOrder = query.sort_order?.toUpperCase() === 'ASC' ? 'ASC' : 'DESC';
|
||||||
|
|
||||||
// Build base params
|
// Build WHERE conditions dynamically
|
||||||
|
const conditions: string[] = [
|
||||||
|
'(c.teacher_id = $1 OR EXISTS (SELECT 1 FROM social_features.teacher_classrooms tc WHERE tc.teacher_id = $1 AND tc.classroom_id = c.id))',
|
||||||
|
'profile.tenant_id = $2',
|
||||||
|
];
|
||||||
const params: any[] = [teacherId, tenantId];
|
const params: any[] = [teacherId, tenantId];
|
||||||
let paramIndex = 3;
|
let paramIndex = 3;
|
||||||
|
|
||||||
// Build dynamic conditions for attempts
|
|
||||||
// Note: teacher access is validated via classrooms.teacher_id
|
|
||||||
const buildConditions = () => {
|
|
||||||
const conditions: string[] = [
|
|
||||||
'c.teacher_id = $1',
|
|
||||||
'profile.tenant_id = $2',
|
|
||||||
];
|
|
||||||
return conditions;
|
|
||||||
};
|
|
||||||
|
|
||||||
let attemptsConditions = buildConditions();
|
|
||||||
let submissionsConditions = buildConditions();
|
|
||||||
|
|
||||||
// Add dynamic filters
|
|
||||||
const dynamicParams: any[] = [];
|
|
||||||
|
|
||||||
if (query.student_id) {
|
if (query.student_id) {
|
||||||
attemptsConditions.push(`profile.id = $${paramIndex}`);
|
conditions.push(`profile.id = $${paramIndex}`);
|
||||||
submissionsConditions.push(`profile.id = $${paramIndex}`);
|
params.push(query.student_id);
|
||||||
dynamicParams.push(query.student_id);
|
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.exercise_id) {
|
if (query.exercise_id) {
|
||||||
attemptsConditions.push(`attempt.exercise_id = $${paramIndex}`);
|
conditions.push(`attempt.exercise_id = $${paramIndex}`);
|
||||||
submissionsConditions.push(`sub.exercise_id = $${paramIndex}`);
|
params.push(query.exercise_id);
|
||||||
dynamicParams.push(query.exercise_id);
|
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.module_id) {
|
if (query.module_id) {
|
||||||
attemptsConditions.push(`exercise.module_id = $${paramIndex}`);
|
conditions.push(`exercise.module_id = $${paramIndex}`);
|
||||||
submissionsConditions.push(`exercise.module_id = $${paramIndex}`);
|
params.push(query.module_id);
|
||||||
dynamicParams.push(query.module_id);
|
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.classroom_id) {
|
if (query.classroom_id) {
|
||||||
attemptsConditions.push(`c.id = $${paramIndex}`);
|
conditions.push(`c.id = $${paramIndex}`);
|
||||||
submissionsConditions.push(`c.id = $${paramIndex}`);
|
params.push(query.classroom_id);
|
||||||
dynamicParams.push(query.classroom_id);
|
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.from_date) {
|
if (query.from_date) {
|
||||||
attemptsConditions.push(`attempt.submitted_at >= $${paramIndex}`);
|
conditions.push(`attempt.submitted_at >= $${paramIndex}`);
|
||||||
submissionsConditions.push(`sub.submitted_at >= $${paramIndex}`);
|
params.push(query.from_date);
|
||||||
dynamicParams.push(query.from_date);
|
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.to_date) {
|
if (query.to_date) {
|
||||||
attemptsConditions.push(`attempt.submitted_at <= $${paramIndex}`);
|
conditions.push(`attempt.submitted_at <= $${paramIndex}`);
|
||||||
submissionsConditions.push(`sub.submitted_at <= $${paramIndex}`);
|
params.push(query.to_date);
|
||||||
dynamicParams.push(query.to_date);
|
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.is_correct !== undefined) {
|
if (query.is_correct !== undefined) {
|
||||||
attemptsConditions.push(`attempt.is_correct = $${paramIndex}`);
|
conditions.push(`attempt.is_correct = $${paramIndex}`);
|
||||||
submissionsConditions.push(`sub.is_correct = $${paramIndex}`);
|
params.push(query.is_correct);
|
||||||
dynamicParams.push(query.is_correct);
|
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.student_search) {
|
if (query.student_search) {
|
||||||
const searchPattern = `%${query.student_search}%`;
|
const searchPattern = `%${query.student_search}%`;
|
||||||
const searchCondition = `(
|
conditions.push(`(
|
||||||
profile.first_name ILIKE $${paramIndex}
|
profile.first_name ILIKE $${paramIndex}
|
||||||
OR profile.last_name ILIKE $${paramIndex}
|
OR profile.last_name ILIKE $${paramIndex}
|
||||||
OR CONCAT(profile.first_name, ' ', profile.last_name) ILIKE $${paramIndex}
|
OR CONCAT(profile.first_name, ' ', profile.last_name) ILIKE $${paramIndex}
|
||||||
)`;
|
)`);
|
||||||
attemptsConditions.push(searchCondition);
|
params.push(searchPattern);
|
||||||
submissionsConditions.push(searchCondition);
|
|
||||||
dynamicParams.push(searchPattern);
|
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submissions: exclude drafts
|
const whereClause = conditions.join(' AND ');
|
||||||
submissionsConditions.push("sub.status != 'draft'");
|
|
||||||
|
|
||||||
const attemptsWhere = attemptsConditions.join(' AND ');
|
// Main query using raw SQL for cross-schema JOINs
|
||||||
const submissionsWhere = submissionsConditions.join(' AND ');
|
|
||||||
|
|
||||||
// Merge dynamic params
|
|
||||||
params.push(...dynamicParams);
|
|
||||||
|
|
||||||
// G20 FIX: UNION query combining both tables
|
|
||||||
// Note: to_jsonb(sub.comodines_used) converts text[] to jsonb for UNION compatibility
|
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT * FROM (
|
SELECT
|
||||||
-- Ejercicios autocorregibles (exercise_attempts)
|
attempt.id AS attempt_id,
|
||||||
SELECT
|
attempt.user_id AS attempt_user_id,
|
||||||
'attempt' AS source,
|
attempt.exercise_id AS attempt_exercise_id,
|
||||||
attempt.id AS id,
|
attempt.attempt_number AS attempt_attempt_number,
|
||||||
attempt.user_id AS user_id,
|
attempt.submitted_answers AS attempt_submitted_answers,
|
||||||
attempt.exercise_id AS exercise_id,
|
attempt.is_correct AS attempt_is_correct,
|
||||||
attempt.attempt_number AS attempt_number,
|
attempt.score AS attempt_score,
|
||||||
attempt.submitted_answers AS submitted_answers,
|
attempt.time_spent_seconds AS attempt_time_spent_seconds,
|
||||||
attempt.is_correct AS is_correct,
|
attempt.hints_used AS attempt_hints_used,
|
||||||
attempt.score AS score,
|
attempt.comodines_used AS attempt_comodines_used,
|
||||||
attempt.time_spent_seconds AS time_spent_seconds,
|
attempt.xp_earned AS attempt_xp_earned,
|
||||||
attempt.hints_used AS hints_used,
|
attempt.ml_coins_earned AS attempt_ml_coins_earned,
|
||||||
attempt.comodines_used AS comodines_used,
|
attempt.submitted_at AS attempt_submitted_at,
|
||||||
attempt.xp_earned AS xp_earned,
|
profile.id AS profile_id,
|
||||||
attempt.ml_coins_earned AS ml_coins_earned,
|
profile.first_name AS profile_first_name,
|
||||||
attempt.submitted_at AS submitted_at,
|
profile.last_name AS profile_last_name,
|
||||||
NULL::text AS status,
|
exercise.id AS exercise_id,
|
||||||
NULL::text AS feedback,
|
exercise.title AS exercise_title,
|
||||||
false AS requires_manual_grading,
|
module.id AS module_id,
|
||||||
profile.id AS profile_id,
|
module.title AS module_name
|
||||||
profile.first_name AS first_name,
|
FROM progress_tracking.exercise_attempts attempt
|
||||||
profile.last_name AS last_name,
|
LEFT JOIN auth_management.profiles profile ON profile.user_id = attempt.user_id
|
||||||
exercise.title AS exercise_title,
|
LEFT JOIN educational_content.exercises exercise ON exercise.id = attempt.exercise_id
|
||||||
module.title AS module_name
|
LEFT JOIN educational_content.modules module ON module.id = exercise.module_id
|
||||||
FROM progress_tracking.exercise_attempts attempt
|
LEFT JOIN social_features.classroom_members cm ON cm.student_id = profile.id
|
||||||
LEFT JOIN auth_management.profiles profile ON profile.user_id = attempt.user_id
|
LEFT JOIN social_features.classrooms c ON c.id = cm.classroom_id
|
||||||
LEFT JOIN educational_content.exercises exercise ON exercise.id = attempt.exercise_id
|
WHERE ${whereClause}
|
||||||
LEFT JOIN educational_content.modules module ON module.id = exercise.module_id
|
|
||||||
LEFT JOIN social_features.classroom_members cm ON cm.student_id = profile.id
|
|
||||||
LEFT JOIN social_features.classrooms c ON c.id = cm.classroom_id
|
|
||||||
WHERE ${attemptsWhere}
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
-- Ejercicios de revision manual (exercise_submissions)
|
|
||||||
SELECT
|
|
||||||
'submission' AS source,
|
|
||||||
sub.id AS id,
|
|
||||||
sub.user_id AS user_id,
|
|
||||||
sub.exercise_id AS exercise_id,
|
|
||||||
sub.attempt_number AS attempt_number,
|
|
||||||
sub.answer_data AS submitted_answers,
|
|
||||||
sub.is_correct AS is_correct,
|
|
||||||
sub.score AS score,
|
|
||||||
sub.time_spent_seconds AS time_spent_seconds,
|
|
||||||
sub.hints_count AS hints_used,
|
|
||||||
to_jsonb(sub.comodines_used) AS comodines_used,
|
|
||||||
sub.xp_earned AS xp_earned,
|
|
||||||
sub.ml_coins_earned AS ml_coins_earned,
|
|
||||||
sub.submitted_at AS submitted_at,
|
|
||||||
sub.status AS status,
|
|
||||||
sub.feedback AS feedback,
|
|
||||||
true AS requires_manual_grading,
|
|
||||||
profile.id AS profile_id,
|
|
||||||
profile.first_name AS first_name,
|
|
||||||
profile.last_name AS last_name,
|
|
||||||
exercise.title AS exercise_title,
|
|
||||||
module.title AS module_name
|
|
||||||
FROM progress_tracking.exercise_submissions sub
|
|
||||||
LEFT JOIN auth_management.profiles profile ON profile.user_id = sub.user_id
|
|
||||||
LEFT JOIN educational_content.exercises exercise ON exercise.id = sub.exercise_id
|
|
||||||
LEFT JOIN educational_content.modules module ON module.id = exercise.module_id
|
|
||||||
LEFT JOIN social_features.classroom_members cm ON cm.student_id = profile.id
|
|
||||||
LEFT JOIN social_features.classrooms c ON c.id = cm.classroom_id
|
|
||||||
WHERE ${submissionsWhere}
|
|
||||||
) AS combined
|
|
||||||
ORDER BY ${sortField} ${sortOrder}
|
ORDER BY ${sortField} ${sortOrder}
|
||||||
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||||
`;
|
`;
|
||||||
@ -276,25 +199,15 @@ export class ExerciseResponsesService {
|
|||||||
// Execute main query
|
// Execute main query
|
||||||
const rawResults = await this.dataSource.query(sql, params);
|
const rawResults = await this.dataSource.query(sql, params);
|
||||||
|
|
||||||
// Count query for both tables
|
// Count query (separate for efficiency)
|
||||||
const countSql = `
|
const countSql = `
|
||||||
SELECT (
|
SELECT COUNT(DISTINCT attempt.id) as total
|
||||||
(SELECT COUNT(DISTINCT attempt.id)
|
FROM progress_tracking.exercise_attempts attempt
|
||||||
FROM progress_tracking.exercise_attempts attempt
|
LEFT JOIN auth_management.profiles profile ON profile.user_id = attempt.user_id
|
||||||
LEFT JOIN auth_management.profiles profile ON profile.user_id = attempt.user_id
|
LEFT JOIN social_features.classroom_members cm ON cm.student_id = profile.id
|
||||||
LEFT JOIN social_features.classroom_members cm ON cm.student_id = profile.id
|
LEFT JOIN social_features.classrooms c ON c.id = cm.classroom_id
|
||||||
LEFT JOIN social_features.classrooms c ON c.id = cm.classroom_id
|
LEFT JOIN educational_content.exercises exercise ON exercise.id = attempt.exercise_id
|
||||||
LEFT JOIN educational_content.exercises exercise ON exercise.id = attempt.exercise_id
|
WHERE ${whereClause}
|
||||||
WHERE ${attemptsWhere})
|
|
||||||
+
|
|
||||||
(SELECT COUNT(DISTINCT sub.id)
|
|
||||||
FROM progress_tracking.exercise_submissions sub
|
|
||||||
LEFT JOIN auth_management.profiles profile ON profile.user_id = sub.user_id
|
|
||||||
LEFT JOIN social_features.classroom_members cm ON cm.student_id = profile.id
|
|
||||||
LEFT JOIN social_features.classrooms c ON c.id = cm.classroom_id
|
|
||||||
LEFT JOIN educational_content.exercises exercise ON exercise.id = sub.exercise_id
|
|
||||||
WHERE ${submissionsWhere})
|
|
||||||
) AS total
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Remove LIMIT/OFFSET params for count query
|
// Remove LIMIT/OFFSET params for count query
|
||||||
@ -302,29 +215,24 @@ export class ExerciseResponsesService {
|
|||||||
const countResult = await this.dataSource.query(countSql, countParams);
|
const countResult = await this.dataSource.query(countSql, countParams);
|
||||||
const total = parseInt(countResult[0]?.total || '0', 10);
|
const total = parseInt(countResult[0]?.total || '0', 10);
|
||||||
|
|
||||||
// Transform raw results to DTOs with new G20 fields
|
// Transform raw results to DTOs
|
||||||
const data: AttemptResponseDto[] = rawResults.map((row: any) => ({
|
const data: AttemptResponseDto[] = rawResults.map((row: any) => ({
|
||||||
id: row.id,
|
id: row.attempt_id,
|
||||||
student_id: row.user_id,
|
student_id: row.attempt_user_id,
|
||||||
student_name: `${row.first_name || ''} ${row.last_name || ''}`.trim() || 'Unknown',
|
student_name: `${row.profile_first_name || ''} ${row.profile_last_name || ''}`.trim() || 'Unknown',
|
||||||
exercise_id: row.exercise_id,
|
exercise_id: row.attempt_exercise_id,
|
||||||
exercise_title: row.exercise_title || 'Unknown Exercise',
|
exercise_title: row.exercise_title || 'Unknown Exercise',
|
||||||
module_name: row.module_name || 'Unknown Module',
|
module_name: row.module_name || 'Unknown Module',
|
||||||
attempt_number: row.attempt_number,
|
attempt_number: row.attempt_attempt_number,
|
||||||
submitted_answers: row.submitted_answers,
|
submitted_answers: row.attempt_submitted_answers,
|
||||||
is_correct: row.is_correct ?? false,
|
is_correct: row.attempt_is_correct ?? false,
|
||||||
score: row.score ?? 0,
|
score: row.attempt_score ?? 0,
|
||||||
time_spent_seconds: row.time_spent_seconds ?? 0,
|
time_spent_seconds: row.attempt_time_spent_seconds ?? 0,
|
||||||
hints_used: row.hints_used ?? 0,
|
hints_used: row.attempt_hints_used,
|
||||||
comodines_used: Array.isArray(row.comodines_used) ? row.comodines_used : (row.comodines_used || []),
|
comodines_used: row.attempt_comodines_used,
|
||||||
xp_earned: row.xp_earned ?? 0,
|
xp_earned: row.attempt_xp_earned,
|
||||||
ml_coins_earned: row.ml_coins_earned ?? 0,
|
ml_coins_earned: row.attempt_ml_coins_earned,
|
||||||
submitted_at: row.submitted_at ? new Date(row.submitted_at).toISOString() : new Date().toISOString(),
|
submitted_at: row.attempt_submitted_at ? new Date(row.attempt_submitted_at).toISOString() : new Date().toISOString(),
|
||||||
// G20 FIX: New fields
|
|
||||||
source: row.source === 'submission' ? ResponseSource.SUBMISSION : ResponseSource.ATTEMPT,
|
|
||||||
status: row.status as ExerciseSubmissionStatus | undefined,
|
|
||||||
feedback: row.feedback || undefined,
|
|
||||||
requires_manual_grading: row.requires_manual_grading ?? false,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -389,17 +297,13 @@ export class ExerciseResponsesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get detailed information for a specific attempt or submission
|
* Get detailed information for a specific attempt
|
||||||
*
|
|
||||||
* G20 FIX: Now searches BOTH tables:
|
|
||||||
* - exercise_attempts: Auto-graded exercises (modules 1-3)
|
|
||||||
* - exercise_submissions: Manual review exercises (modules 4-5)
|
|
||||||
*
|
*
|
||||||
* @param userId - Teacher's user ID (from auth.users)
|
* @param userId - Teacher's user ID (from auth.users)
|
||||||
* @param attemptId - Attempt or Submission ID
|
* @param attemptId - Attempt ID
|
||||||
* @returns Detailed attempt information including correct answers
|
* @returns Detailed attempt information including correct answers
|
||||||
*
|
*
|
||||||
* @throws NotFoundException if attempt/submission not found
|
* @throws NotFoundException if attempt not found
|
||||||
* @throws ForbiddenException if teacher doesn't have access
|
* @throws ForbiddenException if teacher doesn't have access
|
||||||
*/
|
*/
|
||||||
async getAttemptDetail(
|
async getAttemptDetail(
|
||||||
@ -411,34 +315,30 @@ export class ExerciseResponsesService {
|
|||||||
const teacherId = teacherProfile.id;
|
const teacherId = teacherProfile.id;
|
||||||
const tenantId = teacherProfile.tenant_id;
|
const tenantId = teacherProfile.tenant_id;
|
||||||
|
|
||||||
// G20 FIX: First try exercise_attempts table
|
// Raw SQL query for cross-schema JOINs
|
||||||
const attemptSql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
'attempt' AS source,
|
attempt.id AS attempt_id,
|
||||||
attempt.id AS record_id,
|
attempt.user_id AS attempt_user_id,
|
||||||
attempt.user_id AS user_id,
|
attempt.exercise_id AS attempt_exercise_id,
|
||||||
attempt.exercise_id AS exercise_id,
|
attempt.attempt_number AS attempt_attempt_number,
|
||||||
attempt.attempt_number AS attempt_number,
|
attempt.submitted_answers AS attempt_submitted_answers,
|
||||||
attempt.submitted_answers AS submitted_answers,
|
attempt.is_correct AS attempt_is_correct,
|
||||||
attempt.is_correct AS is_correct,
|
attempt.score AS attempt_score,
|
||||||
attempt.score AS score,
|
attempt.time_spent_seconds AS attempt_time_spent_seconds,
|
||||||
attempt.time_spent_seconds AS time_spent_seconds,
|
attempt.hints_used AS attempt_hints_used,
|
||||||
attempt.hints_used AS hints_used,
|
attempt.comodines_used AS attempt_comodines_used,
|
||||||
attempt.comodines_used AS comodines_used,
|
attempt.xp_earned AS attempt_xp_earned,
|
||||||
attempt.xp_earned AS xp_earned,
|
attempt.ml_coins_earned AS attempt_ml_coins_earned,
|
||||||
attempt.ml_coins_earned AS ml_coins_earned,
|
attempt.submitted_at AS attempt_submitted_at,
|
||||||
attempt.submitted_at AS submitted_at,
|
|
||||||
NULL::text AS status,
|
|
||||||
NULL::text AS feedback,
|
|
||||||
false AS requires_manual_grading,
|
|
||||||
profile.id AS profile_id,
|
profile.id AS profile_id,
|
||||||
profile.first_name AS first_name,
|
profile.first_name AS profile_first_name,
|
||||||
profile.last_name AS last_name,
|
profile.last_name AS profile_last_name,
|
||||||
exercise.id AS ex_id,
|
exercise.id AS exercise_id,
|
||||||
exercise.title AS exercise_title,
|
exercise.title AS exercise_title,
|
||||||
exercise.exercise_type AS exercise_type,
|
exercise.exercise_type AS exercise_type,
|
||||||
exercise.content AS exercise_content,
|
exercise.content AS exercise_content,
|
||||||
exercise.max_points AS max_points,
|
exercise.max_points AS exercise_max_points,
|
||||||
module.id AS module_id,
|
module.id AS module_id,
|
||||||
module.title AS module_name
|
module.title AS module_name
|
||||||
FROM progress_tracking.exercise_attempts attempt
|
FROM progress_tracking.exercise_attempts attempt
|
||||||
@ -449,62 +349,15 @@ export class ExerciseResponsesService {
|
|||||||
LEFT JOIN social_features.classrooms c ON c.id = cm.classroom_id
|
LEFT JOIN social_features.classrooms c ON c.id = cm.classroom_id
|
||||||
WHERE attempt.id = $1
|
WHERE attempt.id = $1
|
||||||
AND profile.tenant_id = $2
|
AND profile.tenant_id = $2
|
||||||
AND c.teacher_id = $3
|
AND (c.teacher_id = $3 OR EXISTS (SELECT 1 FROM social_features.teacher_classrooms tc WHERE tc.teacher_id = $3 AND tc.classroom_id = c.id))
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`;
|
`;
|
||||||
|
|
||||||
let results = await this.dataSource.query(attemptSql, [attemptId, tenantId, teacherId]);
|
const results = await this.dataSource.query(sql, [attemptId, tenantId, teacherId]);
|
||||||
let row = results[0];
|
const row = results[0];
|
||||||
|
|
||||||
// G20 FIX: If not found in attempts, try exercise_submissions
|
|
||||||
if (!row) {
|
|
||||||
const submissionSql = `
|
|
||||||
SELECT
|
|
||||||
'submission' AS source,
|
|
||||||
sub.id AS record_id,
|
|
||||||
sub.user_id AS user_id,
|
|
||||||
sub.exercise_id AS exercise_id,
|
|
||||||
sub.attempt_number AS attempt_number,
|
|
||||||
sub.answer_data AS submitted_answers,
|
|
||||||
sub.is_correct AS is_correct,
|
|
||||||
sub.score AS score,
|
|
||||||
sub.time_spent_seconds AS time_spent_seconds,
|
|
||||||
sub.hints_count AS hints_used,
|
|
||||||
to_jsonb(sub.comodines_used) AS comodines_used,
|
|
||||||
sub.xp_earned AS xp_earned,
|
|
||||||
sub.ml_coins_earned AS ml_coins_earned,
|
|
||||||
sub.submitted_at AS submitted_at,
|
|
||||||
sub.status AS status,
|
|
||||||
sub.feedback AS feedback,
|
|
||||||
true AS requires_manual_grading,
|
|
||||||
profile.id AS profile_id,
|
|
||||||
profile.first_name AS first_name,
|
|
||||||
profile.last_name AS last_name,
|
|
||||||
exercise.id AS ex_id,
|
|
||||||
exercise.title AS exercise_title,
|
|
||||||
exercise.exercise_type AS exercise_type,
|
|
||||||
exercise.content AS exercise_content,
|
|
||||||
exercise.max_points AS max_points,
|
|
||||||
module.id AS module_id,
|
|
||||||
module.title AS module_name
|
|
||||||
FROM progress_tracking.exercise_submissions sub
|
|
||||||
LEFT JOIN auth_management.profiles profile ON profile.user_id = sub.user_id
|
|
||||||
LEFT JOIN educational_content.exercises exercise ON exercise.id = sub.exercise_id
|
|
||||||
LEFT JOIN educational_content.modules module ON module.id = exercise.module_id
|
|
||||||
LEFT JOIN social_features.classroom_members cm ON cm.student_id = profile.id
|
|
||||||
LEFT JOIN social_features.classrooms c ON c.id = cm.classroom_id
|
|
||||||
WHERE sub.id = $1
|
|
||||||
AND profile.tenant_id = $2
|
|
||||||
AND c.teacher_id = $3
|
|
||||||
LIMIT 1
|
|
||||||
`;
|
|
||||||
|
|
||||||
results = await this.dataSource.query(submissionSql, [attemptId, tenantId, teacherId]);
|
|
||||||
row = results[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new NotFoundException(`Attempt/Submission ${attemptId} not found or access denied`);
|
throw new NotFoundException(`Attempt ${attemptId} not found or access denied`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse exercise content if it's a string
|
// Parse exercise content if it's a string
|
||||||
@ -524,31 +377,26 @@ export class ExerciseResponsesService {
|
|||||||
const correctAnswer = this.extractCorrectAnswers(exerciseContent, row.exercise_type);
|
const correctAnswer = this.extractCorrectAnswers(exerciseContent, row.exercise_type);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: row.record_id,
|
id: row.attempt_id,
|
||||||
student_id: row.user_id,
|
student_id: row.attempt_user_id,
|
||||||
student_name: `${row.first_name || ''} ${row.last_name || ''}`.trim() || 'Unknown',
|
student_name: `${row.profile_first_name || ''} ${row.profile_last_name || ''}`.trim() || 'Unknown',
|
||||||
exercise_id: row.exercise_id,
|
exercise_id: row.attempt_exercise_id,
|
||||||
exercise_title: row.exercise_title || 'Unknown Exercise',
|
exercise_title: row.exercise_title || 'Unknown Exercise',
|
||||||
module_name: row.module_name || 'Unknown Module',
|
module_name: row.module_name || 'Unknown Module',
|
||||||
attempt_number: row.attempt_number,
|
attempt_number: row.attempt_attempt_number,
|
||||||
submitted_answers: row.submitted_answers,
|
submitted_answers: row.attempt_submitted_answers,
|
||||||
is_correct: row.is_correct ?? false,
|
is_correct: row.attempt_is_correct ?? false,
|
||||||
score: row.score ?? 0,
|
score: row.attempt_score ?? 0,
|
||||||
time_spent_seconds: row.time_spent_seconds ?? 0,
|
time_spent_seconds: row.attempt_time_spent_seconds ?? 0,
|
||||||
hints_used: row.hints_used ?? 0,
|
hints_used: row.attempt_hints_used,
|
||||||
comodines_used: Array.isArray(row.comodines_used) ? row.comodines_used : (row.comodines_used || []),
|
comodines_used: row.attempt_comodines_used,
|
||||||
xp_earned: row.xp_earned ?? 0,
|
xp_earned: row.attempt_xp_earned,
|
||||||
ml_coins_earned: row.ml_coins_earned ?? 0,
|
ml_coins_earned: row.attempt_ml_coins_earned,
|
||||||
submitted_at: row.submitted_at ? new Date(row.submitted_at).toISOString() : new Date().toISOString(),
|
submitted_at: row.attempt_submitted_at ? new Date(row.attempt_submitted_at).toISOString() : new Date().toISOString(),
|
||||||
// G20 FIX: New fields
|
|
||||||
source: row.source === 'submission' ? ResponseSource.SUBMISSION : ResponseSource.ATTEMPT,
|
|
||||||
status: row.status as ExerciseSubmissionStatus | undefined,
|
|
||||||
feedback: row.feedback || undefined,
|
|
||||||
requires_manual_grading: row.requires_manual_grading ?? false,
|
|
||||||
// Additional detail fields
|
// Additional detail fields
|
||||||
correct_answer: correctAnswer,
|
correct_answer: correctAnswer,
|
||||||
exercise_type: row.exercise_type || 'unknown',
|
exercise_type: row.exercise_type || 'unknown',
|
||||||
max_score: row.max_points || 100,
|
max_score: row.exercise_max_points || 100,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -674,7 +522,6 @@ export class ExerciseResponsesService {
|
|||||||
const tenantId = teacherProfile.tenant_id;
|
const tenantId = teacherProfile.tenant_id;
|
||||||
|
|
||||||
// Raw SQL for cross-schema verification
|
// Raw SQL for cross-schema verification
|
||||||
// G20 FIX: Simplified to use only c.teacher_id (removed non-existent teacher_classrooms reference)
|
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM auth_management.profiles profile
|
FROM auth_management.profiles profile
|
||||||
@ -682,7 +529,7 @@ export class ExerciseResponsesService {
|
|||||||
LEFT JOIN social_features.classrooms c ON c.id = cm.classroom_id
|
LEFT JOIN social_features.classrooms c ON c.id = cm.classroom_id
|
||||||
WHERE profile.id = $1
|
WHERE profile.id = $1
|
||||||
AND profile.tenant_id = $2
|
AND profile.tenant_id = $2
|
||||||
AND c.teacher_id = $3
|
AND (c.teacher_id = $3 OR EXISTS (SELECT 1 FROM social_features.teacher_classrooms tc WHERE tc.teacher_id = $3 AND tc.classroom_id = c.id))
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@ -210,20 +210,9 @@ DO $$ BEGIN
|
|||||||
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||||
|
|
||||||
-- 📚 Documentación: educational_content.difficulty_level
|
-- 📚 Documentación: educational_content.difficulty_level
|
||||||
-- RESTAURADO (2025-12-19): Necesario en prerequisites para que tablas puedan crearse
|
-- REMOVIDO (2025-11-11): Migrado a ddl/schemas/educational_content/enums/difficulty_level.sql
|
||||||
-- 8 niveles CEFR: beginner (A1) → native (C2+)
|
-- Razón: Evitar duplicación (Política de Carga Limpia)
|
||||||
DO $$ BEGIN
|
-- El ENUM se define en el schema específico con documentación completa (8 niveles CEFR)
|
||||||
CREATE TYPE educational_content.difficulty_level AS ENUM (
|
|
||||||
'beginner', -- A1: Nivel básico de supervivencia
|
|
||||||
'elementary', -- A2: Nivel elemental
|
|
||||||
'pre_intermediate', -- B1: Pre-intermedio
|
|
||||||
'intermediate', -- B2: Intermedio
|
|
||||||
'upper_intermediate', -- C1: Intermedio avanzado
|
|
||||||
'advanced', -- C2: Avanzado
|
|
||||||
'proficient', -- C2+: Competente
|
|
||||||
'native' -- Nativo: Dominio total
|
|
||||||
);
|
|
||||||
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
|
||||||
|
|
||||||
-- 📚 Documentación: educational_content.module_status
|
-- 📚 Documentación: educational_content.module_status
|
||||||
-- VERSIÓN: 1.2 (2025-11-23) - Agregado 'backlog' para módulos fuera de alcance de entrega
|
-- VERSIÓN: 1.2 (2025-11-23) - Agregado 'backlog' para módulos fuera de alcance de entrega
|
||||||
@ -239,16 +228,9 @@ DO $$ BEGIN
|
|||||||
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||||
|
|
||||||
-- 📚 Documentación: content_management.content_status
|
-- 📚 Documentación: content_management.content_status
|
||||||
-- RESTAURADO (2025-12-19): Necesario en prerequisites para que tablas puedan crearse
|
-- REMOVIDO (2025-11-11): Migrado a ddl/schemas/content_management/enums/content_status.sql
|
||||||
-- Estados del ciclo de vida del contenido educativo
|
-- Razón: Evitar duplicación (Política de Carga Limpia)
|
||||||
DO $$ BEGIN
|
-- El ENUM se define en el schema específico con documentación completa
|
||||||
CREATE TYPE content_management.content_status AS ENUM (
|
|
||||||
'draft', -- Borrador
|
|
||||||
'published', -- Publicado
|
|
||||||
'archived', -- Archivado
|
|
||||||
'under_review' -- En revisión
|
|
||||||
);
|
|
||||||
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
|
||||||
|
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN
|
||||||
CREATE TYPE educational_content.cognitive_level AS ENUM ('recordar', 'comprender', 'aplicar', 'analizar', 'evaluar', 'crear');
|
CREATE TYPE educational_content.cognitive_level AS ENUM ('recordar', 'comprender', 'aplicar', 'analizar', 'evaluar', 'crear');
|
||||||
@ -274,18 +256,9 @@ EXCEPTION WHEN duplicate_object THEN null; END $$;
|
|||||||
-- 4. ENUMs de Progreso
|
-- 4. ENUMs de Progreso
|
||||||
|
|
||||||
-- 📚 Documentación: progress_tracking.progress_status
|
-- 📚 Documentación: progress_tracking.progress_status
|
||||||
-- RESTAURADO (2025-12-19): Necesario en prerequisites para que tablas puedan crearse
|
-- REMOVIDO (2025-11-11): Migrado a ddl/schemas/progress_tracking/enums/progress_status.sql
|
||||||
-- Estados de progreso para módulos y ejercicios
|
-- Razón: Evitar duplicación (Política de Carga Limpia)
|
||||||
DO $$ BEGIN
|
-- El ENUM se define en el schema específico con documentación exhaustiva (112 líneas)
|
||||||
CREATE TYPE progress_tracking.progress_status AS ENUM (
|
|
||||||
'not_started', -- El usuario no ha comenzado el contenido
|
|
||||||
'in_progress', -- El usuario está trabajando en el contenido
|
|
||||||
'completed', -- El usuario completó el contenido exitosamente
|
|
||||||
'needs_review', -- El contenido fue completado pero requiere revisión
|
|
||||||
'mastered', -- El usuario dominó el contenido (nivel de excelencia)
|
|
||||||
'abandoned' -- El usuario abandonó el contenido sin completar
|
|
||||||
);
|
|
||||||
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
|
||||||
|
|
||||||
-- 📚 Documentación: progress_tracking.attempt_status
|
-- 📚 Documentación: progress_tracking.attempt_status
|
||||||
-- Requerimiento: docs/01-requerimientos/04-progreso-seguimiento/RF-PRG-001-estados-progreso.md
|
-- Requerimiento: docs/01-requerimientos/04-progreso-seguimiento/RF-PRG-001-estados-progreso.md
|
||||||
|
|||||||
@ -9,8 +9,6 @@
|
|||||||
-- #1: Added module_progress initialization (CRITICAL)
|
-- #1: Added module_progress initialization (CRITICAL)
|
||||||
-- #2: Added ON CONFLICT to user_ranks (prevents duplicate key errors)
|
-- #2: Added ON CONFLICT to user_ranks (prevents duplicate key errors)
|
||||||
-- #3: Kept initialize_user_missions commented (function not implemented yet)
|
-- #3: Kept initialize_user_missions commented (function not implemented yet)
|
||||||
-- Updated: 2025-12-19 - BUG FIX CRÍTICO:
|
|
||||||
-- #4: Todas las tablas tienen FK a profiles.id, usar NEW.id en todos los inserts
|
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION gamilit.initialize_user_stats()
|
CREATE OR REPLACE FUNCTION gamilit.initialize_user_stats()
|
||||||
@ -21,16 +19,14 @@ BEGIN
|
|||||||
-- Initialize gamification for students, teachers, and admins
|
-- Initialize gamification for students, teachers, and admins
|
||||||
-- Only these roles have gamification enabled
|
-- Only these roles have gamification enabled
|
||||||
IF NEW.role IN ('student', 'admin_teacher', 'super_admin') THEN
|
IF NEW.role IN ('student', 'admin_teacher', 'super_admin') THEN
|
||||||
-- IMPORTANTE: Todas las tablas (user_stats, user_ranks, comodines_inventory, module_progress)
|
-- Use NEW.user_id which points to auth.users.id (correct foreign key reference)
|
||||||
-- tienen FK user_id que referencia profiles.id, NO auth.users.id
|
|
||||||
-- Por lo tanto, debemos usar NEW.id (profiles.id) en todos los inserts
|
|
||||||
INSERT INTO gamification_system.user_stats (
|
INSERT INTO gamification_system.user_stats (
|
||||||
user_id,
|
user_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
ml_coins,
|
ml_coins,
|
||||||
ml_coins_earned_total
|
ml_coins_earned_total
|
||||||
) VALUES (
|
) VALUES (
|
||||||
NEW.id, -- FIXED 2025-12-19: usar NEW.id (profiles.id), FK apunta a profiles(id)
|
NEW.user_id, -- Fixed: usar user_id en lugar de id
|
||||||
NEW.tenant_id,
|
NEW.tenant_id,
|
||||||
100, -- Welcome bonus
|
100, -- Welcome bonus
|
||||||
100
|
100
|
||||||
@ -38,25 +34,27 @@ BEGIN
|
|||||||
ON CONFLICT (user_id) DO NOTHING; -- Prevent duplicates
|
ON CONFLICT (user_id) DO NOTHING; -- Prevent duplicates
|
||||||
|
|
||||||
-- Create comodines inventory
|
-- Create comodines inventory
|
||||||
|
-- IMPORTANT: comodines_inventory.user_id references profiles.id (NOT auth.users.id)
|
||||||
INSERT INTO gamification_system.comodines_inventory (
|
INSERT INTO gamification_system.comodines_inventory (
|
||||||
user_id
|
user_id
|
||||||
) VALUES (
|
) VALUES (
|
||||||
NEW.id -- profiles.id - FK apunta a profiles(id)
|
NEW.id -- CORRECTED: usar NEW.id (profiles.id) porque FK apunta a profiles(id)
|
||||||
)
|
)
|
||||||
ON CONFLICT (user_id) DO NOTHING;
|
ON CONFLICT (user_id) DO NOTHING;
|
||||||
|
|
||||||
-- Create initial user rank (starting with Ajaw - lowest rank)
|
-- Create initial user rank (starting with Ajaw - lowest rank)
|
||||||
|
-- BUG FIX #2: Use WHERE NOT EXISTS instead of ON CONFLICT (no unique constraint on user_id)
|
||||||
INSERT INTO gamification_system.user_ranks (
|
INSERT INTO gamification_system.user_ranks (
|
||||||
user_id,
|
user_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
current_rank
|
current_rank
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
NEW.id, -- FIXED 2025-12-19: usar NEW.id (profiles.id), FK apunta a profiles(id)
|
NEW.user_id,
|
||||||
NEW.tenant_id,
|
NEW.tenant_id,
|
||||||
'Ajaw'::gamification_system.maya_rank
|
'Ajaw'::gamification_system.maya_rank
|
||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
SELECT 1 FROM gamification_system.user_ranks WHERE user_id = NEW.id
|
SELECT 1 FROM gamification_system.user_ranks WHERE user_id = NEW.user_id
|
||||||
);
|
);
|
||||||
|
|
||||||
-- BUG FIX #1: Initialize module progress for all active modules
|
-- BUG FIX #1: Initialize module progress for all active modules
|
||||||
|
|||||||
@ -821,24 +821,27 @@ load_seeds() {
|
|||||||
local failed=0
|
local failed=0
|
||||||
|
|
||||||
# Array con orden específico respetando dependencias
|
# Array con orden específico respetando dependencias
|
||||||
# IMPORTANTE: Los módulos deben cargarse ANTES de los profiles
|
|
||||||
# porque el trigger trg_initialize_user_stats crea module_progress
|
|
||||||
# y necesita que los módulos ya existan
|
|
||||||
local seed_files=(
|
local seed_files=(
|
||||||
# === FASE 1: INFRAESTRUCTURA BASE ===
|
|
||||||
"$SEEDS_DIR/auth_management/01-tenants.sql"
|
"$SEEDS_DIR/auth_management/01-tenants.sql"
|
||||||
"$SEEDS_DIR/auth_management/02-auth_providers.sql"
|
"$SEEDS_DIR/auth_management/02-auth_providers.sql"
|
||||||
|
"$SEEDS_DIR/auth/01-demo-users.sql"
|
||||||
|
"$SEEDS_DIR/auth/02-production-users.sql" # ✅ PROD: Usuarios reales (13)
|
||||||
|
"$SEEDS_DIR/auth/02-test-users.sql" # ✅ DEV: Usuarios de prueba (3)
|
||||||
|
"$SEEDS_DIR/auth_management/03-profiles.sql"
|
||||||
|
"$SEEDS_DIR/auth_management/04-profiles-testing.sql" # ✅ PROD: Profiles @gamilit.com (3)
|
||||||
|
"$SEEDS_DIR/auth_management/05-profiles-demo.sql" # ✅ PROD: Profiles demo (20)
|
||||||
|
"$SEEDS_DIR/auth_management/06-profiles-production.sql" # ✅ PROD: Profiles reales (13)
|
||||||
|
"$SEEDS_DIR/auth_management/04-user_roles.sql"
|
||||||
|
"$SEEDS_DIR/auth_management/05-user_preferences.sql"
|
||||||
|
"$SEEDS_DIR/auth_management/06-auth_attempts.sql"
|
||||||
|
"$SEEDS_DIR/auth_management/07-security_events.sql"
|
||||||
"$SEEDS_DIR/system_configuration/01-system_settings.sql"
|
"$SEEDS_DIR/system_configuration/01-system_settings.sql"
|
||||||
"$SEEDS_DIR/system_configuration/02-feature_flags.sql"
|
"$SEEDS_DIR/system_configuration/02-feature_flags.sql"
|
||||||
|
|
||||||
# === FASE 2: GAMIFICATION BASE (antes de profiles) ===
|
|
||||||
"$SEEDS_DIR/gamification_system/01-achievement_categories.sql"
|
"$SEEDS_DIR/gamification_system/01-achievement_categories.sql"
|
||||||
"$SEEDS_DIR/gamification_system/02-leaderboard_metadata.sql"
|
"$SEEDS_DIR/gamification_system/02-leaderboard_metadata.sql"
|
||||||
"$SEEDS_DIR/gamification_system/03-maya_ranks.sql"
|
"$SEEDS_DIR/gamification_system/03-maya_ranks.sql"
|
||||||
"$SEEDS_DIR/gamification_system/04-achievements.sql"
|
"$SEEDS_DIR/gamification_system/04-achievements.sql"
|
||||||
|
"$SEEDS_DIR/gamification_system/04-initialize_user_gamification.sql"
|
||||||
# === FASE 3: MÓDULOS Y EJERCICIOS (ANTES de profiles - CRÍTICO) ===
|
|
||||||
# El trigger trg_initialize_user_stats necesita módulos publicados
|
|
||||||
"$SEEDS_DIR/educational_content/01-modules.sql"
|
"$SEEDS_DIR/educational_content/01-modules.sql"
|
||||||
"$SEEDS_DIR/educational_content/02-exercises-module1.sql"
|
"$SEEDS_DIR/educational_content/02-exercises-module1.sql"
|
||||||
"$SEEDS_DIR/educational_content/03-exercises-module2.sql"
|
"$SEEDS_DIR/educational_content/03-exercises-module2.sql"
|
||||||
@ -846,28 +849,6 @@ load_seeds() {
|
|||||||
"$SEEDS_DIR/educational_content/05-exercises-module4.sql"
|
"$SEEDS_DIR/educational_content/05-exercises-module4.sql"
|
||||||
"$SEEDS_DIR/educational_content/06-exercises-module5.sql"
|
"$SEEDS_DIR/educational_content/06-exercises-module5.sql"
|
||||||
"$SEEDS_DIR/educational_content/07-assessment-rubrics.sql"
|
"$SEEDS_DIR/educational_content/07-assessment-rubrics.sql"
|
||||||
|
|
||||||
# === FASE 4: USUARIOS (auth.users) ===
|
|
||||||
"$SEEDS_DIR/auth/01-demo-users.sql"
|
|
||||||
"$SEEDS_DIR/auth/02-production-users.sql"
|
|
||||||
"$SEEDS_DIR/auth/02-test-users.sql"
|
|
||||||
|
|
||||||
# === FASE 5: PROFILES (activa trigger que crea module_progress) ===
|
|
||||||
"$SEEDS_DIR/auth_management/03-profiles.sql"
|
|
||||||
"$SEEDS_DIR/auth_management/04-profiles-complete.sql"
|
|
||||||
"$SEEDS_DIR/auth_management/04-profiles-testing.sql"
|
|
||||||
"$SEEDS_DIR/auth_management/05-profiles-demo.sql"
|
|
||||||
"$SEEDS_DIR/auth_management/06-profiles-production.sql"
|
|
||||||
"$SEEDS_DIR/auth_management/07-profiles-production-additional.sql"
|
|
||||||
"$SEEDS_DIR/auth_management/04-user_roles.sql"
|
|
||||||
"$SEEDS_DIR/auth_management/05-user_preferences.sql"
|
|
||||||
"$SEEDS_DIR/auth_management/06-auth_attempts.sql"
|
|
||||||
"$SEEDS_DIR/auth_management/07-security_events.sql"
|
|
||||||
|
|
||||||
# === FASE 6: GAMIFICATION USUARIOS (post-profiles) ===
|
|
||||||
"$SEEDS_DIR/gamification_system/04-initialize_user_gamification.sql"
|
|
||||||
|
|
||||||
# === FASE 7: CONTENIDO ADICIONAL ===
|
|
||||||
"$SEEDS_DIR/content_management/01-marie-curie-bio.sql"
|
"$SEEDS_DIR/content_management/01-marie-curie-bio.sql"
|
||||||
"$SEEDS_DIR/content_management/02-media-files.sql"
|
"$SEEDS_DIR/content_management/02-media-files.sql"
|
||||||
"$SEEDS_DIR/content_management/03-tags.sql"
|
"$SEEDS_DIR/content_management/03-tags.sql"
|
||||||
|
|||||||
@ -119,37 +119,6 @@ INSERT INTO auth_management.tenants (
|
|||||||
}'::jsonb,
|
}'::jsonb,
|
||||||
gamilit.now_mexico(),
|
gamilit.now_mexico(),
|
||||||
gamilit.now_mexico()
|
gamilit.now_mexico()
|
||||||
),
|
|
||||||
-- Tenant 4: Gamilit Production (para usuarios de producción)
|
|
||||||
-- AGREGADO 2025-12-19: Necesario para profiles de producción
|
|
||||||
(
|
|
||||||
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid,
|
|
||||||
'Gamilit Production',
|
|
||||||
'gamilit-prod',
|
|
||||||
'gamilit.com',
|
|
||||||
NULL,
|
|
||||||
'enterprise',
|
|
||||||
10000,
|
|
||||||
1000,
|
|
||||||
true,
|
|
||||||
NULL,
|
|
||||||
'{
|
|
||||||
"theme": "detective",
|
|
||||||
"language": "es",
|
|
||||||
"timezone": "America/Mexico_City",
|
|
||||||
"features": {
|
|
||||||
"analytics_enabled": true,
|
|
||||||
"gamification_enabled": true,
|
|
||||||
"social_features_enabled": true
|
|
||||||
}
|
|
||||||
}'::jsonb,
|
|
||||||
'{
|
|
||||||
"description": "Tenant principal de producción",
|
|
||||||
"environment": "production",
|
|
||||||
"created_by": "seed_script"
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
gamilit.now_mexico()
|
|
||||||
)
|
)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
name = EXCLUDED.name,
|
name = EXCLUDED.name,
|
||||||
|
|||||||
@ -119,37 +119,6 @@ INSERT INTO auth_management.tenants (
|
|||||||
}'::jsonb,
|
}'::jsonb,
|
||||||
gamilit.now_mexico(),
|
gamilit.now_mexico(),
|
||||||
gamilit.now_mexico()
|
gamilit.now_mexico()
|
||||||
),
|
|
||||||
-- Tenant 4: Gamilit Production (para usuarios de producción)
|
|
||||||
-- AGREGADO 2025-12-19: Necesario para profiles de producción
|
|
||||||
(
|
|
||||||
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid,
|
|
||||||
'Gamilit Production',
|
|
||||||
'gamilit-prod',
|
|
||||||
'gamilit.com',
|
|
||||||
NULL,
|
|
||||||
'enterprise',
|
|
||||||
10000,
|
|
||||||
1000,
|
|
||||||
true,
|
|
||||||
NULL,
|
|
||||||
'{
|
|
||||||
"theme": "detective",
|
|
||||||
"language": "es",
|
|
||||||
"timezone": "America/Mexico_City",
|
|
||||||
"features": {
|
|
||||||
"analytics_enabled": true,
|
|
||||||
"gamification_enabled": true,
|
|
||||||
"social_features_enabled": true
|
|
||||||
}
|
|
||||||
}'::jsonb,
|
|
||||||
'{
|
|
||||||
"description": "Tenant principal de producción",
|
|
||||||
"environment": "production",
|
|
||||||
"created_by": "seed_script"
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
gamilit.now_mexico()
|
|
||||||
)
|
)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
name = EXCLUDED.name,
|
name = EXCLUDED.name,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user