plain.admin 0.28.0__py3-none-any.whl → 0.29.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.
@@ -50,6 +50,12 @@ var plainToolbar = {
50
50
  var toolbar = document.querySelector("#plaintoolbar");
51
51
  var tab = toolbar.querySelector("div[data-toolbar-tab=" + tabName + "]");
52
52
 
53
+ // If the tab doesn't exist for some reason, quit
54
+ if (!tab) {
55
+ console.warn("Toolbar tab " + tabName + " does not exist");
56
+ return;
57
+ }
58
+
53
59
  // Hide all children in the tab parent
54
60
  for (var i = 0; i < tab.parentNode.children.length; i++) {
55
61
  var child = tab.parentNode.children[i];
@@ -1,3 +1,4 @@
1
+ import datetime
1
2
  import time
2
3
  import traceback
3
4
  from collections import Counter
@@ -143,7 +144,7 @@ class QueryStats:
143
144
  "method": request.method,
144
145
  "unique_id": request.unique_id,
145
146
  },
146
- "timestamp": time.time(),
147
+ "timestamp": datetime.datetime.now().isoformat(),
147
148
  "total_time_display": self.total_time_display,
148
149
  "queries": self.queries,
149
150
  }
@@ -55,14 +55,15 @@ class QueryStatsMiddleware:
55
55
  if self.should_ignore_request(request):
56
56
  return self.get_response(request)
57
57
 
58
- # Previously we wrapped this in execute_wrapper too,
59
- # but now there's a chicken and the egg issue with "querystats" being by the view,
60
- # which hasn't been processed yet (on the initial request)
61
- is_admin = self.is_admin_request(request)
58
+ def is_tracking():
59
+ return "querystats" in request.session
60
+
61
+ querystats = QueryStats(include_tracebacks=is_tracking())
62
+
63
+ with connection.execute_wrapper(querystats):
64
+ is_admin = self.is_admin_request(request)
62
65
 
63
66
  if settings.DEBUG or is_admin:
64
- tracking = "querystats" in request.session
65
- querystats = QueryStats(include_tracebacks=tracking)
66
67
  with connection.execute_wrapper(querystats):
67
68
  response = self.get_response(request)
68
69
 
@@ -74,7 +75,7 @@ class QueryStatsMiddleware:
74
75
  # by using the server timing API which can be parsed client-side
75
76
  response.headers["Server-Timing"] = querystats.as_server_timing()
76
77
 
77
- if tracking and querystats.num_queries > 0:
78
+ if is_tracking() and querystats.num_queries > 0:
78
79
  request.session["querystats"][request.unique_id] = json.dumps(
79
80
  querystats.as_context_dict(request), cls=QueryStatsJSONEncoder
80
81
  )
@@ -1,4 +1,3 @@
1
- import datetime
2
1
  import json
3
2
 
4
3
  from plain.auth.views import AuthViewMixin
@@ -56,10 +55,6 @@ class QuerystatsView(AuthViewMixin, TemplateView):
56
55
  )
57
56
  )
58
57
 
59
- # Convert the timestamps back to a python datetime object
60
- for data in querystats.values():
61
- data["timestamp"] = datetime.datetime.fromtimestamp(data["timestamp"])
62
-
63
58
  context["querystats"] = querystats
64
59
  context["querystats_enabled"] = "querystats" in self.request.session
65
60
 
@@ -41,7 +41,7 @@
41
41
  <span class="text-sm">{{ qs.request.path }}</span>
42
42
  <span class="font-semibold bg-white/5 rounded-sm px-1 py-0.5 text-xs">{{ qs.request.method }}</span>
43
43
  <div class="text-xs text-stone-400">{{ qs.summary }}</div>
44
- <div class="text-xs text-stone-500">{{ qs.timestamp|timesince }} ago</div>
44
+ <div class="text-xs text-stone-500">{{ qs.timestamp|fromisoformat|timesince }} ago</div>
45
45
  </button>
46
46
  </li>
47
47
  {% endfor %}
@@ -58,7 +58,7 @@
58
58
  </div>
59
59
  <div class="text-right">
60
60
  <div class="text-xs text-white/60">Request ID <code>{{ qs.request.unique_id }}</code></div>
61
- <div class="text-xs text-white/60"><code>{{ qs.timestamp }}</code></div>
61
+ <div class="text-xs text-white/60"><code>{{ qs.timestamp|fromisoformat }}</code></div>
62
62
  </div>
63
63
  </div>
64
64
 
@@ -92,10 +92,12 @@
92
92
  <span class="font-medium">Parameters</span>
