plain.admin 0.27.1__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.
@@ -32,6 +32,48 @@ var plainToolbar = {
32
32
  toggleExpand: function () {
33
33
  this.expanded = !this.expanded;
34
34
  document.querySelector("#plaintoolbar-details").classList.toggle("hidden");
35
+ localStorage.setItem('plaintoolbar.expanded', this.expanded ? '1' : '0');
36
+ },
37
+ expand: function () {
38
+ this.expanded = true;
39
+ document.querySelector("#plaintoolbar-details").classList.remove("hidden");
40
+ localStorage.setItem('plaintoolbar.expanded', '1');
41
+ },
42
+ collapse: function () {
43
+ this.expanded = false;
44
+ document.querySelector("#plaintoolbar-details").classList.add("hidden");
45
+ localStorage.setItem('plaintoolbar.expanded', '0');
46
+ },
47
+ showTab: function (tabName) {
48
+ this.expand();
49
+
50
+ var toolbar = document.querySelector("#plaintoolbar");
51
+ var tab = toolbar.querySelector("div[data-toolbar-tab=" + tabName + "]");
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
+
59
+ // Hide all children in the tab parent
60
+ for (var i = 0; i < tab.parentNode.children.length; i++) {
61
+ var child = tab.parentNode.children[i];
62
+ if (child !== tab) {
63
+ child.style.display = "none";
64
+ }
65
+ }
66
+
67
+ tab.style.display = "block";
68
+
69
+ toolbar.querySelectorAll("button[data-toolbar-tab]").forEach(function (tab) {
70
+ if (tab.dataset.toolbarTab === tabName) {
71
+ tab.setAttribute("data-active", true);
72
+ } else {
73
+ tab.removeAttribute("data-active");
74
+ }
75
+ });
76
+ localStorage.setItem('plaintoolbar.tab', tabName);
35
77
  },
36
78
  };
37
79
 
@@ -40,12 +82,87 @@ if (plainToolbar.shouldHide()) {
40
82
  plainToolbar.hide();
41
83
  }
42
84
 
