plain.admin 0.22.0__py3-none-any.whl → 0.25.1__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 +112 -209
- plain/admin/assets/admin/admin.js +6 -1
- plain/admin/config.py +1 -1
- plain/admin/querystats/README.md +1 -3
- plain/admin/views/models.py +0 -33
- plain/admin/views/objects.py +54 -54
- plain_admin-0.25.1.dist-info/METADATA +178 -0
- {plain_admin-0.22.0.dist-info → plain_admin-0.25.1.dist-info}/RECORD +10 -10
- plain_admin-0.22.0.dist-info/METADATA +0 -275
- {plain_admin-0.22.0.dist-info → plain_admin-0.25.1.dist-info}/WHEEL +0 -0
- {plain_admin-0.22.0.dist-info → plain_admin-0.25.1.dist-info}/licenses/LICENSE +0 -0
plain/admin/README.md
CHANGED
@@ -1,260 +1,163 @@
|
|
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
|
13
|
-
- detached from login (do your own login (oauth, passkeys, etc))
|
7
|
+

|
14
8
|
|
15
9
|
## Installation
|
16
10
|
|
17
|
-
|
18
|
-
- add url
|
11
|
+
Install the `plain.admin` package and its dependencies.
|
19
12
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
<!-- # plain.querystats
|
25
|
-
|
26
|
-
On-page database query stats in development and production.
|
27
|
-
|
28
|
-
On each page, the query stats will display how many database queries were performed and how long they took.
|
29
|
-
|
30
|
-
[Watch on YouTube](https://www.youtube.com/watch?v=NX8VXxVJm08)
|
31
|
-
|
32
|
-
Clicking the stats in the toolbar will show the full SQL query log with tracebacks and timings.
|
33
|
-
This is even designed to work in production,
|
34
|
-
making it much easier to discover and debug performance issues on production data!
|
35
|
-
|
36
|
-

|
13
|
+
```console
|
14
|
+
uv add plain.admin
|
15
|
+
```
|
37
16
|
|
38
|
-
|
39
|
-
which can typically be removed by using `select_related`,
|
40
|
-
`prefetch_related`, or otherwise refactoring your code.
|
41
|
-
|
42
|
-
## Installation
|
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:
|
43
18
|
|
44
19
|
```python
|
45
|
-
# settings.py
|
20
|
+
# app/settings.py
|
46
21
|
INSTALLED_PACKAGES = [
|
47
|
-
|
48
|
-
"plain.
|
22
|
+
"plain.models",
|
23
|
+
"plain.tailwind",
|
24
|
+
"plain.auth",
|
25
|
+
"plain.sessions",
|
26
|
+
"plain.htmx",
|
27
|
+
"plain.admin",
|
28
|
+
"plain.elements",
|
29
|
+
# other packages...
|
49
30
|
]
|
50
31
|
|
32
|
+
AUTH_USER_MODEL = "users.User"
|
33
|
+
AUTH_LOGIN_URL = "login"
|
34
|
+
|
51
35
|
MIDDLEWARE = [
|
52
36
|
"plain.sessions.middleware.SessionMiddleware",
|
53
37
|
"plain.auth.middleware.AuthenticationMiddleware",
|
54
|
-
|
55
|
-
"plain.admin.querystats.QueryStatsMiddleware",
|
56
|
-
# Put additional middleware below querystats
|
57
|
-
# ...
|
38
|
+
"plain.admin.AdminMiddleware",
|
58
39
|
]
|
59
40
|
```
|
60
41
|
|
61
|
-
|
62
|
-
but if you aren't,
|
63
|
-
you can add the querystats to your frontend templates with this include:
|
64
|
-
|
65
|
-
```html
|
66
|
-
{% include "querystats/button.html" %}
|
67
|
-
```
|
68
|
-
|
69
|
-
*Note that you will likely want to surround this with an if `DEBUG` or `is_admin` check.*
|
70
|
-
|
71
|
-
To view querystats you need to send a POST request to `?querystats=store` (i.e. via a `<form>`),
|
72
|
-
and the template include is the easiest way to do that.
|
42
|
+
Your User model is expected to have an `is_admin` field (or attribute) for checking who has permission to access the admin.
|
73
43
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
and pairs well with [`plain-tailwind`](https://github.com/plainpackages/plain-tailwind).
|
44
|
+
```python
|
45
|
+
# app/users/models.py
|
46
|
+
from plain import models
|
78
47
|
|
79
|
-
If you are using your own Tailwind implementation,
|
80
|
-
you can modify the "content" in your Tailwind config to include any Plain packages:
|
81
48
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
// ...
|
87
|
-
".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
|
88
|
-
],
|
89
|
-
// ...
|
90
|
-
}
|
49
|
+
@models.register_model
|
50
|
+
class User(models.Model):
|
51
|
+
is_admin = models.BooleanField(default=False)
|
52
|
+
# other fields...
|
91
53
|
```
|
92
54
|
|
93
|
-
|
94
|
-
|
55
|
+
To make the admin accessible, add the `AdminRouter` to your root URLs.
|
95
56
|
|
96
|
-
|
57
|
+
```python
|
58
|
+
# app/urls.py
|
59
|
+
from plain.admin.urls import AdminRouter
|
60
|
+
from plain.urls import Router, include, path
|
97
61
|
|
98
|
-
|
62
|
+
from . import views
|
99
63
|
|
100
|
-

|
101
64
|
|
102
|
-
|
65
|
+
class AppRouter(Router):
|
66
|
+
namespace = ""
|
67
|
+
urls = [
|
68
|
+
include("admin/", AdminRouter),
|
69
|
+
path("login/", views.LoginView, name="login"),
|
70
|
+
path("logout/", LogoutView, name="logout"),
|
71
|
+
# other urls...
|
72
|
+
]
|
103
73
|
|
104
|
-
Add `plaintoolbar` to your `INSTALLED_PACKAGES`,
|
105
|
-
and the `{% toolbar %}` to your base template:
|
106
|
-
|
107
|
-
```python
|
108
|
-
# settings.py
|
109
|
-
INSTALLED_PACKAGES += [
|
110
|
-
"plaintoolbar",
|
111
|
-
]
|
112
74
|
```
|
113
75
|
|
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!).
|
77
|
+
|
114
78
|
```html
|
115
|
-
<!-- base.
|
116
|
-
|
117
|
-
<!doctype html>
|
79
|
+
<!-- app/templates/base.html -->
|
80
|
+
<!DOCTYPE html>
|
118
81
|
<html lang="en">
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
{%
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
More specific settings can be found below.
|
129
|
-
|
130
|
-
## Tailwind CSS
|
82
|
+
<head>
|
83
|
+
<meta charset="UTF-8">
|
84
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
85
|
+
<title>{{ html_title|default("My App") }}</title>
|
86
|
+
{% tailwind_css %}
|
87
|
+
</head>
|
88
|
+
<body>
|
89
|
+
{% block content required %}{% endblock %}
|
131
90
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
If you are using your own Tailwind implementation,
|
136
|
-
you can modify the "content" in your Tailwind config to include any Plain packages:
|
137
|
-
|
138
|
-
```js
|
139
|
-
// tailwind.config.js
|
140
|
-
module.exports = {
|
141
|
-
content: [
|
142
|
-
// ...
|
143
|
-
".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
|
144
|
-
],
|
145
|
-
// ...
|
146
|
-
}
|
91
|
+
{% toolbar %}
|
92
|
+
</body>
|
93
|
+
</html>
|
147
94
|
```
|
148
95
|
|
149
|
-
|
150
|
-
|
96
|
+
## Admin viewsets
|
151
97
|
|
152
|
-
|
153
|
-
|
154
|
-
The request log stores a local history of HTTP requests and responses during `plain work` (Django runserver).
|
155
|
-
|
156
|
-
The request history will make it easy to see redirects,
|
157
|
-
400 and 500 level errors,
|
158
|
-
form submissions,
|
159
|
-
API calls,
|
160
|
-
webhooks,
|
161
|
-
and more.
|
162
|
-
|
163
|
-
[Watch on YouTube](https://www.youtube.com/watch?v=AwI7Pt5oZnM)
|
164
|
-
|
165
|
-
Requests can be re-submitted by clicking the "replay" button.
|
166
|
-
|
167
|
-
[](https://user-images.githubusercontent.com/649496/213781414-417ad043-de67-4836-9ef1-2b91404336c3.png)
|
168
|
-
|
169
|
-
## Installation
|
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.
|
170
99
|
|
171
100
|
```python
|
172
|
-
#
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
212
141
|
```
|
213
142
|
|
214
|
-
|
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.
|
215
144
|
|
145
|
+
## Admin cards
|
216
146
|
|
217
|
-
|
147
|
+
TODO
|
218
148
|
|
219
|
-
|
149
|
+
## Admin forms
|
220
150
|
|
221
|
-
|
222
|
-
With `impersonate` installed, you can impersonate a user by finding them in the Django admin and clicking the "Impersonate" button.
|
151
|
+
TODO
|
223
152
|
|
224
|
-
|
153
|
+
## Toolbar
|
225
154
|
|
226
|
-
|
155
|
+
TODO
|
227
156
|
|
228
|
-
|
157
|
+
## Impersonate
|
229
158
|
|
230
|
-
|
159
|
+
TODO
|
231
160
|
|
232
|
-
|
161
|
+
## Querystats
|
233
162
|
|
234
|
-
|
235
|
-
# settings.py
|
236
|
-
INSTALLED_PACKAGES = INSTALLED_PACKAGES + [
|
237
|
-
"plain.admin.impersonate",
|
238
|
-
]
|
239
|
-
|
240
|
-
MIDDLEWARE = MIDDLEWARE + [
|
241
|
-
"plain.admin.impersonate.ImpersonateMiddleware",
|
242
|
-
]
|
243
|
-
```
|
244
|
-
|
245
|
-
```python
|
246
|
-
# urls.py
|
247
|
-
urlpatterns = [
|
248
|
-
# ...
|
249
|
-
path("impersonate/", include("plain.admin.impersonate.urls")),
|
250
|
-
]
|
251
|
-
```
|
252
|
-
|
253
|
-
## Settings
|
254
|
-
|
255
|
-
By default, all admin users can impersonate other users.
|
256
|
-
|
257
|
-
```python
|
258
|
-
# settings.py
|
259
|
-
IMPERSONATE_ALLOWED = lambda user: user.is_admin
|
260
|
-
``` -->
|
163
|
+
TODO
|
@@ -58,8 +58,13 @@ jQuery(function($) {
|
|
58
58
|
// Column already has a link, so don't add another
|
59
59
|
return;
|
60
60
|
}
|
61
|
+
var autolinkUrl = $this.data("column-autolink");
|
62
|
+
if (!autolinkUrl) {
|
63
|
+
// No URL, so don't add a link
|
64
|
+
return;
|
65
|
+
}
|
61
66
|
var $link = $(document.createElement("a"));
|
62
|
-
$link.attr("href",
|
67
|
+
$link.attr("href", autolinkUrl);
|
63
68
|
$link.addClass("flex p-2 -m-2 text-white/80 hover:no-underline");
|
64
69
|
$(this).wrapInner($link);
|
65
70
|
})
|
plain/admin/config.py
CHANGED
plain/admin/querystats/README.md
CHANGED
@@ -43,7 +43,7 @@ you can add the querystats to your frontend templates with this include:
|
|
43
43
|
{% include "querystats/button.html" %}
|
44
44
|
```
|
45
45
|
|
46
|
-
|
46
|
+
_Note that you will likely want to surround this with an if `DEBUG` or `is_admin` check._
|
47
47
|
|
48
48
|
To view querystats you need to send a POST request to `?querystats=store` (i.e. via a `<form>`),
|
49
49
|
and the template include is the easiest way to do that.
|
@@ -69,7 +69,6 @@ module.exports = {
|
|
69
69
|
|
70
70
|
If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
|
71
71
|
|
72
|
-
|
73
72
|
# plain.toolbar
|
74
73
|
|
75
74
|
The admin toolbar is enabled for every user who `is_admin`.
|
@@ -125,7 +124,6 @@ module.exports = {
|
|
125
124
|
|
126
125
|
If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
|
127
126
|
|
128
|
-
|
129
127
|
# plain.requestlog
|
130
128
|
|
131
129
|
The request log stores a local history of HTTP requests and responses during `plain work` (Django runserver).
|
plain/admin/views/models.py
CHANGED
@@ -162,17 +162,6 @@ class AdminModelDetailView(AdminDetailView):
|
|
162
162
|
def get_object(self):
|
163
163
|
return self.model.objects.get(pk=self.url_kwargs["pk"])
|
164
164
|
|
165
|
-
def get_template_names(self) -> list[str]:
|
166
|
-
template_names = super().get_template_names()
|
167
|
-
|
168
|
-
if not self.template_name and isinstance(self.object, models.Model):
|
169
|
-
object_meta = self.object._meta
|
170
|
-
template_names = [
|
171
|
-
f"admin/{object_meta.package_label}/{object_meta.model_name}{self.template_name_suffix}.html"
|
172
|
-
] + template_names
|
173
|
-
|
174
|
-
return template_names
|
175
|
-
|
176
165
|
|
177
166
|
class AdminModelCreateView(AdminCreateView):
|
178
167
|
model: "models.Model"
|
@@ -191,17 +180,6 @@ class AdminModelCreateView(AdminCreateView):
|
|
191
180
|
|
192
181
|
return f"{cls.model._meta.model_name}/create/"
|
193
182
|
|
194
|
-
def get_template_names(self):
|
195
|
-
template_names = super().get_template_names()
|
196
|
-
|
197
|
-
if not self.template_name and issubclass(self.model, models.Model):
|
198
|
-
model_meta = self.model._meta
|
199
|
-
template_names = [
|
200
|
-
f"admin/{model_meta.package_label}/{model_meta.model_name}{self.template_name_suffix}.html"
|
201
|
-
] + template_names
|
202
|
-
|
203
|
-
return template_names
|
204
|
-
|
205
183
|
|
206
184
|
class AdminModelUpdateView(AdminUpdateView):
|
207
185
|
model: "models.Model"
|
@@ -224,17 +202,6 @@ class AdminModelUpdateView(AdminUpdateView):
|
|
224
202
|
def get_object(self):
|
225
203
|
return self.model.objects.get(pk=self.url_kwargs["pk"])
|
226
204
|
|
227
|
-
def get_template_names(self):
|
228
|
-
template_names = super().get_template_names()
|
229
|
-
|
230
|
-
if not self.template_name and isinstance(self.object, models.Model):
|
231
|
-
object_meta = self.object._meta
|
232
|
-
template_names = [
|
233
|
-
f"admin/{object_meta.package_label}/{object_meta.model_name}{self.template_name_suffix}.html"
|
234
|
-
] + template_names
|
235
|
-
|
236
|
-
return template_names
|
237
|
-
|
238
205
|
|
239
206
|
class AdminModelDeleteView(AdminDeleteView):
|
240
207
|
model: "models.Model"
|
plain/admin/views/objects.py
CHANGED
@@ -72,7 +72,7 @@ class AdminListView(HTMXViewMixin, AdminView):
|
|
72
72
|
|
73
73
|
if self.is_htmx_request() and not hx_from_this_page and not self._page:
|
74
74
|
# Don't render anything
|
75
|
-
return Response(
|
75
|
+
return Response(status_code=204)
|
76
76
|
|
77
77
|
return response
|
78
78
|
|
@@ -144,29 +144,29 @@ class AdminListView(HTMXViewMixin, AdminView):
|
|
144
144
|
"admin/values/default.html",
|
145
145
|
]
|
146
146
|
|
147
|
-
def get_list_url(self) -> str
|
148
|
-
return
|
147
|
+
def get_list_url(self) -> str:
|
148
|
+
return ""
|
149
149
|
|
150
|
-
def get_create_url(self) -> str
|
151
|
-
return
|
150
|
+
def get_create_url(self) -> str:
|
151
|
+
return ""
|
152
152
|
|
153
|
-
def get_detail_url(self, obj) -> str
|
154
|
-
return
|
153
|
+
def get_detail_url(self, obj) -> str:
|
154
|
+
return ""
|
155
155
|
|
156
|
-
def get_update_url(self, obj) -> str
|
157
|
-
return
|
156
|
+
def get_update_url(self, obj) -> str:
|
157
|
+
return ""
|
158
158
|
|
159
|
-
def get_delete_url(self, obj) -> str
|
160
|
-
return
|
159
|
+
def get_delete_url(self, obj) -> str:
|
160
|
+
return ""
|
161
161
|
|
162
|
-
def get_object_url(self, obj) -> str
|
162
|
+
def get_object_url(self, obj) -> str:
|
163
163
|
if url := self.get_detail_url(obj):
|
164
164
|
return url
|
165
165
|
if url := self.get_update_url(obj):
|
166
166
|
return url
|
167
167
|
if url := self.get_delete_url(obj):
|
168
168
|
return url
|
169
|
-
return
|
169
|
+
return ""
|
170
170
|
|
171
171
|
def get_object_links(self, obj) -> dict[str]:
|
172
172
|
links = {}
|
@@ -191,20 +191,20 @@ class AdminListView(HTMXViewMixin, AdminView):
|
|
191
191
|
class AdminCreateView(AdminView, CreateView):
|
192
192
|
template_name = None
|
193
193
|
|
194
|
-
def get_list_url(self) -> str
|
195
|
-
return
|
194
|
+
def get_list_url(self) -> str:
|
195
|
+
return ""
|
196
196
|
|
197
|
-
def get_create_url(self) -> str
|
198
|
-
return
|
197
|
+
def get_create_url(self) -> str:
|
198
|
+
return ""
|
199
199
|
|
200
|
-
def get_detail_url(self, obj) -> str
|
201
|
-
return
|
200
|
+
def get_detail_url(self, obj) -> str:
|
201
|
+
return ""
|
202
202
|
|
203
|
-
def get_update_url(self, obj) -> str
|
204
|
-
return
|
203
|
+
def get_update_url(self, obj) -> str:
|
204
|
+
return ""
|
205
205
|
|
206
|
-
def get_delete_url(self, obj) -> str
|
207
|
-
return
|
206
|
+
def get_delete_url(self, obj) -> str:
|
207
|
+
return ""
|
208
208
|
|
209
209
|
def get_success_url(self, form):
|
210
210
|
if list_url := self.get_list_url():
|
@@ -227,7 +227,7 @@ class AdminDetailView(AdminView, DetailView):
|
|
227
227
|
|
228
228
|
def get_template_names(self) -> list[str]:
|
229
229
|
return super().get_template_names() + [
|
230
|
-
"admin/detail.html",
|
230
|
+
"admin/detail.html", # A generic detail view for rendering any object
|
231
231
|
]
|
232
232
|
|
233
233
|
def get_description(self):
|
@@ -274,20 +274,20 @@ class AdminDetailView(AdminView, DetailView):
|
|
274
274
|
|
275
275
|
return templates
|
276
276
|
|
277
|
-
def get_list_url(self) -> str
|
278
|
-
return
|
277
|
+
def get_list_url(self) -> str:
|
278
|
+
return ""
|
279
279
|
|
280
|
-
def get_create_url(self) -> str
|
281
|
-
return
|
280
|
+
def get_create_url(self) -> str:
|
281
|
+
return ""
|
282
282
|
|
283
|
-
def get_detail_url(self, obj) -> str
|
284
|
-
return
|
283
|
+
def get_detail_url(self, obj) -> str:
|
284
|
+
return ""
|
285
285
|
|
286
|
-
def get_update_url(self, obj) -> str
|
287
|
-
return
|
286
|
+
def get_update_url(self, obj) -> str:
|
287
|
+
return ""
|
288
288
|
|
289
|
-
def get_delete_url(self, obj) -> str
|
290
|
-
return
|
289
|
+
def get_delete_url(self, obj) -> str:
|
290
|
+
return ""
|
291
291
|
|
292
292
|
def get_fields(self):
|
293
293
|
return self.fields.copy() # Avoid mutating the class attribute itself
|
@@ -311,20 +311,20 @@ class AdminUpdateView(AdminView, UpdateView):
|
|
311
311
|
template_name = None
|
312
312
|
nav_section = ""
|
313
313
|
|
314
|
-
def get_list_url(self) -> str
|
315
|
-
return
|
314
|
+
def get_list_url(self) -> str:
|
315
|
+
return ""
|
316
316
|
|
317
|
-
def get_create_url(self) -> str
|
318
|
-
return
|
317
|
+
def get_create_url(self) -> str:
|
318
|
+
return ""
|
319
319
|
|
320
|
-
def get_detail_url(self, obj) -> str
|
321
|
-
return
|
320
|
+
def get_detail_url(self, obj) -> str:
|
321
|
+
return ""
|
322
322
|
|
323
|
-
def get_update_url(self, obj) -> str
|
324
|
-
return
|
323
|
+
def get_update_url(self, obj) -> str:
|
324
|
+
return ""
|
325
325
|
|
326
|
-
def get_delete_url(self, obj) -> str
|
327
|
-
return
|
326
|
+
def get_delete_url(self, obj) -> str:
|
327
|
+
return ""
|
328
328
|
|
329
329
|
def get_description(self):
|
330
330
|
return repr(self.object)
|
@@ -363,20 +363,20 @@ class AdminDeleteView(AdminView, DeleteView):
|
|
363
363
|
def get_description(self):
|
364
364
|
return repr(self.object)
|
365
365
|
|
366
|
-
def get_list_url(self) -> str
|
367
|
-
return
|
366
|
+
def get_list_url(self) -> str:
|
367
|
+
return ""
|
368
368
|
|
369
|
-
def get_create_url(self) -> str
|
370
|
-
return
|
369
|
+
def get_create_url(self) -> str:
|
370
|
+
return ""
|
371
371
|
|
372
|
-
def get_detail_url(self, obj) -> str
|
373
|
-
return
|
372
|
+
def get_detail_url(self, obj) -> str:
|
373
|
+
return ""
|
374
374
|
|
375
|
-
def get_update_url(self, obj) -> str
|
376
|
-
return
|
375
|
+
def get_update_url(self, obj) -> str:
|
376
|
+
return ""
|
377
377
|
|
378
|
-
def get_delete_url(self, obj) -> str
|
379
|
-
return
|
378
|
+
def get_delete_url(self, obj) -> str:
|
379
|
+
return ""
|
380
380
|
|
381
381
|
def get_links(self):
|
382
382
|
links = super().get_links()
|
@@ -0,0 +1,178 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: plain.admin
|
3
|
+
Version: 0.25.1
|
4
|
+
Summary: Admin dashboard and tools for Plain.
|
5
|
+
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
6
|
+
License-Expression: BSD-3-Clause
|
7
|
+
License-File: LICENSE
|
8
|
+
Requires-Python: >=3.11
|
9
|
+
Requires-Dist: plain-auth<1.0.0
|
10
|
+
Requires-Dist: plain-htmx<1.0.0
|
11
|
+
Requires-Dist: plain-tailwind<1.0.0
|
12
|
+
Requires-Dist: plain<1.0.0
|
13
|
+
Requires-Dist: sqlparse>=0.2.2
|
14
|
+
Description-Content-Type: text/markdown
|
15
|
+
|
16
|
+
# plain.admin
|
17
|
+
|
18
|
+
**Manage your app with a backend interface.**
|
19
|
+
|
20
|
+
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.
|
21
|
+
|
22
|
+

|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Install the `plain.admin` package and its dependencies.
|
27
|
+
|
28
|
+
```console
|
29
|
+
uv add plain.admin
|
30
|
+
```
|
31
|
+
|
32
|
+
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:
|
33
|
+
|
34
|
+
```python
|
35
|
+
# app/settings.py
|
36
|
+
INSTALLED_PACKAGES = [
|
37
|
+
"plain.models",
|
38
|
+
"plain.tailwind",
|
39
|
+
"plain.auth",
|
40
|
+
"plain.sessions",
|
41
|
+
"plain.htmx",
|
42
|
+
"plain.admin",
|
43
|
+
"plain.elements",
|
44
|
+
# other packages...
|
45
|
+
]
|
46
|
+
|
47
|
+
AUTH_USER_MODEL = "users.User"
|
48
|
+
AUTH_LOGIN_URL = "login"
|
49
|
+
|
50
|
+
MIDDLEWARE = [
|
51
|
+
"plain.sessions.middleware.SessionMiddleware",
|
52
|
+
"plain.auth.middleware.AuthenticationMiddleware",
|
53
|
+
"plain.admin.AdminMiddleware",
|
54
|
+
]
|
55
|
+
```
|
56
|
+
|
57
|
+
Your User model is expected to have an `is_admin` field (or attribute) for checking who has permission to access the admin.
|
58
|
+
|
59
|
+
```python
|
60
|
+
# app/users/models.py
|
61
|
+
from plain import models
|
62
|
+
|
63
|
+
|
64
|
+
@models.register_model
|
65
|
+
class User(models.Model):
|
66
|
+
is_admin = models.BooleanField(default=False)
|
67
|
+
# other fields...
|
68
|
+
```
|
69
|
+
|
70
|
+
To make the admin accessible, add the `AdminRouter` to your root URLs.
|
71
|
+
|
72
|
+
```python
|
73
|
+
# app/urls.py
|
74
|
+
from plain.admin.urls import AdminRouter
|
75
|
+
from plain.urls import Router, include, path
|
76
|
+
|
77
|
+
from . import views
|
78
|
+
|
79
|
+
|
80
|
+
class AppRouter(Router):
|
81
|
+
namespace = ""
|
82
|
+
urls = [
|
83
|
+
include("admin/", AdminRouter),
|
84
|
+
path("login/", views.LoginView, name="login"),
|
85
|
+
path("logout/", LogoutView, name="logout"),
|
86
|
+
# other urls...
|
87
|
+
]
|
88
|
+
|
89
|
+
```
|
90
|
+
|
91
|
+
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!).
|
92
|
+
|
93
|
+
```html
|
94
|
+
<!-- app/templates/base.html -->
|
95
|
+
<!DOCTYPE html>
|
96
|
+
<html lang="en">
|
97
|
+
<head>
|
98
|
+
<meta charset="UTF-8">
|
99
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
100
|
+
<title>{{ html_title|default("My App") }}</title>
|
101
|
+
{% tailwind_css %}
|
102
|
+
</head>
|
103
|
+
<body>
|
104
|
+
{% block content required %}{% endblock %}
|
105
|
+
|
106
|
+
{% toolbar %}
|
107
|
+
</body>
|
108
|
+
</html>
|
109
|
+
```
|
110
|
+
|
111
|
+
## Admin viewsets
|
112
|
+
|
113
|
+
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.
|
114
|
+
|
115
|
+
```python
|
116
|
+
# app/users/admin.py
|
117
|
+
from plain.admin.views import (
|
118
|
+
AdminModelDetailView,
|
119
|
+
AdminModelListView,
|
120
|
+
AdminModelUpdateView,
|
121
|
+
AdminViewset,
|
122
|
+
register_viewset,
|
123
|
+
)
|
124
|
+
from plain.models.forms import ModelForm
|
125
|
+
|
126
|
+
from .models import User
|
127
|
+
|
128
|
+
|
129
|
+
class UserForm(ModelForm):
|
130
|
+
class Meta:
|
131
|
+
model = User
|
132
|
+
fields = ["email"]
|
133
|
+
|
134
|
+
|
135
|
+
@register_viewset
|
136
|
+
class UserAdmin(AdminViewset):
|
137
|
+
class ListView(AdminModelListView):
|
138
|
+
model = User
|
139
|
+
fields = [
|
140
|
+
"id",
|
141
|
+
"email",
|
142
|
+
"created_at__date",
|
143
|
+
]
|
144
|
+
queryset_order = ["-created_at"]
|
145
|
+
search_fields = [
|
146
|
+
"email",
|
147
|
+
]
|
148
|
+
|
149
|
+
class DetailView(AdminModelDetailView):
|
150
|
+
model = User
|
151
|
+
|
152
|
+
class UpdateView(AdminModelUpdateView):
|
153
|
+
template_name = "admin/users/user_form.html"
|
154
|
+
model = User
|
155
|
+
form_class = UserForm
|
156
|
+
```
|
157
|
+
|
158
|
+
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.
|
159
|
+
|
160
|
+
## Admin cards
|
161
|
+
|
162
|
+
TODO
|
163
|
+
|
164
|
+
## Admin forms
|
165
|
+
|
166
|
+
TODO
|
167
|
+
|
168
|
+
## Toolbar
|
169
|
+
|
170
|
+
TODO
|
171
|
+
|
172
|
+
## Impersonate
|
173
|
+
|
174
|
+
TODO
|
175
|
+
|
176
|
+
## Querystats
|
177
|
+
|
178
|
+
TODO
|
@@ -1,6 +1,6 @@
|
|
1
|
-
plain/admin/README.md,sha256=
|
1
|
+
plain/admin/README.md,sha256=Ro2YkrKS-RXsmFBFN0QUpLh4OHIDvMkVgDIE6Wu4PMQ,3800
|
2
2
|
plain/admin/__init__.py,sha256=bPv9iftT8aLqBH6dDy-HTVXW66dQUhfIiEZ-LIUMC0Y,78
|
3
|
-
plain/admin/config.py,sha256=
|
3
|
+
plain/admin/config.py,sha256=TDYmJe4UYmKw4bz0x5s9PkDa-X4V-9JoJlka162-J7M,676
|
4
4
|
plain/admin/dates.py,sha256=EEhcQhHt3-k6kE9yvPdH5X6EecmUQ259xywbDBec3Dg,10253
|
5
5
|
plain/admin/default_settings.py,sha256=j7RdgGqksCmCgPO7zCcFiVV9f8yW-EULvqDcFOhQap8,127
|
6
6
|
plain/admin/middleware.py,sha256=k3yP1o3CzvLiZZSoxqq-DvAZlp4sICRauaT-kD3FJKM,398
|
@@ -8,7 +8,7 @@ plain/admin/templates.py,sha256=jLhJkuvqnPMBQTP-kzojFaqmFi50GZHvrVzuZCLc3rk,836
|
|
8
8
|
plain/admin/toolbar.py,sha256=dsZa_I-tTbaeOluCbvHGEqy4_Suw6Q_JSrKl8Eu08qY,973
|
9
9
|
plain/admin/urls.py,sha256=HtYsTDyV6s-k6ClT2H2oZqUDIANLq-PACpZfrR538js,1292
|
10
10
|
plain/admin/assets/admin/admin.css,sha256=-KdI7geASBsSbTve26VeJ-wCrdHWyD3EdjDZ9o393Yc,2653
|
11
|
-
plain/admin/assets/admin/admin.js,sha256=
|
11
|
+
plain/admin/assets/admin/admin.js,sha256=8R4VestYByRd2THe5gg8I35Zu3rokm6TQTkEf2mEB1c,2919
|
12
12
|
plain/admin/assets/admin/chart.js,sha256=GZiCYXjL6SmyuSCGE0Df80QvOUkw6H2YD-zsVID05lo,205089
|
13
13
|
plain/admin/assets/admin/jquery-3.6.1.slim.min.js,sha256=W2eb4M1jdKpuZ_-_KnDgqI9X9SwGLrXtO0dknpNPJyE,72534
|
14
14
|
plain/admin/assets/admin/list.js,sha256=_DPneRvk3VSzjVzfEaxyif4vLD75sCWz7bkHYp89uL8,1826
|
@@ -27,7 +27,7 @@ plain/admin/impersonate/permissions.py,sha256=N0EFshs0pgwFIAsK2MUgfnyhdb2rYheY_l
|
|
27
27
|
plain/admin/impersonate/settings.py,sha256=4wbWBN9eZIzei4fwkFLfw-_T5pvP_GG4l1lDdVpL_Co,193
|
28
28
|
plain/admin/impersonate/urls.py,sha256=s8bwi8qPueKCCYcLW75p-hPFkBKhm2AMi6AQKQcZsWc,304
|
29
29
|
plain/admin/impersonate/views.py,sha256=p8kEGC2ZNntAaLJRgwCaGSJABjLWoarpya9IuBpNW5A,789
|
30
|
-
plain/admin/querystats/README.md,sha256=
|
30
|
+
plain/admin/querystats/README.md,sha256=ONscu4dQOVe20CPHFyI8vR8iL2kvo3cOM8iwVO-lDyM,4821
|
31
31
|
plain/admin/querystats/__init__.py,sha256=VmP1aQ5Pviq4Z3izCB8G9g0Weq-2SYR88UFNtwqAPpo,81
|
32
32
|
plain/admin/querystats/core.py,sha256=GLhKwWwO2OwN2wneAgfbKRQzIIjZqegZYb1fMVwiljY,4281
|
33
33
|
plain/admin/querystats/middleware.py,sha256=M1EVdX11H545IdZlppbSIL_h8hzBIrMELrYrcAb4aq0,3192
|
@@ -71,12 +71,12 @@ plain/admin/templates/querystats/toolbar.html,sha256=dePs614akVWUD8IlgzvQ0TREThv
|
|
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
|
74
|
-
plain/admin/views/models.py,sha256=
|
75
|
-
plain/admin/views/objects.py,sha256=
|
74
|
+
plain/admin/views/models.py,sha256=mq_c13bdTs7WQ_MShVvTo3uCy09FOlBCrGIrGeK0sEo,5946
|
75
|
+
plain/admin/views/objects.py,sha256=7BXrDpHbdZ0vpzTHoLbSNdXO-rYSRw5YOBTiTK12E1U,11140
|
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.25.1.dist-info/METADATA,sha256=rCe1mK18fufoT3Xr5JSFSNe98TS5O75JYmxtBFZ3oqM,4237
|
80
|
+
plain_admin-0.25.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
81
|
+
plain_admin-0.25.1.dist-info/licenses/LICENSE,sha256=cvKM3OlqHx3ijD6e34zsSUkPvzl-ya3Dd63A6EHL94U,1500
|
82
|
+
plain_admin-0.25.1.dist-info/RECORD,,
|
@@ -1,275 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: plain.admin
|
3
|
-
Version: 0.22.0
|
4
|
-
Summary: Admin dashboard and tools for Plain.
|
5
|
-
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
6
|
-
License-Expression: BSD-3-Clause
|
7
|
-
License-File: LICENSE
|
8
|
-
Requires-Python: >=3.11
|
9
|
-
Requires-Dist: plain-auth<1.0.0
|
10
|
-
Requires-Dist: plain-htmx<1.0.0
|
11
|
-
Requires-Dist: plain-tailwind<1.0.0
|
12
|
-
Requires-Dist: plain<1.0.0
|
13
|
-
Requires-Dist: sqlparse>=0.2.2
|
14
|
-
Description-Content-Type: text/markdown
|
15
|
-
|
16
|
-
# Admin
|
17
|
-
|
18
|
-
An admin interface for admin users.
|
19
|
-
|
20
|
-
The Plain Admin is a new package built from the ground up.
|
21
|
-
It leverages class-based views and standard URLs and templates to provide a flexible admin where
|
22
|
-
you can quickly create your own pages and cards,
|
23
|
-
in addition to models.
|
24
|
-
|
25
|
-
- cards
|
26
|
-
- dashboards
|
27
|
-
- diy forms
|
28
|
-
- detached from login (do your own login (oauth, passkeys, etc))
|
29
|
-
|
30
|
-
## Installation
|
31
|
-
|
32
|
-
- install plain.admin and plain.htmx, add plain.admin.admin and plain.htmx to installed packages
|
33
|
-
- add url
|
34
|
-
|
35
|
-
## Models in the admin
|
36
|
-
|
37
|
-
## Dashboards
|
38
|
-
|
39
|
-
<!-- # plain.querystats
|
40
|
-
|
41
|
-
On-page database query stats in development and production.
|
42
|
-
|
43
|
-
On each page, the query stats will display how many database queries were performed and how long they took.
|
44
|
-
|
45
|
-
[Watch on YouTube](https://www.youtube.com/watch?v=NX8VXxVJm08)
|
46
|
-
|
47
|
-
Clicking the stats in the toolbar will show the full SQL query log with tracebacks and timings.
|
48
|
-
This is even designed to work in production,
|
49
|
-
making it much easier to discover and debug performance issues on production data!
|
50
|
-
|
51
|
-

|
52
|
-
|
53
|
-
It will also point out duplicate queries,
|
54
|
-
which can typically be removed by using `select_related`,
|
55
|
-
`prefetch_related`, or otherwise refactoring your code.
|
56
|
-
|
57
|
-
## Installation
|
58
|
-
|
59
|
-
```python
|
60
|
-
# settings.py
|
61
|
-
INSTALLED_PACKAGES = [
|
62
|
-
# ...
|
63
|
-
"plain.admin.querystats",
|
64
|
-
]
|
65
|
-
|
66
|
-
MIDDLEWARE = [
|
67
|
-
"plain.sessions.middleware.SessionMiddleware",
|
68
|
-
"plain.auth.middleware.AuthenticationMiddleware",
|
69
|
-
|
70
|
-
"plain.admin.querystats.QueryStatsMiddleware",
|
71
|
-
# Put additional middleware below querystats
|
72
|
-
# ...
|
73
|
-
]
|
74
|
-
```
|
75
|
-
|
76
|
-
We strongly recommend using the plain-toolbar along with this,
|
77
|
-
but if you aren't,
|
78
|
-
you can add the querystats to your frontend templates with this include:
|
79
|
-
|
80
|
-
```html
|
81
|
-
{% include "querystats/button.html" %}
|
82
|
-
```
|
83
|
-
|
84
|
-
*Note that you will likely want to surround this with an if `DEBUG` or `is_admin` check.*
|
85
|
-
|
86
|
-
To view querystats you need to send a POST request to `?querystats=store` (i.e. via a `<form>`),
|
87
|
-
and the template include is the easiest way to do that.
|
88
|
-
|
89
|
-
## Tailwind CSS
|
90
|
-
|
91
|
-
This package is styled with [Tailwind CSS](https://tailwindcss.com/),
|
92
|
-
and pairs well with [`plain-tailwind`](https://github.com/plainpackages/plain-tailwind).
|
93
|
-
|
94
|
-
If you are using your own Tailwind implementation,
|
95
|
-
you can modify the "content" in your Tailwind config to include any Plain packages:
|
96
|
-
|
97
|
-
```js
|
98
|
-
// tailwind.config.js
|
99
|
-
module.exports = {
|
100
|
-
content: [
|
101
|
-
// ...
|
102
|
-
".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
|
103
|
-
],
|
104
|
-
// ...
|
105
|
-
}
|
106
|
-
```
|
107
|
-
|
108
|
-
If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
|
109
|
-
|
110
|
-
|
111
|
-
# plain.toolbar
|
112
|
-
|
113
|
-
The admin toolbar is enabled for every user who `is_admin`.
|
114
|
-
|
115
|
-

|
116
|
-
|
117
|
-
## Installation
|
118
|
-
|
119
|
-
Add `plaintoolbar` to your `INSTALLED_PACKAGES`,
|
120
|
-
and the `{% toolbar %}` to your base template:
|
121
|
-
|
122
|
-
```python
|
123
|
-
# settings.py
|
124
|
-
INSTALLED_PACKAGES += [
|
125
|
-
"plaintoolbar",
|
126
|
-
]
|
127
|
-
```
|
128
|
-
|
129
|
-
```html
|
130
|
-
<!-- base.template.html -->
|
131
|
-
{% load toolbar %}
|
132
|
-
<!doctype html>
|
133
|
-
<html lang="en">
|
134
|
-
<head>
|
135
|
-
...
|
136
|
-
</head>
|
137
|
-
<body>
|
138
|
-
{% toolbar %}
|
139
|
-
...
|
140
|
-
</body>
|
141
|
-
```
|
142
|
-
|
143
|
-
More specific settings can be found below.
|
144
|
-
|
145
|
-
## Tailwind CSS
|
146
|
-
|
147
|
-
This package is styled with [Tailwind CSS](https://tailwindcss.com/),
|
148
|
-
and pairs well with [`plain-tailwind`](https://github.com/plainpackages/plain-tailwind).
|
149
|
-
|
150
|
-
If you are using your own Tailwind implementation,
|
151
|
-
you can modify the "content" in your Tailwind config to include any Plain packages:
|
152
|
-
|
153
|
-
```js
|
154
|
-
// tailwind.config.js
|
155
|
-
module.exports = {
|
156
|
-
content: [
|
157
|
-
// ...
|
158
|
-
".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
|
159
|
-
],
|
160
|
-
// ...
|
161
|
-
}
|
162
|
-
```
|
163
|
-
|
164
|
-
If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
|
165
|
-
|
166
|
-
|
167
|
-
# plain.requestlog
|
168
|
-
|
169
|
-
The request log stores a local history of HTTP requests and responses during `plain work` (Django runserver).
|
170
|
-
|
171
|
-
The request history will make it easy to see redirects,
|
172
|
-
400 and 500 level errors,
|
173
|
-
form submissions,
|
174
|
-
API calls,
|
175
|
-
webhooks,
|
176
|
-
and more.
|
177
|
-
|
178
|
-
[Watch on YouTube](https://www.youtube.com/watch?v=AwI7Pt5oZnM)
|
179
|
-
|
180
|
-
Requests can be re-submitted by clicking the "replay" button.
|
181
|
-
|
182
|
-
[](https://user-images.githubusercontent.com/649496/213781414-417ad043-de67-4836-9ef1-2b91404336c3.png)
|
183
|
-
|
184
|
-
## Installation
|
185
|
-
|
186
|
-
```python
|
187
|
-
# settings.py
|
188
|
-
INSTALLED_PACKAGES += [
|
189
|
-
"plainrequestlog",
|
190
|
-
]
|
191
|
-
|
192
|
-
MIDDLEWARE = MIDDLEWARE + [
|
193
|
-
# ...
|
194
|
-
"plainrequestlog.RequestLogMiddleware",
|
195
|
-
]
|
196
|
-
```
|
197
|
-
|
198
|
-
The default settings can be customized if needed:
|
199
|
-
|
200
|
-
```python
|
201
|
-
# settings.py
|
202
|
-
DEV_REQUESTS_IGNORE_PATHS = [
|
203
|
-
"/sw.js",
|
204
|
-
"/favicon.ico",
|
205
|
-
"/admin/jsi18n/",
|
206
|
-
]
|
207
|
-
DEV_REQUESTS_MAX = 50
|
208
|
-
```
|
209
|
-
|
210
|
-
## Tailwind CSS
|
211
|
-
|
212
|
-
This package is styled with [Tailwind CSS](https://tailwindcss.com/),
|
213
|
-
and pairs well with [`plain-tailwind`](https://github.com/plainpackages/plain-tailwind).
|
214
|
-
|
215
|
-
If you are using your own Tailwind implementation,
|
216
|
-
you can modify the "content" in your Tailwind config to include any Plain packages:
|
217
|
-
|
218
|
-
```js
|
219
|
-
// tailwind.config.js
|
220
|
-
module.exports = {
|
221
|
-
content: [
|
222
|
-
// ...
|
223
|
-
".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
|
224
|
-
],
|
225
|
-
// ...
|
226
|
-
}
|
227
|
-
```
|
228
|
-
|
229
|
-
If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
|
230
|
-
|
231
|
-
|
232
|
-
# plain.impersonate
|
233
|
-
|
234
|
-
See what your users see.
|
235
|
-
|
236
|
-
A key feature for providing customer support is to be able to view the site through their account.
|
237
|
-
With `impersonate` installed, you can impersonate a user by finding them in the Django admin and clicking the "Impersonate" button.
|
238
|
-
|
239
|
-

|
240
|
-
|
241
|
-
Then with the [admin toolbar](/docs/plain-toolbar/) enabled, you'll get a notice of the impersonation and a button to exit:
|
242
|
-
|
243
|
-

|
244
|
-
|
245
|
-
## Installation
|
246
|
-
|
247
|
-
To impersonate users, you need the app, middleware, and URLs:
|
248
|
-
|
249
|
-
```python
|
250
|
-
# settings.py
|
251
|
-
INSTALLED_PACKAGES = INSTALLED_PACKAGES + [
|
252
|
-
"plain.admin.impersonate",
|
253
|
-
]
|
254
|
-
|
255
|
-
MIDDLEWARE = MIDDLEWARE + [
|
256
|
-
"plain.admin.impersonate.ImpersonateMiddleware",
|
257
|
-
]
|
258
|
-
```
|
259
|
-
|
260
|
-
```python
|
261
|
-
# urls.py
|
262
|
-
urlpatterns = [
|
263
|
-
# ...
|
264
|
-
path("impersonate/", include("plain.admin.impersonate.urls")),
|
265
|
-
]
|
266
|
-
```
|
267
|
-
|
268
|
-
## Settings
|
269
|
-
|
270
|
-
By default, all admin users can impersonate other users.
|
271
|
-
|
272
|
-
```python
|
273
|
-
# settings.py
|
274
|
-
IMPERSONATE_ALLOWED = lambda user: user.is_admin
|
275
|
-
``` -->
|
File without changes
|
File without changes
|