93
93
  <pre><code class="font-mono">{{ query.params|pprint }}</code></pre>
94
94
  </div>
95
+ {% if query.tb|default(false) %}
95
96
  <details>
96
97
  <summary>Traceback</summary>
97
98
  <pre><code class="block overflow-x-auto font-mono text-xs">{{ query.tb }}</code></pre>
98
99
  </details>
100
+ {% endif %}
99
101
  </div>
100
102
  </details>
101
103
  {% else %}
@@ -0,0 +1,16 @@
1
+ <div class="px-6 py-4">
2
+ <div class="p-2 mb-5 border-amber-500 border rounded">
3
+ <div class="text-amber-500 text-lg flex justify-between items-center">
4
+ <div>
5
+ <span class="font-bold">Exception</span>
6
+ {{ exception }}
7
+ </div>
8
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-5 h-5 bi bi-exclamation-triangle-fill" viewBox="0 0 16 16">
9
+ <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5m.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/>
10
+ </svg>
11
+ </div>
12
+ <div class="text-amber-400 text-xs mt-3 bg-white/5 p-2 rounded overflow-auto">
13
+ <pre><code>{{ exception._traceback_string }}</code></pre>
14
+ </div>
15
+ </div>
16
+ </div>
@@ -0,0 +1,28 @@
1
+ <div id="querystats-container" class="h-full">
2
+ <div class="px-6 py-4 text-center">
3
+ <p>Loading querystats...</p>
4
+ </div>
5
+ </div>
6
+ <script>
7
+ (function() {
8
+ var container = document.getElementById('querystats-container');
9
+ var loaded = false;
10
+ var parent = container.parentNode;
11
+ var observer = new IntersectionObserver(function(entries) {
12
+ entries.forEach(function(entry) {
13
+ if (entry.isIntersecting && !loaded) {
14
+ loaded = true;
15
+ var iframe = document.createElement('iframe');
16
+ iframe.src = "{{ url('admin:querystats:querystats') }}";
17
+ iframe.frameBorder = "0";
18
+ iframe.style.width = "100%";
19
+ iframe.style.height = "100%";
20
+ container.innerHTML = '';
21
+ container.appendChild(iframe);
22
+ observer.disconnect();
23
+ }
24
+ });
25
+ }, { root: parent, threshold: 0 });
26
+ observer.observe(container);
27
+ })();
28
+ </script>
@@ -0,0 +1,60 @@
1
+ <dl class="text-sm grid grid-cols-1 sm:grid-cols-[max-content_1fr] sm:gap-y-2 gap-x-8 px-6 py-4">
2
+ <dt>Request ID</dt>
3
+ <dd class="text-sm text-white/50">{{ request.unique_id }}</dd>
4
+
5
+ <dt>Query params</dt>
6
+ <dd class="text-sm text-white/50">{{ request.query_params }}</dd>
7
+
8
+ <dt>Method</dt>
9
+ <dd class="text-sm text-white/50">{{ request.method }}</dd>
10
+
11
+ {% if request.resolver_match %}
12
+ <dt>View</dt>
13
+ <dd class="text-sm text-white/50">{{ request.resolver_match.view.view_class|pprint }}</dd>
14
+
15
+ <dt>URL pattern</dt>
16
+ <dd class="text-sm text-white/50">
17
+ <pre><code>{{ request.resolver_match.route }}</code></pre>
18
+ </dd>
19
+
20
+ <dt>URL name</dt>
21
+ <dd class="text-sm text-white/50">
22
+ <pre><code>{{ request.resolver_match.namespaced_url_name }}</code></pre>
23
+ </dd>
24
+
25
+ <dt>URL args</dt>
26
+ <dd class="text-sm text-white/50">
27
+ <pre><code>{{ request.resolver_match.args }}</code></pre>
28
+ </dd>
29
+
30
+ <dt>URL kwargs</dt>
31
+ <dd class="text-sm text-white/50">
32
+ <pre><code>{{ request.resolver_match.kwargs }}</code></pre>
33
+ </dd>
34
+ {% endif %}
35
+
36
+ {% if template_names is defined %}
37
+ <dt>Template names</dt>
38
+ <dd class="text-sm text-white/50">
39
+ <pre><code>{{ template_names }}</code></pre>
40
+ </dd>
41
+ {% endif %}
42
+
43
+ {% if object|default(false) %}
44
+ <dt>Primary object</dt>
45
+ <dd class="text-sm text-white/50 inline-flex items-center" title="PK: {{ object.pk|default('unknown') }}">
46
+ <pre><code>{{ object.__repr__() }}</code></pre>
47
+ {% if object|get_admin_model_detail_url %}
48
+ <a class="ml-2 inline-flex items-center p-1 text-blue-500 hover:text-blue-400" href="{{ object|get_admin_model_detail_url }}">
49
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-3 h-3 bi bi-database-fill" viewBox="0 0 16 16">
50
+ <path d="M3.904 1.777C4.978 1.289 6.427 1 8 1s3.022.289 4.096.777C13.125 2.245 14 2.993 14 4s-.875 1.755-1.904 2.223C11.022 6.711 9.573 7 8 7s-3.022-.289-4.096-.777C2.875 5.755 2 5.007 2 4s.875-1.755 1.904-2.223"/>
51
+ <path d="M2 6.161V7c0 1.007.875 1.755 1.904 2.223C4.978 9.71 6.427 10 8 10s3.022-.289 4.096-.777C13.125 8.755 14 8.007 14 7v-.839c-.457.432-1.004.751-1.49.972C11.278 7.693 9.682 8 8 8s-3.278-.307-4.51-.867c-.486-.22-1.033-.54-1.49-.972"/>
52
+ <path d="M2 9.161V10c0 1.007.875 1.755 1.904 2.223C4.978 12.711 6.427 13 8 13s3.022-.289 4.096-.777C13.125 11.755 14 11.007 14 10v-.839c-.457.432-1.004.751-1.49.972-1.232.56-2.828.867-4.51.867s-3.278-.307-4.51-.867c-.486-.22-1.033-.54-1.49-.972"/>
53
+ <path d="M2 12.161V13c0 1.007.875 1.755 1.904 2.223C4.978 15.711 6.427 16 8 16s3.022-.289 4.096-.777C13.125 14.755 14 14.007 14 13v-.839c-.457.432-1.004.751-1.49.972-1.232.56-2.828.867-4.51.867s-3.278-.307-4.51-.867c-.486-.22-1.033-.54-1.49-.972"/>
54
+ </svg>
55
+ </a>
56
+ {% endif %}
57
+ </dd>
58
+ {% endif %}
59
+
60
+ </dl>
@@ -1,20 +1,16 @@
1
1
  {% if toolbar.should_render() %}
