forked from val-lop20/Pagina-de-Asistencia-MAC
Cambio a 8 digitos del numero de cuenta
This commit is contained in:
@@ -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)}')
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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)}')
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' }}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user