""" Admin pour les tâches planifiées. """ from django.contrib import admin from django.utils.html import format_html from django.urls import reverse, path from django.utils.safestring import mark_safe from django.contrib import messages from django.core.management import call_command from django.core.mail import EmailMessage from django.conf import settings from django.utils import timezone from django.shortcuts import redirect from django.http import HttpResponseRedirect from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator from common.models_scheduled_tasks import ScheduledCommand, CommandExecutionLog import json import io import traceback import os @admin.register(ScheduledCommand) class ScheduledCommandAdmin(admin.ModelAdmin): list_display = [ 'name', 'command_name', 'frequency_display', 'schedule_display', 'is_active_display', 'last_run_display', 'next_run_display', 'execution_count', 'success_rate' ] list_filter = ['is_active', 'frequency', 'send_email'] search_fields = ['name', 'command_name', 'description'] readonly_fields = ['last_run', 'next_run', 'created_at', 'created_by', 'execution_history_link', 'execute_now_button'] actions = ['execute_immediately'] fieldsets = [ ('Informations générales', { 'fields': ['name', 'description', 'is_active'] }), ('Commande', { 'fields': ['command_name', 'command_args'] }), ('Planification', { 'fields': [ 'frequency', 'hour', 'minute', 'day_of_month', 'day_of_week', ] }), ('Notification par email', { 'fields': ['send_email', 'email_recipients', 'email_subject'], 'classes': ['collapse'] }), ('Exécution', { 'fields': ['execute_now_button'], 'description': 'Exécuter cette commande immédiatement, sans attendre la prochaine planification.' }), ('Informations système', { 'fields': ['last_run', 'next_run', 'created_at', 'created_by', 'execution_history_link'], 'classes': ['collapse'] }) ] def get_urls(self): """Ajoute des URLs personnalisées pour l'admin.""" urls = super().get_urls() custom_urls = [ path( '/execute/', csrf_exempt(self.admin_site.admin_view(self.execute_command_view)), name='execute_scheduled_command', ), ] return custom_urls + urls def execute_command_view(self, request, command_id): """Vue pour exécuter une commande immédiatement.""" # Accepter seulement les requêtes GET (pas de CSRF nécessaire) if request.method != 'GET': messages.error(request, "Méthode non autorisée.") return redirect('admin:common_scheduledcommand_changelist') # Récupérer la commande (hors du try pour pouvoir l'utiliser après) try: scheduled_command = ScheduledCommand.objects.get(pk=command_id) except ScheduledCommand.DoesNotExist: messages.error(request, "Commande introuvable.") return redirect('admin:common_scheduledcommand_changelist') # Créer un log d'exécution log = CommandExecutionLog.objects.create( scheduled_command=scheduled_command, status='running' ) try: # Préparer les arguments args = [] kwargs = {} if scheduled_command.command_args: if isinstance(scheduled_command.command_args, dict): kwargs = scheduled_command.command_args elif isinstance(scheduled_command.command_args, list): args = scheduled_command.command_args # Capturer la sortie output_buffer = io.StringIO() # Exécuter la commande call_command( scheduled_command.command_name, *args, stdout=output_buffer, stderr=output_buffer, **kwargs ) # Mettre à jour le log log.finished_at = timezone.now() log.status = 'success' log.output = output_buffer.getvalue() log.save() # Mettre à jour la commande planifiée scheduled_command.last_run = log.started_at scheduled_command.save() # Envoyer l'email si configuré if scheduled_command.send_email and scheduled_command.email_recipients: self.send_success_email(scheduled_command, log, request) messages.success( request, f"Commande '{scheduled_command.name}' exécutée avec succès. " f"Consultez l'historique pour voir les détails." ) except Exception as e: # Mettre à jour le log avec l'erreur log.finished_at = timezone.now() log.status = 'error' log.output = output_buffer.getvalue() log.error_message = f"{str(e)}\n\n{traceback.format_exc()}" log.save() # Envoyer l'email d'erreur si configuré if scheduled_command.send_email and scheduled_command.email_recipients: self.send_error_email(scheduled_command, log, request) messages.error( request, f"Erreur lors de l'exécution de '{scheduled_command.name}': {str(e)}" ) # Rediriger vers la page de détail de la commande return redirect('admin:common_scheduledcommand_change', scheduled_command.pk) def send_success_email(self, task, log, request): """Envoie un email de succès après exécution depuis l'admin.""" try: # Calculer la durée si pas déjà fait if log.duration_seconds is None and log.finished_at and log.started_at: log.calculate_duration() log.save(update_fields=['duration_seconds']) # Formater la durée duration_str = f"{log.duration_seconds:.1f}" if log.duration_seconds is not None else "N/A" subject = task.email_subject or f"[StreetUp] Tâche planifiée réussie: {task.name}" # Vérifier si un fichier a été généré output_file = task.command_args.get('output', '') if isinstance(task.command_args, dict) else '' # Substituer les variables dans le nom du fichier if output_file and '%(year)s' in output_file: from datetime import date from dateutil.relativedelta import relativedelta today = date.today() prev_month = today.replace(day=1) - relativedelta(months=1) output_file = output_file.replace('%(year)s', str(prev_month.year)) output_file = output_file.replace('%(month)s', f'{prev_month.month:02d}') output_file = output_file.replace('%(day)s', f'{today.day:02d}') output_file = output_file.replace('%(date)s', today.strftime('%Y-%m-%d')) # Convertir en chemin absolu if not os.path.isabs(output_file): output_file = os.path.join(settings.BASE_DIR, output_file) body = f"""Bonjour, La tâche planifiée "{task.name}" s'est exécutée avec succès depuis l'interface d'administration. Détails: - Commande: {task.command_name} - Démarré: {log.started_at.strftime('%Y-%m-%d %H:%M:%S')} - Terminé: {log.finished_at.strftime('%Y-%m-%d %H:%M:%S')} - Durée: {duration_str} secondes - Exécuté par: {request.user.username} ({request.user.email}) Prochaine exécution planifiée: {task.next_run.strftime('%Y-%m-%d %H:%M') if task.next_run else 'Non planifiée'} --- Sortie de la commande: {log.output} --- Cordialement, StreetUp - Système de tâches planifiées """ recipients = [email.strip() for email in task.email_recipients.split(',')] email = EmailMessage( subject=subject, body=body, from_email=settings.DEFAULT_FROM_EMAIL, to=recipients, ) # Attacher le fichier si il existe if output_file and os.path.exists(output_file): email.attach_file(output_file) email.send(fail_silently=False) log.email_sent = True log.save(update_fields=['email_sent']) messages.info(request, f"Email envoyé à: {', '.join(recipients)}") except Exception as e: messages.warning(request, f"Erreur lors de l'envoi de l'email: {str(e)}") def send_error_email(self, task, log, request): """Envoie un email d'erreur après échec d'exécution depuis l'admin.""" try: subject = f"[StreetUp] ERREUR - Tâche planifiée: {task.name}" body = f"""Bonjour, La tâche planifiée "{task.name}" a rencontré une erreur lors de son exécution depuis l'interface d'administration. Détails: - Commande: {task.command_name} - Démarré: {log.started_at.strftime('%Y-%m-%d %H:%M:%S')} - Terminé: {log.finished_at.strftime('%Y-%m-%d %H:%M:%S')} - Durée: {log.duration_seconds:.1f} secondes - Exécuté par: {request.user.username} ({request.user.email}) Prochaine exécution planifiée: {task.next_run.strftime('%Y-%m-%d %H:%M') if task.next_run else 'Non planifiée'} --- Message d'erreur: {log.error_message} --- Sortie de la commande: {log.output} --- Veuillez vérifier les logs dans l'administration Django pour plus de détails. Cordialement, StreetUp - Système de tâches planifiées """ recipients = [email.strip() for email in task.email_recipients.split(',')] email = EmailMessage( subject=subject, body=body, from_email=settings.DEFAULT_FROM_EMAIL, to=recipients, ) email.send(fail_silently=False) log.email_sent = True log.save(update_fields=['email_sent']) messages.warning(request, f"Email d'erreur envoyé à: {', '.join(recipients)}") except Exception as e: messages.warning(request, f"Erreur lors de l'envoi de l'email d'erreur: {str(e)}") def save_model(self, request, obj, form, change): if not change: # Nouvelle création obj.created_by = request.user # Calculer next_run si pas déjà défini if not obj.next_run: obj.next_run = obj.calculate_next_run() super().save_model(request, obj, form, change) def frequency_display(self, obj): """Affiche la fréquence avec une icône.""" icons = { 'hourly': '⏰', 'daily': '📅', 'weekly': '📆', 'monthly': '🗓️' } icon = icons.get(obj.frequency, '⏱️') return f"{icon} {obj.get_frequency_display()}" frequency_display.short_description = "Fréquence" def schedule_display(self, obj): """Affiche le planning de manière lisible.""" schedule = f"{obj.hour:02d}:{obj.minute:02d}" if obj.frequency == 'monthly' and obj.day_of_month: schedule += f" le {obj.day_of_month}" elif obj.frequency == 'weekly' and obj.day_of_week is not None: days = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'] schedule += f" le {days[obj.day_of_week]}" return schedule schedule_display.short_description = "Planning" def is_active_display(self, obj): """Affiche le statut avec une icône.""" if obj.is_active: return format_html( '✓ Active' ) return format_html( '✗ Inactive' ) is_active_display.short_description = "Statut" def last_run_display(self, obj): """Affiche la dernière exécution.""" if obj.last_run: return obj.last_run.strftime('%Y-%m-%d %H:%M') return "-" last_run_display.short_description = "Dernière exécution" def next_run_display(self, obj): """Affiche la prochaine exécution.""" if obj.next_run: return obj.next_run.strftime('%Y-%m-%d %H:%M') return "-" next_run_display.short_description = "Prochaine exécution" def execution_count(self, obj): """Nombre total d'exécutions.""" return obj.execution_logs.count() execution_count.short_description = "Exécutions" def success_rate(self, obj): """Taux de succès.""" total = obj.execution_logs.count() if total == 0: return "-" success = obj.execution_logs.filter(status='success').count() rate = (success / total) * 100 rate_formatted = "{:.1f}".format(rate) color = 'green' if rate >= 90 else 'orange' if rate >= 70 else 'red' return format_html( '{}% ({}/{})', color, rate_formatted, success, total ) success_rate.short_description = "Taux de succès" def execution_history_link(self, obj): """Lien vers l'historique d'exécution.""" if obj.pk: count = obj.execution_logs.count() url = reverse('admin:common_commandexecutionlog_changelist') + '?scheduled_command__id__exact={}'.format(obj.pk) return format_html( 'Voir l\'historique ({} exécutions)', url, count ) return "-" execution_history_link.short_description = "Historique" def execute_now_button(self, obj): """Bouton pour exécuter la commande immédiatement.""" if obj.pk: url = reverse('admin:execute_scheduled_command', args=[obj.pk]) return format_html( '''

