Code

Ticket #10863: 10863-2-html-error-email.diff

File 10863-2-html-error-email.diff, 10.4 KB (added by brodie, 3 years ago)
Line 
1# HG changeset patch
2# User Brodie Rao <brodie@bitheap.org>
3# Date 1291452498 -39600
4# Node ID 876fd3c1a04fb41f27fc552f0171a6d5573caa47
5# Parent  e08464eca04c6a8ce8c09dc029e8b287a5291903
6Added support for sending Django error emails in HTML using the debug exception template
7
8diff --git a/django/core/mail/__init__.py b/django/core/mail/__init__.py
9--- a/django/core/mail/__init__.py
10+++ b/django/core/mail/__init__.py
11@@ -83,22 +83,30 @@ def send_mass_mail(datatuple, fail_silen
12     return connection.send_messages(messages)
13 
14 
15-def mail_admins(subject, message, fail_silently=False, connection=None):
16+def mail_admins(subject, message, fail_silently=False, connection=None,
17+                html_message=None):
18     """Sends a message to the admins, as defined by the ADMINS setting."""
19     if not settings.ADMINS:
20         return
21-    EmailMessage(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject), message,
22-                 settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS],
23-                 connection=connection).send(fail_silently=fail_silently)
24+    mail = EmailMultiAlternatives(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject),
25+                message, settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS],
26+                connection=connection)
27+    if html_message:
28+        mail.attach_alternative(html_message, 'text/html')
29+    mail.send(fail_silently=fail_silently)
30 
31 
32-def mail_managers(subject, message, fail_silently=False, connection=None):
33+def mail_managers(subject, message, fail_silently=False, connection=None,
34+                  html_message=None):
35     """Sends a message to the managers, as defined by the MANAGERS setting."""
36     if not settings.MANAGERS:
37         return
38-    EmailMessage(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject), message,
39-                 settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS],
40-                 connection=connection).send(fail_silently=fail_silently)
41+    mail = EmailMultiAlternatives(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject),
42+                message, settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS],
43+                connection=connection)
44+    if html_message:
45+        mail.attach_alternative(html_message, 'text/html')
46+    mail.send(fail_silently=fail_silently)
47 
48 
49 class SMTPConnection(_SMTPConnection):
50diff --git a/django/utils/log.py b/django/utils/log.py
51--- a/django/utils/log.py
52+++ b/django/utils/log.py
53@@ -56,6 +56,7 @@ class AdminEmailHandler(logging.Handler)
54     def emit(self, record):
55         import traceback
56         from django.conf import settings
57+        from django.views.debug import ExceptionReporter
58 
59         try:
60             if sys.version_info < (2,5):
61@@ -75,12 +76,18 @@ class AdminEmailHandler(logging.Handler)
62             request_repr = repr(request)
63         except:
64             subject = 'Error: Unknown URL'
65+            request = None
66             request_repr = "Request repr() unavailable"
67 
68         if record.exc_info:
69+            exc_info = record.exc_info
70             stack_trace = '\n'.join(traceback.format_exception(*record.exc_info))
71         else:
72+            exc_info = ()
73             stack_trace = 'No stack trace available'
74 
75         message = "%s\n\n%s" % (stack_trace, request_repr)
76-        mail.mail_admins(subject, message, fail_silently=True)
77+        reporter = ExceptionReporter(request, *exc_info, is_email=True)
78+        html_message = reporter.get_traceback_html()
79+        mail.mail_admins(subject, message, fail_silently=True,
80+                         html_message=html_message)
81diff --git a/django/views/debug.py b/django/views/debug.py
82--- a/django/views/debug.py
83+++ b/django/views/debug.py
84@@ -62,11 +62,12 @@ class ExceptionReporter:
85     """
86     A class to organize and coordinate reporting on exceptions.
87     """
88-    def __init__(self, request, exc_type, exc_value, tb):
89+    def __init__(self, request, exc_type, exc_value, tb, is_email=False):
90         self.request = request
91         self.exc_type = exc_type
92         self.exc_value = exc_value
93         self.tb = tb
94+        self.is_email = is_email
95 
96         self.template_info = None
97         self.template_does_not_exist = False
98@@ -118,6 +119,7 @@ class ExceptionReporter:
99         from django import get_version
100         t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template')
101         c = Context({
102+            'is_email': self.is_email,
103             'exception_type': self.exc_type.__name__,
104             'exception_value': smart_unicode(self.exc_value, errors='replace'),
105             'unicode_hint': unicode_hint,
106@@ -353,6 +355,7 @@ TECHNICAL_500_TEMPLATE = """
107     span.commands a:link {color:#5E5694;}
108     pre.exception_value { font-family: sans-serif; color: #666; font-size: 1.5em; margin: 10px 0 10px 0; }
109   </style>
110+  {% if not is_email %}
111   <script type="text/javascript">
112   //<!--
113     function getElementsByClassName(oElm, strTagName, strClassName){
114@@ -408,10 +411,11 @@ TECHNICAL_500_TEMPLATE = """
115     }
116     //-->
117   </script>
118+  {% endif %}
119 </head>
120 <body>
121 <div id="summary">
122-  <h1>{{ exception_type }} at {{ request.path_info|escape }}</h1>
123+  <h1>{{ exception_type }}{% if request %} at {{ request.path_info|escape }}{% endif %}</h1>
124   <pre class="exception_value">{{ exception_value|force_escape }}</pre>
125   <table class="meta">
126     <tr>
127@@ -498,7 +502,7 @@ TECHNICAL_500_TEMPLATE = """
128 </div>
129 {% endif %}
130 <div id="traceback">
131-  <h2>Traceback <span class="commands"><a href="#" onclick="return switchPastebinFriendly(this);">Switch to copy-and-paste view</a></span></h2>
132+  <h2>Traceback <span class="commands">{% if not is_email %}<a href="#" onclick="return switchPastebinFriendly(this);">Switch to copy-and-paste view</a></span>{% endif %}</h2>
133   {% autoescape off %}
134   <div id="browserTraceback">
135     <ul class="traceback">
136@@ -545,16 +549,19 @@ TECHNICAL_500_TEMPLATE = """
137   </div>
138   {% endautoescape %}
139   <form action="http://dpaste.com/" name="pasteform" id="pasteform" method="post">
140+{% if not is_email %}
141   <div id="pastebinTraceback" class="pastebin">
142     <input type="hidden" name="language" value="PythonConsole">
143-    <input type="hidden" name="title" value="{{ exception_type|escape }} at {{ request.path_info|escape }}">
144+    <input type="hidden" name="title" value="{{ exception_type|escape }}{% if request %} at {{ request.path_info|escape }}{% endif %}">
145     <input type="hidden" name="source" value="Django Dpaste Agent">
146     <input type="hidden" name="poster" value="Django">
147     <textarea name="content" id="traceback_area" cols="140" rows="25">
148 Environment:
149 
150+{% if request %}
151 Request Method: {{ request.META.REQUEST_METHOD }}
152 Request URL: {{ request.build_absolute_uri|escape }}
153+{% endif %}
154 Django Version: {{ django_version_info }}
155 Python Version: {{ sys_version_info }}
156 Installed Applications:
157@@ -581,7 +588,7 @@ Traceback:
158 {% for frame in frames %}File "{{ frame.filename|escape }}" in {{ frame.function|escape }}
159 {% if frame.context_line %}  {{ frame.lineno }}. {{ frame.context_line|escape }}{% endif %}
160 {% endfor %}
161-Exception Type: {{ exception_type|escape }} at {{ request.path_info|escape }}
162+Exception Type: {{ exception_type|escape }}{% if request %} at {{ request.path_info|escape }}{% endif %}
163 Exception Value: {{ exception_value|force_escape }}
164 </textarea>
165   <br><br>
166@@ -589,10 +596,12 @@ Exception Value: {{ exception_value|forc
167   </div>
168 </form>
169 </div>
170+{% endif %}
171 
172 <div id="requestinfo">
173   <h2>Request information</h2>
174 
175+{% if request %}
176   <h3 id="get-info">GET</h3>
177   {% if request.GET %}
178     <table class="req">
179@@ -698,6 +707,7 @@ Exception Value: {{ exception_value|forc
180       {% endfor %}
181     </tbody>
182   </table>
183+{% endif %}
184 
185   <h3 id="settings-info">Settings</h3>
186   <h4>Using settings module <code>{{ settings.SETTINGS_MODULE }}</code></h4>
187diff --git a/docs/topics/email.txt b/docs/topics/email.txt
188--- a/docs/topics/email.txt
189+++ b/docs/topics/email.txt
190@@ -109,7 +109,7 @@ a single connection for all of its messa
191 mail_admins()
192 =============
193 
194-.. function:: mail_admins(subject, message, fail_silently=False, connection=None)
195+.. function:: mail_admins(subject, message, fail_silently=False, connection=None, html_message=None)
196 
197 ``django.core.mail.mail_admins()`` is a shortcut for sending an e-mail to the
198 site admins, as defined in the :setting:`ADMINS` setting.
199@@ -122,10 +122,16 @@ The "From:" header of the e-mail will be
200 
201 This method exists for convenience and readability.
202 
203+.. versionchanged:: 1.3
204+
205+If ``html_message`` is provided, the resulting e-mail will be a
206+multipart/alternative e-mail with ``message`` as the "text/plain"
207+content type and ``html_message`` as the "text/html" content type.
208+
209 mail_managers()
210 ===============
211 
212-.. function:: mail_managers(subject, message, fail_silently=False, connection=None)
213+.. function:: mail_managers(subject, message, fail_silently=False, connection=None, html_message=None)
214 
215 ``django.core.mail.mail_managers()`` is just like ``mail_admins()``, except it
216 sends an e-mail to the site managers, as defined in the :setting:`MANAGERS`
217diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py
218--- a/tests/regressiontests/mail/tests.py
219+++ b/tests/regressiontests/mail/tests.py
220@@ -364,6 +364,31 @@ class MailTests(TestCase):
221         settings.ADMINS = old_admins
222         settings.MANAGERS = old_managers
223 
224+    def test_html_mail(self):
225+        """Test html_message argument to mail_admins and mail_managers"""
226+        old_admins = settings.ADMINS
227+        old_managers = settings.MANAGERS
228+        settings.ADMINS = settings.MANAGERS = [('nobody','nobody@example.com')]
229+
230+        mail.outbox = []
231+        mail_managers('Subject', 'Content', html_message='HTML Content')
232+        self.assertEqual(len(mail.outbox), 1)
233+        message = mail.outbox[0]
234+        self.assertEqual(message.subject, '[Django] Subject')
235+        self.assertEqual(message.body, 'Content')
236+        self.assertEqual(message.alternatives, [('HTML Content', 'text/html')])
237+
238+        mail.outbox = []
239+        mail_admins('Subject', 'Content', html_message='HTML Content')
240+        self.assertEqual(len(mail.outbox), 1)
241+        message = mail.outbox[0]
242+        self.assertEqual(message.subject, '[Django] Subject')
243+        self.assertEqual(message.body, 'Content')
244+        self.assertEqual(message.alternatives, [('HTML Content', 'text/html')])
245+
246+        settings.ADMINS = old_admins
247+        settings.MANAGERS = old_managers
248+
249     def test_idn_validation(self):
250         """Test internationalized email adresses"""
251         # Regression for #14301.