2
- {% set exception=toolbar.request_exception() %}
2
+ {% set panels=toolbar.get_panels() %}
3
3
  <script defer src="{{ asset('toolbar/toolbar.js') }}"></script>
4
4
  <div id="plaintoolbar" class="print:hidden text-stone-300 fixed bottom-0 w-full z-30 hidden sm:flex sm:flex-col">
5
5
 
6
- <div id="plaintoolbar-details" class="{% if not exception %}hidden{% endif %} relative text-sm border-white/5 shadow-xl border-t inset-shadow-xs inset-shadow-stone-800 rounded-t-xl bg-stone-950/95 backdrop-blur-sm">
6
+ <div id="plaintoolbar-details" class="hidden relative text-sm border-white/5 shadow-xl border-t inset-shadow-xs inset-shadow-stone-800 rounded-t-xl bg-stone-950/95 backdrop-blur-sm">
7
7
 
8
- <div class="flex items-center border-b border-white/5 px-2 justify-between">
9
- <div class="flex items-center">
10
- <button data-active data-toolbar-tab="request" class="data-active:border-yellow-500 px-4 py-2.5 -mb-px cursor-pointer border-b border-transparent hover:border-yellow-600" type="button">Request</button>
11
- {% if request.session is defined %}
12
- <button data-toolbar-tab="session" class="data-active:border-yellow-500 px-4 py-2.5 -mb-px cursor-pointer border-b border-transparent hover:border-yellow-600" type="button">Session</button>
13
- {% endif %}
14
- <button data-toolbar-tab="querystats" class="data-active:border-yellow-500 px-4 py-2.5 -mb-px cursor-pointer border-b border-transparent hover:border-yellow-600" type="button">Querystats</button>
15
- {% if toolbar.metadata %}
16
- <button data-toolbar-tab="metadata" class="data-active:border-yellow-500 px-4 py-2.5 -mb-px cursor-pointer border-b border-transparent hover:border-yellow-600" type="button">Metadata</button>
17
- {% endif %}
8
+ <div class="flex border-b border-white/5 px-2">
9
+ <div class="flex flex-grow">
10
+ {% for panel in panels %}
11
+ <button {% if loop.first %}data-active{% endif %} data-toolbar-tab="{{ panel.name }}" class="data-active:border-yellow-500 px-4 py-2.5 -mb-px cursor-pointer border-b border-transparent hover:border-yellow-600" type="button">{{ panel.name }}</button>
12
+ {% endfor %}
13
+ <button data-plaintoolbar-expand class="flex-grow cursor-pointer inline-flex h-full" type="button"></button>
18
14
  </div>