La commande sera exécutée immédiatement dans une tâche en arrière-plan. Consultez l'historique d'exécution pour voir le résultat.

''', url ) return "-" execute_now_button.short_description = "Exécution immédiate" def execute_immediately(self, request, queryset): """Action pour exécuter les commandes sélectionnées immédiatement.""" executed_count = 0 error_count = 0 for scheduled_command in queryset: # Créer un log d'exécution log = CommandExecutionLog.objects.create( scheduled_command=scheduled_command, status='running' ) try: # Préparer les arguments args = [] kwargs = {} if scheduled_command.command_args: if isinstance(scheduled_command.command_args, dict): kwargs = scheduled_command.command_args elif isinstance(scheduled_command.command_args, list): args = scheduled_command.command_args # Capturer la sortie output_buffer = io.StringIO() # Exécuter la commande call_command( scheduled_command.command_name, *args, stdout=output_buffer, stderr=output_buffer, **kwargs ) # Mettre à jour le log log.finished_at = timezone.now() log.status = 'success' log.output = output_buffer.getvalue() log.save() # Mettre à jour la commande planifiée scheduled_command.last_run = log.started_at scheduled_command.save() executed_count += 1 except Exception as e: # Mettre à jour le log avec l'erreur log.finished_at = timezone.now() log.status = 'error' log.error_message = f"{str(e)}\n\n{traceback.format_exc()}" log.save() error_count += 1 # Messages de retour if executed_count > 0: self.message_user( request, f"{executed_count} commande(s) exécutée(s) avec succès.", messages.SUCCESS ) if error_count > 0: self.message_user( request, f"{error_count} commande(s) en erreur. Consultez l'historique pour les détails.", messages.ERROR ) execute_immediately.short_description = "▶️ Exécuter immédiatement les commandes sélectionnées" @admin.register(CommandExecutionLog) class CommandExecutionLogAdmin(admin.ModelAdmin): list_display = [ 'scheduled_command', 'started_at_display', 'status_display', 'duration_display', 'email_sent_display' ] list_filter = ['status', 'email_sent', 'scheduled_command'] search_fields = ['scheduled_command__name', 'output', 'error_message'] readonly_fields = [ 'scheduled_command', 'started_at', 'finished_at', 'status', 'output', 'error_message', 'duration_seconds', 'email_sent', 'output_formatted', 'error_formatted' ] fieldsets = [ ('Exécution', { 'fields': [ 'scheduled_command', 'started_at', 'finished_at', 'status', 'duration_seconds', 'email_sent' ] }), ('Sortie', { 'fields': ['output_formatted'], }), ('Erreurs', { 'fields': ['error_formatted'], 'classes': ['collapse'] }) ] def has_add_permission(self, request): """Empêche la création manuelle de logs.""" return False def has_change_permission(self, request, obj=None): """Lecture seule.""" return True def has_delete_permission(self, request, obj=None): """Permet la suppression.""" return True def started_at_display(self, obj): """Affiche la date de début.""" return obj.started_at.strftime('%Y-%m-%d %H:%M:%S') started_at_display.short_description = "Démarré à" def status_display(self, obj): """Affiche le statut avec une couleur.""" colors = { 'running': 'blue', 'success': 'green', 'error': 'red' } icons = { 'running': '⏳', 'success': '✓', 'error': '✗' } color = colors.get(obj.status, 'gray') icon = icons.get(obj.status, '•') return format_html( '{} {}', color, icon, obj.get_status_display() ) status_display.short_description = "Statut" def duration_display(self, obj): """Affiche la durée.""" if obj.duration_seconds is not None: if obj.duration_seconds < 60: return f"{obj.duration_seconds:.1f}s" else: minutes = int(obj.duration_seconds // 60) seconds = int(obj.duration_seconds % 60) return f"{minutes}m {seconds}s" return "-" duration_display.short_description = "Durée" def email_sent_display(self, obj): """Affiche si l'email a été envoyé.""" if obj.email_sent: return format_html('✓') return format_html('-') email_sent_display.short_description = "Email" def output_formatted(self, obj): """Affiche la sortie formatée.""" if obj.output: return format_html( '
{}
', obj.output ) return "-" output_formatted.short_description = "Sortie de la commande" def error_formatted(self, obj): """Affiche les erreurs formatées.""" if obj.error_message: return format_html( '
{}
', obj.error_message ) return "-" error_formatted.short_description = "Message d'erreur"