refactor: move output file resolution to model method with log-based fallback and added unit tests

This commit is contained in:
kdeterme 2026-09-07 11:17:00 +02:00
parent 135dfb41e6
commit 75dd0bd50e
4 changed files with 122 additions and 17 deletions

View file

@ -180,22 +180,7 @@ class ScheduledCommandAdmin(admin.ModelAdmin):
subject = task.email_subject or f"[{prod_name}] Tâche planifiée réussie: {task.name}" subject = task.email_subject or f"[{prod_name}] Tâche planifiée réussie: {task.name}"
# Vérifier si un fichier a été généré # Vérifier si un fichier a été généré
output_file = task.command_args.get('output', '') if isinstance(task.command_args, dict) else '' output_file = task.resolve_output_file(log=log)
# 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, body = f"""Bonjour,

View file

@ -140,7 +140,7 @@ class Command(BaseCommand):
subject = task.email_subject or f"[{prod_name}] Tâche planifiée réussie: {task.name}" subject = task.email_subject or f"[{prod_name}] Tâche planifiée réussie: {task.name}"
# Vérifier si un fichier a été généré # Vérifier si un fichier a été généré
output_file = task.command_args.get('output', '') output_file = task.resolve_output_file(log=log)
body = f""" body = f"""
Bonjour, Bonjour,

View file

@ -7,6 +7,10 @@ from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
import json import json
import os
import re
from datetime import date
from dateutil.relativedelta import relativedelta
class ScheduledCommand(models.Model): class ScheduledCommand(models.Model):
@ -216,6 +220,47 @@ class ScheduledCommand(models.Model):
except: except:
return str(self.command_args) return str(self.command_args)
def resolve_output_file(self, log=None):
"""
Résout le chemin du fichier généré par la commande (s'il existe).
Gère les variables temporelles (%(year)s, %(month)s, %(day)s, %(date)s)
et convertit les chemins relatifs par rapport à settings.BASE_DIR.
Inspecte également log.output en repli si le fichier n'est pas trouvé directement.
"""
output_file = ''
if isinstance(self.command_args, dict):
output_file = self.command_args.get('output', '')
if output_file:
today = date.today()
# Par cohérence avec les exports (ex: export_interventions_xlsx),
# pour le mois et l'année, on utilise le mois précédent par défaut
prev_month = today.replace(day=1) - relativedelta(months=1)
output_file = str(output_file)
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'))
if not os.path.isabs(output_file):
output_file = os.path.join(settings.BASE_DIR, output_file)
if os.path.exists(output_file):
return output_file
# Repli : analyse de log.output pour trouver un fichier généré
if log and getattr(log, 'output', None):
match = re.search(r'(?:Fichier généré|Fichier de sortie)\s*:\s*(.+)', log.output)
if match:
candidate = match.group(1).strip()
if not os.path.isabs(candidate):
candidate = os.path.join(settings.BASE_DIR, candidate)
if os.path.exists(candidate):
return candidate
return output_file if (output_file and os.path.exists(output_file)) else None
class CommandExecutionLog(models.Model): class CommandExecutionLog(models.Model):
""" """

View file

@ -3,6 +3,7 @@ from django.contrib.messages import get_messages
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
import json import json
@ -317,6 +318,80 @@ class UserConfigCopyTests(TestCase):
self.assertTrue(target_ut.can_inspect) self.assertTrue(target_ut.can_inspect)
class ScheduledCommandResolveOutputFileTests(TestCase):
def test_resolve_output_file_with_placeholders(self):
import os
from unittest.mock import patch
from datetime import date
from dateutil.relativedelta import relativedelta
from common.models_scheduled_tasks import ScheduledCommand
cmd = ScheduledCommand(
name="Export test",
command_name="export_interventions_xlsx",
command_args={"output": "exports/interventions_export_%(year)s_%(month)s.xlsx"}
)
today = date.today()
prev_month = today.replace(day=1) - relativedelta(months=1)
expected_filename = f"interventions_export_{prev_month.year}_{prev_month.month:02d}.xlsx"
expected_path = os.path.join(settings.BASE_DIR, "exports", expected_filename)
with patch("os.path.exists", side_effect=lambda p: p == expected_path):
resolved = cmd.resolve_output_file()
self.assertEqual(resolved, expected_path)
def test_resolve_output_file_fallback_from_log_output(self):
import os
from unittest.mock import patch, MagicMock
from common.models_scheduled_tasks import ScheduledCommand
cmd = ScheduledCommand(
name="Export test",
command_name="export_interventions_xlsx",
command_args={"output": "unknown_file.xlsx"}
)
log = MagicMock()
log.output = "Démarrage...\n✓ Fichier généré: /tmp/custom_generated.xlsx\n"
with patch("os.path.exists", side_effect=lambda p: p == "/tmp/custom_generated.xlsx"):
resolved = cmd.resolve_output_file(log=log)
self.assertEqual(resolved, "/tmp/custom_generated.xlsx")
def test_send_success_email_attaches_resolved_file(self):
import os
from unittest.mock import patch, MagicMock
from common.models_scheduled_tasks import ScheduledCommand
from common.management.commands.run_scheduled_commands import Command as RunScheduledCommand
cmd = ScheduledCommand.objects.create(
name="Export mensuel",
command_name="export_interventions_xlsx",
command_args={"output": "exports/interventions_export_%(year)s_%(month)s.xlsx"},
send_email=True,
email_recipients="test@example.com"
)
log = MagicMock()
log.started_at = timezone.now()
log.finished_at = timezone.now()
log.duration_seconds = 1.0
log.output = "Succès"
fake_path = os.path.join(settings.BASE_DIR, "exports", "test.xlsx")
with patch.object(ScheduledCommand, "resolve_output_file", return_value=fake_path), \
patch("os.path.exists", return_value=True), \
patch("django.core.mail.EmailMessage.attach_file") as mock_attach, \
patch("django.core.mail.EmailMessage.send") as mock_send:
runner = RunScheduledCommand()
runner.send_success_email(cmd, log)
mock_attach.assert_called_once_with(fake_path)
mock_send.assert_called_once()