diff --git a/reviewboard/reviews/templatetags/reviewtags.py b/reviewboard/reviews/templatetags/reviewtags.py
index 58d61ad315fb3144479bf4faac86ec2ced6da91a..4bab6340b2fa9f0ac0d3056e4f36d68b3675b095 100644
--- a/reviewboard/reviews/templatetags/reviewtags.py
+++ b/reviewboard/reviews/templatetags/reviewtags.py
@@ -587,3 +587,13 @@ def comment_issue(context, review_request, comment, comment_type):
 def pretty_print_issue_status(status):
     """Turns an issue status code into a human-readable status string."""
     return BaseComment.issue_status_to_string(status)
+
+
+@register.tag
+@basictag(takes_context=True)
+def render_review_ui(context, file_attachment):
+    """Renders the review UI inline for a file attachment."""
+    review_ui = file_attachment.review_ui
+    assert review_ui
+
+    return review_ui.render_to_string(context['request'])
diff --git a/reviewboard/reviews/ui/base.py b/reviewboard/reviews/ui/base.py
index a0ea8444bcdc7a3a1a30afb372707626ef6d9fb1..db95190cc9d40a6b5d35ba0c09deb2d1bdb0f4e6 100644
--- a/reviewboard/reviews/ui/base.py
+++ b/reviewboard/reviews/ui/base.py
@@ -3,8 +3,10 @@ import os
 from uuid import uuid4
 
 import mimeparse
+from django.http import HttpResponse
 from django.shortcuts import render_to_response
 from django.template.context import RequestContext
+from django.template.loader import render_to_string
 from django.utils import simplejson
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
@@ -36,41 +38,62 @@ class ReviewUI(object):
         self.obj = obj
 
     def render_to_response(self, request):
-        if request.GET.get('inline', False):
-            base_template_name = 'reviews/ui/base_inline.html'
-        else:
-            base_template_name = 'reviews/ui/base.html'
+        """Renders the review UI to an HttpResponse.
+
+        This is used to render a page dedicated to the review UI, complete
+        with the standard Review Board chrome.
+        """
+        return HttpResponse(
+            self.render_to_string(request, request.GET.get('inline', False)))
+
+    def render_to_string(self, request, inline=True):
+        """Renders the review UI to an HTML string.
+
+        This renders the review UI to a string for use in embedding into
+        either an existing page or a new page.
 
+        If inline is True, the rendered review UI will be embeddable into
+        an existing page.
+
+        If inline is False, it will be rendered for use as a full, standalone
+        page, compelte with Review Board chrome.
+        """
         self.request = request
 
         draft = self.review_request.get_draft(request.user)
         review_request_details = draft or self.review_request
-        review = self.review_request.get_pending_review(request.user)
+        context = {
+            'caption': self.get_caption(draft),
+            'comments': self.get_comments(),
+            'draft': draft,
+            'review_request_details': review_request_details,
+            'review_request': self.review_request,
+            'review_ui': self,
+            'review_ui_uuid': str(uuid4()),
+            self.object_key: self.obj,
+        }
 
-        if self.review_request.repository_id:
-            diffset_count = DiffSet.objects.filter(
-                history__pk=self.review_request.diffset_history_id).count()
+        if inline:
+            context['base_template'] = 'reviews/ui/base_inline.html'
         else:
-            diffset_count = 0
+            if self.review_request.repository_id:
+                diffset_count = DiffSet.objects.filter(
+                    history__pk=self.review_request.diffset_history_id).count()
+            else:
+                diffset_count = 0
+
+            context.update({
+                'base_template': 'reviews/ui/base.html',
+                'has_diffs': (draft and draft.diffset) or diffset_count > 0,
+                'review': self.review_request.get_pending_review(request.user),
+            })
 
-        return render_to_response(
+        return render_to_string(
             self.template_name,
             RequestContext(
                 request,
-                make_review_request_context(request, self.review_request, {
-                    'base_template': base_template_name,
-                    'caption': self.get_caption(draft),
-                    'comments': self.get_comments(),
-                    'draft': draft,
-                    'has_diffs': (draft and draft.diffset) or
-                                 diffset_count > 0,
-                    'review_request_details': review_request_details,
-                    'review_request': self.review_request,
-                    'review': review,
-                    'review_ui': self,
-                    'review_ui_uuid': str(uuid4()),
-                    self.object_key: self.obj,
-                }),
+                make_review_request_context(request, self.review_request,
+                                            context),
                 **self.get_extra_context(request)))
 
     def get_comments(self):
diff --git a/reviewboard/templates/reviews/ui/base.html b/reviewboard/templates/reviews/ui/base.html
index b36bd4b54a9f289a4bf241bc7640757e1b04b475..4f4c15320fd36147dce1a4b8eccb979807dc4fb1 100644
--- a/reviewboard/templates/reviews/ui/base.html
+++ b/reviewboard/templates/reviews/ui/base.html
@@ -1,6 +1,10 @@
 {% extends "reviews/reviewable_base.html" %}
 {% load djblets_deco i18n reviewtags tz %}
 
+{% block title %}
+ {{review_ui.name}}{% if caption %}: {{caption}}{% endif %}
+{% endblock %}
+
 {% block content %}
 <div id="review_request">
 {%  box "review-request" %}
@@ -48,4 +52,6 @@
 {%  include "reviews/reviewable_page_data.js" %}
     }));
 </script>
+
+{%  block review_ui_scripts %}{% endblock %}
 {% endblock %}
diff --git a/reviewboard/templates/reviews/ui/base_inline.html b/reviewboard/templates/reviews/ui/base_inline.html
index 7fa0e4ad0e16dff0cd9c43e7d1bdfd53226a72d2..f29536670798854c3e831a90a4c32f9b3f9da364 100644
--- a/reviewboard/templates/reviews/ui/base_inline.html
+++ b/reviewboard/templates/reviews/ui/base_inline.html
@@ -1,10 +1,2 @@
-{% extends "reviews/ui/base.html" %}
-
-{% block headerbar %}{% endblock %}
-
-{% block review_banner %}{% endblock %}
-
-{% block content %}
-{%  block review_ui_content %}
-{%  endblock %}
-{% endblock %}
+{% block review_ui_content %}{% endblock %}
+{% block review_ui_scripts %}{% endblock %}
diff --git a/reviewboard/templates/reviews/ui/default.html b/reviewboard/templates/reviews/ui/default.html
index 81ba1b37a81044423f3f500cf63a0d7edece032d..536d6d7e59aa1689f17c1aa61ead2ebd7affb525 100644
--- a/reviewboard/templates/reviews/ui/default.html
+++ b/reviewboard/templates/reviews/ui/default.html
@@ -1,13 +1,7 @@
 {% extends base_template %}
-{% load djblets_js i18n reviewtags %}
-
-{% block title %}
- {{review_ui.name}}{% if caption %}: {{caption}}{% endif %}
-{% endblock %}
-
-{% block scripts-post %}
-{{block.super}}
+{% load djblets_js %}
 
+{% block review_ui_scripts %}
 {%  for js_file in review_ui.js_files %}
 <script src="{{js_file}}"></script>
 {%  endfor %}