19
15
  <div class="px-4 flex items-center space-x-4">
20
16
  <button title="Hide toolbar for 1 hour" class="cursor-pointer hover:text-white text-white/50" type="button" data-plaintoolbar-hideuntil>
@@ -35,158 +31,11 @@
35
31
  <div data-resizer class="cursor-grab w-20 h-1.5 top-1 bg-white/15 rounded-full absolute top-0 left-1/2 -translate-x-1/2"></div>
36
32
 
37
33
  <div class="overflow-auto h-[30vh] flex flex-col">
38
- <div data-toolbar-tab="request" class="px-6 py-4">
39
-
40
- {% if exception %}
41
- <div class="p-2 mb-5 border-amber-500 border rounded">
42
- <div class="text-amber-500 text-lg flex justify-between items-center">
43
- <div>
44
- <span class="font-bold">Exception</span>
45
- {{ exception }}
46
- </div>
47
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-5 h-5 bi bi-exclamation-triangle-fill" viewBox="0 0 16 16">
48
- <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5m.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/>
49
- </svg>
50
- </div>
51
- <div class="text-amber-400 text-xs mt-3 bg-white/5 p-2 rounded overflow-auto">
52
- <pre><code>{{ exception._traceback_string }}</code></pre>
53
- </div>
54
- </div>
55
- {% endif %}
56
-
57
- <dl class="text-sm grid grid-cols-1 sm:grid-cols-[max-content_1fr] sm:gap-y-2 gap-x-8">
58
- <dt>Request ID</dt>
59
- <dd class="text-sm text-white/50">{{ request.unique_id }}</dd>
60
-
61
- <dt>Query params</dt>
62
- <dd class="text-sm text-white/50">{{ request.query_params }}</dd>
63
-
64
- <dt>Method</dt>
65
- <dd class="text-sm text-white/50">{{ request.method }}</dd>
66
-
67
- {% if request.resolver_match %}
68
- <dt>View</dt>
69
- <dd class="text-sm text-white/50">{{ request.resolver_match.view.view_class|pprint }}</dd>
70
-
71
- <dt>URL pattern</dt>
72
- <dd class="text-sm text-white/50">
73
- <pre><code>{{ request.resolver_match.route }}</code></pre>
74
- </dd>
75
-
76
- <dt>URL name</dt>
77
- <dd class="text-sm text-white/50">
78
- <pre><code>{{ request.resolver_match.namespaced_url_name }}</code></pre>
79
- </dd>
80
-
81
- <dt>URL args</dt>
82
- <dd class="text-sm text-white/50">
83
- <pre><code>{{ request.resolver_match.args }}</code></pre>
84
- </dd>
85
-
86
- <dt>URL kwargs</dt>
87
- <dd class="text-sm text-white/50">
88
- <pre><code>{{ request.resolver_match.kwargs }}</code></pre>
89
- </dd>
90
- {% endif %}
91
-
92
- {% if template_names is defined %}
93
- <dt>Template names</dt>
94
- <dd class="text-sm text-white/50">
95
- <pre><code>{{ template_names }}</code></pre>
96
- </dd>
97
- {% endif %}
98
-
99
- {% if object|default(false) %}
100
- <dt>Primary object</dt>
101
- <dd class="text-sm text-white/50 inline-flex items-center" title="PK: {{ object.pk|default('unknown') }}">
102
- <pre><code>{{ object.__repr__() }}</code></pre>
103
- {% if object|get_admin_model_detail_url %}
104
- <a class="ml-2 inline-flex items-center p-1 text-blue-500 hover:text-blue-400" href="{{ object|get_admin_model_detail_url }}">
105
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-3 h-3 bi bi-database-fill" viewBox="0 0 16 16">
106
- <path d="M3.904 1.777C4.978 1.289 6.427 1 8 1s3.022.289 4.096.777C13.125 2.245 14 2.993 14 4s-.875 1.755-1.904 2.223C11.022 6.711 9.573 7 8 7s-3.022-.289-4.096-.777C2.875 5.755 2 5.007 2 4s.875-1.755 1.904-2.223"/>
107
- <path d="M2 6.161V7c0 1.007.875 1.755 1.904 2.223C4.978 9.71 6.427 10 8 10s3.022-.289 4.096-.777C13.125 8.755 14 8.007 14 7v-.839c-.457.432-1.004.751-1.49.972C11.278 7.693 9.682 8 8 8s-3.278-.307-4.51-.867c-.486-.22-1.033-.54-1.49-.972"/>
108
- <path d="M2 9.161V10c0 1.007.875 1.755 1.904 2.223C4.978 12.711 6.427 13 8 13s3.022-.289 4.096-.777C13.125 11.755 14 11.007 14 10v-.839c-.457.432-1.004.751-1.49.972-1.232.56-2.828.867-4.51.867s-3.278-.307-4.51-.867c-.486-.22-1.033-.54-1.49-.972"/>
109
- <path d="M2 12.161V13c0 1.007.875 1.755 1.904 2.223C4.978 15.711 6.427 16 8 16s3.022-.289 4.096-.777C13.125 14.755 14 14.007 14 13v-.839c-.457.432-1.004.751-1.49.972-1.232.56-2.828.867-4.51.867s-3.278-.307-4.51-.867c-.486-.22-1.033-.54-1.49-.972"/>
110
- </svg>
111
- </a>
112
- {% endif %}
113
- </dd>
114
- {% endif %}
115
-
116
- </dl>
117
- </div>
118
-
119
- {% if request.session is defined %}
120
- <div data-toolbar-tab="session" style="display: none;">
121
- <div class="px-6 py-4">
122
- <dl class="text-sm grid grid-cols-1 sm:grid-cols-[max-content_1fr] sm:gap-y-2 gap-x-8">
123
- <dt>Session ID</dt>
124
- <dd class="text-sm text-white/50">{{ request.session.session_key }}</dd>
125
-
126
- {% for k, v in request.session.items() %}
127
- <dt>session["{{ k }}"]</dt>
128
- <dd class="text-sm text-white/50 max-h-32 overflow-auto">
129
- {% if v is iterable and not v|string %}
130
- <pre><code>{{ v }}</code></pre>
131
- {% else %}
132
- {{ v }}
133
- {% endif %}
134
- </dd>
135
- {% endfor %}
136
- </dl>
137
- </div>
34
+ {% for panel in panels %}
35
+ <div data-toolbar-tab="{{ panel.name }}" {% if not loop.first %}style="display: none;"{% endif %}>
36
+ {{ panel.render() }}
138
37
  </div>
