0.2
[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`. Un identifiant
22 unique est automatiquement généré, dans le champ `jeton`.
23 * Pour utiliser cette application, il faut définir son propre modèle pour pouvoir
24 personnaliser le paramétrage des enveloppes, c'est-à-dire leur fournir l'adresse
25 du destinataire et un contexte pour le rendu du corps du message. Cette classe
26 doit :
27 - comporter deux méthodes, `get_contexte_corps()` et `get_adresse()`
28 - comporter une ForeignKey vers le modèle `Enveloppe`, avec unique=True
29 - elle doit être déclarée dans les settings dans le paramètre
30 `MAILING_MODELE_PARAMS_ENVELOPPE` sous le format 'nom_application.nom_modele'
31 * L'envoi est temporisé, d'un nombre de secondes indiqué dans le paramètre
32 `MAILING_TEMPORISATION`. Défaut: 2 secondes
33
34 """
35 import random
36 import smtplib
37 import string
38 import time
39 from django.core.exceptions import ImproperlyConfigured
40 from django.core.mail.message import EmailMessage
41 from django.core.urlresolvers import reverse
42 from django.db import models, transaction
43 from django.db.models.fields import CharField, TextField, BooleanField, DateTimeField
44 from django.db.models.fields.related import ForeignKey
45 import datetime
46 from django.template.base import Template
47 from django.template.context import Context
48 from django.conf import settings
49
50 class ModeleCourriel(models.Model):
51 """
52 Représente un modèle de courriel. Le corps sera interprété comme un template
53 django
54 """
55 code = CharField(max_length=8, unique=True)
56 sujet = CharField(max_length=256)
57 corps = TextField()
58 html = BooleanField(verbose_name=u"Le corps est au format HTML")
59
60
61 TAILLE_JETON = 32
62
63
64 class EnveloppeParametersNotAvailable(Exception):
65 pass
66
67
68 class Enveloppe(models.Model):
69 """
70 Représente un envoi à faire, avec toutes les informations nécessaires.
71 """
72 modele = ForeignKey(ModeleCourriel)
73 jeton = CharField(max_length=128, unique=True)
74
75 def save(self, *args, **kwargs):
76 if not self.jeton:
77 self.jeton = ''.join(random.choice(string.letters + string.digits)\
78 for i in xrange(TAILLE_JETON))
79
80 super(Enveloppe, self).save(*args, **kwargs)
81
82 def get_params(self):
83 """
84 Retourne les paramètres associés à cette enveloppe.
85 Le mécanisme est copié sur celui des profils utilisateurs
86 (cf. `django.contrib.auth.User.get_profile`) et permet à chaque site
87 de définir l'adresse d'envoi et le contexte de rendu du template
88 selon ses propres besoins.
89
90 On s'attend à ce que la classe soit indiquée au format 'application.model'
91 dans le setting MODELE_PARAMS_ENVELOPPE, et que cette classe ait une
92 ForeignKey vers `Enveloppe`, avec unique=True
93
94 Voir aussi l'article de James Bennett à l'adresse :
95 http://www.b-list.org/weblog/2006/jun/06/django-tips-extending-user-model/
96 """
97 if not hasattr(self, '_params_cache'):
98 if not getattr(settings, 'MAILING_MODELE_PARAMS_ENVELOPPE', False):
99 raise EnveloppeParametersNotAvailable()
100 try:
101 app_label, model_name = settings.MAILING_MODELE_PARAMS_ENVELOPPE.split('.')
102 except ValueError:
103 raise EnveloppeParametersNotAvailable()
104 try:
105 model = models.get_model(app_label, model_name)
106 if model is None:
107 raise EnveloppeParametersNotAvailable()
108 self._params_cache = model._default_manager.using(
109 self._state.db).get(enveloppe__id__exact=self.id)
110 self._params_cache.user = self
111 except (ImportError, ImproperlyConfigured):
112 raise EnveloppeParametersNotAvailable()
113 return self._params_cache
114
115 def get_corps_context(self):
116 context = self.get_params().get_corps_context()
117 context['jeton'] = self.jeton
118 return context
119
120 def get_adresse(self):
121 return self.get_params().get_adresse()
122
123 class EntreeLog(models.Model):
124 enveloppe = ForeignKey(Enveloppe)
125 adresse = CharField(max_length=256)
126 date_heure_envoi = DateTimeField(default=datetime.datetime.now)
127 erreur = TextField(null=True)
128
129 @transaction.commit_manually
130 def envoyer(code_modele, adresse_expediteur, site=None, url_name=None):
131 modele = ModeleCourriel.objects.get(code=code_modele)
132
133 enveloppes = Enveloppe.objects.filter(modele=modele)
134
135 temporisation = getattr(settings, 'MAILING_TEMPORISATION', 2)
136
137 try:
138 for enveloppe in enveloppes:
139 # on vérifie qu'on n'a pas déjà envoyé ce courriel à
140 # cet établissement et à cette adresse
141 adresse_envoi = enveloppe.get_adresse()
142 entree_log = EntreeLog.objects.filter(enveloppe=enveloppe,
143 erreur__isnull=True, adresse=adresse_envoi)
144 if entree_log.count() > 0:
145 continue
146
147 modele_corps = Template(enveloppe.modele.corps)
148 contexte_corps = enveloppe.get_corps_context()
149
150 if site and url_name:
151 url = 'http://%s%s' % (site.domain,
152 reverse(url_name, kwargs={'jeton': enveloppe.jeton}))
153 contexte_corps['url'] = url
154
155 corps = modele_corps.render(Context(contexte_corps))
156 message = EmailMessage(enveloppe.modele.sujet,
157 corps,
158 adresse_expediteur, # adresse de retour
159 [adresse_envoi], # adresse du destinataire
160 headers={'precedence' : 'bulk'} # selon les conseils de google
161 )
162 try:
163 # Attention en DEV, devrait simplement écrire le courriel
164 # dans la console, cf. paramètre EMAIL_BACKEND dans conf.py
165 # En PROD, supprimer EMAIL_BACKEND (ce qui fera retomber sur le défaut
166 # qui est d'envoyer par SMTP). Même chose en TEST, mais attention
167 # car les adresses qui sont dans la base seront utilisées:
168 # modifier les données pour y mettre des adresses de test plutôt que
169 # les vraies
170 message.content_subtype = "html" if enveloppe.modele.html else "text"
171 entree_log = EntreeLog()
172 entree_log.enveloppe = enveloppe
173 entree_log.adresse = adresse_envoi
174 message.send()
175 time.sleep(temporisation)
176 except smtplib.SMTPException as e:
177 entree_log.erreur = e.__str__()
178 entree_log.save()
179 transaction.commit()
180 except:
181 transaction.rollback()
182 raise
183
184 transaction.commit() # nécessaire dans le cas où rien n'est envoyé, à cause du décorateur commit_manually
185