django-spire 0.16.13__py3-none-any.whl → 0.17.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_spire/consts.py +1 -1
- django_spire/core/management/commands/spire_startapp.py +84 -46
- django_spire/core/management/commands/spire_startapp_pkg/__init__.py +60 -0
- django_spire/core/management/commands/spire_startapp_pkg/builder.py +91 -0
- django_spire/core/management/commands/spire_startapp_pkg/config.py +115 -0
- django_spire/core/management/commands/spire_startapp_pkg/filesystem.py +125 -0
- django_spire/core/management/commands/spire_startapp_pkg/generator.py +167 -0
- django_spire/core/management/commands/spire_startapp_pkg/maps.py +783 -25
- django_spire/core/management/commands/spire_startapp_pkg/permissions.py +147 -0
- django_spire/core/management/commands/spire_startapp_pkg/processor.py +144 -57
- django_spire/core/management/commands/spire_startapp_pkg/registry.py +89 -0
- django_spire/core/management/commands/spire_startapp_pkg/reporter.py +245 -108
- django_spire/core/management/commands/spire_startapp_pkg/resolver.py +86 -0
- django_spire/core/management/commands/spire_startapp_pkg/user_input.py +252 -0
- django_spire/core/management/commands/spire_startapp_pkg/validator.py +96 -0
- django_spire/core/middleware/__init__.py +1 -2
- django_spire/profiling/__init__.py +13 -0
- django_spire/profiling/middleware/__init__.py +6 -0
- django_spire/{core → profiling}/middleware/profiling.py +63 -58
- django_spire/profiling/panel.py +345 -0
- django_spire/profiling/templates/panel.html +166 -0
- {django_spire-0.16.13.dist-info → django_spire-0.17.0.dist-info}/METADATA +1 -1
- {django_spire-0.16.13.dist-info → django_spire-0.17.0.dist-info}/RECORD +26 -23
- django_spire/core/management/commands/spire_startapp_pkg/constants.py +0 -4
- django_spire/core/management/commands/spire_startapp_pkg/manager.py +0 -176
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_detail_card.html +0 -24
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_form_card.html +0 -9
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_list_card.html +0 -18
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/form/spirechildapp_form.html +0 -22
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/item/spirechildapp_item.html +0 -24
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_detail_page.html +0 -13
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_form_page.html +0 -13
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_list_page.html +0 -9
- {django_spire-0.16.13.dist-info → django_spire-0.17.0.dist-info}/WHEEL +0 -0
- {django_spire-0.16.13.dist-info → django_spire-0.17.0.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.16.13.dist-info → django_spire-0.17.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from dataclasses import asdict, dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing_extensions import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from debug_toolbar.panels import Panel
|
|
12
|
+
from django.conf import settings
|
|
13
|
+
from django.http import Http404, HttpRequest, HttpResponse, JsonResponse
|
|
14
|
+
from django.urls import re_path
|
|
15
|
+
from django.utils import timezone
|
|
16
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
17
|
+
|
|
18
|
+
from django_spire.profiling import lock
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from typing_extensions import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Profile:
|
|
26
|
+
filename: str
|
|
27
|
+
path: str
|
|
28
|
+
method: str
|
|
29
|
+
duration: str
|
|
30
|
+
profile_id: str
|
|
31
|
+
size: str
|
|
32
|
+
modified: str
|
|
33
|
+
timestamp: float
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ProfileStats:
|
|
38
|
+
profiles: list[Profile]
|
|
39
|
+
count: int
|
|
40
|
+
directory: str
|
|
41
|
+
total_size: str
|
|
42
|
+
enabled: bool
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ProfileList:
|
|
47
|
+
profiles: list[dict[str, Any]]
|
|
48
|
+
count: int
|
|
49
|
+
total_size: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ProfilingPanel(Panel):
|
|
53
|
+
nav_title = 'Profiles'
|
|
54
|
+
template = 'panel.html'
|
|
55
|
+
title = 'Profiling'
|
|
56
|
+
|
|
57
|
+
def __init__(self, *args, **kwargs):
|
|
58
|
+
super().__init__(*args, **kwargs)
|
|
59
|
+
|
|
60
|
+
self.directory = self._get_directory()
|
|
61
|
+
|
|
62
|
+
def _format_size(self, size: int) -> str:
|
|
63
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
64
|
+
if size < 1024.0:
|
|
65
|
+
return f'{size:.1f} {unit}'
|
|
66
|
+
|
|
67
|
+
size /= 1024.0
|
|
68
|
+
|
|
69
|
+
return f'{size:.1f} TB'
|
|
70
|
+
|
|
71
|
+
def _get_directory(self) -> Path:
|
|
72
|
+
location = os.getenv('PROFILING_DIR', '.profile')
|
|
73
|
+
|
|
74
|
+
if isinstance(location, str):
|
|
75
|
+
if not Path(location).is_absolute():
|
|
76
|
+
current = Path.cwd()
|
|
77
|
+
base = getattr(settings, 'BASE_DIR', current)
|
|
78
|
+
location = Path(base) / location
|
|
79
|
+
else:
|
|
80
|
+
location = Path(location)
|
|
81
|
+
|
|
82
|
+
return Path(location)
|
|
83
|
+
|
|
84
|
+
def _get_files(self) -> list[Profile]:
|
|
85
|
+
profiles = []
|
|
86
|
+
|
|
87
|
+
if not self.directory.exists():
|
|
88
|
+
return profiles
|
|
89
|
+
|
|
90
|
+
with lock:
|
|
91
|
+
files = self._list_files()
|
|
92
|
+
|
|
93
|
+
for file in files:
|
|
94
|
+
if not file.exists():
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
profile = self._parse_file(file)
|
|
98
|
+
|
|
99
|
+
if profile:
|
|
100
|
+
profiles.append(profile)
|
|
101
|
+
|
|
102
|
+
return profiles
|
|
103
|
+
|
|
104
|
+
def _get_size(self) -> int:
|
|
105
|
+
total = 0
|
|
106
|
+
|
|
107
|
+
if self.directory.exists():
|
|
108
|
+
with lock:
|
|
109
|
+
for file in self.directory.glob('*.html'):
|
|
110
|
+
if file.exists():
|
|
111
|
+
total = total + file.stat().st_size
|
|
112
|
+
|
|
113
|
+
return total
|
|
114
|
+
|
|
115
|
+
def _list_files(self) -> list[Path]:
|
|
116
|
+
files = [
|
|
117
|
+
file for file in self.directory.glob('*.html')
|
|
118
|
+
if file.exists()
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
files.sort(
|
|
122
|
+
key=lambda p: p.stat().st_mtime if p.exists() else 0,
|
|
123
|
+
reverse=True
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return files
|
|
127
|
+
|
|
128
|
+
def _parse_file(self, file: Path) -> Profile | None:
|
|
129
|
+
stat = file.stat()
|
|
130
|
+
parts = file.stem.split('_')
|
|
131
|
+
|
|
132
|
+
timestamp, method, path_part, duration, profile_id = self._parse_filename(
|
|
133
|
+
parts,
|
|
134
|
+
stat
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
display = self._parse_path(path_part)
|
|
138
|
+
|
|
139
|
+
mst = timezone.get_fixed_timezone(-360)
|
|
140
|
+
modified = datetime.fromtimestamp(timestamp, tz=mst)
|
|
141
|
+
|
|
142
|
+
return Profile(
|
|
143
|
+
filename=file.name,
|
|
144
|
+
path=display,
|
|
145
|
+
method=method,
|
|
146
|
+
duration=duration,
|
|
147
|
+
profile_id=profile_id,
|
|
148
|
+
size=self._format_size(stat.st_size),
|
|
149
|
+
modified=modified.strftime('%b %d, %Y %I:%M %p MST'),
|
|
150
|
+
timestamp=timestamp
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def _parse_filename(self, parts: list[str], stat: Any) -> tuple[float, str, str, str, str]:
|
|
154
|
+
if len(parts) >= 4:
|
|
155
|
+
timestamp = int(parts[0]) / 1000
|
|
156
|
+
method = parts[1]
|
|
157
|
+
path_part = '_'.join(parts[2:-2])
|
|
158
|
+
duration = parts[-2] if len(parts) > 3 else 'N/A'
|
|
159
|
+
profile_id = parts[-1] if len(parts) > 4 else 'N/A'
|
|
160
|
+
else:
|
|
161
|
+
timestamp = stat.st_mtime
|
|
162
|
+
method = 'Unknown'
|
|
163
|
+
path_part = '_'.join(parts)
|
|
164
|
+
duration = 'N/A'
|
|
165
|
+
profile_id = 'N/A'
|
|
166
|
+
|
|
167
|
+
return timestamp, method, path_part, duration, profile_id
|
|
168
|
+
|
|
169
|
+
def _parse_path(self, path_part: str) -> str:
|
|
170
|
+
if path_part == 'root':
|
|
171
|
+
return '/'
|
|
172
|
+
|
|
173
|
+
display = path_part.replace('_', '/')
|
|
174
|
+
|
|
175
|
+
if not display.startswith('/'):
|
|
176
|
+
display = '/' + display
|
|
177
|
+
|
|
178
|
+
return display
|
|
179
|
+
|
|
180
|
+
def _resolve_location(self) -> Path:
|
|
181
|
+
location = os.getenv('PROFILING_DIR', '.profile')
|
|
182
|
+
|
|
183
|
+
if not Path(location).is_absolute():
|
|
184
|
+
base = getattr(settings, 'BASE_DIR', Path.cwd())
|
|
185
|
+
location = Path(base) / location
|
|
186
|
+
else:
|
|
187
|
+
location = Path(location)
|
|
188
|
+
|
|
189
|
+
return location
|
|
190
|
+
|
|
191
|
+
def _try_delete(self, filepath: Path) -> JsonResponse | None:
|
|
192
|
+
for i in range(3):
|
|
193
|
+
try:
|
|
194
|
+
filepath.unlink()
|
|
195
|
+
except FileNotFoundError:
|
|
196
|
+
return JsonResponse({'success': True, 'is_deleted': True})
|
|
197
|
+
except PermissionError:
|
|
198
|
+
if i < 2:
|
|
199
|
+
time.sleep(0.1)
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
return JsonResponse(
|
|
203
|
+
{'error': 'File is in use, try again'},
|
|
204
|
+
status=409
|
|
205
|
+
)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
return JsonResponse({'error': str(e)}, status=500)
|
|
208
|
+
else:
|
|
209
|
+
return JsonResponse({'success': True})
|
|
210
|
+
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def _validate_filename(self, filename: str) -> str | None:
|
|
214
|
+
if '..' in filename or '/' in filename or '\\' in filename:
|
|
215
|
+
return 'Invalid filename'
|
|
216
|
+
|
|
217
|
+
if not filename.endswith('.html'):
|
|
218
|
+
return 'Only HTML files are allowed'
|
|
219
|
+
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
def _validate_filepath(self, filepath: Path, location: Path) -> str | None:
|
|
223
|
+
if not filepath.is_file():
|
|
224
|
+
return 'Not a file'
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
filepath.resolve().relative_to(location.resolve())
|
|
228
|
+
except ValueError:
|
|
229
|
+
return 'Invalid file path'
|
|
230
|
+
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def delete_file(cls, request: HttpRequest, filename: str) -> JsonResponse:
|
|
235
|
+
panel = cls(None, None)
|
|
236
|
+
error = panel._validate_filename(filename)
|
|
237
|
+
|
|
238
|
+
if error:
|
|
239
|
+
return JsonResponse({'error': error}, status=400)
|
|
240
|
+
|
|
241
|
+
location = panel._resolve_location()
|
|
242
|
+
filepath = location / filename
|
|
243
|
+
|
|
244
|
+
with lock:
|
|
245
|
+
if not filepath.exists():
|
|
246
|
+
return JsonResponse({'success': True, 'is_deleted': True})
|
|
247
|
+
|
|
248
|
+
error = panel._validate_filepath(filepath, location)
|
|
249
|
+
|
|
250
|
+
if error:
|
|
251
|
+
return JsonResponse({'error': error}, status=400)
|
|
252
|
+
|
|
253
|
+
result = panel._try_delete(filepath)
|
|
254
|
+
|
|
255
|
+
if result:
|
|
256
|
+
return result
|
|
257
|
+
|
|
258
|
+
return JsonResponse({'error': 'Could not delete file'}, status=500)
|
|
259
|
+
|
|
260
|
+
def generate_stats(self, request: HttpRequest, response: HttpResponse) -> None:
|
|
261
|
+
profiles = self._get_files()
|
|
262
|
+
profiling = os.getenv('PROFILING_ENABLED', 'False') == 'True'
|
|
263
|
+
|
|
264
|
+
stats = ProfileStats(
|
|
265
|
+
profiles=[asdict(profile) for profile in profiles],
|
|
266
|
+
count=len(profiles),
|
|
267
|
+
directory=str(self.directory),
|
|
268
|
+
total_size=self._format_size(self._get_size()),
|
|
269
|
+
enabled=profiling
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
self.record_stats(asdict(stats))
|
|
273
|
+
|
|
274
|
+
@classmethod
|
|
275
|
+
def get_urls(cls) -> list:
|
|
276
|
+
return [
|
|
277
|
+
re_path(
|
|
278
|
+
r'^profiling/view/(?P<filename>[^/]+)/$',
|
|
279
|
+
cls.serve_file,
|
|
280
|
+
name='profiling_view'
|
|
281
|
+
),
|
|
282
|
+
re_path(
|
|
283
|
+
r'^profiling/delete/(?P<filename>[^/]+)/$',
|
|
284
|
+
csrf_exempt(cls.delete_file),
|
|
285
|
+
name='profiling_delete'
|
|
286
|
+
),
|
|
287
|
+
re_path(
|
|
288
|
+
r'^profiling/list/$',
|
|
289
|
+
cls.list_files,
|
|
290
|
+
name='profiling_list'
|
|
291
|
+
),
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def list_files(cls, request: HttpRequest) -> JsonResponse:
|
|
296
|
+
panel = cls(None, None)
|
|
297
|
+
panel.directory = panel._resolve_location()
|
|
298
|
+
profiles = panel._get_files()
|
|
299
|
+
|
|
300
|
+
response = ProfileList(
|
|
301
|
+
profiles=[asdict(profile) for profile in profiles],
|
|
302
|
+
count=len(profiles),
|
|
303
|
+
total_size=panel._format_size(panel._get_size())
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return JsonResponse(asdict(response))
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def nav_subtitle(self) -> str:
|
|
310
|
+
count = 0
|
|
311
|
+
|
|
312
|
+
if self.directory.exists():
|
|
313
|
+
with lock:
|
|
314
|
+
count = sum(
|
|
315
|
+
1 for f in self.directory.glob('*.html')
|
|
316
|
+
if f.exists()
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
return f'{count} profiles'
|
|
320
|
+
|
|
321
|
+
@classmethod
|
|
322
|
+
def serve_file(cls, request: HttpRequest, filename: str) -> HttpResponse:
|
|
323
|
+
panel = cls(None, None)
|
|
324
|
+
error = panel._validate_filename(filename)
|
|
325
|
+
|
|
326
|
+
if error:
|
|
327
|
+
raise Http404(error)
|
|
328
|
+
|
|
329
|
+
location = panel._resolve_location()
|
|
330
|
+
filepath = location / filename
|
|
331
|
+
|
|
332
|
+
with lock:
|
|
333
|
+
if not filepath.exists():
|
|
334
|
+
message = 'Profile has been removed'
|
|
335
|
+
raise Http404(message)
|
|
336
|
+
|
|
337
|
+
error = panel._validate_filepath(filepath, location)
|
|
338
|
+
|
|
339
|
+
if error:
|
|
340
|
+
raise Http404(error)
|
|
341
|
+
|
|
342
|
+
with open(filepath, 'r', encoding='utf-8') as handle:
|
|
343
|
+
content = handle.read()
|
|
344
|
+
|
|
345
|
+
return HttpResponse(content, content_type='text/html')
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
|
|
3
|
+
<style>
|
|
4
|
+
.djdt-profiling-panel {
|
|
5
|
+
padding: 10px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.djdt-profiling-stats {
|
|
9
|
+
display: flex;
|
|
10
|
+
gap: 30px;
|
|
11
|
+
margin-bottom: 30px;
|
|
12
|
+
align-items: center;
|
|
13
|
+
padding-bottom: 20px;
|
|
14
|
+
border-bottom: 1px solid #e0e0e0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.djdt-profiling-stat {
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: 10px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.djdt-profiling-stat-label {
|
|
24
|
+
opacity: 0.7;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.djdt-profiling-stat-value {
|
|
28
|
+
font-weight: bold;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.djdt-profiling-table {
|
|
32
|
+
width: 100%;
|
|
33
|
+
border-collapse: collapse;
|
|
34
|
+
font-size: 0.9em;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.djdt-profiling-table th {
|
|
38
|
+
padding: 8px;
|
|
39
|
+
text-align: left;
|
|
40
|
+
font-weight: bold;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.djdt-profiling-table td {
|
|
44
|
+
padding: 8px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.djdt-profile-link {
|
|
48
|
+
text-decoration: none;
|
|
49
|
+
font-weight: bold;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.djdt-profile-link:hover {
|
|
53
|
+
text-decoration: underline;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.djdt-profile-delete {
|
|
57
|
+
text-decoration: none;
|
|
58
|
+
font-weight: bold;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.djdt-profile-delete:hover {
|
|
62
|
+
text-decoration: underline;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.djdt-no-profiles {
|
|
66
|
+
padding: 20px;
|
|
67
|
+
text-align: center;
|
|
68
|
+
font-style: italic;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.djdt-circular-buffer-note {
|
|
72
|
+
padding: 8px;
|
|
73
|
+
border-radius: 3px;
|
|
74
|
+
margin-bottom: 10px;
|
|
75
|
+
font-size: 3.85em;
|
|
76
|
+
}
|
|
77
|
+
</style>
|
|
78
|
+
|
|
79
|
+
<div class="djdt-profiling-panel">
|
|
80
|
+
<div class="djdt-profiling-stats">
|
|
81
|
+
<div class="djdt-profiling-stat">
|
|
82
|
+
<div class="djdt-profiling-stat-label">Status:</div>
|
|
83
|
+
<div class="djdt-profiling-stat-value">
|
|
84
|
+
{% if enabled %}Enabled{% else %}Disabled{% endif %}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="djdt-profiling-stat">
|
|
89
|
+
<div class="djdt-profiling-stat-label">Profiles:</div>
|
|
90
|
+
<div class="djdt-profiling-stat-value" id="profile-count">{{ count }}</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="djdt-profiling-stat">
|
|
94
|
+
<div class="djdt-profiling-stat-label">Size:</div>
|
|
95
|
+
<div class="djdt-profiling-stat-value" id="total-size">{{ total_size }}</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="djdt-profiling-stat">
|
|
99
|
+
<div class="djdt-profiling-stat-label">Directory:</div>
|
|
100
|
+
<div class="djdt-profiling-stat-value">{{ directory }}</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="djdt-profiling-stat" style="margin-left: auto;">
|
|
104
|
+
<a href="#" onclick="if(confirm('Delete all profiles?')){let rows=document.querySelectorAll('#profiling-tbody tr');let promises=[];rows.forEach(row=>{let filename=row.dataset.filename;if(filename)promises.push(fetch('/__debug__/profiling/delete/'+filename+'/').then(r=>r.json()).catch(e=>({error:e.toString()})));});Promise.all(promises).then(results=>{let success_count=0;let is_deleted=0;let failures=0;results.forEach(r=>{if(r.success)success_count++;else if(r.is_deleted)is_deleted++;else failures++;});document.getElementById('profiling-tbody').innerHTML='';document.getElementById('profiling-table').outerHTML='<div class=djdt-no-profiles>No profiles found</div>';document.getElementById('profile-count').textContent='0';document.getElementById('total-size').textContent='0 B';let panels=document.querySelectorAll('.djDebugPanelButton');panels.forEach(p=>{if(p.textContent.includes('Profiles')){let small=p.querySelector('small');if(small)small.textContent='0 profiles';}});if(failures>0){alert('Deleted '+success_count+' profiles. '+failures+' failed (may have been removed by circular buffer).');}}).catch(e=>alert('Error during bulk delete: '+e));}return false;" style="color: #d32f2f; font-weight: bold; text-decoration: none;">
|
|
105
|
+
Delete All
|
|
106
|
+
</a>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<br>
|
|
111
|
+
|
|
112
|
+
<div class="djdt-circular-buffer-note">
|
|
113
|
+
<strong>Note:</strong> The ten most recent profiles are kept, and the remainder are removed.
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<br>
|
|
117
|
+
|
|
118
|
+
{% if not enabled %}
|
|
119
|
+
<div style="padding: 10px; margin-bottom: 15px; font-size: 0.9em; opacity: 0.7;">
|
|
120
|
+
Set PROFILING_ENABLED=True to enable profiling
|
|
121
|
+
</div>
|
|
122
|
+
{% endif %}
|
|
123
|
+
|
|
124
|
+
{% if profiles %}
|
|
125
|
+
<table class="djdt-profiling-table" id="profiling-table">
|
|
126
|
+
<thead>
|
|
127
|
+
<tr>
|
|
128
|
+
<th>Method</th>
|
|
129
|
+
<th>Path</th>
|
|
130
|
+
<th>Duration</th>
|
|
131
|
+
<th>Time</th>
|
|
132
|
+
<th>Size</th>
|
|
133
|
+
<th>View</th>
|
|
134
|
+
<th>Delete</th>
|
|
135
|
+
</tr>
|
|
136
|
+
</thead>
|
|
137
|
+
<tbody id="profiling-tbody">
|
|
138
|
+
{% for profile in profiles %}
|
|
139
|
+
<tr data-filename="{{ profile.filename }}">
|
|
140
|
+
<td>
|
|
141
|
+
<span>{{ profile.method }}</span>
|
|
142
|
+
</td>
|
|
143
|
+
<td>{{ profile.path }}</td>
|
|
144
|
+
<td>{{ profile.duration }}</td>
|
|
145
|
+
<td>{{ profile.modified }}</td>
|
|
146
|
+
<td>{{ profile.size }}</td>
|
|
147
|
+
<td>
|
|
148
|
+
<a href="{% url 'djdt:profiling_view' profile.filename %}" target="_blank" class="djdt-profile-link">
|
|
149
|
+
View
|
|
150
|
+
</a>
|
|
151
|
+
</td>
|
|
152
|
+
<td>
|
|
153
|
+
<a href="#" onclick="if(confirm('Delete this profile?')){fetch('/__debug__/profiling/delete/{{ profile.filename }}/').then(r=>r.json()).then(d=>{if(d.success||d.is_deleted){let row=this.closest('tr');row.remove();let count=document.querySelectorAll('#profiling-tbody tr').length;document.getElementById('profile-count').textContent=count;fetch('/__debug__/profiling/list/').then(r=>r.json()).then(data=>{document.getElementById('total-size').textContent=data.total_size;});let panels=document.querySelectorAll('.djDebugPanelButton');panels.forEach(p=>{if(p.textContent.includes('Profiles')){let small=p.querySelector('small');if(small)small.textContent=count+' profiles';}});if(count===0){document.getElementById('profiling-table').outerHTML='<div class=djdt-no-profiles>No profiles found</div>';}if(d.is_deleted){alert('Profile was already removed (circular buffer)');}}else if(d.error){alert('Failed to delete: '+d.error);}}).catch(e=>{console.error('Delete error:',e);alert('Network error while deleting');});}return false;" class="djdt-profile-delete">
|
|
154
|
+
Delete
|
|
155
|
+
</a>
|
|
156
|
+
</td>
|
|
157
|
+
</tr>
|
|
158
|
+
{% endfor %}
|
|
159
|
+
</tbody>
|
|
160
|
+
</table>
|
|
161
|
+
{% else %}
|
|
162
|
+
<div class="djdt-no-profiles">
|
|
163
|
+
No profiles found in {{ directory }}
|
|
164
|
+
</div>
|
|
165
|
+
{% endif %}
|
|
166
|
+
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-spire
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.0
|
|
4
4
|
Summary: A project for Django Spire
|
|
5
5
|
Author-email: Brayden Carlson <braydenc@stratusadv.com>, Nathan Johnson <nathanj@stratusadv.com>
|
|
6
6
|
License: Copyright (c) 2024 Stratus Advanced Technologies and Contributors.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
django_spire/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
django_spire/conf.py,sha256=EYC1hXqYqheYrb_b6Q93LrSgNl91JV633E4N5j-KGTo,1012
|
|
3
|
-
django_spire/consts.py,sha256=
|
|
3
|
+
django_spire/consts.py,sha256=fGGLMXMV9HZCluwzPHZ9ijrXLb_jLrCjzG0u1BEIV3k,390
|
|
4
4
|
django_spire/exceptions.py,sha256=L5ndRO5ftMmh0pHkO2z_NG3LSGZviJ-dDHNT73SzTNw,48
|
|
5
5
|
django_spire/settings.py,sha256=tGxgEri3TQRBaiwX7YRcWifMf_g1xv1RznplFR6zZnI,821
|
|
6
6
|
django_spire/urls.py,sha256=mKeZszb5U4iIGqddMb5Tt5fRC72U2wABEOi6mvOfEBU,656
|
|
@@ -408,25 +408,23 @@ django_spire/core/forms/widgets.py,sha256=_XgJZ9SoMJ4yz-NhLCAOppSvEWk_5JLUtn3I3_
|
|
|
408
408
|
django_spire/core/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
409
409
|
django_spire/core/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
410
410
|
django_spire/core/management/commands/spire_remove_migration.py,sha256=G3dibVdzvEm9ZMfE_czOk6SyPDn5IEpJ3m1nw-rxuYs,1755
|
|
411
|
-
django_spire/core/management/commands/spire_startapp.py,sha256=
|
|
412
|
-
django_spire/core/management/commands/spire_startapp_pkg/__init__.py,sha256=
|
|
413
|
-
django_spire/core/management/commands/spire_startapp_pkg/
|
|
414
|
-
django_spire/core/management/commands/spire_startapp_pkg/
|
|
415
|
-
django_spire/core/management/commands/spire_startapp_pkg/
|
|
416
|
-
django_spire/core/management/commands/spire_startapp_pkg/
|
|
417
|
-
django_spire/core/management/commands/spire_startapp_pkg/
|
|
411
|
+
django_spire/core/management/commands/spire_startapp.py,sha256=XAA3m7a-dKL4ZYO_WDzKW8ZDgB3__j2RAyiz6qllYjk,4572
|
|
412
|
+
django_spire/core/management/commands/spire_startapp_pkg/__init__.py,sha256=DN9Zllp-XszJawudgbcMKfhhTIPnjWzeTGUSRlwEaG4,1894
|
|
413
|
+
django_spire/core/management/commands/spire_startapp_pkg/builder.py,sha256=1rGZlHdnTM9QdTFFjDf87xCSCA2QIUd8ndtVvf-PbGE,3170
|
|
414
|
+
django_spire/core/management/commands/spire_startapp_pkg/config.py,sha256=qoIGKUYJcRAnYW1WbdpSxKerWckeJGxoDVCZ9yWxrgY,3390
|
|
415
|
+
django_spire/core/management/commands/spire_startapp_pkg/filesystem.py,sha256=shYly0AElxhUsb1WOsK8FdcB3qcsMmghZeEP1IByHq8,3719
|
|
416
|
+
django_spire/core/management/commands/spire_startapp_pkg/generator.py,sha256=MleLiKyFnILhCiQy_BcMRVCdw-1JOhB-7EZBMFdukKc,5989
|
|
417
|
+
django_spire/core/management/commands/spire_startapp_pkg/maps.py,sha256=m0SKcy_TZ4O-mjI74qkxyhVjoDN4QvMmQxgvbbn6mAE,22977
|
|
418
|
+
django_spire/core/management/commands/spire_startapp_pkg/permissions.py,sha256=czSE-ZNBw-3LsGrtkaD7CBFnitYX18KQjgJ_jjbap7k,5260
|
|
419
|
+
django_spire/core/management/commands/spire_startapp_pkg/processor.py,sha256=fqnbgaw8AsNHQBZnbDLmU4WPueFx5bQzA9Cwi_lD_KQ,5928
|
|
420
|
+
django_spire/core/management/commands/spire_startapp_pkg/registry.py,sha256=izf5SHkek2nIEf0UkODXL4uY1SkWXmoL3_s6pihF2QE,2689
|
|
421
|
+
django_spire/core/management/commands/spire_startapp_pkg/reporter.py,sha256=C48v1dLgnzmGijE9m_G_k5dMuOmNq3Am1zETjiEzEuQ,11085
|
|
422
|
+
django_spire/core/management/commands/spire_startapp_pkg/resolver.py,sha256=-y86EMmWU65bMzGeS5mI_O0iVu-xfVrdVKXtrOgbwUg,2835
|
|
423
|
+
django_spire/core/management/commands/spire_startapp_pkg/user_input.py,sha256=VUyY9PrqM07vRvgbVJhkC4lgNolnBik3K8S_8U9s_zM,9518
|
|
424
|
+
django_spire/core/management/commands/spire_startapp_pkg/validator.py,sha256=cVzQqaShGZ0ae-M69tHFDgXlYBmtR2Zg0zblIj55-Nc,3404
|
|
418
425
|
django_spire/core/management/commands/spire_startapp_pkg/template/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
419
|
-
django_spire/core/
|
|
420
|
-
django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_form_card.html,sha256=m-z23qNNn5R4ctuAypXoQgcti2mz-l57uvBmptBZG-E,233
|
|
421
|
-
django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_list_card.html,sha256=tJIZLL89SUV8As6RBRiYwIDVhEjgUu8173YRCDaRZHo,659
|
|
422
|
-
django_spire/core/management/commands/spire_startapp_pkg/template/templates/form/spirechildapp_form.html,sha256=fQ-kULFIGACH7YVWK2S4O3hEp1DwjumqxOgymp15e7E,686
|
|
423
|
-
django_spire/core/management/commands/spire_startapp_pkg/template/templates/item/spirechildapp_item.html,sha256=m_R9HlPcH89eSNcKkNwD8DXnuH7oBU3pw7YrENWgrKI,1244
|
|
424
|
-
django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_detail_page.html,sha256=dOxIi0ISZwYpyPTrH1IuAJjWR4jWzgtpzaWpWYxSA3I,434
|
|
425
|
-
django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_form_page.html,sha256=9eSbAsMK8rjzSYmrq_NZOB6F1EphMIO4KIoDlv6YOrw,331
|
|
426
|
-
django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_list_page.html,sha256=SwTuxzNzTW6_nlZvrglSG2amithT0ZCjnNnrAd9aaA0,236
|
|
427
|
-
django_spire/core/middleware/__init__.py,sha256=peocpyT_vXsjK3MXzpWIbsNVnHBbuyybH90R9V-lN1c,243
|
|
426
|
+
django_spire/core/middleware/__init__.py,sha256=BvBwVHLVebknJkObtKk_s5OqaIJaAmnavTvnnv4TBO8,149
|
|
428
427
|
django_spire/core/middleware/maintenance.py,sha256=jJtmz69UXJBBgPkq5SNspWuER1bGfAjSnsVipYS4TF0,921
|
|
429
|
-
django_spire/core/middleware/profiling.py,sha256=oW8S5oZQYq96a28cmVPLwyhz3xBAZHkTK7NMxLUDXCw,4807
|
|
430
428
|
django_spire/core/redirect/__init__.py,sha256=ubAsz_h3ufwgNNkdtqiJQDgXc6DodE2LTUyBwi9dgkE,71
|
|
431
429
|
django_spire/core/redirect/generic_redirect.py,sha256=bfpA2GSRbkf5Y_zqTiTGzakQauLumm3JbaYMzmsDhjA,1245
|
|
432
430
|
django_spire/core/redirect/safe_redirect.py,sha256=deGLqiR1wWwqlJ8BYp76qDUDHnfRrxL-1Vns3nozSG0,2901
|
|
@@ -994,6 +992,11 @@ django_spire/notification/sms/urls/__init__.py,sha256=9R6ee7PseGpAiiraEU0m4eeF8G
|
|
|
994
992
|
django_spire/notification/sms/urls/media_urls.py,sha256=fYAW8LWMwUe5nP2QuWWdnhuSyv-0PX4nIw0vjbabENQ,271
|
|
995
993
|
django_spire/notification/sms/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
996
994
|
django_spire/notification/sms/views/media_views.py,sha256=STrTC1irBSHEFQ0SZfUhpBI9Xmc6luWDf7Lembfd5Ic,1080
|
|
995
|
+
django_spire/profiling/__init__.py,sha256=V5zHoTaqGCOU-gfwkGQI01XcXeRYoAjO103yDkk1tIQ,251
|
|
996
|
+
django_spire/profiling/panel.py,sha256=BBISa2T35_QV2ODtx0DsL2ypK_BbETFSdr9iu_wIgCk,9570
|
|
997
|
+
django_spire/profiling/middleware/__init__.py,sha256=K6wjo7xUhOAJaB5Vbqa5JRf3EQM78NwToz_VtW3zz78,148
|
|
998
|
+
django_spire/profiling/middleware/profiling.py,sha256=Vl8WHCEYoUl1P50uFog2Gp6hK2INSRZC2t4LuY46DOA,5103
|
|
999
|
+
django_spire/profiling/templates/panel.html,sha256=YNK5ypTpUezWCtT0CE-awIpKPo4pKOPo9wbD4fkzgwc,6684
|
|
997
1000
|
django_spire/theme/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
998
1001
|
django_spire/theme/apps.py,sha256=yol2S7ooTuplc8JKETEg6oAu9hpeHUpJ6wt1Bo3dQ3E,479
|
|
999
1002
|
django_spire/theme/enums.py,sha256=I0Sa5GzAntAPL8d7BJykhzuftA7ZweqZZtFIcH_g6kA,495
|
|
@@ -1043,8 +1046,8 @@ django_spire/theme/urls/page_urls.py,sha256=S8nkKkgbhG3XHI3uMUL-piOjXIrRkuY2UlM_
|
|
|
1043
1046
|
django_spire/theme/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1044
1047
|
django_spire/theme/views/json_views.py,sha256=W1khC2K_EMbEzAFmMxC_P76_MFnkRH4-eVdodrRfAhw,1904
|
|
1045
1048
|
django_spire/theme/views/page_views.py,sha256=pHr8iekjtR99xs7w1taj35HEo133Svq1dvDD0y0VL1c,3933
|
|
1046
|
-
django_spire-0.
|
|
1047
|
-
django_spire-0.
|
|
1048
|
-
django_spire-0.
|
|
1049
|
-
django_spire-0.
|
|
1050
|
-
django_spire-0.
|
|
1049
|
+
django_spire-0.17.0.dist-info/licenses/LICENSE.md,sha256=tlTbOtgKoy-xAQpUk9gPeh9O4oRXCOzoWdW3jJz0wnA,1091
|
|
1050
|
+
django_spire-0.17.0.dist-info/METADATA,sha256=bVDHr6SnMBQaXRxcp9wExF6zN6mOQLORWWzrZDZr_WU,4936
|
|
1051
|
+
django_spire-0.17.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
1052
|
+
django_spire-0.17.0.dist-info/top_level.txt,sha256=xf3QV1e--ONkVpgMDQE9iqjQ1Vg4--_6C8wmO-KxPHQ,13
|
|
1053
|
+
django_spire-0.17.0.dist-info/RECORD,,
|