Author: russellm
Date: 2010-12-06 08:21:51 -0600 (Mon, 06 Dec 2010)
New Revision: 14844

Modified:
   django/trunk/django/core/mail/__init__.py
   django/trunk/django/utils/log.py
   django/trunk/django/views/debug.py
   django/trunk/docs/releases/1.3.txt
   django/trunk/docs/topics/email.txt
   django/trunk/tests/regressiontests/mail/tests.py
Log:
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.

Modified: django/trunk/django/core/mail/__init__.py
===================================================================
--- django/trunk/django/core/mail/__init__.py   2010-12-06 12:17:45 UTC (rev 
14843)
+++ django/trunk/django/core/mail/__init__.py   2010-12-06 14:21:51 UTC (rev 
14844)
@@ -83,22 +83,30 @@
     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):

Modified: django/trunk/django/utils/log.py
===================================================================
--- django/trunk/django/utils/log.py    2010-12-06 12:17:45 UTC (rev 14843)
+++ django/trunk/django/utils/log.py    2010-12-06 14:21:51 UTC (rev 14844)
@@ -56,6 +56,7 @@
     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 @@
             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)

Modified: django/trunk/django/views/debug.py
===================================================================
--- django/trunk/django/views/debug.py  2010-12-06 12:17:45 UTC (rev 14843)
+++ django/trunk/django/views/debug.py  2010-12-06 14:21:51 UTC (rev 14844)
@@ -62,11 +62,12 @@
     """
     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 @@
         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 @@
     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 @@
     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 @@
     }
     //-->
   </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 @@
     </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 @@
 </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 @@
 
           {% 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 @@
                 {% 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 @@
   </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 @@
 {% 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 @@
   </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 @@
         {% 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 @@
         {% 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 @@
             {% 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 @@
         {% 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 @@
       {% 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 @@
       {% 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>

Modified: django/trunk/docs/releases/1.3.txt
===================================================================
--- django/trunk/docs/releases/1.3.txt  2010-12-06 12:17:45 UTC (rev 14843)
+++ django/trunk/docs/releases/1.3.txt  2010-12-06 14:21:51 UTC (rev 14844)
@@ -163,6 +163,12 @@
 
     * 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:

Modified: django/trunk/docs/topics/email.txt
===================================================================
--- django/trunk/docs/topics/email.txt  2010-12-06 12:17:45 UTC (rev 14843)
+++ django/trunk/docs/topics/email.txt  2010-12-06 14:21:51 UTC (rev 14844)
@@ -109,7 +109,7 @@
 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 @@
 
 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`

Modified: django/trunk/tests/regressiontests/mail/tests.py
===================================================================
--- django/trunk/tests/regressiontests/mail/tests.py    2010-12-06 12:17:45 UTC 
(rev 14843)
+++ django/trunk/tests/regressiontests/mail/tests.py    2010-12-06 14:21:51 UTC 
(rev 14844)
@@ -232,7 +232,7 @@
         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 @@
         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','nob...@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','nob...@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.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to django-upda...@googlegroups.com.
To unsubscribe from this group, send email to 
django-updates+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en.

Reply via email to