Skip to content

Commit 184992f

Browse files
committed
Added staticfiles panel class.
1 parent e334bf6 commit 184992f

File tree

7 files changed

+250
-35
lines changed

7 files changed

+250
-35
lines changed

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})
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
from __future__ import absolute_import
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.translation import ungettext, ugettext_lazy as _
15+
from django.utils.datastructures import SortedDict
16+
from django.utils.functional import LazyObject
17+
18+
from debug_toolbar import panels
19+
from debug_toolbar.utils import ThreadCollector
20+
21+
22+
class StaticFile(object):
23+
24+
def __init__(self, path):
25+
self.path = path
26+
27+
def __unicode__(self):
28+
return self.path
29+
30+
def real_path(self):
31+
return finders.find(self.path)
32+
33+
def url(self):
34+
return storage.staticfiles_storage.url(self.path)
35+
36+
37+
class FileCollector(ThreadCollector):
38+
39+
def collect(self, path, thread=None):
40+
# handle the case of {% static "admin/" %}
41+
if path.endswith('/'):
42+
return
43+
super(FileCollector, self).collect(StaticFile(path), thread)
44+
45+
46+
collector = FileCollector()
47+
48+
49+
class DebugConfiguredStorage(LazyObject):
50+
def _setup(self):
51+
52+
configured_storage_cls = get_storage_class(settings.STATICFILES_STORAGE)
53+
54+
class DebugStaticFilesStorage(configured_storage_cls):
55+
56+
def __init__(self, collector, *args, **kwargs):
57+
super(DebugStaticFilesStorage, self).__init__(*args, **kwargs)
58+
self.collector = collector
59+
60+
def url(self, path):
61+
self.collector.collect(path)
62+
return super(DebugStaticFilesStorage, self).url(path)
63+
64+
self._wrapped = DebugStaticFilesStorage(collector)
65+
66+
storage.staticfiles_storage = staticfiles.staticfiles_storage = DebugConfiguredStorage()
67+
68+
69+
class StaticFilesPanel(panels.Panel):
70+
"""
71+
A panel to display the found staticfiles.
72+
"""
73+
name = 'Static files'
74+
template = 'debug_toolbar/panels/staticfiles.html'
75+
76+
@property
77+
def title(self):
78+
return (_("Static files (%(num_found)s found)") %
79+
{'num_found': self.num_found, 'num_used': self.num_used})
80+
81+
def __init__(self, *args, **kwargs):
82+
super(StaticFilesPanel, self).__init__(*args, **kwargs)
83+
self.num_found = 0
84+
self.ignore_patterns = []
85+
self._paths = {}
86+
87+
@property
88+
def has_content(self):
89+
if "django.contrib.staticfiles" not in settings.INSTALLED_APPS:
90+
raise ImproperlyConfigured("Could not find staticfiles in "
91+
"INSTALLED_APPS setting.")
92+
return True
93+
94+
@property
95+
def num_used(self):
96+
return len(self._paths[threading.currentThread()])
97+
98+
nav_title = _('Static files')
99+
100+
@property
101+
def nav_subtitle(self):
102+
num_used = self.num_used
103+
return ungettext("%(num_used)s file used", "%(num_used)s files used",
104+
num_used) % {'num_used': num_used}
105+
106+
def process_request(self, request):
107+
collector.clear_collection()
108+
109+
def process_response(self, request, response):
110+
staticfiles_finders = SortedDict()
111+
for finder in finders.get_finders():
112+
for path, finder_storage in finder.list(self.ignore_patterns):
113+
if getattr(finder_storage, 'prefix', None):
114+
prefixed_path = join(finder_storage.prefix, path)
115+
else:
116+
prefixed_path = path
117+
finder_path = '.'.join([finder.__class__.__module__,
118+
finder.__class__.__name__])
119+
real_path = finder_storage.path(path)
120+
payload = (prefixed_path, real_path)
121+
staticfiles_finders.setdefault(finder_path, []).append(payload)
122+
self.num_found += 1
123+
124+
dirs = getattr(settings, 'STATICFILES_DIRS', ())
125+
126+
used_paths = collector.get_collection()
127+
self._paths[threading.currentThread()] = used_paths
128+
129+
self.record_stats({
130+
'num_found': self.num_found,
131+
'num_used': self.num_used,
132+
'staticfiles': used_paths,
133+
'staticfiles_apps': self.get_static_apps(),
134+
'staticfiles_dirs': [normpath(d) for d in dirs],
135+
'staticfiles_finders': staticfiles_finders,
136+
})
137+
138+
def get_static_apps(self):
139+
for finder in finders.get_finders():
140+
if isinstance(finder, finders.AppDirectoriesFinder):
141+
return finder.apps
142+
return []

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>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{% load i18n %}
2+
{% load static from staticfiles%}
3+
4+
<h4>{% blocktrans count staticfiles_dirs|length as dirs_count %}Static file path{% plural %}Static file paths{% endblocktrans %}</h4>
5+
{% if staticfiles_dirs %}
6+
<ol>
7+
{% for staticfiles_dir in staticfiles_dirs %}
8+
<li>{{ staticfiles_dir }}</li>
9+
{% endfor %}
10+
</ol>
11+
{% else %}
12+
<p>{% trans "None" %}</p>
13+
{% endif %}
14+
15+
<h4>{% blocktrans count staticfiles_apps|length as apps_count %}Static file app{% plural %}Static file apps{% endblocktrans %}</h4>
16+
{% if staticfiles_apps %}
17+
<ol>
18+
{% for static_app in staticfiles_apps %}
19+
<li>{{ static_app }}</li>
20+
{% endfor %}
21+
</ol>
22+
{% else %}
23+
<p>{% trans "None" %}</p>
24+
{% endif %}
25+
26+
<h4>{% blocktrans count staticfiles|length as staticfiles_count %}Static file{% plural %}Static files{% endblocktrans %}</h4>
27+
{% if staticfiles %}
28+
<dl>
29+
{% for staticfile in staticfiles %}
30+
<dt><strong><a class="toggleTemplate" href="{{ staticfile.url }}">{{ staticfile }}</a></strong></dt>
31+
<dd><samp>{{ staticfile.real_path }}</samp></dd>
32+
{% endfor %}
33+
</dl>
34+
{% else %}
35+
<p>{% trans "None" %}</p>
36+
{% endif %}
37+
38+
39+
{% for finder, payload in staticfiles_finders.items %}
40+
<h4>{{ finder }} ({{ payload|length }} files)</h4>
41+
<table>
42+
<thead>
43+
<tr>
44+
<th>{% trans 'Path' %}</th>
45+
<th>{% trans 'Location' %}</th>
46+
</tr>
47+
</thead>
48+
<tbody>
49+
{% for path, real_path in payload %}
50+
<tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}">
51+
<td>{{ path }}</td>
52+
<td>{{ real_path }}</td>
53+
</tr>
54+
{% endfor %}
55+
</tbody>
56+
</table>
57+
{% endfor %}

