forked from val-lop20/Pagina-de-Asistencia-MAC
624 lines
24 KiB
Python
624 lines
24 KiB
Python
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 = '<table style="width:100%; border-collapse: collapse;">'
|
|
html += '<tr style="background-color: #f0f0f0;"><th>Fecha</th><th>Asistente</th><th>Evento</th></tr>'
|
|
|
|
for reg in registros:
|
|
attendee = reg.student.full_name if reg.student else reg.external_user.full_name
|
|
html += f'''
|
|
<tr style="border-bottom: 1px solid #ddd;">
|
|
<td>{reg.timestamp.strftime("%d/%m/%Y %H:%M")}</td>
|
|
<td>{attendee}</td>
|
|
<td>{reg.event.title[:30]}...</td>
|
|
</tr>
|
|
'''
|
|
|
|
html += '</table>'
|
|
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(
|
|
'<a class="button" href="{}" style="background-color: #417690; color: white; padding: 5px 10px; text-decoration: none; border-radius: 3px;">📋 Ver Alumnos</a>',
|
|
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(
|
|
'<span style="color: {}; font-weight: bold;">{} {}</span>',
|
|
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(
|
|
'<span style="color: {};">👤 {}</span><br><small>{}</small>',
|
|
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': '''
|
|
⏱️ <strong>Tiempos de registro de asistencia (aplica a TODAS las ponencias):</strong><br><br>
|
|
• <strong>Minutos antes del evento:</strong> Tiempo permitido para comenzar a registrar asistencia antes del inicio programado.<br>
|
|
Ejemplo: Si el evento inicia a las 10:00 AM y configuras 10 minutos, se podrá registrar desde las 9:50 AM.<br><br>
|
|
• <strong>Minutos después del inicio:</strong> Tiempo límite después del inicio para registrar asistencia.<br>
|
|
Ejemplo: Si el evento inicia a las 10:00 AM y configuras 25 minutos, el límite será a las 10:25 AM.<br><br>
|
|
<em>Estos valores se aplican automáticamente a todos los eventos del sistema.</em>
|
|
'''
|
|
}),
|
|
('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) |