response.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. from django.http import HttpResponse
  2. from django.template import loader, Context, RequestContext
  3. from django.utils import six
  4. class ContentNotRenderedError(Exception):
  5. pass
  6. class SimpleTemplateResponse(HttpResponse):
  7. rendering_attrs = ['template_name', 'context_data', '_post_render_callbacks']
  8. def __init__(self, template, context=None, content_type=None, status=None):
  9. # It would seem obvious to call these next two members 'template' and
  10. # 'context', but those names are reserved as part of the test Client
  11. # API. To avoid the name collision, we use tricky-to-debug problems
  12. self.template_name = template
  13. self.context_data = context
  14. self._post_render_callbacks = []
  15. # content argument doesn't make sense here because it will be replaced
  16. # with rendered template so we always pass empty string in order to
  17. # prevent errors and provide shorter signature.
  18. super(SimpleTemplateResponse, self).__init__('', content_type, status)
  19. # _is_rendered tracks whether the template and context has been baked
  20. # into a final response.
  21. # Super __init__ doesn't know any better than to set self.content to
  22. # the empty string we just gave it, which wrongly sets _is_rendered
  23. # True, so we initialize it to False after the call to super __init__.
  24. self._is_rendered = False
  25. def __getstate__(self):
  26. """Pickling support function.
  27. Ensures that the object can't be pickled before it has been
  28. rendered, and that the pickled state only includes rendered
  29. data, not the data used to construct the response.
  30. """
  31. obj_dict = super(SimpleTemplateResponse, self).__getstate__()
  32. if not self._is_rendered:
  33. raise ContentNotRenderedError('The response content must be '
  34. 'rendered before it can be pickled.')
  35. for attr in self.rendering_attrs:
  36. if attr in obj_dict:
  37. del obj_dict[attr]
  38. return obj_dict
  39. def resolve_template(self, template):
  40. "Accepts a template object, path-to-template or list of paths"
  41. if isinstance(template, (list, tuple)):
  42. return loader.select_template(template)
  43. elif isinstance(template, six.string_types):
  44. return loader.get_template(template)
  45. else:
  46. return template
  47. def resolve_context(self, context):
  48. """Converts context data into a full Context object
  49. (assuming it isn't already a Context object).
  50. """
  51. if isinstance(context, Context):
  52. return context
  53. else:
  54. return Context(context)
  55. @property
  56. def rendered_content(self):
  57. """Returns the freshly rendered content for the template and context
  58. described by the TemplateResponse.
  59. This *does not* set the final content of the response. To set the
  60. response content, you must either call render(), or set the
  61. content explicitly using the value of this property.
  62. """
  63. template = self.resolve_template(self.template_name)
  64. context = self.resolve_context(self.context_data)
  65. content = template.render(context)
  66. return content
  67. def add_post_render_callback(self, callback):
  68. """Adds a new post-rendering callback.
  69. If the response has already been rendered,
  70. invoke the callback immediately.
  71. """
  72. if self._is_rendered:
  73. callback(self)
  74. else:
  75. self._post_render_callbacks.append(callback)
  76. def render(self):
  77. """Renders (thereby finalizing) the content of the response.
  78. If the content has already been rendered, this is a no-op.
  79. Returns the baked response instance.
  80. """
  81. retval = self
  82. if not self._is_rendered:
  83. self.content = self.rendered_content
  84. for post_callback in self._post_render_callbacks:
  85. newretval = post_callback(retval)
  86. if newretval is not None:
  87. retval = newretval
  88. return retval
  89. @property
  90. def is_rendered(self):
  91. return self._is_rendered
  92. def __iter__(self):
  93. if not self._is_rendered:
  94. raise ContentNotRenderedError('The response content must be '
  95. 'rendered before it can be iterated over.')
  96. return super(SimpleTemplateResponse, self).__iter__()
  97. @property
  98. def content(self):
  99. if not self._is_rendered:
  100. raise ContentNotRenderedError('The response content must be '
  101. 'rendered before it can be accessed.')
  102. return super(SimpleTemplateResponse, self).content
  103. @content.setter
  104. def content(self, value):
  105. """Sets the content for the response
  106. """
  107. HttpResponse.content.fset(self, value)
  108. self._is_rendered = True
  109. class TemplateResponse(SimpleTemplateResponse):
  110. rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_request', '_current_app']
  111. def __init__(self, request, template, context=None, content_type=None,
  112. status=None, current_app=None):
  113. # self.request gets over-written by django.test.client.Client - and
  114. # unlike context_data and template_name the _request should not
  115. # be considered part of the public API.
  116. self._request = request
  117. # As a convenience we'll allow callers to provide current_app without
  118. # having to avoid needing to create the RequestContext directly
  119. self._current_app = current_app
  120. super(TemplateResponse, self).__init__(
  121. template, context, content_type, status)
  122. def resolve_context(self, context):
  123. """Convert context data into a full RequestContext object
  124. (assuming it isn't already a Context object).
  125. """
  126. if isinstance(context, Context):
  127. return context
  128. return RequestContext(self._request, context, current_app=self._current_app)