1- import json
21import re
32from contextlib import ExitStack
3+ from pathlib import Path
44from time import perf_counter
55
66from django .db import connections
7+ from django .template import Context , Engine
78
89from . import tracker
910from .conf import (
1011 get_position ,
1112 get_show_bar ,
1213 get_show_headers ,
13- get_thresholds ,
14- get_enable_console ,
1514)
1615
17- STYLE_BLOCK = """<style>
18- #django-devbar {
19- position: fixed; %s; z-index: 999999999;
20- display: flex; align-items: center; gap: 5px;
21- font-family: -apple-system, system-ui, sans-serif;
22- font-size: 11px; font-weight: 500;
23- padding: 4px 8px; margin: 8px; border-radius: 4px;
24- backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
25- box-shadow: 0 2px 8px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.2);
26- transition: all 0.2s ease;
27- cursor: default;
28- line-height: 1.3;
29- background: rgba(20, 20, 20, 0.92);
30- color: #f5f5f5;
31- }
32- #django-devbar.level-warn { border-left: 3px solid #f59e0b; }
33- #django-devbar.level-crit { border-left: 3px solid #dc2626; }
34- #django-devbar span { opacity: 0.7; }
35- #django-devbar strong { opacity: 1; font-weight: 600; }
36- #django-devbar .duplicate-badge { color: #f59e0b; font-weight: 600; }
37- @media (max-width: 640px) { #django-devbar { display: none; } }
38- </style>"""
39-
40- BAR_TEMPLATE = """<div id="django-devbar" class="level-%s">
41- <span>db</span> <strong>%.0fms</strong> <span>·</span>
42- <span>app</span> <strong>%.0fms</strong> <span>·</span>
43- <span>queries</span> <strong>%d</strong>%s
44- </div>"""
45-
46- SCRIPT_TEMPLATE = """<script>
47- (function() {
48- const stats = %s;
49- if (stats.duplicates && stats.duplicates.length > 0) {
50- console.groupCollapsed(`⚠️ Django DevBar: Duplicate Queries Detected (${stats.duplicates.length})`);
51- console.table(stats.duplicates.map(d => ({SQL: d.sql, Parameters: d.params, Duration_ms: d.duration})));
52- console.groupEnd();
53- }
54- })();
55- </script>"""
56-
5716BODY_CLOSE_RE = re .compile (rb"</body\s*>" , re .IGNORECASE )
5817
18+ _template_engine = Engine (
19+ dirs = [Path (__file__ ).parent / "templates" ],
20+ autoescape = True ,
21+ )
22+
5923
6024class DevBarMiddleware :
6125 def __init__ (self , get_response ):
@@ -81,8 +45,7 @@ def __call__(self, request):
8145 stats ["python_time" ] = python_time
8246 stats ["total_time" ] = total_time
8347
84- thresholds = get_thresholds ()
85- level = self ._determine_level (stats , total_time , thresholds )
48+ level = "warn" if stats ["has_duplicates" ] else "ok"
8649
8750 if get_show_headers ():
8851 self ._add_headers (response , stats )
@@ -92,20 +55,6 @@ def __call__(self, request):
9255
9356 return response
9457
95- def _determine_level (self , stats , total_time , thresholds ):
96- if (
97- total_time > thresholds ["time_critical" ]
98- or stats ["count" ] > thresholds ["count_critical" ]
99- ):
100- return "crit"
101- if (
102- stats ["has_duplicates" ]
103- or total_time > thresholds ["time_warning" ]
104- or stats ["count" ] > thresholds ["count_warning" ]
105- ):
106- return "warn"
107- return "ok"
108-
10958 def _add_headers (self , response , stats ):
11059 response ["DevBar-Query-Count" ] = str (stats ["count" ])
11160 response ["DevBar-DB-Time" ] = f"{ stats ['duration' ]:.0f} ms"
@@ -129,28 +78,30 @@ def _inject_devbar(self, response, stats, level):
12978 if not matches :
13079 return
13180
132- dup_marker = (
133- ' <strong class="duplicate-badge">(d)</strong>'
134- if stats ["has_duplicates" ]
135- else ""
81+ duplicates_html = self ._build_duplicates_html (stats .get ("duplicate_queries" , []))
82+
83+ template = _template_engine .get_template ("django_devbar/devbar.html" )
84+ html = template .render (
85+ Context (
86+ {
87+ "position" : get_position (),
88+ "level" : level ,
89+ "db_time" : stats ["duration" ],
90+ "app_time" : stats ["python_time" ],
91+ "query_count" : stats ["count" ],
92+ "duplicates_html" : duplicates_html ,
93+ }
94+ )
13695 )
13796
138- css = STYLE_BLOCK % get_position ()
139- html = BAR_TEMPLATE % (
140- level ,
141- stats ["duration" ],
142- stats ["python_time" ],
143- stats ["count" ],
144- dup_marker ,
145- )
146-
147- script = ""
148- if get_enable_console () and stats .get ("duplicate_queries" ):
149- console_data = {"duplicates" : stats ["duplicate_queries" ]}
150- script = SCRIPT_TEMPLATE % json .dumps (console_data )
151-
152- payload = (css + html + script ).encode (response .charset or "utf-8" )
97+ payload = html .encode (response .charset or "utf-8" )
15398
15499 idx = matches [- 1 ].start ()
155100 response .content = content [:idx ] + payload + content [idx :]
156101 response ["Content-Length" ] = str (len (response .content ))
102+
103+ def _build_duplicates_html (self , duplicates ):
104+ if not duplicates :
105+ return ""
106+ template = _template_engine .get_template ("django_devbar/duplicates.html" )
107+ return template .render (Context ({"duplicates" : duplicates }))
0 commit comments