43
- window.addEventListener("load", function () {
44
- document.querySelector('[data-plaintoolbar-hide]').addEventListener('click', function() {
45
- console.log("Hiding admin toolbar for 1 hour");
46
- plainToolbar.hideUntil(Date.now() + 3600000);
85
+ window.addEventListener("load", function() {
86
+ // Restore expanded/collapsed state
87
+ var state = localStorage.getItem('plaintoolbar.expanded');
88
+ if (state === '1') {
89
+ plainToolbar.expand();
90
+ // Restore last active tab
91
+ var lastTab = localStorage.getItem('plaintoolbar.tab');
92
+ if (lastTab) {
93
+ plainToolbar.showTab(lastTab);
94
+ }
95
+ } else if (state === '0') {
96
+ plainToolbar.collapse();
97
+ }
98
+ var toolbar = document.querySelector("#plaintoolbar");
99
+
100
+ toolbar.querySelectorAll("button[data-toolbar-tab]").forEach(function (tab) {
101
+ tab.addEventListener("click", function () {
102
+ plainToolbar.showTab(tab.dataset.toolbarTab);
47
103
  });
48
- document.querySelector('[data-plaintoolbar-expand]').addEventListener('click', function() {
49
- plainToolbar.toggleExpand();
104
+ });
105
+
106
+ toolbar.querySelectorAll('[data-plaintoolbar-hide]').forEach(function(btn) {
107
+ btn.addEventListener('click', function() {
108
+ plainToolbar.hide();
50
109
  });
110
+ });
111
+
112
+ toolbar.querySelectorAll('[data-plaintoolbar-hideuntil]').forEach(function(btn) {
113
+ btn.addEventListener('click', function() {
114
+ console.log("Hiding admin toolbar for 1 hour");
115
+ plainToolbar.hideUntil(Date.now() + 3600000);
116
+ });
117
+ });
118
+
119
+ toolbar.querySelectorAll('[data-plaintoolbar-expand]').forEach(function(btn) {
120
+ btn.addEventListener('click', function() {
121
+ plainToolbar.toggleExpand();
122
+ });
123
+ });
124
+
125
+ // Enable manual resize of the expanded toolbar via drag handle
126
+ var details = document.getElementById('plaintoolbar-details');
127
+ if (details) {
128
+ var handle = details.querySelector('[data-resizer]');
129
+ var content = handle.nextElementSibling;
130
+ var isDragging = false;
131
+ var startY = 0;
132
+ var startHeight = 0;
133
+ if (handle && content) {
134
+ // Initial cursor
135
+ handle.style.cursor = 'grab';
136
+ // Start dragging
137
+ handle.addEventListener('mousedown', function(e) {
138
+ isDragging = true;
139
+ startY = e.clientY;
140
+ startHeight = content.offsetHeight;
141
+ handle.style.cursor = 'grabbing';
142
+ // Prevent text selection while dragging
143
+ document.body.style.userSelect = 'none';
144
+ e.preventDefault();
145
+ });
146
+ // Handle dragging
147
+ document.addEventListener('mousemove', function(e) {
148
+ if (!isDragging) return;
149
+ var delta = e.clientY - startY;
150
+ // Calculate new height: dragging up increases height
151
+ var newHeight = startHeight - delta;
152
+ // Clamp between reasonable bounds
153
+ var minHeight = 50;
154
+ var maxHeight = window.innerHeight - 100;
155
+ newHeight = Math.max(minHeight, Math.min(maxHeight, newHeight));
156
+ content.style.height = newHeight + 'px';
157
+ });
158
+ // End dragging
159
+ document.addEventListener('mouseup', function() {
160
+ if (isDragging) {
161
+ isDragging = false;
162
+ handle.style.cursor = 'grab';
163
+ document.body.style.userSelect = '';
164
+ }
165
+ });
166
+ }
167
+ }
51
168
  });
@@ -1,3 +1,4 @@
1
+ import datetime
1
2
  import time
2
3
  import traceback
3
4
  from collections import Counter
@@ -141,10 +142,9 @@ class QueryStats:
141
142
  "request": {
142
143
  "path": request.path,
143
144
  "method": request.method,
144
- "headers": dict(request.headers),
145
145
  "unique_id": request.unique_id,
146
146
  },
147
- "timestamp": time.time(),
147
+ "timestamp": datetime.datetime.now().isoformat(),
148
148
  "total_time_display": self.total_time_display,
149
149
  "queries": self.queries,
150
150
  }
@@ -55,12 +55,12 @@ class QueryStatsMiddleware:
55
55
  if self.should_ignore_request(request):
56
56
  return self.get_response(request)
57
57
 
58
- session_querystats_enabled = "querystats" in request.session
58
+ def is_tracking():
59
+ return "querystats" in request.session
60
+
61
+ querystats = QueryStats(include_tracebacks=is_tracking())
59
62
 
60
- querystats = QueryStats(include_tracebacks=session_querystats_enabled)
61
63
  with connection.execute_wrapper(querystats):
62
- # Have to wrap this first call so it is included in the querystats,
63
- # but we don't have to wrap everything else unless we are admin or debug
64
64
  is_admin = self.is_admin_request(request)
65
65
 
66
66
  if settings.DEBUG or is_admin:
@@ -75,7 +75,7 @@ class QueryStatsMiddleware:
75
75
  # by using the server timing API which can be parsed client-side
76
76
  response.headers["Server-Timing"] = querystats.as_server_timing()
77
77
 
78
- if session_querystats_enabled and querystats.num_queries > 0:
78
+ if is_tracking() and querystats.num_queries > 0:
79
79
  request.session["querystats"][request.unique_id] = json.dumps(
80
80
  querystats.as_context_dict(request), cls=QueryStatsJSONEncoder
81
81
  )
@@ -17,15 +17,32 @@ class QuerystatsView(AuthViewMixin, TemplateView):
17
17
 
18
18
  super().check_auth()
19
19
 
20
+ def get_response(self):
21
+ response = super().get_response()
22
+ # So we can load it in the toolbar
23
+ response.headers["X-Frame-Options"] = "SAMEORIGIN"
24
+ return response
25
+
26
+ def get(self):
27
+ # Give an easy out if things get messed up
28
+ if (
29
+ "clear" in self.request.query_params
30
+ and "querystats" in self.request.session
31
+ ):
32
+ del self.request.session["querystats"]
33
+ self.request.session.modified = True
34
+
35
+ return super().get()
36
+
20
37
  def get_template_context(self):
21
38
  context = super().get_template_context()
22
39
 
23
40
  querystats = self.request.session.get("querystats", {})
24
41
 
25
- for request_id, json_data in querystats.items():
42
+ for request_id in list(querystats.keys()):
26
43
  try:
27
- querystats[request_id] = json.loads(json_data)
28
- except json.JSONDecodeError:
44
+ querystats[request_id] = json.loads(querystats[request_id])
45
+ except (json.JSONDecodeError, TypeError):
29
46
  # If decoding fails, remove the entry from the dictionary
30
47
  del querystats[request_id]
31
48
 
@@ -39,6 +56,7 @@ class QuerystatsView(AuthViewMixin, TemplateView):
39
56
  )
40
57
 
41
58
  context["querystats"] = querystats
59
+ context["querystats_enabled"] = "querystats" in self.request.session
42
60
 
43
61
  return context
44
62
 
@@ -6,11 +6,17 @@
6
6
  <title>Querystats</title>
7
7
  {% tailwind_css %}
8
8
  </head>
9
- <body class="bg-stone-950 text-stone-300">
9
+ <body class="text-stone-300">
10
10
 
11
- <div class="flex items-center justify-between px-6 py-4">
12
- <h1 class="text-lg font-semibold">Querystats</h1>
11
+ {% if querystats_enabled %}
12
+ <div class="flex items-center justify-between border-b border-white/5 px-6 h-14 fixed top-0 left-0 right-0 bg-stone-950 z-10">
13
+ <!-- <h1 class="text-lg font-semibold">Querystats</h1> -->
14
+ <div></div>
13
15
  <div class="flex items-center space-x-2">
16
+ <form method="get" action=".">
17
+ {{ csrf_input }}
18
+ <button type="submit" class="px-2 py-px text-sm rounded-sm bg-stone-700 text-stone-300 hover:bg-stone-600 cursor-pointer whitespace-nowrap">Reload</button>
19
+ </form>
14
20
  <form method="post" action=".">
15
21
  {{ csrf_input }}
16
22
  <input type="hidden" name="querystats_action" value="clear">
@@ -23,88 +29,116 @@
23
29
  </form>
24
30
  </div>
25
31
  </div>
32
+ {% endif %}
26
33
 
27
- <div class="space-y-6 px-6 py-4">
28
- {% for request_id, qs in querystats.items() %}
29
- <div class="p-3 bg-white/5 rounded">
30
- <div class="flex justify-between items-center">
31
- <div>
32
- <h2 class="font-medium"><span class="font-semibold">{{ qs.request.method }}</span> {{ qs.request.path }}</h2>
33
- <p class="text-sm text-stone-400">{{ qs.summary }}</p>
34
- </div>
35
- <div class=text-xs>
36
- <p>Request ID <code>{{ qs.request.unique_id }}</code></p>
37
- <p>Timestamp {{ qs.timestamp }}</p>
38
- <details>
39
- <summary>Headers</summary>
40
- <pre><code>{{ qs.request.get("headers", {})|pprint }}</code></pre>
41
- </details>
34
+ {% if querystats %}
35
+ <div class="flex mt-2 h-full">
36
+ <aside id="sidebar" class="fixed left-0 top-14 bottom-0 w-82 overflow-auto p-4">
37
+ <ul class="space-y-2">
38
+ {% for request_id, qs in querystats.items() %}
39
+ <li>
40
+ <button data-request-id="{{ request_id }}" class="w-full text-left px-2 py-1 rounded hover:bg-stone-700 cursor-pointer">
41
+ <span class="text-sm">{{ qs.request.path }}</span>
42
+ <span class="font-semibold bg-white/5 rounded-sm px-1 py-0.5 text-xs">{{ qs.request.method }}</span>
43
+ <div class="text-xs text-stone-400">{{ qs.summary }}</div>
44
+ <div class="text-xs text-stone-500">{{ qs.timestamp|fromisoformat|timesince }} ago</div>
45
+ </button>
46
+ </li>
47
+ {% endfor %}
48
+ </ul>
49
+ </aside>
50
+
51
+ <main id="content" class="flex-1 p-6 overflow-auto ml-82 mt-14">
52
+ {% for request_id, qs in querystats.items() %}
53
+ <div class="request-detail" data-request-id="{{ request_id }}" style="display: none;">
54
+ <div class="flex justify-between">
55
+ <div>
56
+ <h2 class="font-medium text-sm"><span class="font-semibold">{{ qs.request.method }}</span> {{ qs.request.path }}</h2>
57
+ <p class="text-sm text-white/70">{{ qs.summary }}</p>
58
+ </div>
59
+ <div class="text-right">
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|fromisoformat }}</code></div>
62
+ </div>
42
63
  </div>
43
- </div>
44
64
 
45
- <div class="flex w-full mt-5 overflow-auto rounded-sm">
46
- {% for query in qs.queries %}
47
- <a href="#query-{{ loop.index }}"
48
- {{ loop.cycle('class=\"h-4 bg-amber-300\"', 'class=\"h-4 bg-amber-400\"', 'class="h-4 bg-amber-500"', 'class="h-4 bg-amber-600"')|safe }}
49
- title="[{{ query.duration_display }}] {{ query.sql_display }}"
50
- style="width: {{ query.duration / qs.total_time * 100 }}%">
51
- </a>
52
- {% endfor %}
53
- </div>
65
+ <div class="flex w-full mt-3 overflow-auto rounded-sm">
66
+ {% for query in qs.queries %}
67
+ <a href="#query-{{ loop.index }}"
68
+ {{ loop.cycle('class=\"h-2 bg-amber-400\"', 'class=\"h-2 bg-orange-400\"', 'class=\"h-2 bg-yellow-400\"', 'class=\"h-2 bg-amber-600\"')|safe }}
69
+ title="[{{ query.duration_display }}] {{ query.sql_display }}"
70
+ style="width: {{ query.duration / qs.total_time * 100 }}%">
71
+ </a>
72
+ {% endfor %}
73
+ </div>
54
74
 
55
- <div class="mt-4 space-y-3 text-xs">
56
- {% for query in qs.queries %}
57
- <details id="query-{{ loop.index }}" class="p-2 rounded bg-zinc-800">
58
- <summary class="truncate">
59
- <div class="float-right px-2 py-px mb-px ml-2 text-xs rounded-full bg-zinc-700">
60
- <span>{{ query.duration_display }}</span>
61
- {% if query.duplicate_count is defined %}
62
- <span class="text-red-500">&nbsp; duplicated {{ query.duplicate_count }} times</span>
75
+ <div class="mt-4 space-y-3 text-xs">
76
+ {% for query in qs.queries %}
77
+ <details id="query-{{ loop.index }}" class="p-2 rounded bg-white/5">
78
+ <summary class="truncate">
79
+ <div class="float-right px-2 py-px mb-px ml-2 text-xs rounded-full bg-zinc-700">
80
+ <span>{{ query.duration_display }}</span>
81
+ {% if query.duplicate_count is defined %}
82
+ <span class="text-red-500">&nbsp; duplicated {{ query.duplicate_count }} times</span>
83
+ {% endif %}
84
+ </div>
85
+ <code class="font-mono">{{ query.sql }}</code>
86
+ </summary>
87
+ <div class="space-y-3 mt-3">
88
+ <div>
89
+ <pre><code class="font-mono whitespace-pre-wrap text-zinc-100">{{ query.sql_display }}</code></pre>
90
+ </div>
91
+ <div class="text-zinc-400">
92
+ <span class="font-medium">Parameters</span>
93
+ <pre><code class="font-mono">{{ query.params|pprint }}</code></pre>
94
+ </div>
95
+ {% if query.tb|default(false) %}
96
+ <details>
97
+ <summary>Traceback</summary>
98
+ <pre><code class="block overflow-x-auto font-mono text-xs">{{ query.tb }}</code></pre>
99
+ </details>
63
100
  {% endif %}
64
-
65
- {#
66
- <div>many {{ query.many }}</div>
67
- <div>result {{ query.result }}</div>
68
- #}
69
- </div>
70
- <code class="font-mono">{{ query.sql }}</code>
71
- </summary>
72
- <div class="space-y-3 mt-3">
73
- <div>
74
- <pre><code class="font-mono whitespace-pre-wrap text-zinc-100">{{ query.sql_display }}</code></pre>
75
- </div>
76
- <div class="text-zinc-400">
77
- <span class="font-medium">Parameters</span>
78
- <pre><code class="font-mono">{{ query.params|pprint }}</code></pre>
79
101
  </div>
80
- <details>
81
- <summary>Traceback</summary>
82
- <pre><code class="block overflow-x-auto font-mono text-xs">{{ query.tb }}</code></pre>
83
- </details>
84
- </div>
85
- </details>
86
- {% else %}
87
- <div>No queries...</div>
88
- {% endfor %}
102
+ </details>
103
+ {% else %}
104
+ <div>No queries...</div>
105
+ {% endfor %}
106
+ </div>
89
107
  </div>
90
- </div>
91
-
92
- {% else %}
93
-
94
- <div class="text-center">
95
- {% if "querystats" in request.session %}
96
- <div class="text-stone-500">Querystats are enabled but nothing has been tracked yet.</div>
97
- {% else %}
98
- <form method="post" action=".">
99
- {{ csrf_input }}
100
- <input type="hidden" name="querystats_action" value="enable">
101
- <button type="submit" class="px-2 rounded-sm bg-stone-700 text-stone-300 hover:bg-stone-600 cursor-pointer whitespace-nowrap">Enable querystats</button>
102
- </form>
103
- {% endif %}
104
- </div>
105
-
106
- {% endfor %}
108
+ {% endfor %}
109
+ </main>
110
+ </div>
111
+ {% elif querystats_enabled %}
112
+ <div class="text-center text-white/30 py-8">Querystats are enabled but nothing has been recorded yet.</div>
113
+ {% else %}
114
+ <div class="text-center py-8">
115
+ <div class="text-white/30">Querystats are disabled.</div>
116
+ <form method="post" action=".">
117
+ {{ csrf_input }}
118
+ <input type="hidden" name="querystats_action" value="enable">
119
+ <button type="submit" class="mt-2 px-2 py-px text-sm rounded-sm bg-stone-700 text-stone-300 hover:bg-stone-600 cursor-pointer whitespace-nowrap">Enable</button>
120
+ </form>
107
121
  </div>
122
+ {% endif %}
123
+
124
+ <script>
125
+ document.addEventListener('DOMContentLoaded', function() {
126
+ const buttons = document.querySelectorAll('#sidebar [data-request-id]');
127
+ const details = document.querySelectorAll('#content .request-detail');
128
+ buttons.forEach(function(btn) {
129
+ btn.addEventListener('click', function(e) {
130
+ e.preventDefault();
131
+ const id = this.getAttribute('data-request-id');
132
+ details.forEach(div => div.style.display = 'none');
133
+ const sel = document.querySelector('#content .request-detail[data-request-id="' + id + '"]');
134
+ if (sel) sel.style.display = 'block';
135
+ buttons.forEach(b => b.classList.remove('bg-stone-700', 'text-white'));
136
+ this.classList.add('bg-stone-700', 'text-white');
137
+ });
138
+ });
139
+ if (buttons.length > 0) buttons[0].click();
140
+ });
141
+ </script>
108
142
 
109
- </body>
143
+ </body>
110
144
  </html>
@@ -1,25 +1,25 @@
1
1
  <div data-querystats class="relative group/querystats" style="display: none;">
2
2
  {% if "querystats" in request.session %}
3
- <a href="{{ url('admin:querystats:querystats') }}" target="querystats" class="inline-flex items-center cursor-pointer text-xs rounded-full px-2 py-px bg-stone-700 text-stone-300 whitespace-nowrap">
3
+ <button data-toolbar-tab="querystats" class="inline-flex items-center cursor-pointer text-xs rounded-full px-2 py-px bg-white/20 text-white/80 whitespace-nowrap">
4
4
  <span class="relative inline-flex size-2 mr-2">
5
5
  <span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
6
6
  <span class="relative inline-flex size-2 rounded-full bg-green-500"></span>
7
7
  </span>
8
8
  <span data-querystats-summary></span>
9
- </a>
9
+ </button>
10
10
  {% else %}
11
11
  <form action="{{ url('admin:querystats:querystats') }}" method="post">
12
12
  {{ csrf_input }}
13
13
  <input type="hidden" name="redirect_url" value="{{ request.get_full_path() }}">
14
14
  <input type="hidden" name="querystats_action" value="enable">
15
- <button type="submit" class="cursor-pointer text-xs rounded-full px-2 py-px bg-stone-700 text-stone-300 whitespace-nowrap">
15
+ <button type="submit" class="cursor-pointer text-xs rounded-full px-2 py-px bg-white/20 text-white/80 whitespace-nowrap">
16
16
  <span class="rounded-full bg-zinc-500 w-2 h-2 inline-block mr-1"></span>
17
17
  <span data-querystats-summary></span>
18
18
  </button>
19
19
  </form>
20
20
  {% endif %}
21
21
 
22
- <div data-querystats-list style="display: none;" class="absolute z-50 -translate-x-1/2 hidden -translate-y-full left-1/2 -top-1 group-hover/querystats:block">
22
+ <div data-querystats-list style="display: none;" class="absolute z-50 hidden -translate-y-full right-0 -top-1 group-hover/querystats:block">
23
23
  <div class="p-2 text-xs border rounded shadow-md bg-zinc-900 border-zinc-700"><table><tbody></tbody></table></div>
24
24
  </div>
25
25
  <script async defer>
@@ -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,10 +1,48 @@
1
1
  {% if toolbar.should_render() %}
2
- {% set exception=toolbar.request_exception() %}
3
- <script src="{{ asset('toolbar/toolbar.js') }}"></script>
4
- <div id="plaintoolbar" class="print:hidden text-sm py-1.5 text-stone-300 fixed bottom-3 mx-3 max-w-full drop-shadow-sm z-30 ring-1 ring-stone-200/5 rounded-2xl lg:flex lg:flex-col -translate-x-1/2 left-1/2 max-h-[90vh] bg-gradient-to-b from-stone-950/90 to-stone-950/95 backdrop-blur-sm">
5
- <div class="flex justify-between px-3 mx-auto space-x-4">
2
+ {% set panels=toolbar.get_panels() %}
3
+ <script defer src="{{ asset('toolbar/toolbar.js') }}"></script>
4
+ <div id="plaintoolbar" class="print:hidden text-stone-300 fixed bottom-0 w-full z-30 hidden sm:flex sm:flex-col">
5
+
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
+
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>
14
+ </div>
15
+ <div class="px-4 flex items-center space-x-4">
16
+ <button title="Hide toolbar for 1 hour" class="cursor-pointer hover:text-white text-white/50" type="button" data-plaintoolbar-hideuntil>
17
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-4 h-4" viewBox="0 0 16 16">
18
+ <path d="M8.515 1.019A7 7 0 0 0 8 1V0a8 8 0 0 1 .589.022zm2.004.45a7 7 0 0 0-.985-.299l.219-.976q.576.129 1.126.342zm1.37.71a7 7 0 0 0-.439-.27l.493-.87a8 8 0 0 1 .979.654l-.615.789a7 7 0 0 0-.418-.302zm1.834 1.79a7 7 0 0 0-.653-.796l.724-.69q.406.429.747.91zm.744 1.352a7 7 0 0 0-.214-.468l.893-.45a8 8 0 0 1 .45 1.088l-.95.313a7 7 0 0 0-.179-.483m.53 2.507a7 7 0 0 0-.1-1.025l.985-.17q.1.58.116 1.17zm-.131 1.538q.05-.254.081-.51l.993.123a8 8 0 0 1-.23 1.155l-.964-.267q.069-.247.12-.501m-.952 2.379q.276-.436.486-.908l.914.405q-.24.54-.555 1.038zm-.964 1.205q.183-.183.35-.378l.758.653a8 8 0 0 1-.401.432z"/>
19
+ <path d="M8 1a7 7 0 1 0 4.95 11.95l.707.707A8.001 8.001 0 1 1 8 0z"/>
20
+ <path d="M7.5 3a.5.5 0 0 1 .5.5v5.21l3.248 1.856a.5.5 0 0 1-.496.868l-3.5-2A.5.5 0 0 1 7 9V3.5a.5.5 0 0 1 .5-.5"/>
21
+ </svg>
22
+ </button>
23
+ <button class="cursor-pointer hover:text-white text-white/50" type="button" data-plaintoolbar-expand>
24
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-4 h-4" viewBox="0 0 16 16">
25
+ <path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708"/>
26
+ </svg>
27
+ </button>
28
+ </div>
29
+ </div>
30
+
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>
32
+
33
+ <div class="overflow-auto h-[30vh] flex flex-col">
34
+ {% for panel in panels %}
35
+ <div data-toolbar-tab="{{ panel.name }}" {% if not loop.first %}style="display: none;"{% endif %}>
36
+ {{ panel.render() }}
37
+ </div>
38
+ {% endfor %}
39
+ </div>
40
+
41
+ </div>
42
+
43
+ <div class="flex px-3 text-xs border-t border-white/5 space-x-4 py-2 bg-stone-950 shadow-xl">
6
44
  <div class="flex items-center">
7
- <code class="ml-1.5 text-xs whitespace-nowrap text-mono">{{ toolbar.version }}</code>
45
+ <code class="ml-1.5 bg-white/10 px-1.5 rounded-sm whitespace-nowrap text-mono">{{ toolbar.version }}</code>
8
46
 
9
47
  {% if request.impersonator is defined %}
10
48
  <div class="flex items-center ml-1 font-light">
@@ -18,69 +56,39 @@
18
56
  </div>
19
57
  {% endif %}
20
58
  </div>
59
+ <button type="button" data-plaintoolbar-expand class="flex-grow cursor-pointer"></button>
21
60
  <div class="flex items-center space-x-4">
22
61
  {% include "querystats/toolbar.html" %}
23
62
 
24
63
  <div class="flex items-center space-x-3 transition-all">
25
- <a href="{{ url('admin:index') }}" class="hover:underline">Admin</a>
26
- {% if object|default(false) and object|get_admin_model_detail_url %}
27
- <a class="inline-flex items-center p-1 text-blue-500 hover:text-blue-400" href="{{ object|get_admin_model_detail_url }}">
28
- <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">
29
- <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"/>
30
- <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"/>
31
- <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"/>
32
- <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"/>
64
+
65
+ {% if toolbar.request_exception() %}
66
+ <button class="cursor-pointer text-amber-500 hover:text-amber-400" type="button" data-toolbar-tab="Exception">
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">
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"/>
33
69
  </svg>
34
- </a>
70
+ </button>
35
71
  {% endif %}
72
+
73
+ <a href="{{ url('admin:index') }}" class="hover:underline">Admin</a>
74
+
36
75
  {% include "toolbar/links.html" ignore missing %}
37
- <button data-plaintoolbar-expand class="hover:text-orange-500" type="button">
76
+
77
+ <button data-plaintoolbar-expand class="hover:text-white cursor-pointer" type="button" title="Expand toolbar">
38
78
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-4 h-4" viewBox="0 0 16 16">
39
- <path fill-rule="evenodd" d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z"/>
79
+ <path fill-rule="evenodd" d="M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708z"/>
40
80
  </svg>
41
81
  </button>
42
- </div>
43
- </div>
44
- </div>
45
- <div id="plaintoolbar-details" class="{% if not exception %}hidden{% endif %} p-4 overflow-auto text-sm space-y-2">
46
82
 
47
- {% if exception %}
48
- <div class="p-2 border-amber-500 border rounded">
49
- <div class="text-amber-500 text-lg flex justify-between items-center">
50
- <div>
51
- <span class="font-bold">Exception</span>
52
- {{ exception }}
53
- </div>
54
- <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">
55
- <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"/>
56
- </svg>
57
- </div>
58
- <div class="text-amber-400 text-xs mt-3 bg-white/5 p-2 rounded overflow-auto">
59
- <pre><code>{{ exception._traceback_string }}</code></pre>
60
- </div>
61
- </div>
62
- {% endif %}
63
-
64
- <table>
65
- <tbody>
66
- {% for k, v in toolbar.metadata.items() %}
67
- <tr>
68
- <td class="pr-2 font-medium whitespace-nowrap">{{ k }}</td>
69
- <td class="whitespace-nowrap">{{ v }}</td>
70
- </tr>
71
- {% endfor %}
72
- </tbody>
73
- </table>
83
+ <button data-plaintoolbar-hide class="hover:text-red-500 cursor-pointer" type="button" title="Hide toolbar">
84
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x w-4 h-4" viewBox="0 0 16 16">
85
+ <path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z"/>
86
+ </svg>
87
+ </button>
74
88
 
75
- {% if object|default(false) %}
76
- <div class="font-mono" title="PK: {{ object.pk|default('unknown') }}">
77
- {{ object.__repr__() }}
89
+ </div>
78
90
  </div>
79
- {% endif %}
80
-
81
- <button data-plaintoolbar-hide class="hover:text-red-500" type="button">
82
- Hide toolbar for 1 hour
83
- </button>
84
91
  </div>
92
+
85
93
  </div>
86
94
  {% endif %}
plain/admin/toolbar.py CHANGED
@@ -2,16 +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
- "Request ID": request.unique_id,
14
- }
15
14
 
16
15
  def should_render(self):
17
16
  if settings.DEBUG:
@@ -34,3 +33,77 @@ class Toolbar:
34
33
  traceback.format_tb(exception.__traceback__)
35
34
  )
36
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.27.1
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=doW1Eg9rYfLZulRTAyFACDaUDi2xkDlsdVABzCQKHG4,979
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=kRCQ37iQNklzBjjBeHSeBU39mLpQ4Q0pnC3cdbQAy28,1636
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=kh45lRPEv9lYiTDNI_srrfoJue48v3kcrBNbOIHYCmw,4480
33
- plain/admin/querystats/middleware.py,sha256=g5Ld-Xx1eKq1AfED4oBHNkuhr5nUL1ILrzTv_tQVlPY,3528
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=bt6GKEbvqYb1HFhVOXHUbZErmjQMuvAIC5a3Mb5Pfuw,1754
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=oeDswOjN_11weKsj_x1iOKyTucyU1X7aL1vvxqhIUMc,5088
70
- plain/admin/templates/querystats/toolbar.html,sha256=JFuG97PackHuhRFxnOHEiKGMa_gmCsy3l4PotrwKt9Q,4369
71
- plain/admin/templates/toolbar/toolbar.html,sha256=KcGAG6kRmx60wfqEsdD5C4nDMilH-JvPjHoU6EktfaY,5985
69
+ plain/admin/templates/querystats/querystats.html,sha256=xJ6fU22aWFQq3wATmRo7vFakmq-x7P6Z3OdZ2aVYmSs,7514
70
+ plain/admin/templates/querystats/toolbar.html,sha256=mvERfzU-CdzIBS7aQNosazSeCe9iyDOsswCGFxssdXc,4319
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.27.1.dist-info/METADATA,sha256=CNTq99YxmZ1mso29n54YdPJ6wOdQQw6TRajtUIEVA2c,4237
80
- plain_admin-0.27.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
81
- plain_admin-0.27.1.dist-info/licenses/LICENSE,sha256=cvKM3OlqHx3ijD6e34zsSUkPvzl-ya3Dd63A6EHL94U,1500
82
- plain_admin-0.27.1.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,,