forked from val-lop20/Pagina-de-Asistencia-MAC
Importación y Exportación de asistencias
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
# 📥 Guía de Importación/Exportación de Asistencias
|
||||
|
||||
## 📋 Descripción General
|
||||
|
||||
El sistema permite importar y exportar asistencias y estadísticas desde el admin de Django usando archivos Excel (.xlsx) o CSV.
|
||||
|
||||
## ✨ Características
|
||||
|
||||
### 1. **Asistencias (Attendance)**
|
||||
|
||||
#### Importar Asistencias
|
||||
- ✅ Botón "Importar" disponible en `/admin/attendance/attendance/`
|
||||
- ✅ Formatos soportados: Excel (.xlsx), CSV (.csv)
|
||||
- ✅ **Permite importar asistencias pasadas** (fuera de ventana de tiempo)
|
||||
- ✅ Valida duplicados y eventos simultáneos automáticamente
|
||||
- ✅ Auto-crea permisos de Asistente si no existen
|
||||
|
||||
#### Exportar Asistencias
|
||||
- ✅ Botón "Exportar" disponible en la lista
|
||||
- ✅ Formatos: Excel (.xlsx), CSV (.csv)
|
||||
|
||||
### 2. **Estadísticas de Asistencia (AttendanceStats)**
|
||||
|
||||
#### Importar Estadísticas
|
||||
- ✅ Botón "Importar" disponible en `/admin/attendance/attendancestats/`
|
||||
- ✅ Permite actualizar estadísticas masivamente
|
||||
|
||||
#### Exportar Estadísticas
|
||||
- ✅ Botón "Exportar" para todas las estadísticas
|
||||
- ✅ Acción "📊 Exportar estadísticas seleccionadas"
|
||||
- ✅ Acción "📊 Exportar estudiantes que cumplen requisito para constancia"
|
||||
- ✅ Acción "🔄 Actualizar estadísticas seleccionadas"
|
||||
|
||||
---
|
||||
|
||||
## 📊 Formato del Archivo Excel para Importar Asistencias
|
||||
|
||||
### Columnas Requeridas:
|
||||
|
||||
| Columna | Descripción | Ejemplo | Obligatorio |
|
||||
|---------|-------------|---------|-------------|
|
||||
| `account_number` | Número de cuenta del estudiante (8 dígitos) | 32114471 | ✅ Sí |
|
||||
| `student_name` | Nombre completo del estudiante | Lopez Martinez Valeria | ❌ No (solo referencia) |
|
||||
| `event_title` | Título exacto del evento | Matemáticas Aplicadas a Medicina... | ✅ Sí |
|
||||
| `registered_by_account` | Cuenta del asistente que registra | 11111111 | ❌ No (default: 11111111) |
|
||||
| `timestamp` | Fecha y hora del registro | 2025-10-21 11:30:00 | ❌ No (default: ahora) |
|
||||
| `registration_method` | Método de registro | manual | ❌ No (default: manual) |
|
||||
| `notes` | Notas adicionales | Importado desde Excel | ❌ No |
|
||||
| `is_valid` | ¿Es válida la asistencia? | True | ❌ No (default: True) |
|
||||
|
||||
### Ejemplo de Archivo Excel:
|
||||
|
||||
```
|
||||
account_number | student_name | event_title | registered_by_account
|
||||
32114471 | Lopez Martinez Valeria | Matemáticas Aplicadas a Medicina... | 11111111
|
||||
31904405 | Lopez Martinez Angel | La presión fiscal como oportunidad... | 11111111
|
||||
42510571 | Almaraz Ramirez Rafael | Tecnologías IIoT al servicio de la Industria | 11111111
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Comportamiento del Sistema
|
||||
|
||||
### 🔓 skip_validation=True
|
||||
|
||||
Cuando se importan asistencias, el sistema usa `skip_validation=True` que:
|
||||
|
||||
✅ **PERMITE:**
|
||||
- Importar asistencias fuera de la ventana de tiempo del evento
|
||||
- Importar asistencias de eventos pasados
|
||||
- Procesar importaciones masivas históricas
|
||||
|
||||
✅ **MANTIENE VALIDACIÓN DE:**
|
||||
- ❌ **Duplicados**: No permite crear dos asistencias del mismo estudiante al mismo evento
|
||||
- ❌ **Eventos simultáneos**: No permite asistencia a dos eventos que ocurren al mismo tiempo
|
||||
|
||||
### 🔐 Permisos de Asistente
|
||||
|
||||
El sistema **automáticamente**:
|
||||
1. Busca el asistente por `registered_by_account`
|
||||
2. Si no existe, usa el asistente por defecto (11111111)
|
||||
3. Verifica si tiene permisos en la tabla `Asistente`
|
||||
4. Si no tiene permisos, los crea automáticamente con `can_manage_events=True`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Cómo Usar
|
||||
|
||||
### Importar Asistencias desde Admin
|
||||
|
||||
1. Ve a `/admin/attendance/attendance/`
|
||||
2. Click en botón **"IMPORTAR"** (arriba a la derecha)
|
||||
3. Selecciona tu archivo Excel o CSV
|
||||
4. El sistema muestra una **vista previa** de los cambios
|
||||
5. Revisa los errores si los hay
|
||||
6. Click en **"Confirmar importación"**
|
||||
7. ✅ Listo! Las asistencias se importaron
|
||||
|
||||
### Exportar Asistencias
|
||||
|
||||
1. Ve a `/admin/attendance/attendance/`
|
||||
2. Click en botón **"EXPORTAR"**
|
||||
3. Selecciona formato (Excel o CSV)
|
||||
4. Se descarga el archivo automáticamente
|
||||
|
||||
### Exportar Estudiantes con Constancia
|
||||
|
||||
1. Ve a `/admin/attendance/attendancestats/`
|
||||
2. Selecciona estudiantes (o déjalos todos)
|
||||
3. En "Acción": selecciona **"📊 Exportar estudiantes que cumplen requisito para constancia"**
|
||||
4. Click en **"Ir"**
|
||||
5. Se descarga Excel con estudiantes que cumplen el % mínimo
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Errores Comunes y Soluciones
|
||||
|
||||
### Error: "Estudiante con cuenta XXXXX no encontrado"
|
||||
**Causa:** El número de cuenta no existe en la base de datos
|
||||
**Solución:** Verifica que el estudiante esté registrado en el sistema
|
||||
|
||||
### Error: "Evento 'XXXXX' no encontrado"
|
||||
**Causa:** El título del evento no coincide exactamente
|
||||
**Solución:** Copia el título exacto desde `/admin/events/event/`
|
||||
|
||||
### Error: "Este estudiante ya tiene asistencia registrada para este evento"
|
||||
**Causa:** Ya existe una asistencia para ese estudiante en ese evento
|
||||
**Solución:** Es un duplicado, omítelo o elimina el existente primero
|
||||
|
||||
### Error: "El estudiante ya tiene asistencia registrada en un evento simultáneo"
|
||||
**Causa:** El estudiante tiene asistencia en otro evento que ocurre al mismo tiempo
|
||||
**Solución:** El estudiante no puede estar en dos lugares a la vez. Elige uno solo.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Actualizar Estadísticas después de Importar
|
||||
|
||||
Las estadísticas se actualizan **automáticamente** después de cada importación.
|
||||
|
||||
Si necesitas actualizar manualmente:
|
||||
1. Ve a `/admin/attendance/attendancestats/`
|
||||
2. Selecciona los estudiantes
|
||||
3. Acción: **"🔄 Actualizar estadísticas seleccionadas"**
|
||||
4. Click en **"Ir"**
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notas Importantes
|
||||
|
||||
1. **Solo superusuarios** pueden importar asistencias
|
||||
2. El sistema **normaliza automáticamente** números de cuenta (elimina espacios, guiones)
|
||||
3. Los **certificados SSL** pueden variar entre máquinas (regenerar con `./docker/generate_ssl_certs.sh`)
|
||||
4. La **base de datos NO se sincroniza con Git**, solo el código
|
||||
5. Para sincronizar datos entre máquinas, usa **backup/restore de PostgreSQL**
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Desarrollo
|
||||
|
||||
### Archivos Modificados:
|
||||
- `backend/attendance/admin.py` - Admin con ImportExportMixin
|
||||
- `backend/attendance/models.py` - Modelo con skip_validation
|
||||
|
||||
### Dependencias:
|
||||
- `django-import-export` - Manejo de importación/exportación
|
||||
- `openpyxl` - Lectura de archivos Excel
|
||||
- `pandas` - Procesamiento de datos (opcional)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Referencias
|
||||
|
||||
- Django Import-Export: https://django-import-export.readthedocs.io/
|
||||
- Admin de Django: https://docs.djangoproject.com/en/5.0/ref/contrib/admin/
|
||||
+130
-6
@@ -2,9 +2,103 @@ from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.urls import path
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib import messages
|
||||
from import_export import resources, fields
|
||||
from import_export.admin import ExportMixin
|
||||
from import_export.admin import ImportExportMixin
|
||||
from .models import Attendance, AttendanceStats
|
||||
from django.core.exceptions import ValidationError
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AttendanceResource(resources.ModelResource):
|
||||
"""Recurso para importar/exportar asistencias"""
|
||||
student_account_number = fields.Field(column_name='account_number')
|
||||
student_name = fields.Field(column_name='student_name')
|
||||
event_title = fields.Field(column_name='event_title')
|
||||
registered_by_account = fields.Field(column_name='registered_by_account')
|
||||
|
||||
class Meta:
|
||||
model = Attendance
|
||||
fields = ('id', 'student_account_number', 'student_name', 'event_title',
|
||||
'timestamp', 'registered_by_account', 'registration_method', 'notes', 'is_valid')
|
||||
export_order = fields
|
||||
import_id_fields = [] # No usar ID para importación
|
||||
skip_unchanged = True
|
||||
|
||||
def dehydrate_student_account_number(self, attendance):
|
||||
"""Obtener número de cuenta del estudiante"""
|
||||
return attendance.student.account_number
|
||||
|
||||
def dehydrate_student_name(self, attendance):
|
||||
"""Obtener nombre del estudiante"""
|
||||
return attendance.student.full_name
|
||||
|
||||
def dehydrate_event_title(self, attendance):
|
||||
"""Obtener título del evento"""
|
||||
return attendance.event.title
|
||||
|
||||
def dehydrate_registered_by_account(self, attendance):
|
||||
"""Obtener cuenta del asistente que registró"""
|
||||
return attendance.registered_by.account_number if attendance.registered_by else ''
|
||||
|
||||
def before_import_row(self, row, **kwargs):
|
||||
"""Procesar fila antes de importar"""
|
||||
from authentication.models import UserProfile
|
||||
from events.models import Event
|
||||
from django.utils import timezone
|
||||
|
||||
# Normalizar número de cuenta del estudiante
|
||||
account_number = str(row.get('account_number', '')).strip()
|
||||
account_number = account_number.replace(' ', '').replace('-', '')[:8]
|
||||
|
||||
# Buscar estudiante
|
||||
try:
|
||||
student = UserProfile.objects.get(account_number=account_number, user_type='student')
|
||||
except UserProfile.DoesNotExist:
|
||||
raise ValidationError(f"Estudiante con cuenta {account_number} no encontrado")
|
||||
|
||||
# Buscar evento
|
||||
event_title = str(row.get('event_title', '')).strip()
|
||||
try:
|
||||
event = Event.objects.get(title=event_title)
|
||||
except Event.DoesNotExist:
|
||||
raise ValidationError(f"Evento '{event_title}' no encontrado")
|
||||
|
||||
# Buscar o crear asistente que registra
|
||||
registered_by_account = str(row.get('registered_by_account', '11111111')).strip()[:8]
|
||||
try:
|
||||
registered_by = UserProfile.objects.get(account_number=registered_by_account, user_type='assistant')
|
||||
|
||||
# Verificar/crear permisos de asistente
|
||||
from authentication.models import Asistente
|
||||
Asistente.objects.get_or_create(
|
||||
user_profile=registered_by,
|
||||
defaults={'can_manage_events': True}
|
||||
)
|
||||
except UserProfile.DoesNotExist:
|
||||
# Usar asistente por defecto
|
||||
registered_by = UserProfile.objects.get(account_number='11111111', user_type='assistant')
|
||||
|
||||
# Preparar datos para el modelo
|
||||
row['student'] = student.id
|
||||
row['event'] = event.id
|
||||
row['registered_by'] = registered_by.id
|
||||
|
||||
# Timestamp
|
||||
if 'timestamp' not in row or not row['timestamp']:
|
||||
row['timestamp'] = timezone.now()
|
||||
|
||||
def save_instance(self, instance, using_transactions=True, dry_run=False):
|
||||
"""
|
||||
Guardar la instancia usando skip_validation=True para permitir
|
||||
importaciones de asistencias pasadas (fuera de ventana de tiempo)
|
||||
"""
|
||||
if not dry_run:
|
||||
# Usar skip_validation=True para permitir importar asistencias históricas
|
||||
# Esto omite la validación de tiempo pero mantiene validación de duplicados
|
||||
instance.save(skip_validation=True)
|
||||
|
||||
|
||||
class AttendanceStatsResource(resources.ModelResource):
|
||||
@@ -32,7 +126,8 @@ class AttendanceStatsResource(resources.ModelResource):
|
||||
return 'SÍ' if stats.meets_minimum_requirement() else 'NO'
|
||||
|
||||
@admin.register(Attendance)
|
||||
class AttendanceAdmin(admin.ModelAdmin):
|
||||
class AttendanceAdmin(ImportExportMixin, admin.ModelAdmin):
|
||||
resource_class = AttendanceResource
|
||||
list_display = ['attendee_name', 'attendee_identifier', 'event', 'timestamp', 'registration_method', 'get_registered_by', 'is_valid']
|
||||
list_filter = ['registration_method', 'registered_by', 'event__date', 'is_valid', 'event']
|
||||
search_fields = ['student__full_name', 'student__account_number', 'event__title',
|
||||
@@ -64,9 +159,19 @@ class AttendanceAdmin(admin.ModelAdmin):
|
||||
return '-'
|
||||
get_registered_by.short_description = 'Registrado por'
|
||||
|
||||
def get_import_formats(self):
|
||||
"""Formatos permitidos para importar"""
|
||||
from import_export.formats.base_formats import XLSX, CSV
|
||||
return [XLSX, CSV]
|
||||
|
||||
def get_export_formats(self):
|
||||
"""Formatos permitidos para exportar"""
|
||||
from import_export.formats.base_formats import XLSX, CSV
|
||||
return [XLSX, CSV]
|
||||
|
||||
def has_add_permission(self, request):
|
||||
# Prevenir creación manual desde admin (debe hacerse desde la API)
|
||||
return False
|
||||
# Permitir importación pero no creación manual individual
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
# Solo superusuarios pueden editar asistencias (para correcciones excepcionales)
|
||||
@@ -77,13 +182,13 @@ class AttendanceAdmin(admin.ModelAdmin):
|
||||
return request.user.is_superuser
|
||||
|
||||
@admin.register(AttendanceStats)
|
||||
class AttendanceStatsAdmin(ExportMixin, admin.ModelAdmin):
|
||||
class AttendanceStatsAdmin(ImportExportMixin, admin.ModelAdmin):
|
||||
resource_class = AttendanceStatsResource
|
||||
list_display = ['student', 'attended_events', 'total_events', 'attendance_percentage', 'get_cumple_requisito']
|
||||
ordering = ['-attendance_percentage']
|
||||
list_filter = ['attendance_percentage']
|
||||
search_fields = ['student__account_number', 'student__full_name']
|
||||
actions = ['export_selected_stats', 'export_students_with_certificate']
|
||||
actions = ['export_selected_stats', 'export_students_with_certificate', 'update_all_stats']
|
||||
|
||||
def get_cumple_requisito(self, obj):
|
||||
"""Mostrar si cumple el requisito mínimo"""
|
||||
@@ -150,6 +255,25 @@ class AttendanceStatsAdmin(ExportMixin, admin.ModelAdmin):
|
||||
|
||||
export_students_with_certificate.short_description = "📊 Exportar estudiantes que cumplen requisito para constancia"
|
||||
|
||||
def update_all_stats(self, request, queryset):
|
||||
"""Acción para actualizar estadísticas de los estudiantes seleccionados"""
|
||||
count = 0
|
||||
for stats in queryset:
|
||||
stats.update_stats()
|
||||
count += 1
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
f'Se actualizaron las estadísticas de {count} estudiantes.'
|
||||
)
|
||||
|
||||
update_all_stats.short_description = "🔄 Actualizar estadísticas seleccionadas"
|
||||
|
||||
def get_import_formats(self):
|
||||
"""Formatos permitidos para importar"""
|
||||
from import_export.formats.base_formats import XLSX, CSV
|
||||
return [XLSX, CSV]
|
||||
|
||||
def get_export_formats(self):
|
||||
"""Formatos permitidos para exportar"""
|
||||
from import_export.formats.base_formats import XLSX, CSV
|
||||
|
||||
Reference in New Issue
Block a user