ajout limite, ModeleCourriel.__unicode__
[auf_django_mailing.git] / auf / django / mailing / models.py
1 # -*- encoding: utf-8 -*-
2 """
3 =================
4 Module de mailing
5 =================
6
7 Fonctionalité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
13 qui n'avaient pas été envoyés, pour cause d'interruption du processus, ou
14 d'erreur. Les destinataires dont l'adresse aurait été modifiée sont également
15 inclus automatiquement dans un réenvoi.
16
17 Fonctionnement :
18 ----------------
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
21 pour envoyer un courriel : elles ont besoin d'un `ModeleCourriel`.
22 * Pour utiliser cette application, il faut définir son propre modèle pour pouvoir
23 personnaliser le paramétrage des enveloppes, c'est-à-dire leur fournir l'adresse
24 du destinataire et un contexte pour le rendu du corps du message. Cette classe
25 doit :
26 - comporter deux méthodes, `get_contexte_corps()` et `get_adresse()`
27 - comporter une ForeignKey vers le modèle `Enveloppe`, avec unique=True
28 - elle doit être déclarée dans les settings dans le paramètre
29 `MAILING_MODELE_PARAMS_ENVELOPPE` sous le format 'nom_application.nom_modele'
30 * L'envoi est temporisé, d'un nombre de secondes indiqué dans le paramètre
31 `MAILING_TEMPORISATION`. Défaut: 2 secondes
32
33 """
34 import random
35 import smtplib
36 import string
37 import time
38 from django.core.exceptions import ImproperlyConfigured
39 from django.core.mail.message import EmailMessage
40 from django.core.urlresolvers import reverse
41 from django.db import models, transaction
42 from django.db.models.fields import CharField, TextField, BooleanField, DateTimeField
43 from django.db.models.fields.related import ForeignKey
44 import datetime
45 from django.template.base import Template
46 from django.template.context import Context
47 from django.conf import settings
48
49 class ModeleCourriel(models.Model):
50 """
51 Représente un modèle de courriel. Le corps sera interprété comme un template
52 django
53 """
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")
58
59 def __unicode__(self):
60 return self.code + u" / " + self.sujet
61
62
63 TAILLE_JETON = 32
64
65 def generer_jeton(taille=TAILLE_JETON):
66 return ''.join(random.choice(string.letters + string.digits)\
67 for i in xrange(TAILLE_JETON))
68
69
70 class EnveloppeParametersNotAvailable(Exception):
71 pass
72
73
74 class Enveloppe(models.Model):
75 """
76 Représente un envoi à faire, avec toutes les informations nécessaires.
77 """
78 modele = ForeignKey(ModeleCourriel)
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
84 (cf. `django.contrib.auth.User.get_profile`) et permet à chaque site
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
90 ForeignKey vers `Enveloppe`, avec unique=True
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()
115 return context
116
117 def get_adresse(self):
118 return self.get_params().get_adresse()
119
120 class 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
127 def 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 """
145 modele = ModeleCourriel.objects.get(code=code_modele)
146 enveloppes = Enveloppe.objects.filter(modele=modele)
147 temporisation = getattr(settings, 'MAILING_TEMPORISATION', 2)
148 counter = 0
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,
155 adresse=adresse_envoi)
156 if retry_errors:
157 entree_log = entree_log.filter(erreur__isnull=True)
158
159 if entree_log.count() > 0:
160 continue
161
162 modele_corps = Template(enveloppe.modele.corps)
163 contexte_corps = enveloppe.get_corps_context()
164
165 if site and url_name and 'jeton' in contexte_corps:
166 url = 'http://%s%s' % (site.domain,
167 reverse(url_name,
168 kwargs={'jeton': contexte_corps['jeton']}))
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
181 # En PROD, supprimer EMAIL_BACKEND (ce qui fera retomber sur le défaut
182 # qui est d'envoyer par SMTP). Même chose en TEST, mais attention
183 # car les adresses qui sont dans la base seront utilisées:
184 # modifier les données pour y mettre des adresses de test plutôt que
185 # les vraies
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()
191 counter += 1
192 time.sleep(temporisation)
193 except smtplib.SMTPException as e:
194 entree_log.erreur = e.__str__()
195 entree_log.save()
196 transaction.commit()
197 if limit and counter >= limit:
198 break
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