diff --git a/reviewboard/reviews/detail.py b/reviewboard/reviews/detail.py
index 6a258b761fe3c0a5590a83e3b6d15eb6fc672f96..cd6e5b1df049902cff7f8bf9cca9c0bef724a595 100644
--- a/reviewboard/reviews/detail.py
+++ b/reviewboard/reviews/detail.py
@@ -2,6 +2,7 @@
 
 from __future__ import unicode_literals
 
+import logging
 from collections import Counter, defaultdict
 from datetime import datetime
 from itertools import chain
@@ -776,6 +777,73 @@ class BaseReviewRequestPageEntry(object):
         """
         return {}
 
+    def get_extra_context(self, request, context):
+        """Return extra template context for the entry.
+
+        Subclasses can override this to provide additional context needed by
+        the template for the page. By default, this returns an empty
+        dictionary.
+
+        Args:
+            request (django.http.HttpRequest):
+                The HTTP request from the client.
+
+            context (django.template.RequestContext):
+                The existing template context on the page.
+
+        Returns:
+            dict:
+            Extra context to use for the entry's template.
+        """
+        return {}
+
+    def render_to_string(self, request, context):
+        """Render the entry to a string.
+
+        If the entry doesn't have a template associated, or doesn't have
+        any content (as determined by :py:attr:`has_content`), then this
+        will return an empty string.
+
+        Args:
+            request (django.http.HttpRequest):
+                The HTTP request from the client.
+
+            context (django.template.RequestContext):
+                The existing template context on the page.
+
+        Returns:
+            unicode:
+            The resulting HTML for the entry.
+        """
+        if not self.template_name or not self.has_content:
+            return ''
+
+        try:
+            new_context = {
+                'entry': self,
+                'show_entry_statuses_area': (
+                    self.entry_pos !=
+                    BaseReviewRequestPageEntry.ENTRY_POS_INITIAL),
+            }
+            new_context.update(self.get_extra_context(request, context))
+        except Exception as e:
+            logging.exception('Error generating template context for %s '
+                              '(ID=%s): %s',
+                              self.__class__.__name__, self.entry_id, e)
+            return ''
+
+        try:
+            # Note that update() implies push().
+            context.update(new_context)
+
+            return render_to_string(self.template_name, context)
+        except Exception as e:
+            logging.exception('Error rendering template for %s (ID=%s): %s',
+                              self.__class__.__name__, self.entry_id, e)
+            return ''
+        finally:
+            context.pop()
+
     def finalize(self):
         """Perform final computations after all comments have been added."""
         pass
diff --git a/reviewboard/reviews/templatetags/reviewtags.py b/reviewboard/reviews/templatetags/reviewtags.py
index 8acd70bfd6271e3d69cd00e58d2c5e1934a5bdb7..7a7b09fcb5a823a83e0e32ab46834abce343e6da 100644
--- a/reviewboard/reviews/templatetags/reviewtags.py
+++ b/reviewboard/reviews/templatetags/reviewtags.py
@@ -1054,3 +1054,27 @@ def reviewable_page_model_data(context):
         'extraReviewRequestDraftData': extra_review_request_draft_data,
         'editorData': editor_data,
     })
+
+
+@register.simple_tag(takes_context=True)
+def render_review_request_entries(context, entries):
+    """Render a series of entries on the page.
+
+    Args:
+        context (django.template.RequestContext):
+            The existing template context on the page.
+
+        entries (list of
+                 reviewboard.reviews.detail.BaseReviewRequestPageEntry):
+            The entries to render.
+
+    Returns:
+        unicode:
+        The resulting HTML for the entries.
+    """
+    request = context['request']
+
+    return ''.join(
+        entry.render_to_string(request, context)
+        for entry in entries
+    )
diff --git a/reviewboard/reviews/tests/test_entries.py b/reviewboard/reviews/tests/test_entries.py
index 09e307d2569ba7f3f297b06a1de5c49a4ce4299b..784acf3dec5ae083c102ea888290558162ffe955 100644
--- a/reviewboard/reviews/tests/test_entries.py
+++ b/reviewboard/reviews/tests/test_entries.py
@@ -2,15 +2,19 @@
 
 from __future__ import unicode_literals
 
+import logging
 from datetime import timedelta
 
 from django.contrib.auth.models import AnonymousUser
+from django.template import RequestContext
 from django.test.client import RequestFactory
 from django.utils import six
 from djblets.testing.decorators import add_fixtures
+from kgb import SpyAgency
 
 from reviewboard.changedescs.models import ChangeDescription
-from reviewboard.reviews.detail import (ChangeEntry,
+from reviewboard.reviews.detail import (BaseReviewRequestPageEntry,
+                                        ChangeEntry,
                                         InitialStatusUpdatesEntry,
                                         ReviewEntry,
                                         ReviewRequestPageData,
@@ -19,6 +23,115 @@ from reviewboard.reviews.models import GeneralComment, StatusUpdate
 from reviewboard.testing import TestCase
 
 
+class BaseReviewRequestPageEntryTests(SpyAgency, TestCase):
+    """Unit tests for BaseReviewRequestPageEntry."""
+
+    def test_render_to_string(self):
+        """Testing BaseReviewRequestPageEntry.render_to_string"""
+        entry = BaseReviewRequestPageEntry(
+            entry_id='test',
+            timestamp=None,
+            collapsed=False)
+        entry.template_name = 'reviews/entries/base.html'
+
+        request = RequestFactory().request()
+        request.user = AnonymousUser()
+
+        self.assertNotEqual(
+            entry.render_to_string(request, RequestContext(request, {})),
+            '')
+
+    def test_render_to_string_with_entry_pos_main(self):
+        """Testing BaseReviewRequestPageEntry.render_to_string with
+        entry_pos=ENTRY_POS_MAIN
+        """
+        entry = BaseReviewRequestPageEntry(
+            entry_id='test',
+            timestamp=None,
+            collapsed=False)
+        entry.template_name = 'reviews/entries/base.html'
+        entry.entry_pos = BaseReviewRequestPageEntry.ENTRY_POS_MAIN
+
+        request = RequestFactory().request()
+        request.user = AnonymousUser()
+
+        html = entry.render_to_string(request, RequestContext(request, {}))
+        self.assertIn('<div class="box-statuses">', html)
+
+    def test_render_to_string_with_entry_pos_initial(self):
+        """Testing BaseReviewRequestPageEntry.render_to_string with
+        entry_pos=ENTRY_POS_INITIAL
+        """
+        entry = BaseReviewRequestPageEntry(
+            entry_id='test',
+            timestamp=None,
+            collapsed=False)
+        entry.template_name = 'reviews/entries/base.html'
+        entry.entry_pos = BaseReviewRequestPageEntry.ENTRY_POS_INITIAL
+
+        request = RequestFactory().request()
+        request.user = AnonymousUser()
+
+        html = entry.render_to_string(request, RequestContext(request, {}))
+        self.assertNotIn('<div class="box-statuses">', html)
+
+    def test_render_to_string_with_no_template(self):
+        """Testing BaseReviewRequestPageEntry.render_to_string with
+        template_name=None
+        """
+        entry = BaseReviewRequestPageEntry(
+            entry_id='test',
+            timestamp=None,
+            collapsed=False)
+
+        request = RequestFactory().request()
+        request.user = AnonymousUser()
+
+        self.assertEqual(
+            entry.render_to_string(request, RequestContext(request, {})),
+            '')
+
+    def test_render_to_string_with_has_content_false(self):
+        """Testing BaseReviewRequestPageEntry.render_to_string with
+        has_content=False
+        """
+        entry = BaseReviewRequestPageEntry(
+            entry_id='test',
+            timestamp=None,
+            collapsed=False)
+        entry.template_name = 'reviews/entries/base.html'
+        entry.has_content = False
+
+        request = RequestFactory().request()
+        request.user = AnonymousUser()
+
+        self.assertEqual(
+            entry.render_to_string(request, RequestContext(request, {})),
+            '')
+
+    def test_render_to_string_with_exception(self):
+        """Testing BaseReviewRequestPageEntry.render_to_string with
+        exception
+        """
+        entry = BaseReviewRequestPageEntry(
+            entry_id='test',
+            timestamp=None,
+            collapsed=False)
+        entry.template_name = 'reviews/entries/NOT_FOUND.html'
+
+        self.spy_on(logging.exception)
+
+        request = RequestFactory().request()
+        request.user = AnonymousUser()
+
+        self.assertEqual(
+            entry.render_to_string(request, RequestContext(request, {})),
+            '')
+        self.assertTrue(logging.exception.spy.called)
+        self.assertEqual(logging.exception.spy.calls[0].args[0],
+                         'Error rendering template for %s (ID=%s): %s')
+
+
 class StatusUpdatesEntryMixinTests(TestCase):
     """Unit tests for StatusUpdatesEntryMixin."""
 
diff --git a/reviewboard/templates/reviews/review_detail.html b/reviewboard/templates/reviews/review_detail.html
index 7195cb351cc347e164ae5f8d5dc4488997fe2a48..5a7c5e2b20e1abc17ad8c23f31489205cfa1142a 100644
--- a/reviewboard/templates/reviews/review_detail.html
+++ b/reviewboard/templates/reviews/review_detail.html
@@ -27,11 +27,7 @@
 </div>
 
 <div id="reviews">
-{%  for entry in entries.initial %}
-{%   if entry.template_name and entry.has_content %}
-{%    include entry.template_name with show_entry_statuses_area=False %}
-{%   endif %}
-{%  endfor %}
+{%  render_review_request_entries entries.initial %}
 
  <ul id="view_controls">
 {%  if entries %}
@@ -50,11 +46,7 @@
 {%  endif %}
  </ul>
 
-{%  for entry in entries.main %}
-{%   if entry.template_name and entry.has_content %}
-{%    include entry.template_name with show_entry_statuses_area=True %}
-{%   endif %}
-{%  endfor %}
+{%  render_review_request_entries entries.main %}
 </div>
 {% endblock content %}
 