139
- {% endif %}
140
-
141
- <div data-toolbar-tab="querystats" style="display: none;" class="h-full">
142
- <div id="querystats-container" class="h-full">
143
- <div class="px-6 py-4 text-center">
144
- <p>Loading querystats...</p>
145
- </div>
146
- </div>
147
- <script>
148
- (function() {
149
- var container = document.getElementById('querystats-container');
150
- var loaded = false;
151
- var parent = container.parentNode;
152
- var observer = new IntersectionObserver(function(entries) {
153
- entries.forEach(function(entry) {
154
- if (entry.isIntersecting && !loaded) {
155
- loaded = true;
156
- var iframe = document.createElement('iframe');
157
- iframe.src = "{{ url('admin:querystats:querystats') }}";
158
- iframe.frameBorder = "0";
159
- iframe.style.width = "100%";
160
- iframe.style.height = "100%";
161
- container.innerHTML = '';
162
- container.appendChild(iframe);
163
- observer.disconnect();
164
- }
165
- });
166
- }, { root: parent, threshold: 0 });
167
- observer.observe(container);
168
- })();
169
- </script>
170
- </div>
171
-
172
- {% if toolbar.metadata %}
173
- <div data-toolbar-tab="metadata" style="display: none;">
174
- <div class="px-6 py-4">
175
- <dl class="text-sm grid grid-cols-1 sm:grid-cols-[max-content_1fr] sm:gap-y-2 gap-x-8">
176
- {% for k, v in toolbar.metadata.items() %}
177
- <dt>{{ k }}</dt>
178
- <dd class="text-sm text-white/50 max-h-32 overflow-auto">
179
- {% if v is iterable and not v|string %}
180
- <pre><code>{{ v }}</code></pre>
181
- {% else %}
182
- {{ v }}
183
- {% endif %}
184
- </dd>
185
- {% endfor %}
186
- </dl>
187
- </div>
188
- </div>
189
- {% endif %}
38
+ {% endfor %}
190
39
  </div>
191
40
 
192
41
  </div>
@@ -213,8 +62,8 @@
213
62
 
214
63
  <div class="flex items-center space-x-3 transition-all">
215
64
 
