Cambio a 8 digitos del numero de cuenta

This commit is contained in:
Valeria López
2025-10-20 16:14:08 -06:00
parent 6a4fa1e82d
commit e46e07f43a
13 changed files with 836 additions and 32 deletions
@@ -0,0 +1,282 @@
"""
Comando para importar asistencias desde un archivo Excel.
Formato esperado del Excel (asistencias.xlsx):
- Hoja: "Asistencias"
- Columnas:
1. numero_cuenta (str): Número de cuenta del asistente (8 dígitos)
2. nombre_completo (str): Nombre completo del asistente
3. titulo_evento (str): Título del evento (debe existir)
4. fecha_evento (str): Fecha del evento en formato YYYY-MM-DD o DD/MM/YYYY
5. numero_cuenta_asistente (str): Número de cuenta del asistente que registra (8 dígitos)
6. metodo_registro (str, opcional): manual, barcode, external (default: manual)
7. notas (str, opcional): Notas adicionales
Uso:
python manage.py import_attendance ruta/al/archivo.xlsx
"""
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from attendance.models import Attendance
from events.models import Event
from authentication.models import UserProfile, ExternalUser
import openpyxl
from datetime import datetime
class Command(BaseCommand):
help = 'Importa asistencias desde un archivo Excel'
def add_arguments(self, parser):
parser.add_argument('excel_file', type=str, help='Ruta al archivo Excel')
parser.add_argument(
'--sheet',
type=str,
default='Asistencias',
help='Nombre de la hoja de Excel (default: Asistencias)'
)
parser.add_argument(
'--registrador',
type=str,
required=True,
help='Número de cuenta del asistente que realiza la importación'
)
parser.add_argument(
'--skip-validation',
action='store_true',
help='Omitir validación de horarios (para importar asistencias históricas)'
)
parser.add_argument(
'--create-external',
action='store_true',
help='Crear automáticamente usuarios externos si no existen'
)
def handle(self, *args, **options):
excel_file = options['excel_file']
sheet_name = options['sheet']
registrador_cuenta = options['registrador']
skip_validation = options['skip_validation']
create_external = options['create_external']
# Validar que el registrador exista y sea asistente
try:
registrador = UserProfile.objects.get(
account_number=registrador_cuenta,
user_type='assistant'
)
except UserProfile.DoesNotExist:
raise CommandError(
f'No se encontró un asistente con número de cuenta: {registrador_cuenta}'
)
try:
workbook = openpyxl.load_workbook(excel_file)
if sheet_name not in workbook.sheetnames:
raise CommandError(f'La hoja "{sheet_name}" no existe en el archivo')
sheet = workbook[sheet_name]
# Validar encabezados
expected_headers = [
'numero_cuenta', 'nombre_completo', 'titulo_evento',
'fecha_evento', 'metodo_registro', 'notas'
]
headers = [cell.value.lower().strip() if cell.value else ''
for cell in sheet[1]]
if not all(h in headers for h in expected_headers[:4]):
self.stdout.write(
self.style.WARNING(
f'Encabezados esperados: {", ".join(expected_headers[:4])}'
)
)
raise CommandError(
'El archivo debe tener los encabezados correctos en la primera fila'
)
created_count = 0
error_count = 0
external_created = 0
# Procesar filas (comenzando desde la fila 2)
for row_idx, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2):
try:
# Extraer datos
numero_cuenta = str(row[0]).strip() if row[0] else None
nombre_completo = row[1]
titulo_evento = row[2]
fecha_evento_str = row[3]
metodo_registro = row[4] if len(row) > 4 and row[4] else 'manual'
notas = row[5] if len(row) > 5 else None
# Validar datos requeridos
if not all([numero_cuenta, nombre_completo, titulo_evento, fecha_evento_str]):
self.stdout.write(
self.style.WARNING(
f'Fila {row_idx}: Datos incompletos, omitiendo...'
)
)
error_count += 1
continue
# Parsear fecha del evento
if isinstance(fecha_evento_str, datetime):
fecha_evento = fecha_evento_str.date()
else:
try:
fecha_evento = datetime.strptime(str(fecha_evento_str), '%Y-%m-%d').date()
except ValueError:
try:
fecha_evento = datetime.strptime(str(fecha_evento_str), '%d/%m/%Y').date()
except ValueError:
self.stdout.write(
self.style.WARNING(
f'Fila {row_idx}: Formato de fecha inválido ({fecha_evento_str})'
)
)
error_count += 1
continue
# Buscar el evento
try:
event = Event.objects.get(
title=titulo_evento,
date=fecha_evento
)
except Event.DoesNotExist:
self.stdout.write(
self.style.WARNING(
f'Fila {row_idx}: Evento "{titulo_evento}" del {fecha_evento} no encontrado'
)
)
error_count += 1
continue
except Event.MultipleObjectsReturned:
self.stdout.write(
self.style.WARNING(
f'Fila {row_idx}: Múltiples eventos con título "{titulo_evento}" del {fecha_evento}"'
)
)
error_count += 1
continue
# Buscar al estudiante o usuario externo
student = None
external_user = None
# Primero buscar en estudiantes regulares
try:
student = UserProfile.objects.get(
account_number=numero_cuenta,
user_type='student'
)
except UserProfile.DoesNotExist:
# Buscar en usuarios externos
try:
external_user = ExternalUser.objects.get(
account_number=numero_cuenta
)
except ExternalUser.DoesNotExist:
# Si create_external está activo, crear usuario externo
if create_external:
external_user = ExternalUser.objects.create(
account_number=numero_cuenta,
full_name=nombre_completo,
status='approved',
approved_by=registrador
)
external_created += 1
self.stdout.write(
self.style.SUCCESS(
f' ↳ Usuario externo creado: {nombre_completo} ({numero_cuenta})'
)
)
else:
self.stdout.write(
self.style.WARNING(
f'Fila {row_idx}: Usuario {numero_cuenta} no encontrado (usa --create-external para crear)'
)
)
error_count += 1
continue
# Verificar si ya existe la asistencia
if student:
existing = Attendance.objects.filter(
student=student,
event=event,
is_valid=True
).exists()
else:
existing = Attendance.objects.filter(
external_user=external_user,
event=event,
is_valid=True
).exists()
if existing:
self.stdout.write(
self.style.WARNING(
f'Fila {row_idx}: Asistencia ya existe para {nombre_completo} en "{titulo_evento}"'
)
)
error_count += 1
continue
# Crear asistencia
if skip_validation:
# Crear directamente sin validación
attendance = Attendance(
student=student,
external_user=external_user,
event=event,
registered_by=registrador,
registration_method=metodo_registro,
notes=notas,
is_valid=True
)
# Guardar sin validación
super(Attendance, attendance).save()
else:
attendance = Attendance.objects.create(
student=student,
external_user=external_user,
event=event,
registered_by=registrador,
registration_method=metodo_registro,
notes=notas,
is_valid=True
)
created_count += 1
self.stdout.write(
self.style.SUCCESS(
f'✓ Fila {row_idx}: Asistencia de "{nombre_completo}" a "{titulo_evento}" registrada'
)
)
except Exception as e:
self.stdout.write(
self.style.ERROR(
f'✗ Fila {row_idx}: Error - {str(e)}'
)
)
error_count += 1
# Resumen
self.stdout.write('\n' + '='*60)
self.stdout.write(self.style.SUCCESS(f'Asistencias creadas: {created_count}'))
if external_created > 0:
self.stdout.write(self.style.SUCCESS(f'Usuarios externos creados: {external_created}'))
if error_count > 0:
self.stdout.write(self.style.WARNING(f'Errores: {error_count}'))
self.stdout.write('='*60)
except FileNotFoundError:
raise CommandError(f'Archivo no encontrado: {excel_file}')
except Exception as e:
raise CommandError(f'Error al procesar el archivo: {str(e)}')
+5 -2
View File
@@ -154,9 +154,12 @@ class Attendance(models.Model):
)
def save(self, *args, **kwargs):
self.clean()
# Permitir omitir validación para importaciones históricas
skip_validation = kwargs.pop('skip_validation', False)
if not skip_validation:
self.clean()
super().save(*args, **kwargs)
# Actualizar estadísticas si es estudiante regular
if self.student:
self.update_student_stats()
@@ -0,0 +1,24 @@
# Generated by Django 5.2.6 on 2025-10-20 21:21
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0013_alter_userprofile_user'),
]
operations = [
migrations.AlterField(
model_name='externaluser',
name='account_number',
field=models.CharField(max_length=8, unique=True, verbose_name='Número de cuenta'),
),
migrations.AlterField(
model_name='userprofile',
name='account_number',
field=models.CharField(max_length=8, unique=True, validators=[django.core.validators.RegexValidator(message='El número de cuenta debe tener exactamente 8 dígitos.', regex='^\\d{8}$')], verbose_name='Número de cuenta'),
),
]
+4 -4
View File
@@ -12,11 +12,11 @@ class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, blank=True)
account_number = models.CharField(
max_length=7,
max_length=8,
unique=True,
validators=[RegexValidator(
regex=r'^\d{7}$',
message='El número de cuenta debe tener exactamente 7 dígitos.'
regex=r'^\d{8}$',
message='El número de cuenta debe tener exactamente 8 dígitos.'
)],
verbose_name="Número de cuenta"
)
@@ -68,7 +68,7 @@ class ExternalUser(models.Model):
full_name = models.CharField(max_length=200, verbose_name="Nombre completo")
account_number = models.CharField(
max_length=7,
max_length=8,
unique=True,
verbose_name="Número de cuenta"
)
+3 -3
View File
@@ -20,9 +20,9 @@ class LoginSerializer(serializers.Serializer):
account_number = serializers.CharField(max_length=20)
def validate_account_number(self, value):
# Validar formato para números de cuenta (7 dígitos)
if not re.match(r'^\d{7}$', value):
raise serializers.ValidationError('El número de cuenta debe tener exactamente 7 dígitos.')
# Validar formato para números de cuenta (8 dígitos)
if not re.match(r'^\d{8}$', value):
raise serializers.ValidationError('El número de cuenta debe tener exactamente 8 dígitos.')
return value
def validate(self, data):
@@ -0,0 +1,221 @@
"""
Comando para importar eventos desde un archivo Excel.
Formato esperado del Excel (eventos.xlsx):
- Hoja: "Eventos"
- Columnas:
1. titulo (str): Título de la ponencia
2. descripcion (str): Descripción del evento
3. tipo (str): conference, workshop, panel, seminar
4. modalidad (str): presencial, online, hybrid
5. ponente (str): Nombre del ponente
6. fecha (str): Formato YYYY-MM-DD o DD/MM/YYYY
7. hora_inicio (str): Formato HH:MM
8. hora_fin (str): Formato HH:MM
9. ubicacion (str): Ubicación o plataforma
10. capacidad (int): Capacidad máxima
11. enlace_reunion (str, opcional): URL para eventos online/hybrid
12. id_reunion (str, opcional): ID de la reunión
Uso:
python manage.py import_events ruta/al/archivo.xlsx
"""
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from events.models import Event
import openpyxl
from datetime import datetime
class Command(BaseCommand):
help = 'Importa eventos desde un archivo Excel'
def add_arguments(self, parser):
parser.add_argument('excel_file', type=str, help='Ruta al archivo Excel')
parser.add_argument(
'--sheet',
type=str,
default='Eventos',
help='Nombre de la hoja de Excel (default: Eventos)'
)
parser.add_argument(
'--skip-validation',
action='store_true',
help='Omitir validación de fechas pasadas (para importar eventos históricos)'
)
def handle(self, *args, **options):
excel_file = options['excel_file']
sheet_name = options['sheet']
skip_validation = options['skip_validation']
try:
workbook = openpyxl.load_workbook(excel_file)
if sheet_name not in workbook.sheetnames:
raise CommandError(f'La hoja "{sheet_name}" no existe en el archivo')
sheet = workbook[sheet_name]
# Validar encabezados
expected_headers = [
'titulo', 'descripcion', 'tipo', 'modalidad', 'ponente',
'fecha', 'hora_inicio', 'hora_fin', 'ubicacion', 'capacidad',
'enlace_reunion', 'id_reunion'
]
headers = [cell.value.lower().strip() if cell.value else ''
for cell in sheet[1]]
if not all(h in headers for h in expected_headers[:10]):
self.stdout.write(
self.style.WARNING(
f'Encabezados esperados: {", ".join(expected_headers[:10])}'
)
)
raise CommandError(
'El archivo debe tener los encabezados correctos en la primera fila'
)
created_count = 0
error_count = 0
# Procesar filas (comenzando desde la fila 2)
for row_idx, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2):
try:
# Extraer datos
titulo = row[0]
descripcion = row[1]
tipo = row[2]
modalidad = row[3]
ponente = row[4]
fecha_str = row[5]
hora_inicio_str = row[6]
hora_fin_str = row[7]
ubicacion = row[8]
capacidad = row[9]
enlace_reunion = row[10] if len(row) > 10 else None
id_reunion = row[11] if len(row) > 11 else None
# Validar datos requeridos
if not all([titulo, descripcion, tipo, modalidad, ponente,
fecha_str, hora_inicio_str, hora_fin_str, ubicacion]):
self.stdout.write(
self.style.WARNING(
f'Fila {row_idx}: Datos incompletos, omitiendo...'
)
)
error_count += 1
continue
# Parsear fecha
if isinstance(fecha_str, datetime):
fecha = fecha_str.date()
else:
try:
fecha = datetime.strptime(str(fecha_str), '%Y-%m-%d').date()
except ValueError:
try:
fecha = datetime.strptime(str(fecha_str), '%d/%m/%Y').date()
except ValueError:
self.stdout.write(
self.style.WARNING(
f'Fila {row_idx}: Formato de fecha inválido ({fecha_str})'
)
)
error_count += 1
continue
# Parsear horas
if isinstance(hora_inicio_str, datetime):
hora_inicio = hora_inicio_str.time()
else:
try:
hora_inicio = datetime.strptime(str(hora_inicio_str), '%H:%M').time()
except ValueError:
self.stdout.write(
self.style.WARNING(
f'Fila {row_idx}: Formato de hora de inicio inválido ({hora_inicio_str})'
)
)
error_count += 1
continue
if isinstance(hora_fin_str, datetime):
hora_fin = hora_fin_str.time()
else:
try:
hora_fin = datetime.strptime(str(hora_fin_str), '%H:%M').time()
except ValueError:
self.stdout.write(
self.style.WARNING(
f'Fila {row_idx}: Formato de hora de fin inválido ({hora_fin_str})'
)
)
error_count += 1
continue
# Crear evento (sin validación si skip_validation está activo)
if skip_validation:
# Crear directamente sin validación usando el método save del modelo base
event = Event(
title=titulo,
description=descripcion,
event_type=tipo,
modality=modalidad,
speaker=ponente,
date=fecha,
start_time=hora_inicio,
end_time=hora_fin,
location=ubicacion,
max_capacity=int(capacidad) if capacidad else 100,
meeting_link=enlace_reunion if enlace_reunion else None,
meeting_id=id_reunion if id_reunion else None,
is_active=True
)
# Guardar sin llamar a clean() - usar skip_validation
event.save(skip_validation=True)
else:
event = Event.objects.create(
title=titulo,
description=descripcion,
event_type=tipo,
modality=modalidad,
speaker=ponente,
date=fecha,
start_time=hora_inicio,
end_time=hora_fin,
location=ubicacion,
max_capacity=int(capacidad) if capacidad else 100,
meeting_link=enlace_reunion if enlace_reunion else None,
meeting_id=id_reunion if id_reunion else None,
is_active=True
)
created_count += 1
self.stdout.write(
self.style.SUCCESS(
f'✓ Fila {row_idx}: Evento "{titulo}" creado exitosamente'
)
)
except Exception as e:
self.stdout.write(
self.style.ERROR(
f'✗ Fila {row_idx}: Error - {str(e)}'
)
)
error_count += 1
# Resumen
self.stdout.write('\n' + '='*60)
self.stdout.write(self.style.SUCCESS(f'Eventos creados: {created_count}'))
if error_count > 0:
self.stdout.write(self.style.WARNING(f'Errores: {error_count}'))
self.stdout.write('='*60)
except FileNotFoundError:
raise CommandError(f'Archivo no encontrado: {excel_file}')
except Exception as e:
raise CommandError(f'Error al procesar el archivo: {str(e)}')
+4 -1
View File
@@ -104,7 +104,10 @@ class Event(models.Model):
raise ValidationError("Los eventos en línea o híbridos requieren un enlace de reunión.")
def save(self, *args, **kwargs):
self.clean()
# Permitir omitir validación para importaciones históricas
skip_validation = kwargs.pop('skip_validation', False)
if not skip_validation:
self.clean()
super().save(*args, **kwargs)
@property
+3 -3
View File
@@ -56,9 +56,9 @@ def register_external_user(request):
if not account_number or not full_name:
return Response({'error': 'Número de cuenta y nombre completo son requeridos'}, status=400)
# Validar formato de número de cuenta (7 dígitos)
if not re.match(r'^\d{7}$', account_number):
return Response({'error': 'El número de cuenta debe tener exactamente 7 dígitos'}, status=400)
# Validar formato de número de cuenta (8 dígitos)
if not re.match(r'^\d{8}$', account_number):
return Response({'error': 'El número de cuenta debe tener exactamente 8 dígitos'}, status=400)
# Verificar que no exista en usuarios regulares
from authentication.models import UserProfile
+271
View File
@@ -0,0 +1,271 @@
# Guía de Importación de Datos desde Excel
Este documento describe cómo importar eventos y asistencias históricas desde archivos Excel al sistema.
## Requisitos Previos
1. Tener el contenedor Docker del backend ejecutándose
2. Tener los archivos Excel preparados con el formato correcto
3. Conocer el número de cuenta de un asistente autorizado para realizar la importación
## 1. Importación de Eventos
### Formato del Archivo Excel (eventos.xlsx)
El archivo debe tener una hoja llamada **"Eventos"** con las siguientes columnas en la primera fila (encabezados):
| Columna | Tipo | Requerido | Descripción | Ejemplo |
|---------|------|-----------|-------------|---------|
| titulo | Texto | ✓ | Título de la ponencia | "Introducción a Python" |
| descripcion | Texto | ✓ | Descripción del evento | "Taller introductorio sobre Python para principiantes" |
| tipo | Texto | ✓ | Tipo de evento | conference, workshop, panel, seminar |
| modalidad | Texto | ✓ | Modalidad del evento | presencial, online, hybrid |
| ponente | Texto | ✓ | Nombre del ponente | "Dr. Juan Pérez" |
| fecha | Fecha/Texto | ✓ | Fecha del evento | 2024-03-15 o 15/03/2024 |
| hora_inicio | Hora/Texto | ✓ | Hora de inicio | 10:00 |
| hora_fin | Hora/Texto | ✓ | Hora de finalización | 12:00 |
| ubicacion | Texto | ✓ | Ubicación o plataforma | "Aula A-101" o "Zoom" |
| capacidad | Número | ✓ | Capacidad máxima | 100 |
| enlace_reunion | URL | - | Enlace de reunión (para eventos online/hybrid) | https://zoom.us/j/123456 |
| id_reunion | Texto | - | ID de reunión | "123 456 789" |
### Ejemplo de Archivo Excel - Eventos
```
| titulo | descripcion | tipo | modalidad | ponente | fecha | hora_inicio | hora_fin | ubicacion | capacidad | enlace_reunion | id_reunion |
|--------------------------|----------------------------------|------------|-------------|------------------|------------|-------------|----------|-----------|-----------|----------------|------------|
| Introducción a Python | Taller básico de Python | workshop | presencial | Dr. Juan Pérez | 2024-03-15 | 10:00 | 12:00 | Aula A-101| 50 | | |
| Desarrollo Web Moderno | Conferencia sobre React y Django | conference | online | Ing. María López | 2024-03-16 | 14:00 | 16:00 | Zoom | 100 | https://zoom... | 123456789 |
```
### Comando de Importación de Eventos
```bash
# Ingresar al contenedor Docker
docker-compose exec backend bash
# Importar eventos (con validación de fechas)
python manage.py import_events /path/to/eventos.xlsx
# Importar eventos históricos (sin validación de fechas pasadas)
python manage.py import_events /path/to/eventos.xlsx --skip-validation
# Especificar nombre de hoja diferente
python manage.py import_events /path/to/eventos.xlsx --sheet "MisEventos"
```
### Copiar Archivo al Contenedor
Si tu archivo está en tu computadora local, primero cópialo al contenedor:
```bash
# Desde tu terminal local (fuera del contenedor)
docker cp eventos.xlsx pagina-de-asistencia-mac-backend-1:/app/
# Luego importa desde dentro del contenedor
docker-compose exec backend python manage.py import_events /app/eventos.xlsx --skip-validation
```
---
## 2. Importación de Asistencias
### Formato del Archivo Excel (asistencias.xlsx)
El archivo debe tener una hoja llamada **"Asistencias"** con las siguientes columnas en la primera fila (encabezados):
| Columna | Tipo | Requerido | Descripción | Ejemplo |
|---------|------|-----------|-------------|---------|
| numero_cuenta | Texto | ✓ | Número de cuenta del asistente (8 dígitos) | "12345678" |
| nombre_completo | Texto | ✓ | Nombre completo del asistente | "Ana García Rodríguez" |
| titulo_evento | Texto | ✓ | Título exacto del evento (debe existir) | "Introducción a Python" |
| fecha_evento | Fecha/Texto | ✓ | Fecha del evento | 2024-03-15 o 15/03/2024 |
| metodo_registro | Texto | - | Método de registro | manual, barcode, external (default: manual) |
| notas | Texto | - | Notas adicionales | "Llegó tarde" |
### Ejemplo de Archivo Excel - Asistencias
```
| numero_cuenta | nombre_completo | titulo_evento | fecha_evento | metodo_registro | notas |
|--------------|----------------------|--------------------------|--------------|-----------------|-------|
| 12345678 | Ana García Rodríguez | Introducción a Python | 2024-03-15 | manual | |
| 87654321 | Carlos Ruiz Martínez | Introducción a Python | 2024-03-15 | barcode | |
| 11223344 | Luis Fernández Pérez | Desarrollo Web Moderno | 2024-03-16 | external | |
```
### Comando de Importación de Asistencias
```bash
# Ingresar al contenedor Docker
docker-compose exec backend bash
# Importar asistencias (REQUIERE número de cuenta del asistente que importa)
python manage.py import_attendance /path/to/asistencias.xlsx --registrador 12345678
# Importar asistencias históricas (sin validación de horarios)
python manage.py import_attendance /path/to/asistencias.xlsx --registrador 12345678 --skip-validation
# Crear automáticamente usuarios externos si no existen
python manage.py import_attendance /path/to/asistencias.xlsx --registrador 12345678 --skip-validation --create-external
# Especificar nombre de hoja diferente
python manage.py import_attendance /path/to/asistencias.xlsx --registrador 12345678 --sheet "MisAsistencias" --skip-validation
```
### Copiar Archivo al Contenedor
```bash
# Desde tu terminal local (fuera del contenedor)
docker cp asistencias.xlsx pagina-de-asistencia-mac-backend-1:/app/
# Luego importa desde dentro del contenedor
docker-compose exec backend python manage.py import_attendance /app/asistencias.xlsx --registrador 12345678 --skip-validation --create-external
```
---
## Flujo Completo de Importación
### Paso 1: Preparar los archivos Excel
1. Crear `eventos.xlsx` con la hoja "Eventos" y los datos de las conferencias
2. Crear `asistencias.xlsx` con la hoja "Asistencias" y los registros de asistencia
### Paso 2: Copiar archivos al contenedor Docker
```bash
docker cp eventos.xlsx pagina-de-asistencia-mac-backend-1:/app/
docker cp asistencias.xlsx pagina-de-asistencia-mac-backend-1:/app/
```
### Paso 3: Importar eventos primero
```bash
docker-compose exec backend python manage.py import_events /app/eventos.xlsx --skip-validation
```
### Paso 4: Importar asistencias
```bash
# Reemplaza 12345678 con el número de cuenta de un asistente autorizado
docker-compose exec backend python manage.py import_attendance /app/asistencias.xlsx --registrador 12345678 --skip-validation --create-external
```
### Paso 5: Verificar la importación
Puedes verificar en:
- Admin de Django: http://localhost/admin/
- API de eventos: http://localhost/api/events/
- API de asistencias: http://localhost/api/attendance/
---
## Notas Importantes
### Para Eventos Históricos
- **IMPORTANTE**: Usa `--skip-validation` para eventos que ya pasaron, de lo contrario el sistema rechazará eventos con fechas pasadas.
- Los eventos en línea o híbridos requieren un `enlace_reunion`
### Para Asistencias Históricas
- **IMPORTANTE**: Usa `--skip-validation` para asistencias de eventos pasados.
- Los eventos deben existir ANTES de importar asistencias (importa eventos primero).
- El `titulo_evento` y `fecha_evento` deben coincidir exactamente con un evento existente.
- El número de cuenta del asistente debe ser de 8 dígitos.
### Usuarios Externos
- Si un número de cuenta no existe como estudiante regular, el comando buscará en usuarios externos.
- Usa `--create-external` para crear automáticamente usuarios externos que no existan.
- Los usuarios externos creados automáticamente serán marcados como "aprobados".
### Números de Cuenta
- Todos los números de cuenta deben tener **8 dígitos**.
- Si tus datos antiguos tienen 7 dígitos, agrégales un 0 al inicio en Excel (ejemplo: 1234567 → 01234567).
### Formato de Fechas y Horas
Formatos aceptados:
- **Fechas**: `YYYY-MM-DD` (2024-03-15) o `DD/MM/YYYY` (15/03/2024)
- **Horas**: `HH:MM` (10:00, 14:30)
### Tipos de Evento
Valores válidos para la columna `tipo`:
- `conference` - Conferencia
- `workshop` - Taller
- `panel` - Mesa Redonda
- `seminar` - Seminario
### Modalidades
Valores válidos para la columna `modalidad`:
- `presencial` - Presencial
- `online` - En línea
- `hybrid` - Híbrido
### Métodos de Registro
Valores válidos para la columna `metodo_registro`:
- `manual` - Registro Manual (default)
- `barcode` - Código de Barras
- `external` - Usuario Externo
---
## Solución de Problemas
### Error: "Evento no encontrado"
- Verifica que el `titulo_evento` y `fecha_evento` coincidan exactamente con un evento existente.
- Importa los eventos antes de las asistencias.
### Error: "No se encontró un asistente"
- Verifica que el número de cuenta del `--registrador` sea válido y pertenezca a un asistente.
### Error: "Usuario no encontrado"
- Usa `--create-external` para crear automáticamente usuarios externos.
- O crea manualmente los usuarios/estudiantes antes de importar.
### Error: "Formato de fecha inválido"
- Verifica que las fechas estén en formato `YYYY-MM-DD` o `DD/MM/YYYY`.
- En Excel, formatea las columnas de fecha como "Texto" para evitar conversiones automáticas.
### Error: "Datos incompletos"
- Verifica que todas las columnas requeridas (marcadas con ✓) tengan valores.
---
## Ejemplo Completo
Aquí hay un ejemplo completo con dos eventos y sus asistencias:
### eventos.xlsx
```
titulo | descripcion | tipo | modalidad | ponente | fecha | hora_inicio | hora_fin | ubicacion | capacidad
Introducción a Python | Taller básico de Python | workshop | presencial | Dr. Juan Pérez | 2024-03-15 | 10:00 | 12:00 | Aula A-101| 50
Desarrollo Web Moderno | Conferencia sobre React y Django | conference | presencial | Ing. María López | 2024-03-16 | 14:00 | 16:00 | Aula B-202| 100
```
### asistencias.xlsx
```
numero_cuenta | nombre_completo | titulo_evento | fecha_evento
12345678 | Ana García Rodríguez | Introducción a Python | 2024-03-15
87654321 | Carlos Ruiz Martínez | Introducción a Python | 2024-03-15
11223344 | Luis Fernández Pérez | Desarrollo Web Moderno | 2024-03-16
12345678 | Ana García Rodríguez | Desarrollo Web Moderno | 2024-03-16
```
### Comandos
```bash
# Copiar archivos
docker cp eventos.xlsx pagina-de-asistencia-mac-backend-1:/app/
docker cp asistencias.xlsx pagina-de-asistencia-mac-backend-1:/app/
# Importar eventos
docker-compose exec backend python manage.py import_events /app/eventos.xlsx --skip-validation
# Importar asistencias (asumiendo que 12345678 es un asistente)
docker-compose exec backend python manage.py import_attendance /app/asistencias.xlsx --registrador 12345678 --skip-validation --create-external
```
@@ -39,9 +39,9 @@ const AttendancePanel = () => {
}
}, [events])
// Auto-registrar cuando se complete un número de cuenta de 7 dígitos
// Auto-registrar cuando se complete un número de cuenta de 8 dígitos
useEffect(() => {
if (studentAccount.length === 7 && selectedEvent) {
if (studentAccount.length === 8 && selectedEvent) {
registerAttendance()
}
}, [studentAccount])
@@ -161,8 +161,8 @@ const AttendancePanel = () => {
return
}
if (!/^\d{7}$/.test(externalUser.account_number)) {
setMessage('El número de cuenta debe tener exactamente 7 dígitos')
if (!/^\d{8}$/.test(externalUser.account_number)) {
setMessage('El número de cuenta debe tener exactamente 8 dígitos')
setMessageType('error')
return
}
@@ -292,18 +292,18 @@ const AttendancePanel = () => {
<div style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '600', color: '#1e3a8a', fontSize: '1.125rem' }}>
Número de Cuenta (7 dígitos)
Número de Cuenta (8 dígitos)
</label>
<input
ref={inputRef}
type="text"
value={studentAccount}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 7)
const value = e.target.value.replace(/\D/g, '').slice(0, 8)
setStudentAccount(value)
}}
placeholder="Escanea o escribe..."
maxLength="7"
maxLength="8"
autoFocus
style={{
width: '100%',
@@ -317,7 +317,7 @@ const AttendancePanel = () => {
}}
/>
<p style={{ fontSize: '0.875rem', color: '#059669', marginTop: '0.75rem', fontWeight: '500', textAlign: 'center' }}>
El registro es automático al completar 7 dígitos
El registro es automático al completar 8 dígitos
</p>
<p style={{ fontSize: '0.75rem', color: '#6b7280', marginTop: '0.5rem', textAlign: 'center' }}>
Usa el escáner USB o escribe manualmente
@@ -460,17 +460,17 @@ const AttendancePanel = () => {
<form onSubmit={createExternalUser} style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginTop: '1rem' }}>
<div>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500', color: '#1e3a8a', fontSize: '0.875rem' }}>
Número de Cuenta (7 dígitos) *
Número de Cuenta (8 dígitos) *
</label>
<input
type="text"
value={externalUser.account_number}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 7)
const value = e.target.value.replace(/\D/g, '').slice(0, 8)
setExternalUser({ ...externalUser, account_number: value })
}}
placeholder="1234567"
maxLength="7"
placeholder="12345678"
maxLength="8"
required
style={{ width: '100%', padding: '0.5rem', border: '1px solid #d1d5db', borderRadius: '0.375rem' }}
/>
+7 -7
View File
@@ -25,9 +25,9 @@ const Login = () => {
setLoading(true)
setError('')
// Validar formato: 7 dígitos
if (!/^\d{7}$/.test(accountNumber)) {
setError('El número de cuenta debe tener exactamente 7 dígitos')
// Validar formato: 8 dígitos
if (!/^\d{8}$/.test(accountNumber)) {
setError('El número de cuenta debe tener exactamente 8 dígitos')
setLoading(false)
return
}
@@ -79,17 +79,17 @@ const Login = () => {
type="text"
value={accountNumber}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 7)
const value = e.target.value.replace(/\D/g, '').slice(0, 8)
setAccountNumber(value)
}}
className="login-input"
placeholder="1234567"
placeholder="12345678"
required
disabled={loading}
maxLength="7"
maxLength="8"
/>
<small className="login-input-hint">
Ingresa tu número de cuenta de 7 dígitos
Ingresa tu número de cuenta de 8 dígitos
</small>
</div>