-
-
Notifications
You must be signed in to change notification settings - Fork 1k
/
staticfiles.py
179 lines (145 loc) · 5.98 KB
/
staticfiles.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import contextlib
from contextvars import ContextVar
from os.path import join, normpath
from django.conf import settings
from django.contrib.staticfiles import finders, storage
from django.utils.functional import LazyObject
from django.utils.translation import gettext_lazy as _, ngettext
from debug_toolbar import panels
class StaticFile:
"""
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)
# This will collect the StaticFile instances across threads.
used_static_files = ContextVar("djdt_static_used_static_files")
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):
try:
# From Django 4.2 use django.core.files.storage.storages in favor
# of the deprecated django.core.files.storage.get_storage_class
from django.core.files.storage import storages
configured_storage_cls = storages["staticfiles"].__class__
except ImportError:
# Backwards compatibility for Django versions prior to 4.2
from django.core.files.storage import get_storage_class
configured_storage_cls = get_storage_class(settings.STATICFILES_STORAGE)
class DebugStaticFilesStorage(configured_storage_cls):
def url(self, path):
with contextlib.suppress(LookupError):
# For LookupError:
# The ContextVar wasn't set yet. Since the toolbar wasn't properly
# configured to handle this request, we don't need to capture
# the static file.
used_static_files.get().append(StaticFile(path))
return super().url(path)
self._wrapped = DebugStaticFilesStorage()
_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().__init__(*args, **kwargs)
self.num_found = 0
self.used_paths = []
def enable_instrumentation(self):
storage.staticfiles_storage = DebugConfiguredStorage()
def disable_instrumentation(self):
storage.staticfiles_storage = _original_storage
@property
def num_used(self):
stats = self.get_stats()
return stats and stats["num_used"]
nav_title = _("Static files")
@property
def nav_subtitle(self):
num_used = self.num_used
return ngettext(
"%(num_used)s file used", "%(num_used)s files used", num_used
) % {"num_used": num_used}
def process_request(self, request):
reset_token = used_static_files.set([])
response = super().process_request(request)
# Make a copy of the used paths so that when the
# ContextVar is reset, our panel still has the data.
self.used_paths = used_static_files.get().copy()
# Reset the ContextVar to be empty again, removing the reference
# to the list of used files.
used_static_files.reset(reset_token)
return response
def generate_stats(self, request, response):
self.record_stats(
{
"num_found": self.num_found,
"num_used": len(self.used_paths),
"staticfiles": self.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 = {}
for finder in finders.get_finders():
try:
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
except OSError:
# This error should be captured and presented as a part of run_checks.
pass
return finders_mapping
def get_staticfiles_dirs(self):
"""
Returns a list of paths to inspect for additional static files
"""
dirs = []
for finder in finders.get_finders():
if isinstance(finder, finders.FileSystemFinder):
dirs.extend(finder.locations)
return [(prefix, normpath(dir)) for prefix, dir 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