debug_toolbar/utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
import os.path
55
import re
66
import sys
7+
try:
8+
import threading
9+
except ImportError:
10+
threading = None
711

812
import django
913
from django.core.exceptions import ImproperlyConfigured
@@ -199,3 +203,32 @@ def get_stack(context=1):
199203
framelist.append((frame,) + getframeinfo(frame, context))
200204
frame = frame.f_back
201205
return framelist
206+
207+
208+
class ThreadCollector(object):
209+
def __init__(self):
210+
if threading is None:
211+
raise NotImplementedError(
212+
"threading module is not available, "
213+
"this panel cannot be used without it")
214+
self.collections = {} # a dictionary that maps threads to collections
215+
216+
def get_collection(self, thread=None):
217+
"""
218+
Returns a list of collected items for the provided thread, of if none
219+
is provided, returns a list for the current thread.
220+
"""
221+
if thread is None:
222+
thread = threading.currentThread()
223+
if thread not in self.collections:
224+
self.collections[thread] = []
225+
return self.collections[thread]
226+
227+
def clear_collection(self, thread=None):
228+
if thread is None:
229+
thread = threading.currentThread()
230+
if thread in self.collections:
231+
del self.collections[thread]
232+
233+
def collect(self, item, thread=None):
234+
self.get_collection(thread).append(item)

example/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,7 @@
8989
'debug_toolbar.panels.logging.LoggingPanel',
9090
'debug_toolbar.panels.redirects.RedirectsPanel',
9191
'debug_toolbar.panels.profiling.ProfilingPanel',
92+
'debug_toolbar.panels.staticfiles.StaticFilesPanel',
9293
]
94+
95+
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'example', 'static')]

example/static/test.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
body {
2+
color: green;
3+
}

0 commit comments

Comments
 (0)