from django.contrib import admin from django.contrib.auth.models import User, Group from django import forms from django.utils.html import format_html from django.http import HttpResponse from import_export import resources, fields from import_export.admin import ImportExportModelAdmin from import_export.widgets import ForeignKeyWidget from .models import UserProfile, Asistente, ExternalUser, SystemConfiguration, Student, AssistantProfile from .audit import AuditLog # Ocultar modelos de Django que no se usan admin.site.unregister(User) admin.site.unregister(Group) # ===== RECURSOS PARA IMPORT/EXPORT ===== class StudentResource(resources.ModelResource): """Recurso para importar/exportar SOLO estudiantes""" class Meta: model = UserProfile fields = ('account_number', 'full_name') export_order = ('account_number', 'full_name') import_id_fields = ['account_number'] skip_unchanged = True report_skipped = True def before_import_row(self, row, **kwargs): """Validar y limpiar datos antes de importar""" # Limpiar espacios row['account_number'] = str(row.get('account_number', '')).strip() row['full_name'] = str(row.get('full_name', '')).strip() # Asignar automáticamente user_type como student row['user_type'] = 'student' def before_save_instance(self, instance, row, using_transactions, dry_run, **kwargs): """Crear usuario de Django si no existe y asegurar que sea estudiante""" # Asegurar que sea estudiante instance.user_type = 'student' # Crear usuario de Django si no existe if not instance.user_id: # Buscar si ya existe el usuario try: user = User.objects.get(username=instance.account_number) except User.DoesNotExist: if not dry_run: # Solo crear en la base de datos si no es dry_run user = User.objects.create_user( username=instance.account_number, first_name=instance.full_name, password=None ) user.set_unusable_password() user.save() else: # En dry_run, dejar user como None (ahora permitido) user = None if user: instance.user = user class AssistantResource(resources.ModelResource): """Recurso para importar/exportar SOLO asistentes""" class Meta: model = UserProfile fields = ('account_number', 'full_name') export_order = ('account_number', 'full_name') import_id_fields = ['account_number'] skip_unchanged = True report_skipped = True def before_import_row(self, row, **kwargs): """Validar y limpiar datos antes de importar""" # Limpiar espacios row['account_number'] = str(row.get('account_number', '')).strip() row['full_name'] = str(row.get('full_name', '')).strip() # Asignar automáticamente user_type como assistant row['user_type'] = 'assistant' def before_save_instance(self, instance, row, using_transactions, dry_run, **kwargs): """Crear usuario de Django si no existe y asegurar que sea asistente""" # Asegurar que sea asistente instance.user_type = 'assistant' # Crear usuario de Django si no existe if not instance.user_id: # Buscar si ya existe el usuario try: user = User.objects.get(username=instance.account_number) except User.DoesNotExist: if not dry_run: # Solo crear en la base de datos si no es dry_run user = User.objects.create_user( username=instance.account_number, first_name=instance.full_name, password=None ) user.set_unusable_password() user.save() else: # En dry_run, dejar user como None (ahora permitido) user = None if user: instance.user = user def after_save_instance(self, instance, using_transactions, dry_run): """Crear automáticamente el registro de Asistente después de guardar""" if not dry_run: # Crear automáticamente el registro de Asistente si no existe from .models import Asistente Asistente.objects.get_or_create( user_profile=instance, defaults={'can_manage_events': True} ) class UserProfileResource(resources.ModelResource): """Recurso para importar/exportar UserProfile (LEGACY - no usar)""" class Meta: model = UserProfile fields = ('account_number', 'full_name', 'user_type') import_id_fields = ['account_number'] skip_unchanged = True report_skipped = True def before_import_row(self, row, **kwargs): """Validar y limpiar datos antes de importar""" # Limpiar espacios row['account_number'] = str(row.get('account_number', '')).strip() row['full_name'] = str(row.get('full_name', '')).strip() row['user_type'] = str(row.get('user_type', '')).strip().lower() def after_save_instance(self, instance, using_transactions, dry_run): """Crear usuario de Django si no existe""" if not dry_run and not instance.user_id: user = User.objects.create_user( username=instance.account_number, first_name=instance.full_name, password=None ) user.set_unusable_password() user.save() instance.user = user instance.save() class ExternalUserResource(resources.ModelResource): """Recurso para exportar usuarios externos""" approved_by_name = fields.Field() class Meta: model = ExternalUser fields = ('account_number', 'full_name', 'status', 'approved_by_name', 'created_at', 'rejection_reason') export_order = fields def dehydrate_approved_by_name(self, external_user): """Obtener nombre del aprobador""" return external_user.approved_by.full_name if external_user.approved_by else '-' class UserProfileForm(forms.ModelForm): """Formulario personalizado para crear UserProfile sin contraseña""" class Meta: model = UserProfile fields = ['account_number', 'full_name', 'user_type'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Hacer el campo user_type obligatorio self.fields['user_type'].required = True self.fields['user_type'].empty_label = None # Quitar opción vacía del dropdown def save(self, commit=True): profile = super().save(commit=False) # Si el perfil no tiene usuario asociado, crear uno automáticamente if not profile.user_id: # Crear usuario con username = número de cuenta, sin contraseña user = User.objects.create_user( username=profile.account_number, first_name=profile.full_name, password=None # Sin contraseña ) # Desactivar contraseña usable user.set_unusable_password() user.save() profile.user = user if commit: profile.save() return profile class StudentAdmin(ImportExportModelAdmin): """Admin SOLO para estudiantes""" resource_class = StudentResource list_display = ['account_number', 'full_name'] search_fields = ['account_number', 'full_name'] actions = ['export_selected_students'] fieldsets = ( ('Información del Estudiante', { 'fields': ('account_number', 'full_name'), 'description': '📚 Importa el Excel con solo 2 columnas: account_number y full_name. El sistema automáticamente los creará como estudiantes.' }), ) def get_queryset(self, request): """Mostrar SOLO estudiantes""" qs = super().get_queryset(request) return qs.filter(user_type='student') 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 export_selected_students(self, request, queryset): """Acción para exportar estudiantes seleccionados""" resource = StudentResource() dataset = resource.export(queryset) from import_export.formats.base_formats import XLSX xlsx_format = XLSX() export_data = xlsx_format.export_data(dataset) response = HttpResponse( export_data, content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) response['Content-Disposition'] = 'attachment; filename="estudiantes.xlsx"' self.message_user(request, f'Se exportaron {queryset.count()} estudiantes.') return response export_selected_students.short_description = "📊 Exportar estudiantes seleccionados" def save_model(self, request, obj, form, change): """Asegurar que siempre sea estudiante y crear usuario si no existe""" obj.user_type = 'student' # Si no tiene usuario, crear uno if not obj.user_id: user = User.objects.create_user( username=obj.account_number, first_name=obj.full_name, password=None ) user.set_unusable_password() user.save() obj.user = user super().save_model(request, obj, form, change) class AssistantProfileAdmin(ImportExportModelAdmin): """Admin SOLO para asistentes""" resource_class = AssistantResource list_display = ['account_number', 'full_name'] search_fields = ['account_number', 'full_name'] actions = ['export_selected_assistants'] fieldsets = ( ('Información del Asistente', { 'fields': ('account_number', 'full_name'), 'description': '👨‍🏫 Importa el Excel con solo 2 columnas: account_number y full_name. El sistema automáticamente los creará como asistentes.' }), ) def get_queryset(self, request): """Mostrar SOLO asistentes""" qs = super().get_queryset(request) return qs.filter(user_type='assistant') 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 export_selected_assistants(self, request, queryset): """Acción para exportar asistentes seleccionados""" resource = AssistantResource() dataset = resource.export(queryset) from import_export.formats.base_formats import XLSX xlsx_format = XLSX() export_data = xlsx_format.export_data(dataset) response = HttpResponse( export_data, content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) response['Content-Disposition'] = 'attachment; filename="asistentes.xlsx"' self.message_user(request, f'Se exportaron {queryset.count()} asistentes.') return response export_selected_assistants.short_description = "📊 Exportar asistentes seleccionados" def save_model(self, request, obj, form, change): """Asegurar que siempre sea asistente y crear usuario si no existe""" obj.user_type = 'assistant' # Si no tiene usuario, crear uno if not obj.user_id: user = User.objects.create_user( username=obj.account_number, first_name=obj.full_name, password=None ) user.set_unusable_password() user.save() obj.user = user super().save_model(request, obj, form, change) # Crear automáticamente el registro de Asistente si no existe Asistente.objects.get_or_create( user_profile=obj, defaults={'can_manage_events': True} ) # Registrar los admins separados con proxy models admin.site.register(Student, StudentAdmin) admin.site.register(AssistantProfile, AssistantProfileAdmin) # UserProfile está oculto del admin - se usan Student y AssistantProfile en su lugar @admin.register(Asistente) class AsistenteAdmin(admin.ModelAdmin): list_display = ['get_asistente_info', 'get_numero_cuenta', 'get_registros_realizados', 'ver_alumnos_registrados', 'can_manage_events'] list_filter = ['can_manage_events'] search_fields = ['user_profile__full_name', 'user_profile__account_number'] readonly_fields = ['user_profile', 'get_registros_realizados', 'get_ultimos_registros'] actions = ['ver_reporte_registros'] fieldsets = ( ('Asistente', { 'fields': ('user_profile',), 'description': '📋 Los permisos de asistente se crean automáticamente cuando se registra un nuevo asistente.' }), ('Permisos y Configuración', { 'fields': ('can_manage_events',) }), ('Estadísticas de Registros', { 'fields': ('get_registros_realizados', 'get_ultimos_registros'), 'classes': ('collapse',) }), ) def has_add_permission(self, request): """Ocultar botón de agregar - se crean automáticamente""" return False def has_delete_permission(self, request, obj=None): """No permitir eliminar permisos de asistente""" return False def get_asistente_info(self, obj): """Mostrar nombre del asistente""" return obj.user_profile.full_name get_asistente_info.short_description = 'Asistente' def get_numero_cuenta(self, obj): """Mostrar número de cuenta""" return obj.user_profile.account_number get_numero_cuenta.short_description = 'Número de Cuenta' def formfield_for_foreignkey(self, db_field, request, **kwargs): """Filtrar el campo user_profile para mostrar solo asistentes""" if db_field.name == "user_profile": kwargs["queryset"] = UserProfile.objects.filter(user_type='assistant') return super().formfield_for_foreignkey(db_field, request, **kwargs) def get_registros_realizados(self, obj): """Muestra el total de registros realizados por este asistente""" from attendance.models import Attendance count = Attendance.objects.filter(registered_by=obj.user_profile).count() return f"📊 {count} registros" get_registros_realizados.short_description = 'Total de registros' def get_ultimos_registros(self, obj): """Muestra los últimos 5 registros realizados por este asistente""" from django.utils.html import format_html from attendance.models import Attendance registros = Attendance.objects.filter( registered_by=obj.user_profile ).select_related('student', 'external_user', 'event').order_by('-timestamp')[:5] if not registros: return "Sin registros" html = '' html += '' for reg in registros: attendee = reg.student.full_name if reg.student else reg.external_user.full_name html += f''' ''' html += '
FechaAsistenteEvento
{reg.timestamp.strftime("%d/%m/%Y %H:%M")} {attendee} {reg.event.title[:30]}...
' return format_html(html) get_ultimos_registros.short_description = 'Últimos 5 registros' def ver_alumnos_registrados(self, obj): """Botón para ver todos los alumnos registrados por este asistente""" from django.utils.html import format_html from django.urls import reverse url = reverse('admin:attendance_attendance_changelist') + f'?registered_by__id__exact={obj.user_profile.id}' return format_html( '📋 Ver Alumnos', url ) ver_alumnos_registrados.short_description = 'Alumnos Registrados' def ver_reporte_registros(self, request, queryset): """Acción para ver reporte detallado de registros de asistentes seleccionados""" from django.shortcuts import render from attendance.models import Attendance from django.db.models import Count, Q asistentes_data = [] for asistente in queryset: registros = Attendance.objects.filter(registered_by=asistente.user_profile) estudiantes = registros.filter(student__isnull=False).select_related('student', 'event').order_by('-timestamp') externos = registros.filter(external_user__isnull=False).select_related('external_user', 'event').order_by('-timestamp') asistentes_data.append({ 'asistente': asistente, 'total': registros.count(), 'estudiantes': estudiantes, 'externos': externos, 'total_estudiantes': estudiantes.count(), 'total_externos': externos.count(), }) context = { 'asistentes_data': asistentes_data, 'title': 'Reporte de Registros por Asistente', } return render(request, 'admin/asistente_reporte.html', context) ver_reporte_registros.short_description = "📊 Ver reporte detallado de registros" @admin.register(ExternalUser) class ExternalUserAdmin(ImportExportModelAdmin): resource_class = ExternalUserResource list_display = ['account_number', 'full_name', 'get_status', 'get_approved_by', 'created_at'] list_filter = ['status', 'created_at', 'approved_by'] search_fields = ['full_name', 'account_number', 'approved_by__full_name'] ordering = ['-created_at'] readonly_fields = ['created_at', 'processed_at'] date_hierarchy = 'created_at' actions = ['export_selected_external_users'] fieldsets = ( ('Información Personal', { 'fields': ('account_number', 'full_name') }), ('Estado de Aprobación', { 'fields': ('status', 'approved_by', 'processed_at', 'rejection_reason', 'created_at') }), ) def get_export_formats(self): """Solo exportar (no importar usuarios externos)""" from import_export.formats.base_formats import XLSX, CSV return [XLSX, CSV] def has_import_permission(self, request): """No permitir importar usuarios externos (se crean desde la app)""" return False def export_selected_external_users(self, request, queryset): """Acción para exportar usuarios externos seleccionados""" resource = ExternalUserResource() dataset = resource.export(queryset) from import_export.formats.base_formats import XLSX xlsx_format = XLSX() export_data = xlsx_format.export_data(dataset) response = HttpResponse( export_data, content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) response['Content-Disposition'] = 'attachment; filename="usuarios_externos.xlsx"' self.message_user(request, f'Se exportaron {queryset.count()} usuarios externos.') return response export_selected_external_users.short_description = "📊 Exportar usuarios externos seleccionados" def get_status(self, obj): """Muestra el estado con iconos y colores""" status_icons = { 'pending': ('⏳', '#FFA500', 'Pendiente'), 'approved': ('✅', '#28a745', 'Aprobado'), 'rejected': ('❌', '#dc3545', 'Rechazado') } icon, color, text = status_icons.get(obj.status, ('?', '#666', obj.status)) return format_html( '{} {}', color, icon, text ) get_status.short_description = 'Estado' def get_approved_by(self, obj): """Muestra quién aprobó/rechazó al usuario""" if obj.approved_by: color = '#28a745' if obj.status == 'approved' else '#dc3545' action = 'Aprobado por' if obj.status == 'approved' else 'Rechazado por' return format_html( '👤 {}
{}', color, obj.approved_by.full_name, action ) return '-' get_approved_by.short_description = 'Procesado por' @admin.register(AuditLog) class AuditLogAdmin(admin.ModelAdmin): list_display = ['timestamp', 'category', 'severity', 'action', 'username', 'ip_address', 'success', 'message'] list_filter = ['category', 'severity', 'action', 'success', 'timestamp'] search_fields = ['username', 'ip_address', 'message', 'path'] readonly_fields = ['timestamp', 'category', 'severity', 'action', 'user', 'username', 'ip_address', 'user_agent', 'path', 'method', 'message', 'details', 'success', 'status_code'] date_hierarchy = 'timestamp' def has_add_permission(self, request): # No permitir crear logs manualmente desde el admin return False def has_change_permission(self, request, obj=None): # No permitir editar logs (son inmutables) return False def has_delete_permission(self, request, obj=None): # Solo superusuarios pueden eliminar logs return request.user.is_superuser @admin.register(SystemConfiguration) class SystemConfigurationAdmin(admin.ModelAdmin): list_display = ['get_config_name', 'minimum_attendance_percentage', 'minutes_before_event', 'minutes_after_start', 'updated_at', 'updated_by'] readonly_fields = ['updated_at', 'updated_by'] fieldsets = ( ('Configuración de Asistencia', { 'fields': ('minimum_attendance_percentage',), 'description': '⚙️ Este porcentaje se aplica a TODOS los estudiantes del sistema. Define el porcentaje mínimo de asistencia requerido para obtener la constancia.' }), ('Configuración de Tiempos de Registro', { 'fields': ('minutes_before_event', 'minutes_after_start'), 'description': ''' ⏱️ Tiempos de registro de asistencia (aplica a TODAS las ponencias):

Minutos antes del evento: Tiempo permitido para comenzar a registrar asistencia antes del inicio programado.
Ejemplo: Si el evento inicia a las 10:00 AM y configuras 10 minutos, se podrá registrar desde las 9:50 AM.

Minutos después del inicio: Tiempo límite después del inicio para registrar asistencia.
Ejemplo: Si el evento inicia a las 10:00 AM y configuras 25 minutos, el límite será a las 10:25 AM.

Estos valores se aplican automáticamente a todos los eventos del sistema. ''' }), ('Información de Auditoría', { 'fields': ('updated_at', 'updated_by'), 'classes': ('collapse',) }), ) def get_config_name(self, obj): return "Configuración Global del Sistema" get_config_name.short_description = 'Configuración' def has_add_permission(self, request): # Solo permitir una configuración return not SystemConfiguration.objects.exists() def has_delete_permission(self, request, obj=None): # No permitir eliminar la configuración return False def save_model(self, request, obj, form, change): # Registrar quién actualizó la configuración try: obj.updated_by = request.user.profile except: pass super().save_model(request, obj, form, change)