diff --git a/.travis.yml b/.travis.yml index ed22c6b0c..5e1dbf11d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,5 +16,5 @@ matrix: env: DJANGO_VERSION=1.4.10 install: - pip install -e . - - pip install Django==$DJANGO_VERSION sqlparse + - pip install Django==$DJANGO_VERSION sqlparse django-discover-runner script: make test diff --git a/debug_toolbar/panels/cache.py b/debug_toolbar/panels/cache.py index 1c37c6b54..bdc8a4dc8 100644 --- a/debug_toolbar/panels/cache.py +++ b/debug_toolbar/panels/cache.py @@ -10,8 +10,11 @@ from django.core.cache.backends.base import BaseCache from django.dispatch import Signal from django.template import Node -from django.utils.datastructures import SortedDict from django.utils.translation import ugettext_lazy as _, ungettext +try: + from collections import OrderedDict +except ImportError: + from django.utils.datastructures import SortedDict as OrderedDict from debug_toolbar.panels import Panel from debug_toolbar.utils import (tidy_stacktrace, render_stacktrace, @@ -139,7 +142,7 @@ def __init__(self, *args, **kwargs): self.hits = 0 self.misses = 0 self.calls = [] - self.counts = SortedDict(( + self.counts = OrderedDict(( ('add', 0), ('get', 0), ('set', 0), diff --git a/debug_toolbar/panels/logging.py b/debug_toolbar/panels/logging.py index 051d5a336..1ee19cefe 100644 --- a/debug_toolbar/panels/logging.py +++ b/debug_toolbar/panels/logging.py @@ -8,42 +8,19 @@ threading = None from django.utils.translation import ungettext, ugettext_lazy as _ from debug_toolbar.panels import Panel +from debug_toolbar.utils import ThreadCollector MESSAGE_IF_STRING_REPRESENTATION_INVALID = '[Could not get log message]' -class LogCollector(object): - def __init__(self): - if threading is None: - raise NotImplementedError( - "threading module is not available, " - "the logging panel cannot be used without it") - self.records = {} # a dictionary that maps threads to log records +class LogCollector(ThreadCollector): - def add_record(self, record, thread=None): + def collect(self, item, thread=None): # Avoid logging SQL queries since they are already in the SQL panel # TODO: Make this check whether SQL panel is enabled - if record.get('channel', '') == 'django.db.backends': + if item.get('channel', '') == 'django.db.backends': return - - self.get_records(thread).append(record) - - def get_records(self, thread=None): - """ - Returns a list of records for the provided thread, of if none is provided, - returns a list for the current thread. - """ - if thread is None: - thread = threading.currentThread() - if thread not in self.records: - self.records[thread] = [] - return self.records[thread] - - def clear_records(self, thread=None): - if thread is None: - thread = threading.currentThread() - if thread in self.records: - del self.records[thread] + super(LogCollector, self).collect(item, thread) class ThreadTrackingHandler(logging.Handler): @@ -65,7 +42,7 @@ def emit(self, record): 'line': record.lineno, 'channel': record.name, } - self.collector.add_record(record) + self.collector.collect(record) # We don't use enable/disable_instrumentation because logging is global. @@ -96,10 +73,10 @@ def nav_subtitle(self): title = _("Log messages") def process_request(self, request): - collector.clear_records() + collector.clear_collection() def process_response(self, request, response): - records = collector.get_records() + records = collector.get_collection() self._records[threading.currentThread()] = records - collector.clear_records() + collector.clear_collection() self.record_stats({'records': records}) diff --git a/debug_toolbar/panels/settings.py b/debug_toolbar/panels/settings.py index c59d1d1c2..b054f8b70 100644 --- a/debug_toolbar/panels/settings.py +++ b/debug_toolbar/panels/settings.py @@ -3,7 +3,10 @@ from django.conf import settings from django.views.debug import get_safe_settings from django.utils.translation import ugettext_lazy as _ -from django.utils.datastructures import SortedDict +try: + from collections import OrderedDict +except ImportError: + from django.utils.datastructures import SortedDict as OrderedDict from debug_toolbar.panels import Panel @@ -21,5 +24,6 @@ def title(self): def process_response(self, request, response): self.record_stats({ - 'settings': SortedDict(sorted(get_safe_settings().items(), key=lambda s: s[0])), + 'settings': OrderedDict(sorted(get_safe_settings().items(), + key=lambda s: s[0])), }) diff --git a/debug_toolbar/panels/staticfiles.py b/debug_toolbar/panels/staticfiles.py new file mode 100644 index 000000000..f212f2bef --- /dev/null +++ b/debug_toolbar/panels/staticfiles.py @@ -0,0 +1,179 @@ +from __future__ import absolute_import, unicode_literals +from os.path import normpath, join +try: + import threading +except ImportError: + threading = None + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.core.files.storage import get_storage_class +from django.contrib.staticfiles import finders, storage +from django.contrib.staticfiles.templatetags import staticfiles + +from django.utils.encoding import python_2_unicode_compatible +from django.utils.functional import LazyObject +from django.utils.translation import ungettext, ugettext_lazy as _ +try: + from collections import OrderedDict +except ImportError: + from django.utils.datastructures import SortedDict as OrderedDict + +from debug_toolbar import panels +from debug_toolbar.utils import ThreadCollector + + +@python_2_unicode_compatible +class StaticFile(object): + """ + Representing the different properties of a static file. + """ + def __init__(self, path): + self.path = path + + def __str__(self): + return self.path + + def real_path(self): + return finders.find(self.path) + + def url(self): + return storage.staticfiles_storage.url(self.path) + + +class FileCollector(ThreadCollector): + + def collect(self, path, thread=None): + # handle the case of {% static "admin/" %} + if path.endswith('/'): + return + super(FileCollector, self).collect(StaticFile(path), thread) + + +collector = FileCollector() + + +class DebugConfiguredStorage(LazyObject): + """ + A staticfiles storage class to be used for collecting which paths + are resolved by using the {% static %} template tag (which uses the + `url` method). + """ + def _setup(self): + + configured_storage_cls = get_storage_class(settings.STATICFILES_STORAGE) + + class DebugStaticFilesStorage(configured_storage_cls): + + def __init__(self, collector, *args, **kwargs): + super(DebugStaticFilesStorage, self).__init__(*args, **kwargs) + self.collector = collector + + def url(self, path): + self.collector.collect(path) + return super(DebugStaticFilesStorage, self).url(path) + + self._wrapped = DebugStaticFilesStorage(collector) + +_original_storage = storage.staticfiles_storage + + +class StaticFilesPanel(panels.Panel): + """ + A panel to display the found staticfiles. + """ + name = 'Static files' + template = 'debug_toolbar/panels/staticfiles.html' + + @property + def title(self): + return (_("Static files (%(num_found)s found, %(num_used)s used)") % + {'num_found': self.num_found, 'num_used': self.num_used}) + + def __init__(self, *args, **kwargs): + super(StaticFilesPanel, self).__init__(*args, **kwargs) + self.num_found = 0 + self._paths = {} + + def enable_instrumentation(self): + storage.staticfiles_storage = staticfiles.staticfiles_storage = DebugConfiguredStorage() + + def disable_instrumentation(self): + storage.staticfiles_storage = staticfiles.staticfiles_storage = _original_storage + + @property + def has_content(self): + if "django.contrib.staticfiles" not in settings.INSTALLED_APPS: + raise ImproperlyConfigured("Could not find staticfiles in " + "INSTALLED_APPS setting.") + return True + + @property + def num_used(self): + return len(self._paths[threading.currentThread()]) + + nav_title = _('Static files') + + @property + def nav_subtitle(self): + num_used = self.num_used + return ungettext("%(num_used)s file used", + "%(num_used)s files used", + num_used) % {'num_used': num_used} + + def process_request(self, request): + collector.clear_collection() + + def process_response(self, request, response): + used_paths = collector.get_collection() + self._paths[threading.currentThread()] = used_paths + + self.record_stats({ + 'num_found': self.num_found, + 'num_used': self.num_used, + 'staticfiles': used_paths, + 'staticfiles_apps': self.get_staticfiles_apps(), + 'staticfiles_dirs': self.get_staticfiles_dirs(), + 'staticfiles_finders': self.get_staticfiles_finders(), + }) + + def get_staticfiles_finders(self): + """ + Returns a sorted mapping between the finder path and the list + of relative and file system paths which that finder was able + to find. + """ + finders_mapping = OrderedDict() + for finder in finders.get_finders(): + for path, finder_storage in finder.list([]): + if getattr(finder_storage, 'prefix', None): + prefixed_path = join(finder_storage.prefix, path) + else: + prefixed_path = path + finder_cls = finder.__class__ + finder_path = '.'.join([finder_cls.__module__, + finder_cls.__name__]) + real_path = finder_storage.path(path) + payload = (prefixed_path, real_path) + finders_mapping.setdefault(finder_path, []).append(payload) + self.num_found += 1 + return finders_mapping + + def get_staticfiles_dirs(self): + """ + Returns a list of paths to inspect for additional static files + """ + dirs = getattr(settings, 'STATICFILES_DIRS', ()) + return [normpath(d) for d in dirs] + + def get_staticfiles_apps(self): + """ + Returns a list of app paths that have a static directory + """ + apps = [] + for finder in finders.get_finders(): + if isinstance(finder, finders.AppDirectoriesFinder): + for app in finder.apps: + if app not in apps: + apps.append(app) + return apps diff --git a/debug_toolbar/panels/versions.py b/debug_toolbar/panels/versions.py index 85672b811..321ba6e39 100644 --- a/debug_toolbar/panels/versions.py +++ b/debug_toolbar/panels/versions.py @@ -4,9 +4,12 @@ import django from django.conf import settings -from django.utils.translation import ugettext_lazy as _ -from django.utils.datastructures import SortedDict from django.utils.importlib import import_module +from django.utils.translation import ugettext_lazy as _ +try: + from collections import OrderedDict +except ImportError: + from django.utils.datastructures import SortedDict as OrderedDict from debug_toolbar.panels import Panel @@ -46,6 +49,6 @@ def process_response(self, request, response): versions = sorted(versions, key=lambda version: version[0]) self.record_stats({ - 'versions': SortedDict(versions), + 'versions': OrderedDict(versions), 'paths': sys.path, }) diff --git a/debug_toolbar/settings.py b/debug_toolbar/settings.py index bde8a1c53..5ed000519 100644 --- a/debug_toolbar/settings.py +++ b/debug_toolbar/settings.py @@ -76,6 +76,7 @@ 'debug_toolbar.panels.request.RequestPanel', 'debug_toolbar.panels.sql.SQLPanel', 'debug_toolbar.panels.templates.TemplatesPanel', + 'debug_toolbar.panels.staticfiles.StaticFilesPanel', 'debug_toolbar.panels.cache.CachePanel', 'debug_toolbar.panels.signals.SignalsPanel', 'debug_toolbar.panels.logging.LoggingPanel', diff --git a/debug_toolbar/templates/debug_toolbar/panels/cache.html b/debug_toolbar/templates/debug_toolbar/panels/cache.html index 595afd6b8..82ccc4923 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/cache.html +++ b/debug_toolbar/templates/debug_toolbar/panels/cache.html @@ -1,5 +1,5 @@ {% load i18n %} -

{% trans "Summary" %}

+

{% trans "Summary" %}

@@ -18,7 +18,7 @@

{% trans "Summary" %}

-

{% trans "Commands" %}

+

{% trans "Commands" %}

@@ -36,7 +36,7 @@

{% trans "Commands" %}

{% if calls %} -

{% trans "Calls" %}

+

{% trans "Calls" %}

diff --git a/debug_toolbar/templates/debug_toolbar/panels/staticfiles.html b/debug_toolbar/templates/debug_toolbar/panels/staticfiles.html new file mode 100644 index 000000000..eb413b5b3 --- /dev/null +++ b/debug_toolbar/templates/debug_toolbar/panels/staticfiles.html @@ -0,0 +1,57 @@ +{% load i18n %} +{% load static from staticfiles%} + +

{% blocktrans count staticfiles_dirs|length as dirs_count %}Static file path{% plural %}Static file paths{% endblocktrans %}

+{% if staticfiles_dirs %} +
    + {% for staticfiles_dir in staticfiles_dirs %} +
  1. {{ staticfiles_dir }}
  2. + {% endfor %} +
+{% else %} +

{% trans "None" %}

+{% endif %} + +

{% blocktrans count staticfiles_apps|length as apps_count %}Static file app{% plural %}Static file apps{% endblocktrans %}

+{% if staticfiles_apps %} +
    + {% for static_app in staticfiles_apps %} +
  1. {{ static_app }}
  2. + {% endfor %} +
+{% else %} +

{% trans "None" %}

+{% endif %} + +

{% blocktrans count staticfiles|length as staticfiles_count %}Static file{% plural %}Static files{% endblocktrans %}

+{% if staticfiles %} +
+{% for staticfile in staticfiles %} +
{{ staticfile }}
+
{{ staticfile.real_path }}
+{% endfor %} +
+{% else %} +

{% trans "None" %}

+{% endif %} + + +{% for finder, payload in staticfiles_finders.items %} +

{{ finder }} ({% blocktrans count payload|length as payload_count %}{{ payload_count }} file{% plural %}{{ payload_count }} files{% endblocktrans %})

+
+ + + + + + + + {% for path, real_path in payload %} + + + + + {% endfor %} + +
{% trans 'Path' %}{% trans 'Location' %}
{{ path }}{{ real_path }}
+{% endfor %} diff --git a/debug_toolbar/toolbar.py b/debug_toolbar/toolbar.py index 36c301aad..82851b31e 100644 --- a/debug_toolbar/toolbar.py +++ b/debug_toolbar/toolbar.py @@ -9,8 +9,11 @@ from django.conf.urls import patterns, url from django.core.exceptions import ImproperlyConfigured from django.template.loader import render_to_string -from django.utils.datastructures import SortedDict from django.utils.importlib import import_module +try: + from collections import OrderedDict +except ImportError: + from django.utils.datastructures import SortedDict as OrderedDict from debug_toolbar import settings as dt_settings @@ -20,7 +23,7 @@ class DebugToolbar(object): def __init__(self, request): self.request = request self.config = dt_settings.CONFIG.copy() - self._panels = SortedDict() + self._panels = OrderedDict() for panel_class in self.get_panel_classes(): panel_instance = panel_class(self) self._panels[panel_instance.panel_id] = panel_instance @@ -61,7 +64,7 @@ def render_toolbar(self): # Handle storing toolbars in memory and fetching them later on - _store = SortedDict() + _store = OrderedDict() def should_render_panels(self): render_panels = self.config['RENDER_PANELS'] diff --git a/debug_toolbar/utils.py b/debug_toolbar/utils.py index d84c79c4e..0aedb544e 100644 --- a/debug_toolbar/utils.py +++ b/debug_toolbar/utils.py @@ -4,6 +4,10 @@ import os.path import re import sys +try: + import threading +except ImportError: + threading = None import django from django.core.exceptions import ImproperlyConfigured @@ -199,3 +203,32 @@ def get_stack(context=1): framelist.append((frame,) + getframeinfo(frame, context)) frame = frame.f_back return framelist + + +class ThreadCollector(object): + def __init__(self): + if threading is None: + raise NotImplementedError( + "threading module is not available, " + "this panel cannot be used without it") + self.collections = {} # a dictionary that maps threads to collections + + def get_collection(self, thread=None): + """ + Returns a list of collected items for the provided thread, of if none + is provided, returns a list for the current thread. + """ + if thread is None: + thread = threading.currentThread() + if thread not in self.collections: + self.collections[thread] = [] + return self.collections[thread] + + def clear_collection(self, thread=None): + if thread is None: + thread = threading.currentThread() + if thread in self.collections: + del self.collections[thread] + + def collect(self, item, thread=None): + self.get_collection(thread).append(item) diff --git a/docs/configuration.rst b/docs/configuration.rst index b0a67b12c..88226cd2f 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -33,6 +33,7 @@ setting. The default value is:: 'debug_toolbar.panels.headers.HeadersPanel', 'debug_toolbar.panels.request.RequestPanel', 'debug_toolbar.panels.sql.SQLPanel', + 'debug_toolbar.panels.staticfiles.StaticFilesPanel', 'debug_toolbar.panels.templates.TemplatesPanel', 'debug_toolbar.panels.cache.CachePanel', 'debug_toolbar.panels.signals.SignalsPanel', diff --git a/docs/panels.rst b/docs/panels.rst index 9a6d12c57..ed7abbf74 100644 --- a/docs/panels.rst +++ b/docs/panels.rst @@ -63,6 +63,13 @@ Path: ``debug_toolbar.panels.templates.TemplatesPanel`` Templates and context used, and their template paths. +Static files +~~~~~~~~~~~~ + +Path: ``debug_toolbar.panels.staticfiles.StaticFilesPanel`` + +Used static files and their locations (via the staticfiles finders). + Cache ~~~~~ diff --git a/example/settings.py b/example/settings.py index 4c29ba9ef..867b26f0b 100644 --- a/example/settings.py +++ b/example/settings.py @@ -84,9 +84,12 @@ 'debug_toolbar.panels.request.RequestPanel', 'debug_toolbar.panels.sql.SQLPanel', 'debug_toolbar.panels.templates.TemplatesPanel', + 'debug_toolbar.panels.staticfiles.StaticFilesPanel', 'debug_toolbar.panels.cache.CachePanel', 'debug_toolbar.panels.signals.SignalsPanel', 'debug_toolbar.panels.logging.LoggingPanel', 'debug_toolbar.panels.redirects.RedirectsPanel', 'debug_toolbar.panels.profiling.ProfilingPanel', ] + +STATICFILES_DIRS = [os.path.join(BASE_DIR, 'example', 'static')] diff --git a/example/static/test.css b/example/static/test.css new file mode 100644 index 000000000..8d7d12783 --- /dev/null +++ b/example/static/test.css @@ -0,0 +1,3 @@ +body { + color: green; +} \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt index 8d7af9279..74ef847e5 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -13,6 +13,7 @@ coverage flake8 selenium tox +django-discover-runner # Documentation diff --git a/tests/additional_static/base.css b/tests/additional_static/base.css new file mode 100644 index 000000000..8d7d12783 --- /dev/null +++ b/tests/additional_static/base.css @@ -0,0 +1,3 @@ +body { + color: green; +} \ No newline at end of file diff --git a/tests/panels/test_staticfiles.py b/tests/panels/test_staticfiles.py new file mode 100644 index 000000000..70a7b96c5 --- /dev/null +++ b/tests/panels/test_staticfiles.py @@ -0,0 +1,28 @@ +# coding: utf-8 + +from __future__ import absolute_import, unicode_literals + +from django.conf import settings + +from ..base import BaseTestCase + + +class StaticFilesPanelTestCase(BaseTestCase): + + def setUp(self): + super(StaticFilesPanelTestCase, self).setUp() + self.panel = self.toolbar.get_panel_by_id('StaticFilesPanel') + + def test_default_case(self): + self.panel.process_request(self.request) + self.panel.process_response(self.request, self.response) + self.assertIn('django.contrib.staticfiles.finders.' + 'AppDirectoriesFinder', self.panel.content) + self.assertIn('django.contrib.staticfiles.finders.' + 'FileSystemFinder (1 file)', self.panel.content) + self.assertEqual(self.panel.num_used, 0) + self.assertNotEqual(self.panel.num_found, 0) + self.assertEqual(self.panel.get_staticfiles_apps(), + ['django.contrib.admin', 'debug_toolbar']) + self.assertEqual(self.panel.get_staticfiles_dirs(), + settings.STATICFILES_DIRS) diff --git a/tests/settings.py b/tests/settings.py index 24e0cd4b1..68346ff7f 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,6 +1,8 @@ """Django settings for tests.""" import os +import django + BASE_DIR = os.path.dirname(os.path.dirname(__file__)) @@ -40,6 +42,8 @@ STATIC_URL = '/static/' +STATICFILES_DIRS = [os.path.join(BASE_DIR, 'tests', 'additional_static')] + # Cache and database @@ -62,3 +66,6 @@ # Django's test client sets wsgi.multiprocess to True inappropriately 'RENDER_PANELS': False, } + +if django.VERSION[:2] < (1, 6): + TEST_RUNNER = 'discover_runner.DiscoverRunner' diff --git a/tests/tests.py b/tests/tests.py deleted file mode 100644 index 1818fe751..000000000 --- a/tests/tests.py +++ /dev/null @@ -1,13 +0,0 @@ -import django - -if django.VERSION[:2] < (1, 6): # unittest-style discovery isn't available - from .commands.test_debugsqlshell import * # noqa - from .panels.test_cache import * # noqa - from .panels.test_logging import * # noqa - from .panels.test_profiling import * # noqa - from .panels.test_redirects import * # noqa - from .panels.test_request import * # noqa - from .panels.test_sql import * # noqa - from .panels.test_template import * # noqa - from .test_integration import * # noqa - from .test_utils import * # noqa diff --git a/tox.ini b/tox.ini index 00941f49a..98c487ecb 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,7 @@ commands = make test deps = selenium sqlparse + django-discover-runner setenv = PYTHONPATH = {toxinidir} whitelist_externals = make