Skip to content

Commit dbed3e7

Browse files
committed
Merge pull request #494 from django-debug-toolbar/staticfiles
Added staticfiles panel class.
2 parents 59e4931 + bdbe57d commit dbed3e7

File tree

21 files changed

+360
-59
lines changed

21 files changed

+360
-59
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ matrix:
1616
env: DJANGO_VERSION=1.4.10
1717
install:
1818
- pip install -e .
19-
- pip install Django==$DJANGO_VERSION sqlparse
19+
- pip install Django==$DJANGO_VERSION sqlparse django-discover-runner
2020
script: make test

debug_toolbar/panels/cache.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
from django.core.cache.backends.base import BaseCache
1111
from django.dispatch import Signal
1212
from django.template import Node
13-
from django.utils.datastructures import SortedDict
1413
from django.utils.translation import ugettext_lazy as _, ungettext
14+
try:
15+
from collections import OrderedDict
16+
except ImportError:
17+
from django.utils.datastructures import SortedDict as OrderedDict
1518

1619
from debug_toolbar.panels import Panel
1720
from debug_toolbar.utils import (tidy_stacktrace, render_stacktrace,
@@ -139,7 +142,7 @@ def __init__(self, *args, **kwargs):
139142
self.hits = 0
140143
self.misses = 0
141144
self.calls = []
142-
self.counts = SortedDict((
145+
self.counts = OrderedDict((
143146
('add', 0),
144147
('get', 0),
145148
('set', 0),

debug_toolbar/panels/logging.py

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,19 @@
88
threading = None
99
from django.utils.translation import ungettext, ugettext_lazy as _
1010
from debug_toolbar.panels import Panel
11+
from debug_toolbar.utils import ThreadCollector
1112

1213
MESSAGE_IF_STRING_REPRESENTATION_INVALID = '[Could not get log message]'
1314

1415

15-
class LogCollector(object):
16-
def __init__(self):
17-
if threading is None:
18-
raise NotImplementedError(
19-
"threading module is not available, "
20-
"the logging panel cannot be used without it")
21-
self.records = {} # a dictionary that maps threads to log records
16+
class LogCollector(ThreadCollector):
2217

23-
def add_record(self, record, thread=None):
18+
def collect(self, item, thread=None):
2419
# Avoid logging SQL queries since they are already in the SQL panel
2520
# TODO: Make this check whether SQL panel is enabled
26-
if record.get('channel', '') == 'django.db.backends':
21+
if item.get('channel', '') == 'django.db.backends':
2722
return
28-
29-
self.get_records(thread).append(record)
30-
31-
def get_records(self, thread=None):
32-
"""
33-
Returns a list of records for the provided thread, of if none is provided,
34-
returns a list for the current thread.
35-
"""
36-
if thread is None:
37-
thread = threading.currentThread()
38-
if thread not in self.records:
39-
self.records[thread] = []
40-
return self.records[thread]
41-
42-
def clear_records(self, thread=None):
43-
if thread is None:
44-
thread = threading.currentThread()
45-
if thread in self.records:
46-
del self.records[thread]
23+
super(LogCollector, self).collect(item, thread)
4724

4825

4926
class ThreadTrackingHandler(logging.Handler):
@@ -65,7 +42,7 @@ def emit(self, record):
6542
'line': record.lineno,
6643
'channel': record.name,
6744
}
68-
self.collector.add_record(record)
45+
self.collector.collect(record)
6946

7047

7148
# We don't use enable/disable_instrumentation because logging is global.
@@ -96,10 +73,10 @@ def nav_subtitle(self):
9673
title = _("Log messages")
9774

9875
def process_request(self, request):
99-
collector.clear_records()
76+
collector.clear_collection()
10077

10178
def process_response(self, request, response):
102-
records = collector.get_records()
79+
records = collector.get_collection()
10380
self._records[threading.currentThread()] = records
104-
collector.clear_records()
81+
collector.clear_collection()
10582
self.record_stats({'records': records})

debug_toolbar/panels/settings.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
from django.conf import settings
44
from django.views.debug import get_safe_settings
55
from django.utils.translation import ugettext_lazy as _
6-
from django.utils.datastructures import SortedDict
6+
try:
7+
from collections import OrderedDict
8+
except ImportError:
9+
from django.utils.datastructures import SortedDict as OrderedDict
710

811
from debug_toolbar.panels import Panel
912

@@ -21,5 +24,6 @@ def title(self):
2124

2225
def process_response(self, request, response):
2326
self.record_stats({
24-
'settings': SortedDict(sorted(get_safe_settings().items(), key=lambda s: s[0])),
27+
'settings': OrderedDict(sorted(get_safe_settings().items(),
28+
key=lambda s: s[0])),
2529
})

debug_toolbar/panels/staticfiles.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
from __future__ import absolute_import, unicode_literals
2+
from os.path import normpath, join
3+
try:
4+
import threading
5+
except ImportError:
6+
threading = None
7+
8+
from django.conf import settings
9+
from django.core.exceptions import ImproperlyConfigured
10+
from django.core.files.storage import get_storage_class
11+
from django.contrib.staticfiles import finders, storage
12+
from django.contrib.staticfiles.templatetags import staticfiles
13+
14+
from django.utils.encoding import python_2_unicode_compatible
15+
from django.utils.functional import LazyObject
16+
from django.utils.translation import ungettext, ugettext_lazy as _
17+
try:
18+
from collections import OrderedDict
19+
except ImportError:
20+
from django.utils.datastructures import SortedDict as OrderedDict
21+
22+
from debug_toolbar import panels
23+
from debug_toolbar.utils import ThreadCollector
24+
25+
26+
@python_2_unicode_compatible
27+
class StaticFile(object):
28+
"""
29+
Representing the different properties of a static file.
30+
"""
31+
def __init__(self, path):
32+
self.path = path
33+
34+
def __str__(self):
35+
return self.path
36+
37+
def real_path(self):
38+
return finders.find(self.path)
39+
40+
def url(self):
41+
return storage.staticfiles_storage.url(self.path)
42+
43+
44+
class FileCollector(ThreadCollector):
45+
46+
def collect(self, path, thread=None):
47+
# handle the case of {% static "admin/" %}
48+
if path.endswith('/'):
49+
return
50+
super(FileCollector, self).collect(StaticFile(path), thread)
51+
52+
53+
collector = FileCollector()
54+
55+
56+
class DebugConfiguredStorage(LazyObject):
57+
"""
58+
A staticfiles storage class to be used for collecting which paths
59+
are resolved by using the {% static %} template tag (which uses the
60+
`url` method).
61+
"""
62+
def _setup(self):
63+
64+
configured_storage_cls = get_storage_class(settings.STATICFILES_STORAGE)
65+
66+
class DebugStaticFilesStorage(configured_storage_cls):
67+
68+
def __init__(self, collector, *args, **kwargs):
69+
super(DebugStaticFilesStorage, self).__init__(*args, **kwargs)
70+
self.collector = collector
71+
72+
def url(self, path):
73+
self.collector.collect(path)
74+
return super(DebugStaticFilesStorage, self).url(path)
75+
76+
self._wrapped = DebugStaticFilesStorage(collector)
77+
78+
_original_storage = storage.staticfiles_storage
79+
80+
81+
class StaticFilesPanel(panels.Panel):
82+
"""
83+
A panel to display the found staticfiles.
84+
"""
85+
name = 'Static files'
86+
template = 'debug_toolbar/panels/staticfiles.html'
87+
88+
@property
89+
def title(self):
90+
return (_("Static files (%(num_found)s found, %(num_used)s used)") %
91+
{'num_found': self.num_found, 'num_used': self.num_used})
92+
93+
def __init__(self, *args, **kwargs):
94+
super(StaticFilesPanel, self).__init__(*args, **kwargs)
95+
self.num_found = 0
96+
self._paths = {}
97+
98+
def enable_instrumentation(self):
99+
storage.staticfiles_storage = staticfiles.staticfiles_storage = DebugConfiguredStorage()
100+
101+
def disable_instrumentation(self):
102+
storage.staticfiles_storage = staticfiles.staticfiles_storage = _original_storage
103+
104+
@property
105+
def has_content(self):
106+
if "django.contrib.staticfiles" not in settings.INSTALLED_APPS:
107+
raise ImproperlyConfigured("Could not find staticfiles in "
108+
"INSTALLED_APPS setting.")
109+
return True
110+
111+
@property
112+
def num_used(self):
113+
return len(self._paths[threading.currentThread()])
114+
115+
nav_title = _('Static files')
116+
117+
@property
118+
def nav_subtitle(self):
119+
num_used = self.num_used
120+
return ungettext("%(num_used)s file used",
121+
"%(num_used)s files used",
122+
num_used) % {'num_used': num_used}
123+
124+
def process_request(self, request):
125+
collector.clear_collection()
126+
127+
def process_response(self, request, response):
128+
used_paths = collector.get_collection()
129+
self._paths[threading.currentThread()] = used_paths
130+
131+
self.record_stats({
132+
'num_found': self.num_found,
133+
'num_used': self.num_used,
134+
'staticfiles': used_paths,
135+
'staticfiles_apps': self.get_staticfiles_apps(),
136+
'staticfiles_dirs': self.get_staticfiles_dirs(),
137+
'staticfiles_finders': self.get_staticfiles_finders(),
138+
})
139+
140+
def get_staticfiles_finders(self):
141+
"""
142+
Returns a sorted mapping between the finder path and the list
143+
of relative and file system paths which that finder was able
144+
to find.
145+
"""
146+
finders_mapping = OrderedDict()
147+
for finder in finders.get_finders():
148+
for path, finder_storage in finder.list([]):
149+
if getattr(finder_storage, 'prefix', None):
150+
prefixed_path = join(finder_storage.prefix, path)
151+
else:
152+
prefixed_path = path
153+
finder_cls = finder.__class__
154+
finder_path = '.'.join([finder_cls.__module__,
155+
finder_cls.__name__])
156+
real_path = finder_storage.path(path)
157+
payload = (prefixed_path, real_path)
158+
finders_mapping.setdefault(finder_path, []).append(payload)
159+
self.num_found += 1
160+
return finders_mapping
161+
162+
def get_staticfiles_dirs(self):
163+
"""
164+
Returns a list of paths to inspect for additional static files
165+
"""
166+
dirs = getattr(settings, 'STATICFILES_DIRS', ())
167+
return [normpath(d) for d in dirs]
168+
169+
def get_staticfiles_apps(self):
170+
"""
171+
Returns a list of app paths that have a static directory
172+
"""
173+
apps = []
174+
for finder in finders.get_finders():
175+
if isinstance(finder, finders.AppDirectoriesFinder):
176+
for app in finder.apps:
177+
if app not in apps:
178+
apps.append(app)
179+
return apps

debug_toolbar/panels/versions.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
import django
66
from django.conf import settings
7-
from django.utils.translation import ugettext_lazy as _
8-
from django.utils.datastructures import SortedDict
97
from django.utils.importlib import import_module
8+
from django.utils.translation import ugettext_lazy as _
9+
try:
10+
from collections import OrderedDict
11+
except ImportError:
12+
from django.utils.datastructures import SortedDict as OrderedDict
1013

1114
from debug_toolbar.panels import Panel
1215

@@ -46,6 +49,6 @@ def process_response(self, request, response):
4649
versions = sorted(versions, key=lambda version: version[0])
4750

4851
self.record_stats({
49-
'versions': SortedDict(versions),
52+
'versions': OrderedDict(versions),
5053
'paths': sys.path,
5154
})

debug_toolbar/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
'debug_toolbar.panels.request.RequestPanel',
7777
'debug_toolbar.panels.sql.SQLPanel',
7878
'debug_toolbar.panels.templates.TemplatesPanel',
79+
'debug_toolbar.panels.staticfiles.StaticFilesPanel',
7980
'debug_toolbar.panels.cache.CachePanel',
8081
'debug_toolbar.panels.signals.SignalsPanel',
8182
'debug_toolbar.panels.logging.LoggingPanel',

debug_toolbar/templates/debug_toolbar/panels/cache.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% load i18n %}
2-
<h3>{% trans "Summary" %}</h3>
2+
<h4>{% trans "Summary" %}</h4>
33
<table>
44
<thead>
55
<tr>
@@ -18,7 +18,7 @@ <h3>{% trans "Summary" %}</h3>
1818
</tr>
1919
</tbody>
2020
</table>
21-
<h3>{% trans "Commands" %}</h3>
21+
<h4>{% trans "Commands" %}</h4>
2222
<table>
2323
<thead>
2424
<tr>
@@ -36,7 +36,7 @@ <h3>{% trans "Commands" %}</h3>
3636
</tbody>
3737
</table>
3838
{% if calls %}
39-
<h3>{% trans "Calls" %}</h3>
39+
<h4>{% trans "Calls" %}</h4>
4040
<table>
4141
<thead>
4242
<tr>

0 commit comments

Comments
 (0)