plain.admin 0.25.0__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/README.md +103 -241
- 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.26.0.dist-info/METADATA +178 -0
- {plain_admin-0.25.0.dist-info → plain_admin-0.26.0.dist-info}/RECORD +13 -13
- plain_admin-0.25.0.dist-info/METADATA +0 -316
- {plain_admin-0.25.0.dist-info → plain_admin-0.26.0.dist-info}/WHEEL +0 -0
- {plain_admin-0.25.0.dist-info → plain_admin-0.26.0.dist-info}/licenses/LICENSE +0 -0
plain/admin/README.md
CHANGED
@@ -1,22 +1,58 @@
|
|
1
|
-
#
|
1
|
+
# plain.admin
|
2
2
|
|
3
|
-
|
3
|
+
**Manage your app with a backend interface.**
|
4
4
|
|
5
|
-
The Plain Admin
|
6
|
-
It leverages class-based views and standard URLs and templates to provide a flexible admin where
|
7
|
-
you can quickly create your own pages and cards,
|
8
|
-
in addition to models.
|
5
|
+
The Plain Admin provides a combination of built-in views and the flexibility to create your own. You can use it to quickly get visibility into your app's data and to manage it.
|
9
6
|
|
10
|
-
-
|
11
|
-
- dashboards
|
12
|
-
- diy forms - have to install elements if you want to use them (or an app uses them, or maybe plain pkgs should use them just for admin...)
|
13
|
-
- detached from login (do your own login (oauth, passkeys, etc))
|
7
|
+