216
- {% if exception %}
217
- <button class="cursor-pointer text-amber-500 hover:text-amber-400" type="button" data-toolbar-tab="request">
65
+ {% if toolbar.request_exception() %}
66
+ <button class="cursor-pointer text-amber-500 hover:text-amber-400" type="button" data-toolbar-tab="Exception">
218
67
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-4 h-4 bi bi-exclamation-triangle-fill" viewBox="0 0 16 16">
219
68
  <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5m.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/>
220
69
  </svg>
plain/admin/toolbar.py CHANGED
@@ -2,14 +2,15 @@ import sys
2
2
  import traceback
3
3
 
4
4
  from plain.runtime import settings
5
+ from plain.templates import Template
5
6
  from plain.urls.exceptions import Resolver404
7
+ from plain.utils.safestring import mark_safe
6
8
 
7
9
 
8
10
  class Toolbar:
9
11
  def __init__(self, request):
10
12
  self.request = request
11
13
  self.version = settings.ADMIN_TOOLBAR_VERSION
12
- self.metadata = {}
13
14
 
14
15
  def should_render(self):
15
16
  if settings.DEBUG:
@@ -32,3 +33,77 @@ class Toolbar:
32
33
  traceback.format_tb(exception.__traceback__)
33
34
  )
34
35
  return exception
36
+
37
+ def get_panels(self):
38
+ panels = [panel(self.request) for panel in _toolbar_panel_registry.get_panels()]
39
+
40
+ if self.request_exception():
41
+ exception = self.request_exception()
42
+ panels = [
43
+ _ExceptionToolbarPanel(self.request, exception),
44
+ ] + panels
45
+
46
+ return panels
47
+
48
+
49
+ class ToolbarPanel:
50
+ name: str
51
+ template_name: str
52
+
53
+ def __init__(self, request):
54
+ self.request = request
55
+
56
+ def get_template_context(self):
57
+ return {
58
+ "request": self.request,
59
+ }
60
+
61
+ def render(self):
62
+ template = Template(self.template_name)
63
+ context = self.get_template_context()
64
+ return mark_safe(template.render(context))
65
+
66
+
67
+ class _ToolbarPanelRegistry:
68
+ def __init__(self):
69
+ self._panels = {}
70
+
71
+ def register_panel(self, panel_class):
72
+ self._panels[panel_class.name] = panel_class
73
+
74
+ def get_panels(self):
75
+ return self._panels.values()
76
+
77
+
78
+ _toolbar_panel_registry = _ToolbarPanelRegistry()
79
+
80
+
81
+ def register_toolbar_panel(panel_class):
82
+ _toolbar_panel_registry.register_panel(panel_class)
83
+ return panel_class
84
+
85
+
86
+ class _ExceptionToolbarPanel(ToolbarPanel):
87
+ name = "Exception"
88
+ template_name = "toolbar/exception.html"
89
+
90
+ def __init__(self, request, exception):
91
+ super().__init__(request)
92
+ self.exception = exception
93
+
94
+ def get_template_context(self):
95
+ context = super().get_template_context()
96
+ context["exception"] = self.exception
97
+ return context
98
+
99
+
100
+ @register_toolbar_panel
101
+ class _RequestToolbarPanel(ToolbarPanel):
102
+ name = "Request"
103
+ template_name = "toolbar/request.html"
104
+
105
+
106
+ @register_toolbar_panel
107
+ class _QuerystatsToolbarPanel(ToolbarPanel):
108
+ name = "Querystats"
109
+ template_name = "toolbar/querystats.html"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.admin
3
- Version: 0.28.0
3
+ Version: 0.29.0
4
4
  Summary: Admin dashboard and tools for Plain.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-Expression: BSD-3-Clause
@@ -5,7 +5,7 @@ plain/admin/dates.py,sha256=EEhcQhHt3-k6kE9yvPdH5X6EecmUQ259xywbDBec3Dg,10253
5
5
  plain/admin/default_settings.py,sha256=S22r8JtwY-ArlNO4waBOrnRfb2qPbUQ5VSP6niJRzZw,145
6
6
  plain/admin/middleware.py,sha256=k3yP1o3CzvLiZZSoxqq-DvAZlp4sICRauaT-kD3FJKM,398
7
7
  plain/admin/templates.py,sha256=0xgMQmJEbh5U45ZlN2f15Xs42Y2A_lSS-_wdMp1BeD4,854
