Version 0.5
[auf_django_mailing.git] / auf / django / mailing / models.py
CommitLineData
dfc0f84e 1# -*- encoding: utf-8 -*-
2"""
3=================
4Module de mailing
5=================
6
7Fonctionalités :
8----------------
9* Envoi de courriels à une liste de destinataires
10* Le corps du courriel est rendu comme template django
11* Les envois sont loggés, avec les éventuelles erreurs
12* La répétition de l'envoi à une liste ne renvoie que les courriels de la liste
13qui n'avaient pas été envoyés, pour cause d'interruption du processus, ou
14d'erreur. Les destinataires dont l'adresse aurait été modifiée sont également
15inclus automatiquement dans un réenvoi.
16
17Fonctionnement :
18----------------
3a32f41f 19* Définir un modèle de courriel, à l'aide du modèle `ModeleCourriel`
20* Les instances du modèle `Enveloppe` représentent toutes les informations
4f195036 21pour envoyer un courriel : elles ont besoin d'un `ModeleCourriel`.
dfc0f84e 22* Pour utiliser cette application, il faut définir son propre modèle pour pouvoir
23personnaliser le paramétrage des enveloppes, c'est-à-dire leur fournir l'adresse
24du destinataire et un contexte pour le rendu du corps du message. Cette classe
25doit :
3a32f41f 26 - comporter deux méthodes, `get_contexte_corps()` et `get_adresse()`
27 - comporter une ForeignKey vers le modèle `Enveloppe`, avec unique=True
dfc0f84e 28 - elle doit être déclarée dans les settings dans le paramètre
3a32f41f 29 `MAILING_MODELE_PARAMS_ENVELOPPE` sous le format 'nom_application.nom_modele'
dfc0f84e 30* L'envoi est temporisé, d'un nombre de secondes indiqué dans le paramètre
3a32f41f 31`MAILING_TEMPORISATION`. Défaut: 2 secondes
dfc0f84e 32
33"""
34import random
35import smtplib
36import string
37import time
38from django.core.exceptions import ImproperlyConfigured
39from django.core.mail.message import EmailMessage
40from django.core.urlresolvers import reverse
41from django.db import models, transaction
42from django.db.models.fields import CharField, TextField, BooleanField, DateTimeField
43from django.db.models.fields.related import ForeignKey
44import datetime
45from django.template.base import Template
46from django.template.context import Context
47from django.conf import settings
48
49class ModeleCourriel(models.Model):
3a32f41f 50 """
51 Représente un modèle de courriel. Le corps sera interprété comme un template
52 django
53 """
dfc0f84e 54 code = CharField(max_length=8, unique=True)
55 sujet = CharField(max_length=256)
56 corps = TextField()
57 html = BooleanField(verbose_name=u"Le corps est au format HTML")
8a14c16f
E
58
59 def __unicode__(self):
60 return self.code + u" / " + self.sujet
dfc0f84e 61
62
63TAILLE_JETON = 32
64
4f195036 65def generer_jeton(taille=TAILLE_JETON):
66 return ''.join(random.choice(string.letters + string.digits)\
67 for i in xrange(TAILLE_JETON))
68
dfc0f84e 69
70class EnveloppeParametersNotAvailable(Exception):
71 pass
72
73
74class Enveloppe(models.Model):
75 """
76 Représente un envoi à faire, avec toutes les informations nécessaires.
77 """
78 modele = ForeignKey(ModeleCourriel)
dfc0f84e 79
80 def get_params(self):
81 """
82 Retourne les paramètres associés à cette enveloppe.
83 Le mécanisme est copié sur celui des profils utilisateurs
3a32f41f 84 (cf. `django.contrib.auth.User.get_profile`) et permet à chaque site
dfc0f84e 85 de définir l'adresse d'envoi et le contexte de rendu du template
86 selon ses propres besoins.
87
88 On s'attend à ce que la classe soit indiquée au format 'application.model'
89 dans le setting MODELE_PARAMS_ENVELOPPE, et que cette classe ait une
3a32f41f 90 ForeignKey vers `Enveloppe`, avec unique=True
dfc0f84e 91
92 Voir aussi l'article de James Bennett à l'adresse :
93 http://www.b-list.org/weblog/2006/jun/06/django-tips-extending-user-model/
94 """
95 if not hasattr(self, '_params_cache'):
96 if not getattr(settings, 'MAILING_MODELE_PARAMS_ENVELOPPE', False):
97 raise EnveloppeParametersNotAvailable()
98 try:
99 app_label, model_name = settings.MAILING_MODELE_PARAMS_ENVELOPPE.split('.')
100 except ValueError:
101 raise EnveloppeParametersNotAvailable()
102 try:
103 model = models.get_model(app_label, model_name)
104 if model is None:
105 raise EnveloppeParametersNotAvailable()
106 self._params_cache = model._default_manager.using(
107 self._state.db).get(enveloppe__id__exact=self.id)
108 self._params_cache.user = self
109 except (ImportError, ImproperlyConfigured):
110 raise EnveloppeParametersNotAvailable()
111 return self._params_cache
112
113 def get_corps_context(self):
114 context = self.get_params().get_corps_context()
dfc0f84e 115 return context
116
117 def get_adresse(self):
118 return self.get_params().get_adresse()
119
120class EntreeLog(models.Model):
121 enveloppe = ForeignKey(Enveloppe)
122 adresse = CharField(max_length=256)
123 date_heure_envoi = DateTimeField(default=datetime.datetime.now)
124 erreur = TextField(null=True)
125
126@transaction.commit_manually
8a14c16f
E
127def envoyer(code_modele, adresse_expediteur, site=None, url_name=None,
128 limit=None, retry_errors=True):
129 u"""
130 Cette fonction procède à l'envoi proprement dit, pour toutes les enveloppes
131 du modele ayant pour code :code_modele. Si ``site``, ``url_name`` sont spécifiés
132 et que les enveloppes passent un paramètre ``jeton`` dans leur contexte,
133 une url sera générée et passée comme variable au template.
134 :param code_modele: le code du modèle pour lequel faire l'envoi
135 :param adresse_expediteur:
136 :param site: une instance de django.contrib.sites (pour la génération de l'URL)
137 :param url_name: le nom de l'URL à générer
138 :param limit: indique un nombre maximal de courriels à envoyer pour cet appel
139 :param retry_errors: les envois en erreur doivent-ils être retentés ou non ?
140
141 .. warning:: L'utilisation conjointe d'une limite (paramètre ``limit``) et
142 de ``retry_errors`` pourrait faire en sorte que certains courriels ne soient
143 jamais envoyés (si il y a plus de courriels en erreur que ``limit``)
144 """
dfc0f84e 145 modele = ModeleCourriel.objects.get(code=code_modele)
dfc0f84e 146 enveloppes = Enveloppe.objects.filter(modele=modele)
dfc0f84e 147 temporisation = getattr(settings, 'MAILING_TEMPORISATION', 2)
8a14c16f 148 counter = 0
dfc0f84e 149 try:
150 for enveloppe in enveloppes:
151 # on vérifie qu'on n'a pas déjà envoyé ce courriel à
152 # cet établissement et à cette adresse
153 adresse_envoi = enveloppe.get_adresse()
154 entree_log = EntreeLog.objects.filter(enveloppe=enveloppe,
8a14c16f
E
155 adresse=adresse_envoi)
156 if retry_errors:
157 entree_log = entree_log.filter(erreur__isnull=True)
158
dfc0f84e 159 if entree_log.count() > 0:
160 continue
161
162 modele_corps = Template(enveloppe.modele.corps)
163 contexte_corps = enveloppe.get_corps_context()
164
4f195036 165 if site and url_name and 'jeton' in contexte_corps:
dfc0f84e 166 url = 'http://%s%s' % (site.domain,
4f195036 167 reverse(url_name,
168 kwargs={'jeton': contexte_corps['jeton']}))
dfc0f84e 169 contexte_corps['url'] = url
170
171 corps = modele_corps.render(Context(contexte_corps))
172 message = EmailMessage(enveloppe.modele.sujet,
173 corps,
174 adresse_expediteur, # adresse de retour
175 [adresse_envoi], # adresse du destinataire
176 headers={'precedence' : 'bulk'} # selon les conseils de google
177 )
178 try:
179 # Attention en DEV, devrait simplement écrire le courriel
180 # dans la console, cf. paramètre EMAIL_BACKEND dans conf.py
8b0c1ee1
E
181 # En PROD, supprimer EMAIL_BACKEND (ce qui fera retomber sur
182 # le défaut qui est d'envoyer par SMTP). Même chose en TEST,
183 # mais attention car les adresses qui sont dans la base
184 # seront utilisées: modifier les données pour y mettre des
185 # adresses de test plutôt que les vraies
dfc0f84e 186 message.content_subtype = "html" if enveloppe.modele.html else "text"
187 entree_log = EntreeLog()
188 entree_log.enveloppe = enveloppe
189 entree_log.adresse = adresse_envoi
190 message.send()
8a14c16f 191 counter += 1
dfc0f84e 192 time.sleep(temporisation)
8b0c1ee1 193 except (smtplib.socket.error, smtplib.SMTPException) as e:
dfc0f84e 194 entree_log.erreur = e.__str__()
195 entree_log.save()
196 transaction.commit()
8a14c16f
E
197 if limit and counter >= limit:
198 break
dfc0f84e 199 except:
200 transaction.rollback()
201 raise
202
203 transaction.commit() # nécessaire dans le cas où rien n'est envoyé, à cause du décorateur commit_manually
204