Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Fixed #10863 -- Added HTML support to mail_managers() and mail_admins…

…(), and used this to provide more and prettier detail in error emails. Thanks to boxed for the suggestion, and to Rob Hudson and Brodie Rao for their work on the patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14844 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 29c4a578af58f6da7c77830a0ff99260f2338d36 1 parent b407de3
@freakboy3742 freakboy3742 authored
View
24 django/core/mail/__init__.py
@@ -83,22 +83,30 @@ def send_mass_mail(datatuple, fail_silently=False, auth_user=None,
return connection.send_messages(messages)
-def mail_admins(subject, message, fail_silently=False, connection=None):
+def mail_admins(subject, message, fail_silently=False, connection=None,
+ html_message=None):
"""Sends a message to the admins, as defined by the ADMINS setting."""
if not settings.ADMINS:
return
- EmailMessage(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject), message,
- settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS],
- connection=connection).send(fail_silently=fail_silently)
+ mail = EmailMultiAlternatives(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject),
+ message, settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS],
+ connection=connection)
+ if html_message:
+ mail.attach_alternative(html_message, 'text/html')
+ mail.send(fail_silently=fail_silently)
-def mail_managers(subject, message, fail_silently=False, connection=None):
+def mail_managers(subject, message, fail_silently=False, connection=None,
+ html_message=None):
"""Sends a message to the managers, as defined by the MANAGERS setting."""
if not settings.MANAGERS:
return
- EmailMessage(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject), message,
- settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS],
- connection=connection).send(fail_silently=fail_silently)
+ mail = EmailMultiAlternatives(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject),
+ message, settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS],
+ connection=connection)
+ if html_message:
+ mail.attach_alternative(html_message, 'text/html')
+ mail.send(fail_silently=fail_silently)
class SMTPConnection(_SMTPConnection):
View
9 django/utils/log.py
@@ -56,6 +56,7 @@ class AdminEmailHandler(logging.Handler):
def emit(self, record):
import traceback
from django.conf import settings
+ from django.views.debug import ExceptionReporter
try:
if sys.version_info < (2,5):
@@ -75,12 +76,18 @@ def emit(self, record):
request_repr = repr(request)
except:
subject = 'Error: Unknown URL'
+ request = None
request_repr = "Request repr() unavailable"
if record.exc_info:
+ exc_info = record.exc_info
stack_trace = '\n'.join(traceback.format_exception(*record.exc_info))
else:
+ exc_info = ()
stack_trace = 'No stack trace available'
message = "%s\n\n%s" % (stack_trace, request_repr)
- mail.mail_admins(subject, message, fail_silently=True)
+ reporter = ExceptionReporter(request, *exc_info, is_email=True)
+ html_message = reporter.get_traceback_html()
+ mail.mail_admins(subject, message, fail_silently=True,
+ html_message=html_message)
View
54 django/views/debug.py
@@ -62,11 +62,12 @@ class ExceptionReporter:
"""
A class to organize and coordinate reporting on exceptions.
"""
- def __init__(self, request, exc_type, exc_value, tb):
+ def __init__(self, request, exc_type, exc_value, tb, is_email=False):
self.request = request
self.exc_type = exc_type
self.exc_value = exc_value
self.tb = tb
+ self.is_email = is_email
self.template_info = None
self.template_does_not_exist = False
@@ -118,6 +119,7 @@ def get_traceback_html(self):
from django import get_version
t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template')
c = Context({
+ 'is_email': self.is_email,
'exception_type': self.exc_type.__name__,
'exception_value': smart_unicode(self.exc_value, errors='replace'),
'unicode_hint': unicode_hint,
@@ -324,7 +326,7 @@ def empty_urlconf(request):
table.vars { margin:5px 0 2px 40px; }
table.vars td, table.req td { font-family:monospace; }
table td.code { width:100%; }
- table td.code div { overflow:hidden; }
+ table td.code pre { overflow:hidden; }
table.source th { color:#666; }
table.source td { font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
ul.traceback { list-style-type:none; }
@@ -353,6 +355,7 @@ def empty_urlconf(request):
span.commands a:link {color:#5E5694;}
pre.exception_value { font-family: sans-serif; color: #666; font-size: 1.5em; margin: 10px 0 10px 0; }
</style>
+ {% if not is_email %}
<script type="text/javascript">
//<!--
function getElementsByClassName(oElm, strTagName, strClassName){
@@ -408,10 +411,11 @@ def empty_urlconf(request):
}
//-->
</script>
+ {% endif %}
</head>
<body>
<div id="summary">
- <h1>{{ exception_type }} at {{ request.path_info|escape }}</h1>
+ <h1>{{ exception_type }}{% if request %} at {{ request.path_info|escape }}{% endif %}</h1>
<pre class="exception_value">{{ exception_value|force_escape }}</pre>
<table class="meta">
<tr>
@@ -448,7 +452,7 @@ def empty_urlconf(request):
</tr>
<tr>
<th>Python Path:</th>
- <td>{{ sys_path }}</td>
+ <td><pre>{{ sys_path|pprint }}</pre></td>
</tr>
<tr>
<th>Server time:</th>
@@ -498,7 +502,7 @@ def empty_urlconf(request):
</div>
{% endif %}
<div id="traceback">
- <h2>Traceback <span class="commands"><a href="#" onclick="return switchPastebinFriendly(this);">Switch to copy-and-paste view</a></span></h2>
+ <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>
{% autoescape off %}
<div id="browserTraceback">
<ul class="traceback">
@@ -508,19 +512,23 @@ def empty_urlconf(request):
{% if frame.context_line %}
<div class="context" id="c{{ frame.id }}">
- {% if frame.pre_context %}
- <ol start="{{ frame.pre_context_lineno }}" class="pre-context" id="pre{{ frame.id }}">{% for line in frame.pre_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line|escape }}</li>{% endfor %}</ol>
+ {% if frame.pre_context and not is_email %}
+ <ol start="{{ frame.pre_context_lineno }}" class="pre-context" id="pre{{ frame.id }}">{% for line in frame.pre_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')"><pre>{{ line|escape }}</pre></li>{% endfor %}</ol>
{% endif %}
- <ol start="{{ frame.lineno }}" class="context-line"><li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ frame.context_line|escape }} <span>...</span></li></ol>
- {% if frame.post_context %}
- <ol start='{{ frame.lineno|add:"1" }}' class="post-context" id="post{{ frame.id }}">{% for line in frame.post_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line|escape }}</li>{% endfor %}</ol>
+ <ol start="{{ frame.lineno }}" class="context-line"><li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')"><pre>{{ frame.context_line|escape }}</pre>{% if not is_email %} <span>...</span>{% endif %}</li></ol>
+ {% if frame.post_context and not is_email %}
+ <ol start='{{ frame.lineno|add:"1" }}' class="post-context" id="post{{ frame.id }}">{% for line in frame.post_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')"><pre>{{ line|escape }}</pre></li>{% endfor %}</ol>
{% endif %}
</div>
{% endif %}
{% if frame.vars %}
<div class="commands">
- <a href="#" onclick="return varToggle(this, '{{ frame.id }}')"><span>&#x25b6;</span> Local vars</a>
+ {% if is_email %}
+ <h2>Local Vars</h2>
+ {% else %}
+ <a href="#" onclick="return varToggle(this, '{{ frame.id }}')"><span>&#x25b6;</span> Local vars</a>
+ {% endif %}
</div>
<table class="vars" id="v{{ frame.id }}">
<thead>
@@ -533,7 +541,7 @@ def empty_urlconf(request):
{% for var in frame.vars|dictsort:"0" %}
<tr>
<td>{{ var.0|force_escape }}</td>
- <td class="code"><div>{{ var.1|pprint|force_escape }}</div></td>
+ <td class="code"><pre>{{ var.1|pprint|force_escape }}</pre></td>
</tr>
{% endfor %}
</tbody>
@@ -545,16 +553,19 @@ def empty_urlconf(request):
</div>
{% endautoescape %}
<form action="http://dpaste.com/" name="pasteform" id="pasteform" method="post">
+{% if not is_email %}
<div id="pastebinTraceback" class="pastebin">
<input type="hidden" name="language" value="PythonConsole">
- <input type="hidden" name="title" value="{{ exception_type|escape }} at {{ request.path_info|escape }}">
+ <input type="hidden" name="title" value="{{ exception_type|escape }}{% if request %} at {{ request.path_info|escape }}{% endif %}">
<input type="hidden" name="source" value="Django Dpaste Agent">
<input type="hidden" name="poster" value="Django">
<textarea name="content" id="traceback_area" cols="140" rows="25">
Environment:
+{% if request %}
Request Method: {{ request.META.REQUEST_METHOD }}
Request URL: {{ request.build_absolute_uri|escape }}
+{% endif %}
Django Version: {{ django_version_info }}
Python Version: {{ sys_version_info }}
Installed Applications:
@@ -581,7 +592,7 @@ def empty_urlconf(request):
{% for frame in frames %}File "{{ frame.filename|escape }}" in {{ frame.function|escape }}
{% if frame.context_line %} {{ frame.lineno }}. {{ frame.context_line|escape }}{% endif %}
{% endfor %}
-Exception Type: {{ exception_type|escape }} at {{ request.path_info|escape }}
+Exception Type: {{ exception_type|escape }}{% if request %} at {{ request.path_info|escape }}{% endif %}
Exception Value: {{ exception_value|force_escape }}
</textarea>
<br><br>
@@ -589,10 +600,12 @@ def empty_urlconf(request):
</div>
</form>
</div>
+{% endif %}
<div id="requestinfo">
<h2>Request information</h2>
+{% if request %}
<h3 id="get-info">GET</h3>
{% if request.GET %}
<table class="req">
@@ -606,7 +619,7 @@ def empty_urlconf(request):
{% for var in request.GET.items %}
<tr>
<td>{{ var.0 }}</td>
- <td class="code"><div>{{ var.1|pprint }}</div></td>
+ <td class="code"><pre>{{ var.1|pprint }}</pre></td>
</tr>
{% endfor %}
</tbody>
@@ -628,7 +641,7 @@ def empty_urlconf(request):
{% for var in request.POST.items %}
<tr>
<td>{{ var.0 }}</td>
- <td class="code"><div>{{ var.1|pprint }}</div></td>
+ <td class="code"><pre>{{ var.1|pprint }}</pre></td>
</tr>
{% endfor %}
</tbody>
@@ -649,7 +662,7 @@ def empty_urlconf(request):
{% for var in request.FILES.items %}
<tr>
<td>{{ var.0 }}</td>
- <td class="code"><div>{{ var.1|pprint }}</div></td>
+ <td class="code"><pre>{{ var.1|pprint }}</pre></td>
</tr>
{% endfor %}
</tbody>
@@ -672,7 +685,7 @@ def empty_urlconf(request):
{% for var in request.COOKIES.items %}
<tr>
<td>{{ var.0 }}</td>
- <td class="code"><div>{{ var.1|pprint }}</div></td>
+ <td class="code"><pre>{{ var.1|pprint }}</pre></td>
</tr>
{% endfor %}
</tbody>
@@ -693,11 +706,12 @@ def empty_urlconf(request):
{% for var in request.META.items|dictsort:"0" %}
<tr>
<td>{{ var.0 }}</td>
- <td class="code"><div>{{ var.1|pprint }}</div></td>
+ <td class="code"><pre>{{ var.1|pprint }}</pre></td>
</tr>
{% endfor %}
</tbody>
</table>
+{% endif %}
<h3 id="settings-info">Settings</h3>
<h4>Using settings module <code>{{ settings.SETTINGS_MODULE }}</code></h4>
@@ -712,7 +726,7 @@ def empty_urlconf(request):
{% for var in settings.items|dictsort:"0" %}
<tr>
<td>{{ var.0 }}</td>
- <td class="code"><div>{{ var.1|pprint }}</div></td>
+ <td class="code"><pre>{{ var.1|pprint }}</pre></td>
</tr>
{% endfor %}
</tbody>
View
6 docs/releases/1.3.txt
@@ -163,6 +163,12 @@ requests. These include:
* Support for _HTTPOnly cookies.
+ * mail_admins() and mail_managers() now support easily attaching
+ HTML content to messages.
+
+ * Error emails now include more of the detail and formatting of
+ the debug server error page.
+
.. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
.. _backwards-incompatible-changes-1.3:
View
10 docs/topics/email.txt
@@ -109,7 +109,7 @@ a single connection for all of its messages. This makes
mail_admins()
=============
-.. function:: mail_admins(subject, message, fail_silently=False, connection=None)
+.. function:: mail_admins(subject, message, fail_silently=False, connection=None, html_message=None)
``django.core.mail.mail_admins()`` is a shortcut for sending an e-mail to the
site admins, as defined in the :setting:`ADMINS` setting.
@@ -122,10 +122,16 @@ The "From:" header of the e-mail will be the value of the
This method exists for convenience and readability.
+.. versionchanged:: 1.3
+
+If ``html_message`` is provided, the resulting e-mail will be a
+multipart/alternative e-mail with ``message`` as the "text/plain"
+content type and ``html_message`` as the "text/html" content type.
+
mail_managers()
===============
-.. function:: mail_managers(subject, message, fail_silently=False, connection=None)
+.. function:: mail_managers(subject, message, fail_silently=False, connection=None, html_message=None)
``django.core.mail.mail_managers()`` is just like ``mail_admins()``, except it
sends an e-mail to the site managers, as defined in the :setting:`MANAGERS`
View
32 tests/regressiontests/mail/tests.py
@@ -232,7 +232,7 @@ def test_locmem(self):
self.assertEqual(len(mail.outbox), 2)
self.assertEqual(mail.outbox[0].subject, 'Subject')
self.assertEqual(mail.outbox[1].subject, 'Subject 2')
-
+
# Make sure that multiple locmem connections share mail.outbox
mail.outbox = []
connection2 = locmem.EmailBackend()
@@ -364,6 +364,36 @@ def test_mail_prefix(self):
settings.ADMINS = old_admins
settings.MANAGERS = old_managers
+ def test_html_mail_admins(self):
+ """Test html_message argument to mail_admins and mail_managers"""
+ old_admins = settings.ADMINS
+ settings.ADMINS = [('nobody','nobody@example.com')]
+
+ mail.outbox = []
+ mail_admins('Subject', 'Content', html_message='HTML Content')
+ self.assertEqual(len(mail.outbox), 1)
+ message = mail.outbox[0]
+ self.assertEqual(message.subject, '[Django] Subject')
+ self.assertEqual(message.body, 'Content')
+ self.assertEqual(message.alternatives, [('HTML Content', 'text/html')])
+
+ settings.ADMINS = old_admins
+
+ def test_html_mail_managers(self):
+ """Test html_message argument to mail_admins and mail_managers"""
+ old_managers = settings.MANAGERS
+ settings.MANAGERS = [('nobody','nobody@example.com')]
+
+ mail.outbox = []
+ mail_managers('Subject', 'Content', html_message='HTML Content')
+ self.assertEqual(len(mail.outbox), 1)
+ message = mail.outbox[0]
+ self.assertEqual(message.subject, '[Django] Subject')
+ self.assertEqual(message.body, 'Content')
+ self.assertEqual(message.alternatives, [('HTML Content', 'text/html')])
+
+ settings.MANAGERS = old_managers
+
def test_idn_validation(self):
"""Test internationalized email adresses"""
# Regression for #14301.

0 comments on commit 29c4a57

Please sign in to comment.
Something went wrong with that request. Please try again.