8
- plain/admin/toolbar.py,sha256=GrIy_Kdm1KVRkOq4fKmzCfmTcXzoe6vcC3LBG4hEFoM,925
8
+ plain/admin/toolbar.py,sha256=nYZOtsN4li_qFpdClk6mOwwv8ApCleCMcaGXFzAEW4Y,2782
9
9
  plain/admin/urls.py,sha256=sriMi2RCkcrkjCX3CIIP1-Qzs_zDm2pxXeOw28vc7Y4,1301
10
10
  plain/admin/assets/admin/admin.css,sha256=Gu6GpRymJriFitAaKh_P5Sm8ZKrX9jdw6Uflxgypff8,2786
11
11
  plain/admin/assets/admin/admin.js,sha256=2-o4g6EtiiF2HGZIKfnVkC8shXFjY1xFSehDlR9550s,2852
@@ -14,7 +14,7 @@ plain/admin/assets/admin/jquery-3.6.1.slim.min.js,sha256=W2eb4M1jdKpuZ_-_KnDgqI9
14
14
  plain/admin/assets/admin/list.js,sha256=_DPneRvk3VSzjVzfEaxyif4vLD75sCWz7bkHYp89uL8,1826
15
15
  plain/admin/assets/admin/popper.min.js,sha256=SgCxkjQZdrt2puqn62YUu9hknpCBGBEAy9uhQ9PPZaI,20083
16
16
  plain/admin/assets/admin/tippy-bundle.umd.min.js,sha256=oVWBpeGTKMG_iBWGkQF02JnGIMFPYuFqTjUWeJY3pZ0,25668
17
- plain/admin/assets/toolbar/toolbar.js,sha256=lT63RDzAxHn95IaVl7Wx4ycAaOUjS_IVeCCYiK3iRkc,5265
17
+ plain/admin/assets/toolbar/toolbar.js,sha256=v4gUy9YEOOPmBg7_fa80QDX_6X2Fb0mU7TzM-77ZSmg,5422
18
18
  plain/admin/cards/__init__.py,sha256=8NfWrguyJRriJFUc3_QeGaDILhgeU3d1aXktzIuAR1E,172
19
19
  plain/admin/cards/base.py,sha256=ESYY0tX3OossyZi9ubCrLxwxUZ0Z3snZUCVmLvTIg-U,2247
20
20
  plain/admin/cards/charts.py,sha256=uUNO_uN_GVdkwYNSTx1bt1j2L-89rEyIg0vDaxP-9NE,4929
@@ -29,10 +29,10 @@ plain/admin/impersonate/urls.py,sha256=s8bwi8qPueKCCYcLW75p-hPFkBKhm2AMi6AQKQcZs
29
29
  plain/admin/impersonate/views.py,sha256=PzVmzhlS0LmbHgxdLhc2G27ltvk3zgmO-3QWNAcD-l0,807
30
30
  plain/admin/querystats/README.md,sha256=ONscu4dQOVe20CPHFyI8vR8iL2kvo3cOM8iwVO-lDyM,4821
31
31
  plain/admin/querystats/__init__.py,sha256=VmP1aQ5Pviq4Z3izCB8G9g0Weq-2SYR88UFNtwqAPpo,81
32
- plain/admin/querystats/core.py,sha256=g7Iz8i4J7CUFL_HqTneoBKpJcVUxsAzLCPlqdX2cKTU,4430
33
- plain/admin/querystats/middleware.py,sha256=4Y3AE7CSsNkCgvQ0xtp6KkKxB4yc3eDjOcJBH479hFw,3478
32
+ plain/admin/querystats/core.py,sha256=6_SJIxS0T_KIFcS-5mMajp074vnkEKGBv9TamVeI0VU,4470
33
+ plain/admin/querystats/middleware.py,sha256=isMjV5X-8SP-8IQEaJMfXWarZQaQBQsI5uXV0JHKXnY,3347
34
34
  plain/admin/querystats/urls.py,sha256=H8wMpqKBnXqA8ZsdwdxTKQguNYJ0JsMRqqMunccBm2I,198
35
- plain/admin/querystats/views.py,sha256=noSElnwo3DhRL9a77rwaDDZ16HMLRqeUf4MsW4gHjqU,2582
35
+ plain/admin/querystats/views.py,sha256=cXmHZtEgRwXNN5d9HsKBe5G_VnDgSNwpwoRaEvuJ7vo,2375
36
36
  plain/admin/templates/admin/base.html,sha256=npIwsxHa7Gf5FYTmsXfgSor2bbYIWwTSk0-UFmnqmfE,8481
37
37
  plain/admin/templates/admin/delete.html,sha256=lNuU2G-BR6TH6NUmh7VcvjnEuFeI84rwwk_oO1jkUq0,431
