123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- from __future__ import absolute_import, unicode_literals
-
- from collections import OrderedDict
- from contextlib import contextmanager
- from os.path import normpath
- from pprint import pformat, saferepr
-
- from django import http
- from django.conf.urls import url
- from django.core import signing
- from django.db.models.query import QuerySet, RawQuerySet
- from django.template import RequestContext, Template
- from django.test.signals import template_rendered
- from django.test.utils import instrumented_test_render
- from django.utils import six
- from django.utils.encoding import force_text
- from django.utils.translation import ugettext_lazy as _
-
- from debug_toolbar.panels import Panel
- from debug_toolbar.panels.sql.tracking import SQLQueryTriggered, recording
- from debug_toolbar.panels.templates import views
-
- # Monkey-patch to enable the template_rendered signal. The receiver returns
- # immediately when the panel is disabled to keep the overhead small.
-
- # Code taken and adapted from Simon Willison and Django Snippets:
- # https://www.djangosnippets.org/snippets/766/
-
- if Template._render != instrumented_test_render:
- Template.original_render = Template._render
- Template._render = instrumented_test_render
-
-
- # Monkey-patch to store items added by template context processors. The
- # overhead is sufficiently small to justify enabling it unconditionally.
-
- @contextmanager
- def _request_context_bind_template(self, template):
- if self.template is not None:
- raise RuntimeError("Context is already bound to a template")
-
- self.template = template
- # Set context processors according to the template engine's settings.
- processors = (template.engine.template_context_processors +
- self._processors)
- self.context_processors = OrderedDict()
- updates = {}
- for processor in processors:
- name = '%s.%s' % (processor.__module__, processor.__name__)
- context = processor(self.request)
- self.context_processors[name] = context
- updates.update(context)
- self.dicts[self._processors_index] = updates
-
- try:
- yield
- finally:
- self.template = None
- # Unset context processors.
- self.dicts[self._processors_index] = {}
-
-
- RequestContext.bind_template = _request_context_bind_template
-
-
- class TemplatesPanel(Panel):
- """
- A panel that lists all templates used during processing of a response.
- """
- def __init__(self, *args, **kwargs):
- super(TemplatesPanel, self).__init__(*args, **kwargs)
- self.templates = []
- # Refs GitHub issue #910
- # Hold a series of seen dictionaries within Contexts. A dictionary is
- # considered seen if it is `in` this list, requiring that the __eq__
- # for the dictionary matches. If *anything* in the dictionary is
- # different it is counted as a new layer.
- self.seen_layers = []
- # Holds all dictionaries which have been prettified for output.
- # This should align with the seen_layers such that an index here is
- # the same as the index there.
- self.pformat_layers = []
-
- def _store_template_info(self, sender, **kwargs):
- template, context = kwargs['template'], kwargs['context']
-
- # Skip templates that we are generating through the debug toolbar.
- if (isinstance(template.name, six.string_types) and (
- template.name.startswith('debug_toolbar/') or
- template.name.startswith(
- tuple(self.toolbar.config['SKIP_TEMPLATE_PREFIXES'])))):
- return
-
- context_list = []
- for context_layer in context.dicts:
- if hasattr(context_layer, 'items') and context_layer:
- # Refs GitHub issue #910
- # If we can find this layer in our pseudo-cache then find the
- # matching prettified version in the associated list.
- key_values = sorted(context_layer.items())
- if key_values in self.seen_layers:
- index = self.seen_layers.index(key_values)
- pformatted = self.pformat_layers[index]
- context_list.append(pformatted)
- else:
- temp_layer = {}
- for key, value in context_layer.items():
- # Replace any request elements - they have a large
- # unicode representation and the request data is
- # already made available from the Request panel.
- if isinstance(value, http.HttpRequest):
- temp_layer[key] = '<<request>>'
- # Replace the debugging sql_queries element. The SQL
- # data is already made available from the SQL panel.
- elif key == 'sql_queries' and isinstance(value, list):
- temp_layer[key] = '<<sql_queries>>'
- # Replace LANGUAGES, which is available in i18n context processor
- elif key == 'LANGUAGES' and isinstance(value, tuple):
- temp_layer[key] = '<<languages>>'
- # QuerySet would trigger the database: user can run the query from SQL Panel
- elif isinstance(value, (QuerySet, RawQuerySet)):
- model_name = "%s.%s" % (
- value.model._meta.app_label, value.model.__name__)
- temp_layer[key] = '<<%s of %s>>' % (
- value.__class__.__name__.lower(), model_name)
- else:
- try:
- recording(False)
- saferepr(value) # this MAY trigger a db query
- except SQLQueryTriggered:
- temp_layer[key] = '<<triggers database query>>'
- except UnicodeEncodeError:
- temp_layer[key] = '<<unicode encode error>>'
- except Exception:
- temp_layer[key] = '<<unhandled exception>>'
- else:
- temp_layer[key] = value
- finally:
- recording(True)
- # Refs GitHub issue #910
- # If we've not seen the layer before then we will add it
- # so that if we see it again we can skip formatting it.
- self.seen_layers.append(key_values)
- # Note: this *ought* to be len(...) - 1 but let's be safe.
- index = self.seen_layers.index(key_values)
- try:
- pformatted = force_text(pformat(temp_layer))
- except UnicodeEncodeError:
- pass
- else:
- # Note: this *ought* to be len(...) - 1 but let's be safe.
- self.pformat_layers.insert(index, pformatted)
- context_list.append(pformatted)
-
- kwargs['context'] = context_list
- kwargs['context_processors'] = getattr(context, 'context_processors', None)
- self.templates.append(kwargs)
-
- # Implement the Panel API
-
- nav_title = _("Templates")
-
- @property
- def title(self):
- num_templates = len(self.templates)
- return _("Templates (%(num_templates)s rendered)") % {'num_templates': num_templates}
-
- @property
- def nav_subtitle(self):
- if self.templates:
- return self.templates[0]['template'].name
- return ''
-
- template = 'debug_toolbar/panels/templates.html'
-
- @classmethod
- def get_urls(cls):
- return [
- url(r'^template_source/$', views.template_source, name='template_source'),
- ]
-
- def enable_instrumentation(self):
- template_rendered.connect(self._store_template_info)
-
- def disable_instrumentation(self):
- template_rendered.disconnect(self._store_template_info)
-
- def generate_stats(self, request, response):
- template_context = []
- for template_data in self.templates:
- info = {}
- # Clean up some info about templates
- template = template_data.get('template', None)
- if hasattr(template, 'origin') and template.origin and template.origin.name:
- template.origin_name = template.origin.name
- template.origin_hash = signing.dumps(template.origin.name)
- else:
- template.origin_name = _('No origin')
- template.origin_hash = ''
- info['template'] = template
- # Clean up context for better readability
- if self.toolbar.config['SHOW_TEMPLATE_CONTEXT']:
- context_list = template_data.get('context', [])
- info['context'] = '\n'.join(context_list)
- template_context.append(info)
-
- # Fetch context_processors/template_dirs from any template
- if self.templates:
- context_processors = self.templates[0]['context_processors']
- template = self.templates[0]['template']
- # django templates have the 'engine' attribute, while jinja templates use 'backend'
- engine_backend = getattr(template, 'engine', None) or getattr(template, 'backend')
- template_dirs = engine_backend.dirs
- else:
- context_processors = None
- template_dirs = []
-
- self.record_stats({
- 'templates': template_context,
- 'template_dirs': [normpath(x) for x in template_dirs],
- 'context_processors': context_processors,
- })
|