|
14
8
|
|
15
9
|
## Installation
|
16
10
|
|
17
|
-
|
11
|
+
Install the `plain.admin` package and its dependencies.
|
18
12
|
|
19
|
-
|
13
|
+
```console
|
14
|
+
uv add plain.admin
|
15
|
+
```
|
16
|
+
|
17
|
+
The admin uses a combination of other Plain packages, most of which you will already have installed. Ultimately, your settings will look something like this:
|
18
|
+
|
19
|
+
```python
|
20
|
+
# app/settings.py
|
21
|
+
INSTALLED_PACKAGES = [
|
22
|
+
"plain.models",
|
23
|
+
"plain.tailwind",
|
24
|
+
"plain.auth",
|
25
|
+
"plain.sessions",
|
26
|
+
"plain.htmx",
|
27
|
+
"plain.admin",
|
28
|
+
"plain.elements",
|
29
|
+
# other packages...
|
30
|
+
]
|
31
|
+
|
32
|
+
AUTH_USER_MODEL = "users.User"
|
33
|
+
AUTH_LOGIN_URL = "login"
|
34
|
+
|
35
|
+
MIDDLEWARE = [
|
36
|
+
"plain.sessions.middleware.SessionMiddleware",
|
37
|
+
"plain.auth.middleware.AuthenticationMiddleware",
|
38
|
+
"plain.admin.AdminMiddleware",
|
39
|
+
]
|
40
|
+
```
|
41
|
+
|
42
|
+
Your User model is expected to have an `is_admin` field (or attribute) for checking who has permission to access the admin.
|
43
|
+
|
44
|
+
```python
|
45
|
+
# app/users/models.py
|
46
|
+
from plain import models
|
47
|
+
|
48
|
+
|
49
|
+
@models.register_model
|
50
|
+
class User(models.Model):
|
51
|
+
is_admin = models.BooleanField(default=False)
|
52
|
+
# other fields...
|
53
|
+
```
|
54
|
+
|
55
|
+
To make the admin accessible, add the `AdminRouter` to your root URLs.
|
20
56
|
|
21
57
|
```python
|
22
58
|
# app/urls.py
|
@@ -32,12 +68,12 @@ class AppRouter(Router):
|
|
32
68
|
include("admin/", AdminRouter),
|
33
69
|
path("login/", views.LoginView, name="login"),
|
34
70
|
path("logout/", LogoutView, name="logout"),
|
35
|
-
#
|
71
|
+
# other urls...
|
36
72
|
]
|
37
73
|
|
38
74
|
```
|
39
75
|
|
40
|
-
Optionally, add the admin toolbar to your base template.
|
76
|
+
Optionally, you can add the admin toolbar to your base template. The toolbar will appear when `settings.DEBUG` or when `request.user.is_admin` (including in production!).
|
41
77
|
|
42
78
|
```html
|
43
79
|
<!-- app/templates/base.html -->
|
@@ -57,245 +93,71 @@ Optionally, add the admin toolbar to your base template.
|
|
57
93
|
</html>
|
58
94
|
```
|
59
95
|
|
60
|
-
##
|
61
|
-
|
62
|
-
## Dashboards
|
63
|
-
|
64
|
-
<!-- # plain.querystats
|
65
|
-
|
66
|
-
On-page database query stats in development and production.
|
67
|
-
|
68
|
-
On each page, the query stats will display how many database queries were performed and how long they took.
|
69
|
-
|
70
|
-
[Watch on YouTube](https://www.youtube.com/watch?v=NX8VXxVJm08)
|
71
|
-
|
72
|
-
Clicking the stats in the toolbar will show the full SQL query log with tracebacks and timings.
|
73
|
-
This is even designed to work in production,
|
74
|
-
making it much easier to discover and debug performance issues on production data!
|
75
|
-
|
76
|
-

|
77
|
-
|
78
|
-
It will also point out duplicate queries,
|
79
|
-
which can typically be removed by using `select_related`,
|
80
|
-
`prefetch_related`, or otherwise refactoring your code.
|
81
|
-
|
82
|
-
## Installation
|
83
|
-
|
84
|
-
```python
|
85
|
-
# settings.py
|
86
|
-
INSTALLED_PACKAGES = [
|
87
|
-
# ...
|
88
|
-
"plain.admin.querystats",
|
89
|
-
]
|
90
|
-
|
91
|
-
MIDDLEWARE = [
|
92
|
-
"plain.sessions.middleware.SessionMiddleware",
|
93
|
-
"plain.auth.middleware.AuthenticationMiddleware",
|
94
|
-
|
95
|
-
"plain.admin.querystats.QueryStatsMiddleware",
|
96
|
-
# Put additional middleware below querystats
|
97
|
-
# ...
|
98
|
-
]
|
99
|
-
```
|
100
|
-
|
101
|
-
We strongly recommend using the plain-toolbar along with this,
|
102
|
-
but if you aren't,
|
103
|
-
you can add the querystats to your frontend templates with this include:
|
104
|
-
|
105
|
-
```html
|
106
|
-
{% include "querystats/button.html" %}
|
107
|
-
```
|
108
|
-
|
109
|
-
*Note that you will likely want to surround this with an if `DEBUG` or `is_admin` check.*
|
110
|
-
|
111
|
-
To view querystats you need to send a POST request to `?querystats=store` (i.e. via a `<form>`),
|
112
|
-
and the template include is the easiest way to do that.
|
113
|
-
|
114
|
-
## Tailwind CSS
|
115
|
-
|
116
|
-
This package is styled with [Tailwind CSS](https://tailwindcss.com/),
|
117
|
-
and pairs well with [`plain-tailwind`](https://github.com/plainpackages/plain-tailwind).
|
118
|
-
|
119
|
-
If you are using your own Tailwind implementation,
|
120
|
-
you can modify the "content" in your Tailwind config to include any Plain packages:
|
121
|
-
|
122
|
-
```js
|
123
|
-
// tailwind.config.js
|
124
|
-
module.exports = {
|
125
|
-
content: [
|
126
|
-
// ...
|
127
|
-
".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
|
128
|
-
],
|
129
|
-
// ...
|
130
|
-
}
|
131
|
-
```
|
132
|
-
|
133
|
-
If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
|
134
|
-
|
135
|
-
|
136
|
-
# plain.toolbar
|
137
|
-
|
138
|
-
The admin toolbar is enabled for every user who `is_admin`.
|
139
|
-
|
140
|
-

|
141
|
-
|
142
|
-
## Installation
|
143
|
-
|
144
|
-
Add `plaintoolbar` to your `INSTALLED_PACKAGES`,
|
145
|
-
and the `{% toolbar %}` to your base template:
|
146
|
-
|
147
|
-
```python
|
148
|
-
# settings.py
|
149
|
-
INSTALLED_PACKAGES += [
|
150
|
-
"plaintoolbar",
|
151
|
-
]
|
152
|
-
```
|
153
|
-
|
154
|
-
```html
|
155
|
-
<!-- base.template.html -->
|
156
|
-
|
157
|
-
{% load toolbar %}
|
158
|
-
|
159
|
-
<!doctype html>
|
160
|
-
<html lang="en">
|
161
|
-
<head>
|
162
|
-
...
|
163
|
-
</head>
|
164
|
-
<body>
|
165
|
-
{% toolbar %}
|
166
|
-
...
|
167
|
-
</body>
|
168
|
-
```
|
169
|
-
|
170
|
-
More specific settings can be found below.
|
171
|
-
|
172
|
-
## Tailwind CSS
|
173
|
-
|
174
|
-
This package is styled with [Tailwind CSS](https://tailwindcss.com/),
|
175
|
-
and pairs well with [`plain-tailwind`](https://github.com/plainpackages/plain-tailwind).
|
176
|
-
|
177
|
-
If you are using your own Tailwind implementation,
|
178
|
-
you can modify the "content" in your Tailwind config to include any Plain packages:
|
179
|
-
|
180
|
-
```js
|
181
|
-
// tailwind.config.js
|
182
|
-
module.exports = {
|
183
|
-
content: [
|
184
|
-
// ...
|
185
|
-
".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
|
186
|
-
],
|
187
|
-
// ...
|
188
|
-
}
|
189
|
-
```
|
190
|
-
|
191
|
-
If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
|
192
|
-
|
193
|
-
# plain.requestlog
|
194
|
-
|
195
|
-
The request log stores a local history of HTTP requests and responses during `plain work` (Django runserver).
|
196
|
-
|
197
|
-
The request history will make it easy to see redirects,
|
198
|
-
400 and 500 level errors,
|
199
|
-
form submissions,
|
200
|
-
API calls,
|
201
|
-
webhooks,
|
202
|
-
and more.
|
203
|
-
|
204
|
-
[Watch on YouTube](https://www.youtube.com/watch?v=AwI7Pt5oZnM)
|
205
|
-
|
206
|
-
Requests can be re-submitted by clicking the "replay" button.
|
96
|
+
## Admin viewsets
|
207
97
|
|
208
|
-
|
209
|
-
|
210
|
-
## Installation
|
211
|
-
|
212
|
-
```python
|
213
|
-
# settings.py
|
214
|
-
INSTALLED_PACKAGES += [
|
215
|
-
"plainrequestlog",
|
216
|
-
]
|
217
|
-
|
218
|
-
MIDDLEWARE = MIDDLEWARE + [
|
219
|
-
# ...
|
220
|
-
"plainrequestlog.RequestLogMiddleware",
|
221
|
-
]
|
222
|
-
```
|
223
|
-
|
224
|
-
The default settings can be customized if needed:
|
98
|
+
The most common use of the admin is to display and manage your `plain.models`. To do this, create a viewset with a set of inner views.
|
225
99
|
|
226
100
|
```python
|
227
|
-
#
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
101
|
+
# app/users/admin.py
|
102
|
+
from plain.admin.views import (
|
103
|
+
AdminModelDetailView,
|
104
|
+
AdminModelListView,
|
105
|
+
AdminModelUpdateView,
|
106
|
+
AdminViewset,
|
107
|
+
register_viewset,
|
108
|
+
)
|
109
|
+
from plain.models.forms import ModelForm
|
110
|
+
|
111
|
+
from .models import User
|
112
|
+
|
113
|
+
|
114
|
+
class UserForm(ModelForm):
|
115
|
+
class Meta:
|
116
|
+
model = User
|
117
|
+
fields = ["email"]
|
118
|
+
|
119
|
+
|
120
|
+
@register_viewset
|
121
|
+
class UserAdmin(AdminViewset):
|
122
|
+
class ListView(AdminModelListView):
|
123
|
+
model = User
|
124
|
+
fields = [
|
125
|
+
"id",
|
126
|
+
"email",
|
127
|
+
"created_at__date",
|
128
|
+
]
|
129
|
+
queryset_order = ["-created_at"]
|
130
|
+
search_fields = [
|
131
|
+
"email",
|
132
|
+
]
|
133
|
+
|
134
|
+
class DetailView(AdminModelDetailView):
|
135
|
+
model = User
|
136
|
+
|
137
|
+
class UpdateView(AdminModelUpdateView):
|
138
|
+
template_name = "admin/users/user_form.html"
|
139
|
+
model = User
|
140
|
+
form_class = UserForm
|
234
141
|
```
|
235
142
|
|
236
|
-
|
237
|
-
|
238
|
-
This package is styled with [Tailwind CSS](https://tailwindcss.com/),
|
239
|
-
and pairs well with [`plain-tailwind`](https://github.com/plainpackages/plain-tailwind).
|
240
|
-
|
241
|
-
If you are using your own Tailwind implementation,
|
242
|
-
you can modify the "content" in your Tailwind config to include any Plain packages:
|
243
|
-
|
244
|
-
```js
|
245
|
-
// tailwind.config.js
|
246
|
-
module.exports = {
|
247
|
-
content: [
|
248
|
-
// ...
|
249
|
-
".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
|
250
|
-
],
|
251
|
-
// ...
|
252
|
-
}
|
253
|
-
```
|
143
|
+
The [`AdminViewset`](./views/viewsets.py) will automatically recognize inner views named `ListView`, `CreateView`, `DetailView`, `UpdateView`, and `DeleteView`. It will interlink these views automatically in the UI and form success URLs. You can define additional views too, but you will need to implement a couple methods to hook them up.
|
254
144
|
|
255
|
-
|
145
|
+
## Admin cards
|
256
146
|
|
257
|
-
|
147
|
+
TODO
|
258
148
|
|
259
|
-
|
149
|
+
## Admin forms
|
260
150
|
|
261
|
-
|
262
|
-
With `impersonate` installed, you can impersonate a user by finding them in the Django admin and clicking the "Impersonate" button.
|
151
|
+
TODO
|
263
152
|
|
264
|
-
|
153
|
+
## Toolbar
|
265
154
|
|
266
|
-
|
155
|
+
TODO
|
267
156
|
|
268
|
-
|
269
|
-
|
270
|
-
## Installation
|
271
|
-
|
272
|
-
To impersonate users, you need the app, middleware, and URLs:
|
273
|
-
|
274
|
-
```python
|
275
|
-
# settings.py
|
276
|
-
INSTALLED_PACKAGES = INSTALLED_PACKAGES + [
|
277
|
-
"plain.admin.impersonate",
|
278
|
-
]
|
279
|
-
|
280
|
-
MIDDLEWARE = MIDDLEWARE + [
|
281
|
-
"plain.admin.impersonate.ImpersonateMiddleware",
|
282
|
-
]
|
283
|
-
```
|
284
|
-
|
285
|
-
```python
|
286
|
-
# urls.py
|
287
|
-
urlpatterns = [
|
288
|
-
# ...
|
289
|
-
path("impersonate/", include("plain.admin.impersonate.urls")),
|
290
|
-
]
|
291
|
-
```
|
157
|
+
## Impersonate
|
292
158
|
|
293
|
-
|
159
|
+
TODO
|
294
160
|
|
295
|
-
|
161
|
+
## Querystats
|
296
162
|
|
297
|
-
|
298
|
-
# settings.py
|
299
|
-
IMPERSONATE_ALLOWED = lambda user: user.is_admin
|
300
|
-
``` -->
|
301
|
-
````
|
163
|
+
TODO
|
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", "."))
|