38
38
  plain/admin/templates/admin/detail.html,sha256=AizpXs6HguFzwbk7JDbH8poJB5dM2CaVVaQ4FThAHaw,730
@@ -66,9 +66,12 @@ plain/admin/templates/elements/admin/SelectField.html,sha256=P2-vXifOs2-ie20AgLy
66
66
  plain/admin/templates/elements/admin/Submit.html,sha256=1Lgn3Du9rXplbM3V12z2JckSaiWPlPGLP48xIZ887AA,150
67
67
  plain/admin/templates/elements/admin/Textarea.html,sha256=nCSaGa9t5A5Oj6ZPWW-jSJiGqI1NLPahhXJblq62QME,363
68
68
  plain/admin/templates/elements/admin/TextareaField.html,sha256=4IOJapBNEfhUpMkkLW-gliIefZCEMn5aKyW4QagfcNw,223
69
- plain/admin/templates/querystats/querystats.html,sha256=ie6_z5f2Vd0Z6EIwzlu7nE-QzjlcVEdjOeM7hsE5G2A,7385
69
+ plain/admin/templates/querystats/querystats.html,sha256=xJ6fU22aWFQq3wATmRo7vFakmq-x7P6Z3OdZ2aVYmSs,7514
70
70
  plain/admin/templates/querystats/toolbar.html,sha256=mvERfzU-CdzIBS7aQNosazSeCe9iyDOsswCGFxssdXc,4319
71
- plain/admin/templates/toolbar/toolbar.html,sha256=AKISfakxIrjOTEmPsDI58hXpNthQQjSuSe444bSddIA,15561
71
+ plain/admin/templates/toolbar/exception.html,sha256=4WcrcBTCuyO_Jket8aaMFEL17o3FN3pF2QLrP7Pr60o,937
72
+ plain/admin/templates/toolbar/querystats.html,sha256=A_nRAyrVGM94CnQk-jqz9FKFqL0ynVXIykhl8mw_r6I,997
73
+ plain/admin/templates/toolbar/request.html,sha256=VyxNpEISVYZtGkR4J0XiXkv8d3LticUldJYXGE-OQNg,2967
74
+ plain/admin/templates/toolbar/toolbar.html,sha256=i4s1uxchb5MgoNjr6Eo8ahOucuZcMDt9JKS5FRQeqEw,6818
72
75
  plain/admin/views/__init__.py,sha256=nF6AENZ3Xxyi08OTRrF6e-HYBkZSFj7XBK2mVzMYqN4,846
73
76
  plain/admin/views/base.py,sha256=S1oaMUXnMOwRozbn2K-tk9tL4BMimemfMagZD9QxrJw,3512
74
77
  plain/admin/views/models.py,sha256=DAv7YzeSyQHLLAVdUhSPCkmx2B10g5ksAjHm2jrgQfw,5973
@@ -76,7 +79,7 @@ plain/admin/views/objects.py,sha256=eKL8A2B1ZMgTrCbTXnh6vCeju_HObxwetn_xc1vYlfY,
76
79
  plain/admin/views/registry.py,sha256=Lxib4YSQCMHb_zACnLKymJakV8jCZPWYll7J8-aV9Xw,3712
77
80
  plain/admin/views/types.py,sha256=ONMMdUoapgMoUVYgSIe-4YCdfvaVMQ4jgPWYiMo0pDk,178
78
81
  plain/admin/views/viewsets.py,sha256=dqMlQ6kLn9iqd9BwBWAZT1S271wH1FdfM5HXbOgBMEw,1655
79
- plain_admin-0.28.0.dist-info/METADATA,sha256=svNc-LixFxD0pxyDsX5-lQGmwQjYtFvk8c4GwfjHjHo,4237
80
- plain_admin-0.28.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
81
- plain_admin-0.28.0.dist-info/licenses/LICENSE,sha256=cvKM3OlqHx3ijD6e34zsSUkPvzl-ya3Dd63A6EHL94U,1500
82
- plain_admin-0.28.0.dist-info/RECORD,,
82
+ plain_admin-0.29.0.dist-info/METADATA,sha256=JtWV7anBKCg5K6C_nW05YGcyTcCXXp8TS6CJpDNfUVk,4237
83
+ plain_admin-0.29.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
84
+ plain_admin-0.29.0.dist-info/licenses/LICENSE,sha256=cvKM3OlqHx3ijD6e34zsSUkPvzl-ya3Dd63A6EHL94U,1500
85
+ plain_admin-0.29.0.dist-info/RECORD,,