diff --git a/debug_toolbar/locale/en/LC_MESSAGES/django.po b/debug_toolbar/locale/en/LC_MESSAGES/django.po index baebc7019..69e0d02b7 100644 --- a/debug_toolbar/locale/en/LC_MESSAGES/django.po +++ b/debug_toolbar/locale/en/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Django Debug Toolbar\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-05-27 19:40-0400\n" +"POT-Creation-Date: 2015-07-06 16:50-0400\n" "PO-Revision-Date: 2012-03-31 20:10+0000\n" "Last-Translator: \n" "Language-Team: \n" @@ -20,18 +20,18 @@ msgstr "" msgid "Debug Toolbar" msgstr "" -#: panels/cache.py:197 +#: panels/cache.py:209 msgid "Cache" msgstr "" -#: panels/cache.py:202 +#: panels/cache.py:214 #, python-format msgid "%(cache_calls)d call in %(time).2fms" msgid_plural "%(cache_calls)d calls in %(time).2fms" msgstr[0] "" msgstr[1] "" -#: panels/cache.py:210 +#: panels/cache.py:222 #, python-format msgid "Cache calls from %(count)d backend" msgid_plural "Cache calls from %(count)d backends" @@ -292,7 +292,7 @@ msgid "Calls" msgstr "" #: templates/debug_toolbar/panels/cache.html:43 -#: templates/debug_toolbar/panels/sql.html:20 +#: templates/debug_toolbar/panels/sql.html:23 msgid "Time (ms)" msgstr "" @@ -466,36 +466,46 @@ msgid_plural "%(num)s queries" msgstr[0] "" msgstr[1] "" -#: templates/debug_toolbar/panels/sql.html:18 +#: templates/debug_toolbar/panels/sql.html:9 +#, python-format +msgid "including %(dupes)s duplicates" +msgstr "" + +#: templates/debug_toolbar/panels/sql.html:21 msgid "Query" msgstr "" -#: templates/debug_toolbar/panels/sql.html:19 +#: templates/debug_toolbar/panels/sql.html:22 #: templates/debug_toolbar/panels/timer.html:36 msgid "Timeline" msgstr "" -#: templates/debug_toolbar/panels/sql.html:21 +#: templates/debug_toolbar/panels/sql.html:24 msgid "Action" msgstr "" -#: templates/debug_toolbar/panels/sql.html:64 +#: templates/debug_toolbar/panels/sql.html:39 +#, python-format +msgid "Duplicated %(dupes)s times." +msgstr "" + +#: templates/debug_toolbar/panels/sql.html:71 msgid "Connection:" msgstr "" -#: templates/debug_toolbar/panels/sql.html:66 +#: templates/debug_toolbar/panels/sql.html:73 msgid "Isolation level:" msgstr "" -#: templates/debug_toolbar/panels/sql.html:69 +#: templates/debug_toolbar/panels/sql.html:76 msgid "Transaction status:" msgstr "" -#: templates/debug_toolbar/panels/sql.html:83 +#: templates/debug_toolbar/panels/sql.html:90 msgid "(unknown)" msgstr "" -#: templates/debug_toolbar/panels/sql.html:92 +#: templates/debug_toolbar/panels/sql.html:99 msgid "No SQL queries were recorded during this request." msgstr "" diff --git a/debug_toolbar/middleware.py b/debug_toolbar/middleware.py index 60c521ff6..939bb67c4 100644 --- a/debug_toolbar/middleware.py +++ b/debug_toolbar/middleware.py @@ -126,6 +126,10 @@ def process_response(self, request, response): # When the body ends with a newline, there's two trailing groups. bits.append(''.join(m[0] for m in matches if m[1] == '')) if len(bits) > 1: + # When the toolbar will be inserted for sure, generate the stats. + for panel in reversed(toolbar.enabled_panels): + panel.generate_stats(request, response) + bits[-2] += toolbar.render_toolbar() response.content = insert_before.join(bits) if response.get('Content-Length', None): diff --git a/debug_toolbar/panels/__init__.py b/debug_toolbar/panels/__init__.py index 76febd2ff..bf1f63327 100644 --- a/debug_toolbar/panels/__init__.py +++ b/debug_toolbar/panels/__init__.py @@ -167,10 +167,29 @@ def process_view(self, request, view_func, view_args, view_kwargs): def process_response(self, request, response): """ - Like process_response in Django's middleware. + Like process_response in Django's middleware. This is similar to + :meth:`generate_stats `, + but will be executed on every request. It should be used when either + the logic needs to be executed on every request or it needs to change + the response entirely, such as :class:`RedirectsPanel`. Write panel logic related to the response there. Post-process data gathered while the view executed. Save data with :meth:`record_stats`. + + Return a response to overwrite the existing response. + """ + + def generate_stats(self, request, response): + """ + Similar to :meth:`process_response + `, + but may not be executed on every request. This will only be called if + the toolbar will be inserted into the request. + + Write panel logic related to the response there. Post-process data + gathered while the view executed. Save data with :meth:`record_stats`. + + Does not return a value. """ diff --git a/debug_toolbar/panels/cache.py b/debug_toolbar/panels/cache.py index 6e799dee0..87b753e94 100644 --- a/debug_toolbar/panels/cache.py +++ b/debug_toolbar/panels/cache.py @@ -244,7 +244,7 @@ def disable_instrumentation(self): middleware_cache.caches = original_caches cache.get_cache = original_get_cache - def process_response(self, request, response): + def generate_stats(self, request, response): self.record_stats({ 'total_calls': len(self.calls), 'calls': self.calls, diff --git a/debug_toolbar/panels/headers.py b/debug_toolbar/panels/headers.py index 48c9b9e41..0af226594 100644 --- a/debug_toolbar/panels/headers.py +++ b/debug_toolbar/panels/headers.py @@ -47,7 +47,7 @@ def process_request(self, request): 'environ': self.environ, }) - def process_response(self, request, response): + def generate_stats(self, request, response): self.response_headers = OrderedDict(sorted(response.items())) self.record_stats({ 'response_headers': self.response_headers, diff --git a/debug_toolbar/panels/logging.py b/debug_toolbar/panels/logging.py index b451019cd..41601055a 100644 --- a/debug_toolbar/panels/logging.py +++ b/debug_toolbar/panels/logging.py @@ -74,7 +74,7 @@ def nav_subtitle(self): def process_request(self, request): collector.clear_collection() - def process_response(self, request, response): + def generate_stats(self, request, response): records = collector.get_collection() self._records[threading.currentThread()] = records collector.clear_collection() diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py index 8ee3d6f78..707a8245b 100644 --- a/debug_toolbar/panels/profiling.py +++ b/debug_toolbar/panels/profiling.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, division, unicode_literals +from django.utils import six from django.utils.translation import ugettext_lazy as _ from django.utils.safestring import mark_safe from debug_toolbar.panels import Panel @@ -10,6 +11,21 @@ from colorsys import hsv_to_rgb import os +# Occasionally the disable method on the profiler is listed before +# the actual view functions. This function call should be ignored as +# it leads to an error within the tests. +INVALID_PROFILER_FUNC = '_lsprof.Profiler' + + +def contains_profiler(func_tuple): + """Helper function that checks to see if the tuple contains + the INVALID_PROFILE_FUNC in any string value of the tuple.""" + has_profiler = False + for value in func_tuple: + if isinstance(value, six.string_types): + has_profiler |= INVALID_PROFILER_FUNC in value + return has_profiler + class DjangoDebugToolbarStats(Stats): __root = None @@ -17,7 +33,7 @@ class DjangoDebugToolbarStats(Stats): def get_root_func(self): if self.__root is None: for func, (cc, nc, tt, ct, callers) in self.stats.items(): - if len(callers) == 0: + if len(callers) == 0 and not contains_profiler(func): self.__root = func break return self.__root @@ -142,7 +158,7 @@ def add_node(self, func_list, func, max_depth, cum_time=0.1): func.has_subfuncs = True self.add_node(func_list, subfunc, max_depth, cum_time=cum_time) - def process_response(self, request, response): + def generate_stats(self, request, response): if not hasattr(self, 'profiler'): return None # Could be delayed until the panel content is requested (perf. optim.) diff --git a/debug_toolbar/panels/request.py b/debug_toolbar/panels/request.py index b5caa5230..0aecda296 100644 --- a/debug_toolbar/panels/request.py +++ b/debug_toolbar/panels/request.py @@ -25,7 +25,7 @@ def nav_subtitle(self): view_func = self.get_stats().get('view_func', '') return view_func.rsplit('.', 1)[-1] - def process_response(self, request, response): + def generate_stats(self, request, response): self.record_stats({ 'get': [(k, request.GET.getlist(k)) for k in sorted(request.GET)], 'post': [(k, request.POST.getlist(k)) for k in sorted(request.POST)], diff --git a/debug_toolbar/panels/settings.py b/debug_toolbar/panels/settings.py index 54afd23e5..64d73361f 100644 --- a/debug_toolbar/panels/settings.py +++ b/debug_toolbar/panels/settings.py @@ -19,7 +19,7 @@ class SettingsPanel(Panel): def title(self): return _("Settings from %s") % settings.SETTINGS_MODULE - def process_response(self, request, response): + def generate_stats(self, request, response): self.record_stats({ 'settings': OrderedDict(sorted(get_safe_settings().items(), key=lambda s: s[0])), diff --git a/debug_toolbar/panels/signals.py b/debug_toolbar/panels/signals.py index 3ec54d838..90bfbe474 100644 --- a/debug_toolbar/panels/signals.py +++ b/debug_toolbar/panels/signals.py @@ -58,7 +58,7 @@ def signals(self): signals[signal_name] = getattr(signals_mod, signal_name) return signals - def process_response(self, request, response): + def generate_stats(self, request, response): signals = [] for name, signal in sorted(self.signals.items(), key=lambda x: x[0]): if signal is None: diff --git a/debug_toolbar/panels/sql/panel.py b/debug_toolbar/panels/sql/panel.py index 07b184810..2ab21ad13 100644 --- a/debug_toolbar/panels/sql/panel.py +++ b/debug_toolbar/panels/sql/panel.py @@ -136,7 +136,7 @@ def disable_instrumentation(self): for connection in connections.all(): unwrap_cursor(connection) - def process_response(self, request, response): + def generate_stats(self, request, response): colors = contrasting_color_generator() trace_colors = defaultdict(lambda: next(colors)) query_duplicates = defaultdict(lambda: defaultdict(int)) diff --git a/debug_toolbar/panels/staticfiles.py b/debug_toolbar/panels/staticfiles.py index 93dbd7494..dd6d67050 100644 --- a/debug_toolbar/panels/staticfiles.py +++ b/debug_toolbar/panels/staticfiles.py @@ -113,7 +113,7 @@ def nav_subtitle(self): def process_request(self, request): collector.clear_collection() - def process_response(self, request, response): + def generate_stats(self, request, response): used_paths = collector.get_collection() self._paths[threading.currentThread()] = used_paths diff --git a/debug_toolbar/panels/templates/panel.py b/debug_toolbar/panels/templates/panel.py index 1f763e20c..fb2fc0064 100644 --- a/debug_toolbar/panels/templates/panel.py +++ b/debug_toolbar/panels/templates/panel.py @@ -195,7 +195,7 @@ def enable_instrumentation(self): def disable_instrumentation(self): template_rendered.disconnect(self._store_template_info) - def process_response(self, request, response): + def generate_stats(self, request, response): template_context = [] for template_data in self.templates: info = {} diff --git a/debug_toolbar/panels/timer.py b/debug_toolbar/panels/timer.py index 741f57af6..b9fd1c784 100644 --- a/debug_toolbar/panels/timer.py +++ b/debug_toolbar/panels/timer.py @@ -52,7 +52,7 @@ def process_request(self, request): if self.has_content: self._start_rusage = resource.getrusage(resource.RUSAGE_SELF) - def process_response(self, request, response): + def generate_stats(self, request, response): stats = {} if hasattr(self, '_start_time'): stats['total_time'] = (time.time() - self._start_time) * 1000 diff --git a/debug_toolbar/panels/versions.py b/debug_toolbar/panels/versions.py index f1ea56f0a..f56f19c34 100644 --- a/debug_toolbar/panels/versions.py +++ b/debug_toolbar/panels/versions.py @@ -22,7 +22,7 @@ def nav_subtitle(self): template = 'debug_toolbar/panels/versions.html' - def process_response(self, request, response): + def generate_stats(self, request, response): versions = [ ('Python', '%d.%d.%d' % sys.version_info[:3]), ('Django', self.get_app_version(django)), diff --git a/docs/changes.rst b/docs/changes.rst index fb1b90d8e..065a88907 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,6 +1,22 @@ Change log ========== +1.4 +--- + +New features +~~~~~~~~~~~~ + +* New panel method :meth:`debug_toolbar.panels.Panel.generate_stats` allows panels + to only record stats when the toolbar is going to be inserted into the + response. + +Bugfixes +~~~~~~~~ + +* Response time for requests of projects with numerous media files has + been improved. + 1.3 --- diff --git a/docs/panels.rst b/docs/panels.rst index 7707a9a65..71023f049 100644 --- a/docs/panels.rst +++ b/docs/panels.rst @@ -315,6 +315,8 @@ CSS API at this time. .. automethod:: debug_toolbar.panels.Panel.process_response + .. automethod:: debug_toolbar.panels.Panel.generate_stats + .. _javascript-api: JavaScript API diff --git a/tests/panels/test_cache.py b/tests/panels/test_cache.py index a32d3e39a..750974bdf 100644 --- a/tests/panels/test_cache.py +++ b/tests/panels/test_cache.py @@ -46,3 +46,16 @@ def test_recording_get_cache(self): default_cache.set('foo', 'bar') second_cache.get('foo') self.assertEqual(len(self.panel.calls), 2) + + def test_insert_content(self): + """ + Test that the panel only inserts content after generate_stats and + not the process_response. + """ + cache.cache.get('café') + self.panel.process_response(self.request, self.response) + # ensure the panel does not have content yet. + self.assertNotIn('café', self.panel.content) + self.panel.generate_stats(self.request, self.response) + # ensure the panel renders correctly. + self.assertIn('café', self.panel.content) diff --git a/tests/panels/test_logging.py b/tests/panels/test_logging.py index 306481941..3322e9cb0 100644 --- a/tests/panels/test_logging.py +++ b/tests/panels/test_logging.py @@ -1,3 +1,5 @@ +# coding: utf-8 + from __future__ import absolute_import, unicode_literals import logging @@ -24,6 +26,7 @@ def test_happy_case(self): self.logger.info('Nothing to see here, move along!') self.panel.process_response(self.request, self.response) + self.panel.generate_stats(self.request, self.response) records = self.panel.get_stats()['records'] self.assertEqual(1, len(records)) @@ -34,12 +37,26 @@ def test_formatting(self): self.logger.info('There are %d %s', 5, 'apples') self.panel.process_response(self.request, self.response) + self.panel.generate_stats(self.request, self.response) records = self.panel.get_stats()['records'] self.assertEqual(1, len(records)) self.assertEqual('There are 5 apples', records[0]['message']) + def test_insert_content(self): + """ + Test that the panel only inserts content after generate_stats and + not the process_response. + """ + self.logger.info('café') + self.panel.process_response(self.request, self.response) + # ensure the panel does not have content yet. + self.assertNotIn('café', self.panel.content) + self.panel.generate_stats(self.request, self.response) + # ensure the panel renders correctly. + self.assertIn('café', self.panel.content) + def test_failing_formatting(self): class BadClass(object): def __str__(self): @@ -49,6 +66,7 @@ def __str__(self): self.logger.debug('This class is misbehaving: %s', BadClass()) self.panel.process_response(self.request, self.response) + self.panel.generate_stats(self.request, self.response) records = self.panel.get_stats()['records'] self.assertEqual(1, len(records)) diff --git a/tests/panels/test_profiling.py b/tests/panels/test_profiling.py index 2731daa6b..18387a631 100644 --- a/tests/panels/test_profiling.py +++ b/tests/panels/test_profiling.py @@ -7,7 +7,6 @@ from ..base import BaseTestCase from ..views import regular_view -from debug_toolbar.compat import unittest @override_settings(DEBUG_TOOLBAR_PANELS=['debug_toolbar.panels.profiling.ProfilingPanel']) @@ -17,15 +16,26 @@ def setUp(self): super(ProfilingPanelTestCase, self).setUp() self.panel = self.toolbar.get_panel_by_id('ProfilingPanel') - # This test fails randomly for a reason I don't understand. - - @unittest.expectedFailure def test_regular_view(self): self.panel.process_view(self.request, regular_view, ('profiling',), {}) self.panel.process_response(self.request, self.response) + self.panel.generate_stats(self.request, self.response) self.assertIn('func_list', self.panel.get_stats()) self.assertIn('regular_view', self.panel.content) + def test_insert_content(self): + """ + Test that the panel only inserts content after generate_stats and + not the process_response. + """ + self.panel.process_view(self.request, regular_view, ('profiling',), {}) + self.panel.process_response(self.request, self.response) + # ensure the panel does not have content yet. + self.assertNotIn('regular_view', self.panel.content) + self.panel.generate_stats(self.request, self.response) + # ensure the panel renders correctly. + self.assertIn('regular_view', self.panel.content) + @override_settings(DEBUG=True, DEBUG_TOOLBAR_PANELS=['debug_toolbar.panels.profiling.ProfilingPanel']) diff --git a/tests/panels/test_redirects.py b/tests/panels/test_redirects.py index 5f40dd9b2..463059e75 100644 --- a/tests/panels/test_redirects.py +++ b/tests/panels/test_redirects.py @@ -58,3 +58,14 @@ def test_unknown_status_code_with_reason(self): redirect['Location'] = 'http://somewhere/else/' response = self.panel.process_response(self.request, redirect) self.assertContains(response, '369 Look Ma!') + + def test_insert_content(self): + """ + Test that the panel only inserts content after generate_stats and + not the process_response. + """ + redirect = HttpResponse(status=304) + response = self.panel.process_response(self.request, redirect) + self.assertIsNotNone(response) + response = self.panel.generate_stats(self.request, redirect) + self.assertIsNone(response) diff --git a/tests/panels/test_request.py b/tests/panels/test_request.py index 1c60ec708..452086f88 100644 --- a/tests/panels/test_request.py +++ b/tests/panels/test_request.py @@ -19,6 +19,7 @@ def test_non_ascii_session(self): self.request.session['là'.encode('utf-8')] = 'là'.encode('utf-8') self.panel.process_request(self.request) self.panel.process_response(self.request, self.response) + self.panel.generate_stats(self.request, self.response) content = self.panel.content if six.PY3: self.assertIn('où', content) @@ -30,4 +31,18 @@ def test_object_with_non_ascii_repr_in_request_params(self): self.request.path = '/non_ascii_request/' self.panel.process_request(self.request) self.panel.process_response(self.request, self.response) + self.panel.generate_stats(self.request, self.response) + self.assertIn('nôt åscíì', self.panel.content) + + def test_insert_content(self): + """ + Test that the panel only inserts content after generate_stats and + not the process_response. + """ + self.request.path = '/non_ascii_request/' + self.panel.process_response(self.request, self.response) + # ensure the panel does not have content yet. + self.assertNotIn('nôt åscíì', self.panel.content) + self.panel.generate_stats(self.request, self.response) + # ensure the panel renders correctly. self.assertIn('nôt åscíì', self.panel.content) diff --git a/tests/panels/test_sql.py b/tests/panels/test_sql.py index 80ac105e7..748f06e42 100644 --- a/tests/panels/test_sql.py +++ b/tests/panels/test_sql.py @@ -64,10 +64,24 @@ def test_non_ascii_query(self): self.assertEqual(len(self.panel._queries), 3) self.panel.process_response(self.request, self.response) + self.panel.generate_stats(self.request, self.response) # ensure the panel renders correctly self.assertIn('café', self.panel.content) + def test_insert_content(self): + """ + Test that the panel only inserts content after generate_stats and + not the process_response. + """ + list(User.objects.filter(username='café'.encode('utf-8'))) + self.panel.process_response(self.request, self.response) + # ensure the panel does not have content yet. + self.assertNotIn('café', self.panel.content) + self.panel.generate_stats(self.request, self.response) + # ensure the panel renders correctly. + self.assertIn('café', self.panel.content) + @unittest.skipUnless(connection.vendor == 'postgresql', 'Test valid only on PostgreSQL') def test_erroneous_query(self): diff --git a/tests/panels/test_staticfiles.py b/tests/panels/test_staticfiles.py index be4f26705..4002087ac 100644 --- a/tests/panels/test_staticfiles.py +++ b/tests/panels/test_staticfiles.py @@ -16,6 +16,7 @@ def setUp(self): def test_default_case(self): self.panel.process_request(self.request) self.panel.process_response(self.request, self.response) + self.panel.generate_stats(self.request, self.response) self.assertIn('django.contrib.staticfiles.finders.' 'AppDirectoriesFinder', self.panel.content) self.assertIn('django.contrib.staticfiles.finders.' @@ -26,3 +27,18 @@ def test_default_case(self): ['django.contrib.admin', 'debug_toolbar']) self.assertEqual(self.panel.get_staticfiles_dirs(), finders.FileSystemFinder().locations) + + def test_insert_content(self): + """ + Test that the panel only inserts content after generate_stats and + not the process_response. + """ + self.panel.process_request(self.request) + self.panel.process_response(self.request, self.response) + # ensure the panel does not have content yet. + self.assertNotIn('django.contrib.staticfiles.finders.' + 'AppDirectoriesFinder', self.panel.content) + self.panel.generate_stats(self.request, self.response) + # ensure the panel renders correctly. + self.assertIn('django.contrib.staticfiles.finders.' + 'AppDirectoriesFinder', self.panel.content) diff --git a/tests/panels/test_template.py b/tests/panels/test_template.py index 7279783e5..cdf12f9ca 100644 --- a/tests/panels/test_template.py +++ b/tests/panels/test_template.py @@ -48,6 +48,22 @@ def test_object_with_non_ascii_repr_in_context(self): c = Context({'object': NonAsciiRepr()}) t.render(c) self.panel.process_response(self.request, self.response) + self.panel.generate_stats(self.request, self.response) + self.assertIn('nôt åscíì', self.panel.content) + + def test_insert_content(self): + """ + Test that the panel only inserts content after generate_stats and + not the process_response. + """ + t = Template("{{ object }}") + c = Context({'object': NonAsciiRepr()}) + t.render(c) + self.panel.process_response(self.request, self.response) + # ensure the panel does not have content yet. + self.assertNotIn('nôt åscíì', self.panel.content) + self.panel.generate_stats(self.request, self.response) + # ensure the panel renders correctly. self.assertIn('nôt åscíì', self.panel.content) def test_custom_context_processor(self): @@ -56,6 +72,7 @@ def test_custom_context_processor(self): c = RequestContext(self.request, processors=[context_processor]) t.render(c) self.panel.process_response(self.request, self.response) + self.panel.generate_stats(self.request, self.response) self.assertIn('tests.panels.test_template.context_processor', self.panel.content) def test_disabled(self): diff --git a/tests/test_integration.py b/tests/test_integration.py index 009a09f1e..7643e241f 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -45,6 +45,7 @@ def _resolve_stats(self, path): panel = self.toolbar.get_panel_by_id('RequestPanel') panel.process_request(self.request) panel.process_response(self.request, self.response) + panel.generate_stats(self.request, self.response) return panel.get_stats() def test_url_resolving_positional(self):