forked from val-lop20/Pagina-de-Asistencia-MAC
388 lines
15 KiB
Python
388 lines
15 KiB
Python
from rest_framework import status
|
|
from rest_framework.decorators import api_view, permission_classes
|
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from django.shortcuts import get_object_or_404
|
|
from django_ratelimit.decorators import ratelimit
|
|
from authentication.models import UserProfile, ExternalUser
|
|
from events.models import Event
|
|
from .models import Attendance, AttendanceStats
|
|
|
|
@api_view(['GET', 'POST'])
|
|
@permission_classes([IsAuthenticated])
|
|
@ratelimit(key='user', rate='60/m', method='POST', block=True)
|
|
def register_attendance(request):
|
|
"""Registrar asistencia - Solo asistentes: 60 registros por minuto"""
|
|
if request.method == 'GET':
|
|
return Response({
|
|
'message': 'API de registro de asistencia activa',
|
|
'methods': ['POST'],
|
|
'required_fields': ['event_id', 'account_number']
|
|
})
|
|
|
|
# Verificar que el usuario autenticado sea asistente
|
|
try:
|
|
registrar_profile = request.user.userprofile
|
|
if registrar_profile.user_type != 'assistant':
|
|
return Response({
|
|
'error': 'Solo los asistentes pueden registrar asistencias'
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
except:
|
|
return Response({
|
|
'error': 'Usuario sin perfil válido'
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
event_id = request.data.get('event_id')
|
|
account_number = request.data.get('account_number')
|
|
|
|
if not event_id or not account_number:
|
|
return Response({
|
|
'error': 'Se requiere event_id y account_number'
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# Buscar evento
|
|
try:
|
|
event = Event.objects.get(id=event_id, is_active=True)
|
|
except Event.DoesNotExist:
|
|
return Response({
|
|
'error': 'Evento no encontrado'
|
|
}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
# Buscar estudiante regular o usuario externo
|
|
student_profile = None
|
|
external_user = None
|
|
attendee_name = None
|
|
|
|
# Primero buscar en estudiantes regulares
|
|
try:
|
|
student_profile = UserProfile.objects.get(
|
|
account_number=account_number,
|
|
user_type='student'
|
|
)
|
|
attendee_name = student_profile.full_name
|
|
except UserProfile.DoesNotExist:
|
|
# Si no es estudiante, buscar en usuarios externos
|
|
try:
|
|
external_user = ExternalUser.objects.get(
|
|
account_number=account_number,
|
|
status='approved'
|
|
)
|
|
attendee_name = external_user.full_name
|
|
except ExternalUser.DoesNotExist:
|
|
return Response({
|
|
'error': f'Usuario con número de cuenta {account_number} no encontrado o no aprobado'
|
|
}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
# Usar el asistente autenticado como registrador
|
|
assistant_profile = registrar_profile
|
|
|
|
# Verificar si ya tiene asistencia
|
|
if student_profile:
|
|
if Attendance.objects.filter(student=student_profile, event=event, is_valid=True).exists():
|
|
return Response({
|
|
'error': 'El estudiante ya tiene asistencia registrada para este evento'
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
elif external_user:
|
|
if Attendance.objects.filter(external_user=external_user, event=event, is_valid=True).exists():
|
|
return Response({
|
|
'error': 'El usuario externo ya tiene asistencia registrada para este evento'
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# Crear asistencia
|
|
try:
|
|
attendance = Attendance.objects.create(
|
|
student=student_profile,
|
|
external_user=external_user,
|
|
event=event,
|
|
registered_by=assistant_profile,
|
|
registration_method='manual'
|
|
)
|
|
|
|
return Response({
|
|
'message': f'Asistencia registrada para {attendee_name}',
|
|
'attendance_id': attendance.id,
|
|
'event': event.title,
|
|
'registered_by': assistant_profile.full_name,
|
|
'attendee_type': 'student' if student_profile else 'external'
|
|
}, status=status.HTTP_201_CREATED)
|
|
|
|
except Exception as e:
|
|
return Response({
|
|
'error': f'Error al crear asistencia: {str(e)}'
|
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAuthenticated])
|
|
@ratelimit(key='user', rate='30/m', method='GET', block=True)
|
|
def get_student_stats(request):
|
|
"""Obtener estadísticas de estudiante: 30 consultas por minuto"""
|
|
account_number = request.GET.get('account_number')
|
|
|
|
if not account_number:
|
|
return Response({'error': 'Se requiere account_number'}, status=400)
|
|
|
|
# Verificar permisos: estudiantes solo pueden ver sus propias estadísticas
|
|
try:
|
|
requester_profile = request.user.userprofile
|
|
|
|
# Si es estudiante, solo puede consultar sus propias estadísticas
|
|
if requester_profile.user_type == 'student':
|
|
if requester_profile.account_number != account_number:
|
|
return Response({
|
|
'error': 'Solo puedes consultar tus propias estadísticas'
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
# Asistentes pueden ver estadísticas de cualquier estudiante
|
|
elif requester_profile.user_type != 'assistant':
|
|
return Response({
|
|
'error': 'No tienes permisos para consultar estadísticas'
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
except:
|
|
return Response({
|
|
'error': 'Usuario sin perfil válido'
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
try:
|
|
student_profile = UserProfile.objects.get(
|
|
account_number=account_number,
|
|
user_type='student'
|
|
)
|
|
stats, created = AttendanceStats.objects.get_or_create(
|
|
student=student_profile
|
|
)
|
|
stats.update_stats()
|
|
|
|
return Response({
|
|
'total_events': stats.total_events,
|
|
'attended_events': stats.attended_events,
|
|
'attendance_percentage': stats.attendance_percentage
|
|
})
|
|
except UserProfile.DoesNotExist:
|
|
return Response({'error': 'Estudiante no encontrado'}, status=404)
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAuthenticated])
|
|
@ratelimit(key='user', rate='60/m', method='GET', block=True)
|
|
def get_recent_attendances(request):
|
|
"""Obtener asistencias recientes - Solo asistentes: 60 consultas por minuto"""
|
|
# Solo asistentes pueden ver asistencias recientes
|
|
try:
|
|
requester_profile = request.user.userprofile
|
|
if requester_profile.user_type != 'assistant':
|
|
return Response({
|
|
'error': 'Solo los asistentes pueden consultar asistencias recientes'
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
except:
|
|
return Response({
|
|
'error': 'Usuario sin perfil válido'
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
recent = Attendance.objects.select_related('student', 'event').order_by('-timestamp')[:5]
|
|
|
|
data = []
|
|
for attendance in recent:
|
|
data.append({
|
|
'attendee_name': attendance.attendee_name,
|
|
'event_title': attendance.event.title,
|
|
'timestamp': attendance.timestamp.strftime('%H:%M')
|
|
})
|
|
|
|
return Response(data)
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAuthenticated])
|
|
@ratelimit(key='user', rate='60/m', method='GET', block=True)
|
|
def get_my_attendances(request):
|
|
"""Obtener mis asistencias - Estudiantes pueden ver sus propias asistencias: 60 consultas por minuto"""
|
|
account_number = request.GET.get('account_number')
|
|
|
|
if not account_number:
|
|
return Response({'error': 'Se requiere account_number'}, status=400)
|
|
|
|
# Verificar permisos: estudiantes solo pueden ver sus propias asistencias
|
|
try:
|
|
requester_profile = request.user.userprofile
|
|
|
|
# Si es estudiante, solo puede consultar sus propias asistencias
|
|
if requester_profile.user_type == 'student':
|
|
if requester_profile.account_number != account_number:
|
|
return Response({
|
|
'error': 'Solo puedes consultar tus propias asistencias'
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
# Asistentes pueden ver asistencias de cualquier estudiante
|
|
elif requester_profile.user_type != 'assistant':
|
|
return Response({
|
|
'error': 'No tienes permisos para consultar asistencias'
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
except:
|
|
return Response({
|
|
'error': 'Usuario sin perfil válido'
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
try:
|
|
student_profile = UserProfile.objects.get(
|
|
account_number=account_number,
|
|
user_type='student'
|
|
)
|
|
|
|
# Obtener todas las asistencias válidas del estudiante
|
|
attendances = Attendance.objects.filter(
|
|
student=student_profile,
|
|
is_valid=True
|
|
).select_related('event').order_by('-timestamp')
|
|
|
|
data = []
|
|
for attendance in attendances:
|
|
data.append({
|
|
'id': attendance.id,
|
|
'event': attendance.event.id,
|
|
'event_title': attendance.event.title,
|
|
'event_date': attendance.event.date.strftime('%Y-%m-%d'),
|
|
'timestamp': attendance.timestamp.strftime('%Y-%m-%d %H:%M:%S')
|
|
})
|
|
|
|
return Response(data)
|
|
except UserProfile.DoesNotExist:
|
|
return Response({'error': 'Estudiante no encontrado'}, status=404)
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAuthenticated])
|
|
@ratelimit(key='user', rate='60/m', method='GET', block=True)
|
|
def get_external_user_attendances(request):
|
|
"""Obtener asistencias de usuario externo: 60 consultas por minuto"""
|
|
account_number = request.GET.get('account_number')
|
|
|
|
if not account_number:
|
|
return Response({'error': 'Se requiere account_number'}, status=400)
|
|
|
|
# Normalizar el username para comparar (eliminar prefijo "ext_")
|
|
user_account = request.user.username.replace('ext_', '')
|
|
|
|
# Verificar que el usuario externo solo pueda ver sus propias asistencias
|
|
if user_account != account_number:
|
|
# Verificar si es asistente (pueden ver todas)
|
|
try:
|
|
requester_profile = request.user.userprofile
|
|
if requester_profile.user_type != 'assistant':
|
|
return Response({
|
|
'error': 'Solo puedes consultar tus propias asistencias'
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
except:
|
|
return Response({
|
|
'error': 'Solo puedes consultar tus propias asistencias'
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
try:
|
|
external_user = ExternalUser.objects.get(
|
|
account_number=account_number,
|
|
status='approved'
|
|
)
|
|
|
|
# Obtener todas las asistencias válidas del usuario externo
|
|
attendances = Attendance.objects.filter(
|
|
external_user=external_user,
|
|
is_valid=True
|
|
).select_related('event').order_by('-timestamp')
|
|
|
|
data = []
|
|
for attendance in attendances:
|
|
data.append({
|
|
'id': attendance.id,
|
|
'event': attendance.event.id,
|
|
'event_title': attendance.event.title,
|
|
'event_date': attendance.event.date.strftime('%Y-%m-%d'),
|
|
'timestamp': attendance.timestamp.strftime('%Y-%m-%d %H:%M:%S')
|
|
})
|
|
|
|
return Response(data)
|
|
except ExternalUser.DoesNotExist:
|
|
return Response({'error': 'Usuario externo no encontrado o no aprobado'}, status=404)
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAuthenticated])
|
|
@ratelimit(key='user', rate='30/m', method='GET', block=True)
|
|
def get_external_user_stats(request):
|
|
"""Obtener estadísticas de usuario externo: 30 consultas por minuto"""
|
|
account_number = request.GET.get('account_number')
|
|
|
|
if not account_number:
|
|
return Response({'error': 'Se requiere account_number'}, status=400)
|
|
|
|
# Normalizar el username para comparar (eliminar prefijo "ext_")
|
|
user_account = request.user.username.replace('ext_', '')
|
|
|
|
# Verificar que el usuario externo solo pueda ver sus propias estadísticas
|
|
if user_account != account_number:
|
|
# Verificar si es asistente (pueden ver todas)
|
|
try:
|
|
requester_profile = request.user.userprofile
|
|
if requester_profile.user_type != 'assistant':
|
|
return Response({
|
|
'error': 'Solo puedes consultar tus propias estadísticas'
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
except:
|
|
return Response({
|
|
'error': 'Solo puedes consultar tus propias estadísticas'
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
try:
|
|
external_user = ExternalUser.objects.get(
|
|
account_number=account_number,
|
|
status='approved'
|
|
)
|
|
|
|
# Obtener todos los eventos activos
|
|
all_events = Event.objects.filter(is_active=True).order_by('date', 'start_time')
|
|
|
|
# Agrupar eventos por bloques de horario (misma fecha y horarios que se solapan)
|
|
# Usar la misma lógica que AttendanceStats.update_stats()
|
|
event_slots = {} # {(fecha, hora_inicio, hora_fin): [lista de eventos]}
|
|
|
|
for event in all_events:
|
|
# Crear una clave única para el bloque horario
|
|
slot_key = (event.date, event.start_time, event.end_time)
|
|
|
|
# Buscar si hay un slot existente que se solape con este evento
|
|
found_slot = False
|
|
for existing_slot in list(event_slots.keys()):
|
|
existing_date, existing_start, existing_end = existing_slot
|
|
|
|
# Verificar si es el mismo día y hay solapamiento de horarios
|
|
if event.date == existing_date:
|
|
# Hay solapamiento si el inicio de uno es menor al fin del otro
|
|
if (event.start_time < existing_end and event.end_time > existing_start):
|
|
event_slots[existing_slot].append(event)
|
|
found_slot = True
|
|
break
|
|
|
|
# Si no encontramos un slot existente, crear uno nuevo
|
|
if not found_slot:
|
|
event_slots[slot_key] = [event]
|
|
|
|
# El total de "bloques" es la cantidad de slots únicos
|
|
total_slots = len(event_slots)
|
|
|
|
# Obtener asistencias del usuario externo
|
|
user_attendances = Attendance.objects.filter(
|
|
external_user=external_user,
|
|
is_valid=True
|
|
).values_list('event_id', flat=True)
|
|
|
|
# Contar cuántos bloques tiene asistencia
|
|
attended_slots = 0
|
|
for slot_events in event_slots.values():
|
|
# Si asistió a al menos uno de los eventos del bloque, cuenta
|
|
event_ids = [e.id for e in slot_events]
|
|
if any(event_id in user_attendances for event_id in event_ids):
|
|
attended_slots += 1
|
|
|
|
# Calcular porcentaje
|
|
attendance_percentage = round((attended_slots / total_slots) * 100, 2) if total_slots > 0 else 0.0
|
|
|
|
return Response({
|
|
'total_events': total_slots,
|
|
'attended_events': attended_slots,
|
|
'attendance_percentage': attendance_percentage
|
|
})
|
|
except ExternalUser.DoesNotExist:
|
|
return Response({'error': 'Usuario externo no encontrado o no aprobado'}, status=404)
|