0.2
[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
21pour envoyer un courriel : elles ont besoin d'un `ModeleCourriel`. Un identifiant
22unique est automatiquement généré, dans le champ `jeton`.
dfc0f84e 23* Pour utiliser cette application, il faut définir son propre modèle pour pouvoir
24personnaliser le paramétrage des enveloppes, c'est-à-dire leur fournir l'adresse
25du destinataire et un contexte pour le rendu du corps du message. Cette classe
26doit :
3a32f41f 27 - comporter deux méthodes, `get_contexte_corps()` et `get_adresse()`
28 - comporter une ForeignKey vers le modèle `Enveloppe`, avec unique=True
dfc0f84e 29 - elle doit être déclarée dans les settings dans le paramètre
3a32f41f 30 `MAILING_MODELE_PARAMS_ENVELOPPE` sous le format 'nom_application.nom_modele'
dfc0f84e 31* L'envoi est temporisé, d'un nombre de secondes indiqué dans le paramètre
3a32f41f 32`MAILING_TEMPORISATION`. Défaut: 2 secondes
dfc0f84e 33
34"""
35import random
36import smtplib
37import string
38import time
39from django.core.exceptions import ImproperlyConfigured
40from django.core.mail.message import EmailMessage
41from django.core.urlresolvers import reverse
42from django.db import models, transaction
43from django.db.models.fields import CharField, TextField, BooleanField, DateTimeField
44from django.db.models.fields.related import ForeignKey
45import datetime
46from django.template.base import Template
47from django.template.context import Context
48from django.conf import settings
49
50class ModeleCourriel(models.Model):
3a32f41f 51 """
52 Représente un modèle de courriel. Le corps sera interprété comme un template
53 django
54 """
dfc0f84e 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
61TAILLE_JETON = 32
62
63
64class EnveloppeParametersNotAvailable(Exception):
65 pass
66
67
68class 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
3a32f41f 86 (cf. `django.contrib.auth.User.get_profile`) et permet à chaque site
dfc0f84e 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
3a32f41f 92 ForeignKey vers `Enveloppe`, avec unique=True
dfc0f84e 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
123class 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
130def 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