feat: add custom notification recipient fields and helpers to Symptom model
This commit is contained in:
parent
9ae7b0631b
commit
836b5c8edb
6 changed files with 444 additions and 48 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
from django.contrib.gis import admin
|
from django.contrib.gis import admin
|
||||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language, gettext_lazy as _
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
@ -353,16 +353,65 @@ class SymptomAdminForm(forms.ModelForm):
|
||||||
)
|
)
|
||||||
return cats
|
return cats
|
||||||
|
|
||||||
|
def _clean_emails(self, field_name):
|
||||||
|
emails_raw = self.cleaned_data.get(field_name, '')
|
||||||
|
if not emails_raw:
|
||||||
|
return ''
|
||||||
|
import re
|
||||||
|
from django.core.validators import validate_email
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
cleaned_emails = []
|
||||||
|
for email in re.split(r'[;,]', emails_raw):
|
||||||
|
email = email.strip()
|
||||||
|
if email:
|
||||||
|
try:
|
||||||
|
validate_email(email)
|
||||||
|
cleaned_emails.append(email)
|
||||||
|
except ValidationError:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_("L'adresse e-mail '%(email)s' n'est pas valide.") % {'email': email}
|
||||||
|
)
|
||||||
|
return ', '.join(cleaned_emails)
|
||||||
|
|
||||||
|
def clean_notification_emails(self):
|
||||||
|
return self._clean_emails('notification_emails')
|
||||||
|
|
||||||
|
def clean_cc_notification_emails(self):
|
||||||
|
return self._clean_emails('cc_notification_emails')
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Symptom)
|
@admin.register(Symptom)
|
||||||
class SymptomAdmin(admin.ModelAdmin):
|
class SymptomAdmin(admin.ModelAdmin):
|
||||||
form = SymptomAdminForm
|
form = SymptomAdminForm
|
||||||
change_list_template = "admin/interventions/symptom_changelist.html"
|
change_list_template = "admin/interventions/symptom_changelist.html"
|
||||||
list_display = ("id", "code", "thematic", "name_fr", "name_nl", "is_active", "auto_create_intervention", "auto_create_intervention_status", "require_equipment_selection", "asset_categories_list", "contract", "teams_list")
|
list_display = ("id", "code", "thematic", "name_fr", "name_nl", "is_active", "notify_contract_recipients", "auto_create_intervention", "auto_create_intervention_status", "require_equipment_selection", "asset_categories_list", "contract", "teams_list")
|
||||||
list_filter = ("thematic", "asset_categories", "priority", "is_active", "auto_create_intervention", "require_equipment_selection", "teams")
|
list_filter = ("thematic", "asset_categories", "priority", "is_active", "notify_contract_recipients", "auto_create_intervention", "require_equipment_selection", "teams")
|
||||||
search_fields = ("name_fr", "name_nl", "code")
|
search_fields = ("name_fr", "name_nl", "code")
|
||||||
filter_horizontal = ("teams",)
|
filter_horizontal = ("teams",)
|
||||||
inlines = [SymptomRecommendationInline, SymptomContractAutoCloseInline]
|
inlines = [SymptomRecommendationInline, SymptomContractAutoCloseInline]
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('code', 'name_fr', 'name_nl', 'order_number', 'is_active'),
|
||||||
|
}),
|
||||||
|
(_("Classification"), {
|
||||||
|
'fields': ('thematic', 'asset_categories', 'priority', 'check_priority', 'origin_type', 'maintenance_type', 'require_equipment_selection'),
|
||||||
|
}),
|
||||||
|
(_("Attribution / Contrat"), {
|
||||||
|
'fields': ('provider', 'contract', 'teams'),
|
||||||
|
}),
|
||||||
|
(_("Notifications email"), {
|
||||||
|
'fields': ('notify_contract_recipients', 'notification_emails', 'cc_notification_emails'),
|
||||||
|
'description': _(
|
||||||
|
"Configuration des destinataires pour ce symptôme. "
|
||||||
|
"Par défaut, les destinataires configurés sur le contrat associé sont prévenus. "
|
||||||
|
"Vous pouvez désactiver cette notification du contrat et/ou spécifier des adresses email supplémentaires (principales et en copie, séparées par des virgules ou points-virgules)."
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
(_("Workflow & Création automatique"), {
|
||||||
|
'fields': ('recommended_next_status', 'auto_create_intervention', 'auto_create_intervention_status'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
urls = super().get_urls()
|
urls = super().get_urls()
|
||||||
|
|
@ -444,6 +493,9 @@ class SymptomAdmin(admin.ModelAdmin):
|
||||||
"Actif",
|
"Actif",
|
||||||
"Statut recommandé",
|
"Statut recommandé",
|
||||||
"Équipes",
|
"Équipes",
|
||||||
|
"Notifier contrat",
|
||||||
|
"Emails supplémentaires symptôme",
|
||||||
|
"Emails CC supplémentaires symptôme",
|
||||||
"Emails notifications contrat",
|
"Emails notifications contrat",
|
||||||
"Emails CC contrat",
|
"Emails CC contrat",
|
||||||
"Société (prestataire)",
|
"Société (prestataire)",
|
||||||
|
|
@ -506,6 +558,9 @@ class SymptomAdmin(admin.ModelAdmin):
|
||||||
"Oui" if symptom.is_active else "Non",
|
"Oui" if symptom.is_active else "Non",
|
||||||
recommended_status,
|
recommended_status,
|
||||||
teams,
|
teams,
|
||||||
|
"Oui" if symptom.notify_contract_recipients else "Non",
|
||||||
|
symptom.notification_emails or "—",
|
||||||
|
symptom.cc_notification_emails or "—",
|
||||||
contract_emails,
|
contract_emails,
|
||||||
contract_cc_emails,
|
contract_cc_emails,
|
||||||
provider_name,
|
provider_name,
|
||||||
|
|
@ -515,7 +570,7 @@ class SymptomAdmin(admin.ModelAdmin):
|
||||||
])
|
])
|
||||||
|
|
||||||
# Largeurs de colonnes
|
# Largeurs de colonnes
|
||||||
col_widths = [6, 14, 40, 40, 20, 30, 12, 16, 18, 25, 14, 8, 20, 40, 40, 40, 30, 20, 12, 50]
|
col_widths = [6, 14, 40, 40, 20, 30, 12, 16, 18, 25, 14, 8, 20, 40, 18, 40, 40, 40, 40, 30, 20, 12, 50]
|
||||||
for i, width in enumerate(col_widths, start=1):
|
for i, width in enumerate(col_widths, start=1):
|
||||||
ws.column_dimensions[ws.cell(row=1, column=i).column_letter].width = width
|
ws.column_dimensions[ws.cell(row=1, column=i).column_letter].width = width
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_internal_emails_forward(apps, schema_editor):
|
||||||
|
Symptom = apps.get_model('interventions', 'Symptom')
|
||||||
|
for symptom in Symptom.objects.exclude(internal_notification_emails__isnull=True).exclude(internal_notification_emails=''):
|
||||||
|
if not symptom.notification_emails:
|
||||||
|
symptom.notification_emails = symptom.internal_notification_emails
|
||||||
|
symptom.save(update_fields=['notification_emails'])
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_internal_emails_reverse(apps, schema_editor):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('interventions', '0007_cleanup_blocked_status_interventions'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='symptom',
|
||||||
|
name='notify_contract_recipients',
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Indique si les destinataires définis sur le contrat associé doivent être prévenus (oui par défaut).",
|
||||||
|
verbose_name="Notifier les destinataires du contrat",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='symptom',
|
||||||
|
name='notification_emails',
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Adresses email supplémentaires à notifier pour ce symptôme (séparées par des virgules ou points-virgules).",
|
||||||
|
max_length=1000,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Emails supplémentaires",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='symptom',
|
||||||
|
name='cc_notification_emails',
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Adresses email supplémentaires à ajouter en copie pour ce symptôme (séparées par des virgules ou points-virgules).",
|
||||||
|
max_length=1000,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Emails en copie (CC) supplémentaires",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_internal_emails_forward, migrate_internal_emails_reverse),
|
||||||
|
]
|
||||||
|
|
@ -231,6 +231,25 @@ class Symptom(models.Model):
|
||||||
help_text=_("Statut dans lequel l'intervention automatique doit être créée.")
|
help_text=_("Statut dans lequel l'intervention automatique doit être créée.")
|
||||||
)
|
)
|
||||||
internal_notification_emails = models.CharField(blank=True, null=True, max_length=1000, help_text=_("Liste d'emails pour les notifications internes."))
|
internal_notification_emails = models.CharField(blank=True, null=True, max_length=1000, help_text=_("Liste d'emails pour les notifications internes."))
|
||||||
|
notify_contract_recipients = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name=_("Notifier les destinataires du contrat"),
|
||||||
|
help_text=_("Indique si les destinataires définis sur le contrat associé doivent être prévenus (oui par défaut)."),
|
||||||
|
)
|
||||||
|
notification_emails = models.CharField(
|
||||||
|
max_length=1000,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_("Emails supplémentaires"),
|
||||||
|
help_text=_("Adresses email supplémentaires à notifier pour ce symptôme (séparées par des virgules ou points-virgules)."),
|
||||||
|
)
|
||||||
|
cc_notification_emails = models.CharField(
|
||||||
|
max_length=1000,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_("Emails en copie (CC) supplémentaires"),
|
||||||
|
help_text=_("Adresses email supplémentaires à ajouter en copie pour ce symptôme (séparées par des virgules ou points-virgules)."),
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
lang = get_language()
|
lang = get_language()
|
||||||
|
|
|
||||||
246
loko/interventions/tests/test_symptom_notifications.py
Normal file
246
loko/interventions/tests/test_symptom_notifications.py
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
from common.models import Thematic
|
||||||
|
from contracts.models import Company, Contract
|
||||||
|
from assets.models import AssetCategory
|
||||||
|
from interventions.models import Intervention, Symptom
|
||||||
|
from interventions.admin import SymptomAdminForm
|
||||||
|
from interventions.views.helpers import get_contract_recipients, get_contract_cc_recipients
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(EMAIL_OVERRIDE=None)
|
||||||
|
class SymptomNotificationRecipientsTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.thematic, _ = Thematic.objects.get_or_create(code='trafficlights', defaults={'name': 'Feux de signalisation'})
|
||||||
|
self.category, _ = AssetCategory.objects.get_or_create(name_fr='Test Category', thematic=self.thematic)
|
||||||
|
|
||||||
|
self.company = Company.objects.create(name='Prestataire Feux')
|
||||||
|
today = timezone.now().date()
|
||||||
|
|
||||||
|
self.contract = Contract.objects.create(
|
||||||
|
contract_number='CTR-FEUX-001',
|
||||||
|
company=self.company,
|
||||||
|
start_date=today,
|
||||||
|
end_date=today + timezone.timedelta(days=365),
|
||||||
|
is_active=True,
|
||||||
|
is_blocked=False,
|
||||||
|
notification_emails='contract_to1@example.com; contract_to2@example.com',
|
||||||
|
cc_notification_emails='contract_cc1@example.com, contract_cc2@example.com',
|
||||||
|
send_email_notifications=True,
|
||||||
|
)
|
||||||
|
self.contract.thematics.add(self.thematic)
|
||||||
|
|
||||||
|
self.contract_blocked = Contract.objects.create(
|
||||||
|
contract_number='CTR-FEUX-BLOCKED',
|
||||||
|
company=self.company,
|
||||||
|
start_date=today,
|
||||||
|
end_date=today + timezone.timedelta(days=365),
|
||||||
|
is_active=True,
|
||||||
|
is_blocked=True,
|
||||||
|
blocked_reason='Gel budget',
|
||||||
|
notification_emails='blocked_contract@example.com',
|
||||||
|
cc_notification_emails='blocked_cc@example.com',
|
||||||
|
send_email_notifications=True,
|
||||||
|
)
|
||||||
|
self.contract_blocked.thematics.add(self.thematic)
|
||||||
|
|
||||||
|
def test_default_symptom_notifies_contract_recipients(self):
|
||||||
|
"""Par défaut (notify_contract_recipients=True, sans emails supplémentaires), seuls les emails du contrat sont retournés."""
|
||||||
|
symptom = Symptom.objects.create(
|
||||||
|
name_fr='Ampoule grillée',
|
||||||
|
thematic=self.thematic,
|
||||||
|
contract=self.contract,
|
||||||
|
notify_contract_recipients=True,
|
||||||
|
)
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
title='Remplacement ampoule',
|
||||||
|
thematic=self.thematic,
|
||||||
|
contract=self.contract,
|
||||||
|
symptom=symptom,
|
||||||
|
)
|
||||||
|
|
||||||
|
recipients = get_contract_recipients(self.contract, intervention)
|
||||||
|
self.assertIn('contract_to1@example.com', recipients)
|
||||||
|
self.assertIn('contract_to2@example.com', recipients)
|
||||||
|
self.assertEqual(len(recipients), 2)
|
||||||
|
|
||||||
|
cc_recipients = get_contract_cc_recipients(self.contract, intervention)
|
||||||
|
self.assertIn('contract_cc1@example.com', cc_recipients)
|
||||||
|
self.assertIn('contract_cc2@example.com', cc_recipients)
|
||||||
|
self.assertIn('bm.perm.tech@sprb.brussels', cc_recipients)
|
||||||
|
|
||||||
|
def test_symptom_with_additional_recipients_and_cc(self):
|
||||||
|
"""Avec notify_contract_recipients=True et des emails supplémentaires, les emails du contrat et du symptôme sont cumulés."""
|
||||||
|
symptom = Symptom.objects.create(
|
||||||
|
name_fr='Panne contrôleur critique',
|
||||||
|
thematic=self.thematic,
|
||||||
|
contract=self.contract,
|
||||||
|
notify_contract_recipients=True,
|
||||||
|
notification_emails='extra_expert@sprb.brussels; contract_to1@example.com', # Doublon volontaire avec contrat
|
||||||
|
cc_notification_emails='extra_supervision@sprb.brussels',
|
||||||
|
)
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
title='Contrôleur HS',
|
||||||
|
thematic=self.thematic,
|
||||||
|
contract=self.contract,
|
||||||
|
symptom=symptom,
|
||||||
|
)
|
||||||
|
|
||||||
|
recipients = get_contract_recipients(self.contract, intervention)
|
||||||
|
self.assertEqual(
|
||||||
|
set(recipients),
|
||||||
|
{'contract_to1@example.com', 'contract_to2@example.com', 'extra_expert@sprb.brussels'}
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_recipients = get_contract_cc_recipients(self.contract, intervention)
|
||||||
|
self.assertEqual(
|
||||||
|
set(cc_recipients),
|
||||||
|
{'contract_cc1@example.com', 'contract_cc2@example.com', 'extra_supervision@sprb.brussels', 'bm.perm.tech@sprb.brussels'}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_symptom_disabling_contract_recipients(self):
|
||||||
|
"""Avec notify_contract_recipients=False, les emails du contrat sont exclus, seuls les emails du symptôme sont prévenus."""
|
||||||
|
symptom = Symptom.objects.create(
|
||||||
|
name_fr='Vandalisme interne',
|
||||||
|
thematic=self.thematic,
|
||||||
|
contract=self.contract,
|
||||||
|
notify_contract_recipients=False,
|
||||||
|
notification_emails='police_bruxelles@sprb.brussels, securite@sprb.brussels',
|
||||||
|
cc_notification_emails='responsable_securite@sprb.brussels',
|
||||||
|
)
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
title='Dégradation armoire',
|
||||||
|
thematic=self.thematic,
|
||||||
|
contract=self.contract,
|
||||||
|
symptom=symptom,
|
||||||
|
)
|
||||||
|
|
||||||
|
recipients = get_contract_recipients(self.contract, intervention)
|
||||||
|
self.assertEqual(
|
||||||
|
set(recipients),
|
||||||
|
{'police_bruxelles@sprb.brussels', 'securite@sprb.brussels'}
|
||||||
|
)
|
||||||
|
self.assertNotIn('contract_to1@example.com', recipients)
|
||||||
|
self.assertNotIn('contract_to2@example.com', recipients)
|
||||||
|
|
||||||
|
cc_recipients = get_contract_cc_recipients(self.contract, intervention)
|
||||||
|
self.assertIn('responsable_securite@sprb.brussels', cc_recipients)
|
||||||
|
self.assertIn('bm.perm.tech@sprb.brussels', cc_recipients)
|
||||||
|
self.assertNotIn('contract_cc1@example.com', cc_recipients)
|
||||||
|
self.assertNotIn('contract_cc2@example.com', cc_recipients)
|
||||||
|
|
||||||
|
def test_symptom_disabling_contract_recipients_without_extra_emails(self):
|
||||||
|
"""Avec notify_contract_recipients=False et sans emails supplémentaires, aucun destinataire principal n'est retourné."""
|
||||||
|
symptom = Symptom.objects.create(
|
||||||
|
name_fr='Test sans notification',
|
||||||
|
thematic=self.thematic,
|
||||||
|
contract=self.contract,
|
||||||
|
notify_contract_recipients=False,
|
||||||
|
)
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
title='Intervention silencieuse',
|
||||||
|
thematic=self.thematic,
|
||||||
|
contract=self.contract,
|
||||||
|
symptom=symptom,
|
||||||
|
)
|
||||||
|
|
||||||
|
recipients = get_contract_recipients(self.contract, intervention)
|
||||||
|
self.assertEqual(recipients, [])
|
||||||
|
|
||||||
|
def test_blocked_contract_returns_empty_even_with_symptom_emails(self):
|
||||||
|
"""Un marché bloqué ne doit envoyer aucun email, même si le symptôme configure des destinataires."""
|
||||||
|
symptom = Symptom.objects.create(
|
||||||
|
name_fr='Panne sur marché bloqué',
|
||||||
|
thematic=self.thematic,
|
||||||
|
contract=self.contract_blocked,
|
||||||
|
notify_contract_recipients=True,
|
||||||
|
notification_emails='extra@sprb.brussels',
|
||||||
|
cc_notification_emails='extra_cc@sprb.brussels',
|
||||||
|
)
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
title='Intervention bloquée',
|
||||||
|
thematic=self.thematic,
|
||||||
|
contract=self.contract_blocked,
|
||||||
|
symptom=symptom,
|
||||||
|
)
|
||||||
|
|
||||||
|
recipients = get_contract_recipients(self.contract_blocked, intervention)
|
||||||
|
self.assertEqual(recipients, [])
|
||||||
|
|
||||||
|
cc_recipients = get_contract_cc_recipients(self.contract_blocked, intervention)
|
||||||
|
self.assertEqual(cc_recipients, [])
|
||||||
|
|
||||||
|
def test_intervention_without_contract_uses_symptom_recipients(self):
|
||||||
|
"""Une intervention sans contrat mais avec un symptôme configuré peut quand même notifier ses destinataires spécifiques."""
|
||||||
|
symptom = Symptom.objects.create(
|
||||||
|
name_fr='Alerte voirie spéciale',
|
||||||
|
thematic=self.thematic,
|
||||||
|
contract=None,
|
||||||
|
notify_contract_recipients=True,
|
||||||
|
notification_emails='service_voirie@sprb.brussels',
|
||||||
|
cc_notification_emails='cc_voirie@sprb.brussels',
|
||||||
|
)
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
title='Intervention sans contrat',
|
||||||
|
thematic=self.thematic,
|
||||||
|
contract=None,
|
||||||
|
symptom=symptom,
|
||||||
|
)
|
||||||
|
|
||||||
|
recipients = get_contract_recipients(None, intervention)
|
||||||
|
self.assertEqual(recipients, ['service_voirie@sprb.brussels'])
|
||||||
|
|
||||||
|
cc_recipients = get_contract_cc_recipients(None, intervention)
|
||||||
|
self.assertIn('cc_voirie@sprb.brussels', cc_recipients)
|
||||||
|
self.assertIn('bm.perm.tech@sprb.brussels', cc_recipients)
|
||||||
|
|
||||||
|
def test_backward_compatibility_without_intervention(self):
|
||||||
|
"""L'appel sans intervention (intervention=None) conserve le comportement historique du contrat."""
|
||||||
|
recipients = get_contract_recipients(self.contract)
|
||||||
|
self.assertEqual(
|
||||||
|
set(recipients),
|
||||||
|
{'contract_to1@example.com', 'contract_to2@example.com'}
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_recipients = get_contract_cc_recipients(self.contract)
|
||||||
|
self.assertIn('contract_cc1@example.com', cc_recipients)
|
||||||
|
self.assertIn('contract_cc2@example.com', cc_recipients)
|
||||||
|
self.assertIn('bm.perm.tech@sprb.brussels', cc_recipients)
|
||||||
|
|
||||||
|
def test_symptom_admin_form_email_validation(self):
|
||||||
|
"""Le formulaire d'administration valide et normalise les listes d'adresses email."""
|
||||||
|
# Emails valides avec mélange de virgules et points-virgules
|
||||||
|
form = SymptomAdminForm(data={
|
||||||
|
'name_fr': 'Test Form',
|
||||||
|
'name_nl': 'Test Form NL',
|
||||||
|
'thematic': self.thematic.pk,
|
||||||
|
'priority': '3',
|
||||||
|
'is_active': True,
|
||||||
|
'notify_contract_recipients': True,
|
||||||
|
'notification_emails': 'alpha@example.com; beta@example.com, gamma@example.com',
|
||||||
|
'cc_notification_emails': 'delta@example.com; epsilon@example.com',
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid(), form.errors)
|
||||||
|
self.assertEqual(
|
||||||
|
form.cleaned_data['notification_emails'],
|
||||||
|
'alpha@example.com, beta@example.com, gamma@example.com'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
form.cleaned_data['cc_notification_emails'],
|
||||||
|
'delta@example.com, epsilon@example.com'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Email invalide
|
||||||
|
form_invalid = SymptomAdminForm(data={
|
||||||
|
'name_fr': 'Test Form Invalid',
|
||||||
|
'name_nl': 'Test Form Invalid NL',
|
||||||
|
'thematic': self.thematic.pk,
|
||||||
|
'priority': '3',
|
||||||
|
'is_active': True,
|
||||||
|
'notify_contract_recipients': True,
|
||||||
|
'notification_emails': 'adresse_pas_valide',
|
||||||
|
})
|
||||||
|
self.assertFalse(form_invalid.is_valid())
|
||||||
|
self.assertIn('notification_emails', form_invalid.errors)
|
||||||
|
|
@ -3681,77 +3681,98 @@ def is_user_same_as_provider(user, intervention) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def get_contract_recipients(contract, intervention=None) -> List[str]:
|
def get_contract_recipients(contract, intervention=None) -> List[str]:
|
||||||
"""Retourne une liste unique d'adresses email associées au contrat."""
|
"""Retourne une liste unique d'adresses email associées au contrat et/ou au symptôme."""
|
||||||
|
|
||||||
# En dev/test, redirige tous les emails vers l'adresse de test
|
# En dev/test, redirige tous les emails vers l'adresse de test
|
||||||
email_override = getattr(settings, 'EMAIL_OVERRIDE', None)
|
email_override = getattr(settings, 'EMAIL_OVERRIDE', None)
|
||||||
if email_override:
|
if email_override:
|
||||||
return [email_override]
|
return [email_override]
|
||||||
|
|
||||||
if not contract:
|
if not contract and intervention:
|
||||||
|
contract = getattr(intervention, 'contract', None)
|
||||||
|
|
||||||
|
if contract and getattr(contract, 'is_blocked', False):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if getattr(contract, 'is_blocked', False):
|
symptom = getattr(intervention, 'symptom', None) if intervention else None
|
||||||
return []
|
notify_contract = getattr(symptom, 'notify_contract_recipients', True) if symptom else True
|
||||||
|
|
||||||
if hasattr(contract, 'send_email_notifications') and not contract.send_email_notifications:
|
|
||||||
return []
|
|
||||||
|
|
||||||
if getattr(contract, 'email_only_if_urgent', False) and intervention and getattr(intervention, 'priority', None) != '1':
|
|
||||||
return []
|
|
||||||
|
|
||||||
emails = set()
|
emails = set()
|
||||||
|
|
||||||
# 1) Champ texte sur le contrat (séparé par ; ou ,)
|
# 1) Destinataires du contrat (si notif contrat autorisée pour ce symptôme)
|
||||||
|
if notify_contract and contract:
|
||||||
|
contract_notifs_enabled = not (hasattr(contract, 'send_email_notifications') and not contract.send_email_notifications)
|
||||||
|
urgent_only_ok = not (getattr(contract, 'email_only_if_urgent', False) and intervention and getattr(intervention, 'priority', None) != '1')
|
||||||
|
|
||||||
|
if contract_notifs_enabled and urgent_only_ok:
|
||||||
|
# Champ texte sur le contrat (séparé par ; ou ,)
|
||||||
if hasattr(contract, 'notification_emails') and contract.notification_emails:
|
if hasattr(contract, 'notification_emails') and contract.notification_emails:
|
||||||
for e in re.split(r'[;,]', contract.notification_emails):
|
for e in re.split(r'[;,]', contract.notification_emails):
|
||||||
e = e.strip()
|
e = e.strip()
|
||||||
if e:
|
if e:
|
||||||
emails.add(e)
|
emails.add(e)
|
||||||
|
|
||||||
# 2) Relations de contacts (ex: contract.contacts)
|
# Relations de contacts (ex: contract.contacts)
|
||||||
if hasattr(contract, 'contacts'):
|
if hasattr(contract, 'contacts'):
|
||||||
qs = contract.contacts.all()
|
qs = contract.contacts.all()
|
||||||
# Filtre facultatif si vous avez un flag pour recevoir les rapports
|
|
||||||
if hasattr(qs.model, 'receive_intervention_reports'):
|
if hasattr(qs.model, 'receive_intervention_reports'):
|
||||||
qs = qs.filter(receive_intervention_reports=True)
|
qs = qs.filter(receive_intervention_reports=True)
|
||||||
emails.update([c.email for c in qs if getattr(c, 'email', None)])
|
emails.update([c.email for c in qs if getattr(c, 'email', None)])
|
||||||
|
|
||||||
|
# 2) Destinataires supplémentaires configurés sur le symptôme
|
||||||
|
if symptom:
|
||||||
|
for raw in [getattr(symptom, 'notification_emails', None), getattr(symptom, 'internal_notification_emails', None)]:
|
||||||
|
if raw:
|
||||||
|
for e in re.split(r'[;,]', raw):
|
||||||
|
e = e.strip()
|
||||||
|
if e:
|
||||||
|
emails.add(e)
|
||||||
|
|
||||||
return [e for e in emails if e]
|
return [e for e in emails if e]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_contract_cc_recipients(contract, intervention=None) -> List[str]:
|
def get_contract_cc_recipients(contract, intervention=None) -> List[str]:
|
||||||
"""Retourne une liste unique d'adresses email en copie (CC) associées au contrat."""
|
"""Retourne une liste unique d'adresses email en copie (CC) associées au contrat et/ou au symptôme."""
|
||||||
|
|
||||||
# En dev/test, ne pas utiliser de CC pour éviter les envois externes
|
# En dev/test, ne pas utiliser de CC pour éviter les envois externes
|
||||||
email_override = getattr(settings, 'EMAIL_OVERRIDE', None)
|
email_override = getattr(settings, 'EMAIL_OVERRIDE', None)
|
||||||
if email_override:
|
if email_override:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if not contract:
|
if not contract and intervention:
|
||||||
|
contract = getattr(intervention, 'contract', None)
|
||||||
|
|
||||||
|
if contract and getattr(contract, 'is_blocked', False):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if getattr(contract, 'is_blocked', False):
|
symptom = getattr(intervention, 'symptom', None) if intervention else None
|
||||||
return []
|
notify_contract = getattr(symptom, 'notify_contract_recipients', True) if symptom else True
|
||||||
|
|
||||||
if hasattr(contract, 'send_email_notifications') and not contract.send_email_notifications:
|
|
||||||
return []
|
|
||||||
|
|
||||||
if getattr(contract, 'email_only_if_urgent', False) and intervention and getattr(intervention, 'priority', None) != '1':
|
|
||||||
return []
|
|
||||||
|
|
||||||
emails = set()
|
emails = set()
|
||||||
|
|
||||||
# Champ texte CC sur le contrat (séparé par ; ou ,)
|
# 1) CC du contrat (si notif contrat autorisée pour ce symptôme)
|
||||||
|
if notify_contract and contract:
|
||||||
|
contract_notifs_enabled = not (hasattr(contract, 'send_email_notifications') and not contract.send_email_notifications)
|
||||||
|
urgent_only_ok = not (getattr(contract, 'email_only_if_urgent', False) and intervention and getattr(intervention, 'priority', None) != '1')
|
||||||
|
|
||||||
|
if contract_notifs_enabled and urgent_only_ok:
|
||||||
if hasattr(contract, 'cc_notification_emails') and contract.cc_notification_emails:
|
if hasattr(contract, 'cc_notification_emails') and contract.cc_notification_emails:
|
||||||
for e in re.split(r'[;,]', contract.cc_notification_emails):
|
for e in re.split(r'[;,]', contract.cc_notification_emails):
|
||||||
e = e.strip()
|
e = e.strip()
|
||||||
if e:
|
if e:
|
||||||
emails.add(e)
|
emails.add(e)
|
||||||
|
|
||||||
# Toujours ajouter l'email par défaut en CC
|
# 2) CC supplémentaires configurés sur le symptôme
|
||||||
|
if symptom and hasattr(symptom, 'cc_notification_emails') and symptom.cc_notification_emails:
|
||||||
|
for e in re.split(r'[;,]', symptom.cc_notification_emails):
|
||||||
|
e = e.strip()
|
||||||
|
if e:
|
||||||
|
emails.add(e)
|
||||||
|
|
||||||
|
# Toujours ajouter l'email par défaut en CC si des destinataires ou un contrat/symptôme existent
|
||||||
|
if contract or symptom:
|
||||||
emails.add(getattr(settings, 'EMAIL_TECH_SUPPORT', 'bm.perm.tech@sprb.brussels'))
|
emails.add(getattr(settings, 'EMAIL_TECH_SUPPORT', 'bm.perm.tech@sprb.brussels'))
|
||||||
|
|
||||||
return [e for e in emails if e]
|
return [e for e in emails if e]
|
||||||
|
|
|
||||||
|
|
@ -807,8 +807,8 @@ def attempt_status_update(request, intervention, user, new_status, pause_reason=
|
||||||
reverse("interventions:intervention_detail", args=[intervention.id])
|
reverse("interventions:intervention_detail", args=[intervention.id])
|
||||||
)
|
)
|
||||||
|
|
||||||
recipients = get_contract_recipients(contract, intervention) if contract else []
|
recipients = get_contract_recipients(contract, intervention)
|
||||||
cc_recipients = get_contract_cc_recipients(contract, intervention) if contract else [getattr(settings, 'EMAIL_TECH_SUPPORT', 'bm.perm.tech@sprb.brussels')]
|
cc_recipients = get_contract_cc_recipients(contract, intervention)
|
||||||
|
|
||||||
# Si le contrat utilise l'envoi groupé journalier, ne pas envoyer l'email individuel
|
# Si le contrat utilise l'envoi groupé journalier, ne pas envoyer l'email individuel
|
||||||
if recipients and not (contract and getattr(contract, 'send_daily_summary', False)):
|
if recipients and not (contract and getattr(contract, 'send_daily_summary', False)):
|
||||||
|
|
@ -1000,8 +1000,8 @@ def attempt_status_update(request, intervention, user, new_status, pause_reason=
|
||||||
intervention.save(update_fields=['intervention_sheet'])
|
intervention.save(update_fields=['intervention_sheet'])
|
||||||
|
|
||||||
contract = getattr(intervention, 'contract', None)
|
contract = getattr(intervention, 'contract', None)
|
||||||
recipients = get_contract_recipients(contract, intervention) if contract else []
|
recipients = get_contract_recipients(contract, intervention)
|
||||||
cc_recipients = get_contract_cc_recipients(contract, intervention) if contract else [getattr(settings, 'EMAIL_TECH_SUPPORT', 'bm.perm.tech@sprb.brussels')]
|
cc_recipients = get_contract_cc_recipients(contract, intervention)
|
||||||
|
|
||||||
if recipients:
|
if recipients:
|
||||||
detail_url = request.build_absolute_uri(
|
detail_url = request.build_absolute_uri(
|
||||||
|
|
@ -1241,7 +1241,7 @@ def send_intervention_reminder(request, intervention_id):
|
||||||
if not recipients:
|
if not recipients:
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': _("Aucun destinataire n'a été configuré pour le contrat associé."),
|
'error': _("Aucun destinataire n'a été configuré pour le contrat ou le symptôme associé."),
|
||||||
}, status=400)
|
}, status=400)
|
||||||
|
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
|
|
@ -1381,7 +1381,7 @@ def send_intervention_modification(request, intervention_id):
|
||||||
if not recipients:
|
if not recipients:
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': _("Aucun destinataire n'a été configuré pour le contrat associé."),
|
'error': _("Aucun destinataire n'a été configuré pour le contrat ou le symptôme associé."),
|
||||||
}, status=400)
|
}, status=400)
|
||||||
|
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue