plain.admin 0.25.1__py3-none-any.whl → 0.26.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.
- plain/admin/default_settings.py +3 -3
- plain/admin/querystats/core.py +14 -10
- plain/admin/querystats/middleware.py +32 -28
- plain/admin/querystats/views.py +32 -11
- plain/admin/templates/querystats/querystats.html +89 -57
- plain/admin/templates/querystats/toolbar.html +31 -21
- plain/admin/templates.py +3 -3
- plain/admin/toolbar.py +1 -1
- {plain_admin-0.25.1.dist-info → plain_admin-0.26.0.dist-info}/METADATA +1 -1
- {plain_admin-0.25.1.dist-info → plain_admin-0.26.0.dist-info}/RECORD +12 -12
- {plain_admin-0.25.1.dist-info → plain_admin-0.26.0.dist-info}/WHEEL +0 -0
- {plain_admin-0.25.1.dist-info → plain_admin-0.26.0.dist-info}/licenses/LICENSE +0 -0
plain/admin/default_settings.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
ADMIN_TOOLBAR_CLASS = "plain.admin.toolbar.Toolbar"
|
2
|
+
ADMIN_TOOLBAR_VERSION: str = "dev"
|
3
3
|
|
4
|
-
|
4
|
+
ADMIN_QUERYSTATS_IGNORE_URLS: list[str] = ["/assets/.*"]
|
plain/admin/querystats/core.py
CHANGED
@@ -8,6 +8,8 @@ from plain.utils.functional import cached_property
|
|
8
8
|
|
9
9
|
IGNORE_STACK_FILES = [
|
10
10
|
"threading",
|
11
|
+
"concurrent/futures",
|
12
|
+
"functools.py",
|
11
13
|
"socketserver",
|
12
14
|
"wsgiref",
|
13
15
|
"gunicorn",
|
@@ -15,11 +17,8 @@ IGNORE_STACK_FILES = [
|
|
15
17
|
"sentry_sdk",
|
16
18
|
"querystats/core",
|
17
19
|
"plain/template/base",
|
18
|
-
"plain/
|
19
|
-
"plain/
|
20
|
-
"plain/utils/functional",
|
21
|
-
"plain/core/servers",
|
22
|
-
"plain/core/handlers",
|
20
|
+
"plain/models",
|
21
|
+
"plain/internal",
|
23
22
|
]
|
24
23
|
|
25
24
|
|
@@ -74,7 +73,7 @@ class QueryStats:
|
|
74
73
|
|
75
74
|
# if many, then X times is len(params)
|
76
75
|
|
77
|
-
current_query["result"] = result
|
76
|
+
# current_query["result"] = result
|
78
77
|
|
79
78
|
current_query["duration"] = time.monotonic() - start
|
80
79
|
|
@@ -126,7 +125,7 @@ class QueryStats:
|
|
126
125
|
"num_duplicate_queries": self.num_duplicate_queries,
|
127
126
|
}
|
128
127
|
|
129
|
-
def as_context_dict(self):
|
128
|
+
def as_context_dict(self, request):
|
130
129
|
# If we don't create a dict, the instance of this class
|
131
130
|
# is lost before we can use it in the template
|
132
131
|
for query in self.queries:
|
@@ -137,10 +136,15 @@ class QueryStats:
|
|
137
136
|
if duplicates:
|
138
137
|
query["duplicate_count"] = duplicates
|
139
138
|
|
140
|
-
summary = self.as_summary_dict()
|
141
|
-
|
142
139
|
return {
|
143
|
-
**
|
140
|
+
**self.as_summary_dict(),
|
141
|
+
"request": {
|
142
|
+
"path": request.path,
|
143
|
+
"method": request.method,
|
144
|
+
"headers": dict(request.headers),
|
145
|
+
"unique_id": request.unique_id,
|
146
|
+
},
|
147
|
+
"timestamp": time.time(),
|
144
148
|
"total_time_display": self.total_time_display,
|
145
149
|
"queries": self.queries,
|
146
150
|
}
|
@@ -1,26 +1,19 @@
|
|
1
1
|
import json
|
2
2
|
import logging
|
3
3
|
import re
|
4
|
-
import threading
|
5
4
|
|
6
|
-
from plain.http import ResponseRedirect
|
7
5
|
from plain.json import PlainJSONEncoder
|
8
6
|
from plain.models import connection
|
9
7
|
from plain.runtime import settings
|
10
|
-
from plain.urls import reverse
|
11
8
|
|
12
9
|
from .core import QueryStats
|
13
10
|
|
14
11
|
try:
|
15
|
-
|
16
|
-
import psycopg
|
17
|
-
except ImportError:
|
18
|
-
import psycopg2 as psycopg
|
12
|
+
import psycopg
|
19
13
|
except ImportError:
|
20
14
|
psycopg = None
|
21
15
|
|
22
16
|
logger = logging.getLogger(__name__)
|
23
|
-
_local = threading.local()
|
24
17
|
|
25
18
|
|
26
19
|
class QueryStatsJSONEncoder(PlainJSONEncoder):
|
@@ -28,8 +21,11 @@ class QueryStatsJSONEncoder(PlainJSONEncoder):
|
|
28
21
|
try:
|
29
22
|
return super().default(obj)
|
30
23
|
except TypeError:
|
31
|
-
|
32
|
-
|
24
|
+
print(type(obj))
|
25
|
+
if psycopg and isinstance(obj, psycopg.types.json.Json):
|
26
|
+
return obj.obj
|
27
|
+
elif psycopg and isinstance(obj, psycopg.types.json.Jsonb):
|
28
|
+
return obj.obj
|
33
29
|
else:
|
34
30
|
raise
|
35
31
|
|
@@ -38,7 +34,7 @@ class QueryStatsMiddleware:
|
|
38
34
|
def __init__(self, get_response):
|
39
35
|
self.get_response = get_response
|
40
36
|
self.ignore_url_patterns = [
|
41
|
-
re.compile(url) for url in settings.
|
37
|
+
re.compile(url) for url in settings.ADMIN_QUERYSTATS_IGNORE_URLS
|
42
38
|
]
|
43
39
|
|
44
40
|
def should_ignore_request(self, request):
|
@@ -49,41 +45,49 @@ class QueryStatsMiddleware:
|
|
49
45
|
return False
|
50
46
|
|
51
47
|
def __call__(self, request):
|
52
|
-
|
48
|
+
"""
|
49
|
+
Enables querystats for the current request.
|
50
|
+
|
51
|
+
If DEBUG or an admin, then Server-Timing headers are always added to the response.
|
52
|
+
Full querystats are only stored in the session if they are manually enabled.
|
53
|
+
"""
|
54
|
+
|
55
|
+
if self.should_ignore_request(request):
|
53
56
|
return self.get_response(request)
|
54
57
|
|
55
|
-
|
56
|
-
# Only want these if we're getting ready to show it
|
57
|
-
include_tracebacks=request.GET.get("querystats") == "store"
|
58
|
-
)
|
58
|
+
session_querystats_enabled = "querystats" in request.session
|
59
59
|
|
60
|
+
querystats = QueryStats(include_tracebacks=session_querystats_enabled)
|
60
61
|
with connection.execute_wrapper(querystats):
|
61
62
|
# Have to wrap this first call so it is included in the querystats,
|
62
63
|
# but we don't have to wrap everything else unless we are admin or debug
|
63
64
|
is_admin = self.is_admin_request(request)
|
64
65
|
|
65
|
-
if
|
66
|
-
|
67
|
-
_local.querystats = querystats
|
68
|
-
|
69
|
-
with connection.execute_wrapper(_local.querystats):
|
66
|
+
if settings.DEBUG or is_admin:
|
67
|
+
with connection.execute_wrapper(querystats):
|
70
68
|
response = self.get_response(request)
|
71
69
|
|
72
70
|
if settings.DEBUG:
|
73
71
|
# TODO logging settings
|
74
|
-
logger.debug("Querystats: %s",
|
72
|
+
logger.debug("Querystats: %s", querystats)
|
75
73
|
|
76
74
|
# Make current querystats available on the current page
|
77
75
|
# by using the server timing API which can be parsed client-side
|
78
|
-
response.headers["Server-Timing"] =
|
76
|
+
response.headers["Server-Timing"] = querystats.as_server_timing()
|
79
77
|
|
80
|
-
if
|
81
|
-
request.session["querystats"] = json.dumps(
|
82
|
-
|
78
|
+
if session_querystats_enabled and querystats.num_queries > 0:
|
79
|
+
request.session["querystats"][request.unique_id] = json.dumps(
|
80
|
+
querystats.as_context_dict(request), cls=QueryStatsJSONEncoder
|
83
81
|
)
|
84
|
-
return ResponseRedirect(reverse("querystats:querystats"))
|
85
82
|
|
86
|
-
|
83
|
+
# Keep 30 requests max, in case it is left on by accident
|
84
|
+
if len(request.session["querystats"]) > 30:
|
85
|
+
del request.session["querystats"][
|
86
|
+
list(request.session["querystats"])[0]
|
87
|
+
]
|
88
|
+
|
89
|
+
# Did a deeper modification to the session dict...
|
90
|
+
request.session.modified = True
|
87
91
|
|
88
92
|
return response
|
89
93
|
|
plain/admin/querystats/views.py
CHANGED
@@ -1,27 +1,48 @@
|
|
1
1
|
import json
|
2
2
|
|
3
3
|
from plain.auth.views import AuthViewMixin
|
4
|
+
from plain.http import ResponseRedirect
|
4
5
|
from plain.views import TemplateView
|
5
6
|
|
6
7
|
|
7
8
|
class QuerystatsView(AuthViewMixin, TemplateView):
|
8
9
|
template_name = "querystats/querystats.html"
|
9
|
-
admin_required = True
|
10
|
+
admin_required = True
|
10
11
|
|
11
12
|
def get_template_context(self):
|
12
13
|
context = super().get_template_context()
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
15
|
+
querystats = self.request.session.get("querystats", {})
|
16
|
+
|
17
|
+
for request_id, json_data in querystats.items():
|
18
|
+
try:
|
19
|
+
querystats[request_id] = json.loads(json_data)
|
20
|
+
except json.JSONDecodeError:
|
21
|
+
# If decoding fails, remove the entry from the dictionary
|
22
|
+
del querystats[request_id]
|
23
|
+
|
24
|
+
# Order them by timestamp
|
25
|
+
querystats = dict(
|
26
|
+
sorted(
|
27
|
+
querystats.items(),
|
28
|
+
key=lambda item: item[1].get("timestamp", ""),
|
29
|
+
reverse=True,
|
30
|
+
)
|
31
|
+
)
|
32
|
+
|
33
|
+
context["querystats"] = querystats
|
21
34
|
|
22
35
|
return context
|
23
36
|
|
24
|
-
def
|
25
|
-
|
37
|
+
def post(self):
|
38
|
+
querystats_action = self.request.POST["querystats_action"]
|
39
|
+
|
40
|
+
if querystats_action == "enable":
|
41
|
+
self.request.session.setdefault("querystats", {})
|
42
|
+
elif querystats_action == "clear":
|
43
|
+
self.request.session["querystats"] = {}
|
44
|
+
elif querystats_action == "disable" and "querystats" in self.request.session:
|
45
|
+
del self.request.session["querystats"]
|
26
46
|
|
27
|
-
|
47
|
+
# Redirect back to the page that submitted the form
|
48
|
+
return ResponseRedirect(self.request.POST.get("redirect_url", "."))
|
@@ -3,75 +3,107 @@
|
|
3
3
|
<head>
|
4
4
|
<meta charset="UTF-8">
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
-
<title>
|
6
|
+
<title>Querystats</title>
|
7
7
|
{% tailwind_css %}
|
8
8
|
</head>
|
9
|
-
<body>
|
9
|
+
<body class="bg-stone-950 text-stone-300">
|
10
10
|
|
11
|
-
<div class="px-6 py-4">
|
12
|
-
<
|
13
|
-
|
14
|
-
<
|
15
|
-
|
16
|
-
|
17
|
-
</
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
</button>
|
25
|
-
</form>
|
26
|
-
</div>
|
11
|
+
<div class="flex items-center justify-between px-6 py-4">
|
12
|
+
<h1 class="text-lg font-semibold">Querystats</h1>
|
13
|
+
<div class="flex items-center space-x-2">
|
14
|
+
<form method="post" action=".">
|
15
|
+
{{ csrf_input }}
|
16
|
+
<input type="hidden" name="querystats_action" value="clear">
|
17
|
+
<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">Clear</button>
|
18
|
+
</form>
|
19
|
+
<form method="post" action=".">
|
20
|
+
{{ csrf_input }}
|
21
|
+
<input type="hidden" name="querystats_action" value="disable">
|
22
|
+
<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">Disable</button>
|
23
|
+
</form>
|
27
24
|
</div>
|
28
|
-
|
29
|
-
<div class="mt-2 font-mono text-xs">
|
30
|
-
{{ querystats_resolver_match }}
|
31
|
-
and template {{ querystats_template_name }}
|
32
|
-
</div>
|
33
|
-
#}
|
34
|
-
|
35
|
-
<div class="flex w-full mt-5 overflow-auto rounded-sm">
|
36
|
-
{% for query in querystats.queries %}
|
37
|
-
<a href="#query-{{ loop.index }}"
|
38
|
-
{{ loop.cycle('class=\"h-4 bg-amber-400\"', 'class="h-4 bg-amber-500"', 'class="h-4 bg-amber-600"')|safe }}
|
39
|
-
title="[{{ query.duration_display }}] {{ query.sql_display }}"
|
40
|
-
style="width: {{ query.duration / querystats.total_time * 100 }}%">
|
41
|
-
</a>
|
42
|
-
{% endfor %}
|
43
|
-
</div>
|
44
|
-
|
45
|
-
<div class="mt-4 space-y-4 text-sm">
|
46
|
-
{% for query in querystats.queries %}
|
47
|
-
<div id="query-{{ loop.index }}" class="p-2 rounded bg-zinc-800">
|
48
|
-
<div class="float-right px-2 py-px mb-px ml-2 text-xs rounded-full bg-zinc-700">
|
49
|
-
<span>{{ query.duration_display }}</span>
|
50
|
-
{% if query.duplicate_count is defined %}
|
51
|
-
<span class="text-red-500"> duplicated {{ query.duplicate_count }} times</span>
|
52
|
-
{% endif %}
|
25
|
+
</div>
|
53
26
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
</div>
|
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">
|
59
31
|
<div>
|
60
|
-
<
|
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>
|
61
34
|
</div>
|
62
|
-
<div class=
|
63
|
-
<
|
64
|
-
<
|
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>
|
65
42
|
</div>
|
66
|
-
|
67
|
-
|
68
|
-
|
43
|
+
</div>
|
44
|
+
|
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>
|
54
|
+
|
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"> duplicated {{ query.duplicate_count }} times</span>
|
63
|
+
{% 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
|
+
</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>
|
69
85
|
</details>
|
86
|
+
{% else %}
|
87
|
+
<div>No queries...</div>
|
88
|
+
{% endfor %}
|
70
89
|
</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>
|
71
97
|
{% else %}
|
72
|
-
<
|
73
|
-
|
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 %}
|
74
104
|
</div>
|
105
|
+
|
106
|
+
{% endfor %}
|
75
107
|
</div>
|
76
108
|
|
77
109
|
</body>
|
@@ -1,13 +1,25 @@
|
|
1
|
-
<
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
1
|
+
<div data-querystats class="relative group/querystats" style="display: none;">
|
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">
|
4
|
+
<span class="relative inline-flex size-2 mr-2">
|
5
|
+
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
6
|
+
<span class="relative inline-flex size-2 rounded-full bg-green-500"></span>
|
7
|
+
</span>
|
8
|
+
<span data-querystats-summary></span>
|
9
|
+
</a>
|
10
|
+
{% else %}
|
11
|
+
<form action="{{ url('admin:querystats:querystats') }}" method="post">
|
12
|
+
{{ csrf_input }}
|
13
|
+
<input type="hidden" name="redirect_url" value="{{ request.get_full_path() }}">
|
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">
|
16
|
+
<span class="rounded-full bg-zinc-500 w-2 h-2 inline-block mr-1"></span>
|
17
|
+
<span data-querystats-summary></span>
|
18
|
+
</button>
|
19
|
+
</form>
|
20
|
+
{% endif %}
|
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">
|
11
23
|
<div class="p-2 text-xs border rounded shadow-md bg-zinc-900 border-zinc-700"><table><tbody></tbody></table></div>
|
12
24
|
</div>
|
13
25
|
<script async defer>
|
@@ -15,9 +27,8 @@
|
|
15
27
|
// https://bugs.webkit.org/show_bug.cgi?id=209216
|
16
28
|
var querystatsTimings = [];
|
17
29
|
function renderQuerystats() {
|
18
|
-
// Render the
|
19
|
-
|
20
|
-
let summary = latestTiming.description;
|
30
|
+
// Render the original timing call
|
31
|
+
let summary = querystatsTimings[0].description;
|
21
32
|
if (querystatsTimings.length > 1) {
|
22
33
|
summary += ` *`;
|
23
34
|
}
|
@@ -47,7 +58,6 @@
|
|
47
58
|
}
|
48
59
|
}
|
49
60
|
try {
|
50
|
-
// Create the performance observer.
|
51
61
|
const po = new PerformanceObserver((list) => {
|
52
62
|
for (const entry of list.getEntries()) {
|
53
63
|
if (!entry.serverTiming) {
|
@@ -55,15 +65,15 @@
|
|
55
65
|
return;
|
56
66
|
}
|
57
67
|
for (const timing of entry.serverTiming) {
|
58
|
-
if (querystatsTimings.length > 0) {
|
59
|
-
if (querystatsTimings[querystatsTimings.length - 1] === timing) {
|
60
|
-
// Skip duplicate timings (happens on initial load...)
|
61
|
-
continue;
|
62
|
-
}
|
63
|
-
}
|
64
68
|
if (timing.name === "querystats") {
|
65
69
|
console.log("Querystats timing", entry)
|
66
70
|
timing.url = entry.name; // Store this for reference later
|
71
|
+
for (const existingTiming of querystatsTimings) {
|
72
|
+
if (existingTiming == timing) {
|
73
|
+
// Skip duplicate timings (happens on initial load...)
|
74
|
+
return;
|
75
|
+
}
|
76
|
+
}
|
67
77
|
querystatsTimings.push(timing);
|
68
78
|
renderQuerystats();
|
69
79
|
}
|
@@ -76,4 +86,4 @@
|
|
76
86
|
// Do nothing if the browser doesn't support this API.
|
77
87
|
}
|
78
88
|
</script>
|
79
|
-
</
|
89
|
+
</div>
|
plain/admin/templates.py
CHANGED
@@ -12,10 +12,10 @@ class ToolbarExtension(InclusionTagExtension):
|
|
12
12
|
template_name = "toolbar/toolbar.html"
|
13
13
|
|
14
14
|
def get_context(self, context, *args, **kwargs):
|
15
|
-
if isinstance(settings.
|
16
|
-
cls = import_string(settings.
|
15
|
+
if isinstance(settings.ADMIN_TOOLBAR_CLASS, str):
|
16
|
+
cls = import_string(settings.ADMIN_TOOLBAR_CLASS)
|
17
17
|
else:
|
18
|
-
cls = settings.
|
18
|
+
cls = settings.ADMIN_TOOLBAR_CLASS
|
19
19
|
context.vars["toolbar"] = cls(request=context["request"])
|
20
20
|
return context
|
21
21
|
|
plain/admin/toolbar.py
CHANGED
@@ -8,7 +8,7 @@ from plain.urls.exceptions import Resolver404
|
|
8
8
|
class Toolbar:
|
9
9
|
def __init__(self, request):
|
10
10
|
self.request = request
|
11
|
-
self.version = settings.
|
11
|
+
self.version = settings.ADMIN_TOOLBAR_VERSION
|
12
12
|
self.metadata = {
|
13
13
|
"Request ID": request.unique_id,
|
14
14
|
}
|
@@ -2,10 +2,10 @@ plain/admin/README.md,sha256=Ro2YkrKS-RXsmFBFN0QUpLh4OHIDvMkVgDIE6Wu4PMQ,3800
|
|
2
2
|
plain/admin/__init__.py,sha256=bPv9iftT8aLqBH6dDy-HTVXW66dQUhfIiEZ-LIUMC0Y,78
|
3
3
|
plain/admin/config.py,sha256=TDYmJe4UYmKw4bz0x5s9PkDa-X4V-9JoJlka162-J7M,676
|
4
4
|
plain/admin/dates.py,sha256=EEhcQhHt3-k6kE9yvPdH5X6EecmUQ259xywbDBec3Dg,10253
|
5
|
-
plain/admin/default_settings.py,sha256=
|
5
|
+
plain/admin/default_settings.py,sha256=S22r8JtwY-ArlNO4waBOrnRfb2qPbUQ5VSP6niJRzZw,145
|
6
6
|
plain/admin/middleware.py,sha256=k3yP1o3CzvLiZZSoxqq-DvAZlp4sICRauaT-kD3FJKM,398
|
7
|
-
plain/admin/templates.py,sha256=
|
8
|
-
plain/admin/toolbar.py,sha256=
|
7
|
+
plain/admin/templates.py,sha256=0xgMQmJEbh5U45ZlN2f15Xs42Y2A_lSS-_wdMp1BeD4,854
|
8
|
+
plain/admin/toolbar.py,sha256=doW1Eg9rYfLZulRTAyFACDaUDi2xkDlsdVABzCQKHG4,979
|
9
9
|
plain/admin/urls.py,sha256=HtYsTDyV6s-k6ClT2H2oZqUDIANLq-PACpZfrR538js,1292
|
10
10
|
plain/admin/assets/admin/admin.css,sha256=-KdI7geASBsSbTve26VeJ-wCrdHWyD3EdjDZ9o393Yc,2653
|
11
11
|
plain/admin/assets/admin/admin.js,sha256=8R4VestYByRd2THe5gg8I35Zu3rokm6TQTkEf2mEB1c,2919
|
@@ -29,10 +29,10 @@ plain/admin/impersonate/urls.py,sha256=s8bwi8qPueKCCYcLW75p-hPFkBKhm2AMi6AQKQcZs
|
|
29
29
|
plain/admin/impersonate/views.py,sha256=p8kEGC2ZNntAaLJRgwCaGSJABjLWoarpya9IuBpNW5A,789
|
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=
|
33
|
-
plain/admin/querystats/middleware.py,sha256=
|
32
|
+
plain/admin/querystats/core.py,sha256=kh45lRPEv9lYiTDNI_srrfoJue48v3kcrBNbOIHYCmw,4480
|
33
|
+
plain/admin/querystats/middleware.py,sha256=g5Ld-Xx1eKq1AfED4oBHNkuhr5nUL1ILrzTv_tQVlPY,3528
|
34
34
|
plain/admin/querystats/urls.py,sha256=H8wMpqKBnXqA8ZsdwdxTKQguNYJ0JsMRqqMunccBm2I,198
|
35
|
-
plain/admin/querystats/views.py,sha256=
|
35
|
+
plain/admin/querystats/views.py,sha256=hCADEZMZSyIAqdDQU0J21HveWqAH_hX2RcRHNS7-I1k,1573
|
36
36
|
plain/admin/templates/admin/base.html,sha256=M3z5JwRPSS9fc3Rcg9YxPWTNL0wNo98oaEv3Ue3xlvs,8466
|
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,8 +66,8 @@ 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=
|
70
|
-
plain/admin/templates/querystats/toolbar.html,sha256=
|
69
|
+
plain/admin/templates/querystats/querystats.html,sha256=oeDswOjN_11weKsj_x1iOKyTucyU1X7aL1vvxqhIUMc,5088
|
70
|
+
plain/admin/templates/querystats/toolbar.html,sha256=JFuG97PackHuhRFxnOHEiKGMa_gmCsy3l4PotrwKt9Q,4369
|
71
71
|
plain/admin/templates/toolbar/toolbar.html,sha256=KcGAG6kRmx60wfqEsdD5C4nDMilH-JvPjHoU6EktfaY,5985
|
72
72
|
plain/admin/views/__init__.py,sha256=nF6AENZ3Xxyi08OTRrF6e-HYBkZSFj7XBK2mVzMYqN4,846
|
73
73
|
plain/admin/views/base.py,sha256=S1oaMUXnMOwRozbn2K-tk9tL4BMimemfMagZD9QxrJw,3512
|
@@ -76,7 +76,7 @@ plain/admin/views/objects.py,sha256=7BXrDpHbdZ0vpzTHoLbSNdXO-rYSRw5YOBTiTK12E1U,
|
|
76
76
|
plain/admin/views/registry.py,sha256=Lxib4YSQCMHb_zACnLKymJakV8jCZPWYll7J8-aV9Xw,3712
|
77
77
|
plain/admin/views/types.py,sha256=ONMMdUoapgMoUVYgSIe-4YCdfvaVMQ4jgPWYiMo0pDk,178
|
78
78
|
plain/admin/views/viewsets.py,sha256=dqMlQ6kLn9iqd9BwBWAZT1S271wH1FdfM5HXbOgBMEw,1655
|
79
|
-
plain_admin-0.
|
80
|
-
plain_admin-0.
|
81
|
-
plain_admin-0.
|
82
|
-
plain_admin-0.
|
79
|
+
plain_admin-0.26.0.dist-info/METADATA,sha256=4nnt50UwdYNs753lk8AfrS9RnF8I1YviI3KyF3GjlDs,4237
|
80
|
+
plain_admin-0.26.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
81
|
+
plain_admin-0.26.0.dist-info/licenses/LICENSE,sha256=cvKM3OlqHx3ijD6e34zsSUkPvzl-ya3Dd63A6EHL94U,1500
|
82
|
+
plain_admin-0.26.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|