plain.admin 0.22.1__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 CHANGED
@@ -1,261 +1,163 @@
1
- # Admin
1
+ # plain.admin
2
2
 
3
- An admin interface for admin users.
3
+ **Manage your app with a backend interface.**
4
4
 
5
- The Plain Admin is a new package built from the ground up.
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
- - cards
11
- - dashboards
12
- - diy forms
13
- - detached from login (do your own login (oauth, passkeys, etc))
7
+ ![Plain Admin user example](https://assets.plainframework.com/docs/plain-pageviews-user.png)
14
8
 
15
9
  ## Installation
16
10
 
17
- - install plain.admin and plain.htmx, add plain.admin.admin and plain.htmx to installed packages
18
- - add url
11
+ Install the `plain.admin` package and its dependencies.
19
12
 
20
- ## Models in the admin
21
-
22
- ## Dashboards
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
- ![Django query stats](https://user-images.githubusercontent.com/649496/213781593-54197bb6-36a8-4c9d-8294-5b43bd86a4c9.png)
13
+ ```console
14
+ uv add plain.admin
15
+ ```
37
16
 
38
- It will also point out duplicate queries,
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.admin.querystats",
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
- We strongly recommend using the plain-toolbar along with this,
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
- ## Tailwind CSS
75
-
76
- This package is styled with [Tailwind CSS](https://tailwindcss.com/),
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
- ```js
83
- // tailwind.config.js
84
- module.exports = {
85
- content: [
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
- If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
94
-
55
+ To make the admin accessible, add the `AdminRouter` to your root URLs.
95
56
 
96
- # plain.toolbar
57
+ ```python
58
+ # app/urls.py
59
+ from plain.admin.urls import AdminRouter
60
+ from plain.urls import Router, include, path
97
61
 
98
- The admin toolbar is enabled for every user who `is_admin`.
62
+ from . import views
99
63
 
100
- ![Plain Admin toolbar](https://user-images.githubusercontent.com/649496/213781915-a2094f54-99b8-4a05-a36e-dee107405229.png)
101
64
 
102
- ## Installation
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
 
114
- ```html
115
- <!-- base.template.html -->
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!).
116
77
 
117
- {% load toolbar %}
118
-
119
- <!doctype html>
78
+ ```html
79
+ <!-- app/templates/base.html -->
80
+ <!DOCTYPE html>
120
81
  <html lang="en">
121
- <head>
122
- ...
123
- </head>
124
- <body>
125
- {% toolbar %}
126
- ...
127
- </body>
128
- ```
129
-
130
- More specific settings can be found below.
131
-
132
- ## Tailwind CSS
133
-
134
- This package is styled with [Tailwind CSS](https://tailwindcss.com/),
135
- and pairs well with [`plain-tailwind`](https://github.com/plainpackages/plain-tailwind).
136
-
137
- If you are using your own Tailwind implementation,
138
- you can modify the "content" in your Tailwind config to include any Plain packages:
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 %}
139
90
 
140
- ```js
141
- // tailwind.config.js
142
- module.exports = {
143
- content: [
144
- // ...
145
- ".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
146
- ],
147
- // ...
148
- }
91
+ {% toolbar %}
92
+ </body>
93
+ </html>
149
94
  ```
150
95
 
151
- If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
152
-
153
- # plain.requestlog
154
-
155
- The request log stores a local history of HTTP requests and responses during `plain work` (Django runserver).
156
-
157
- The request history will make it easy to see redirects,
158
- 400 and 500 level errors,
159
- form submissions,
160
- API calls,
161
- webhooks,
162
- and more.
163
-
164
- [Watch on YouTube](https://www.youtube.com/watch?v=AwI7Pt5oZnM)
96
+ ## Admin viewsets
165
97
 
166
- Requests can be re-submitted by clicking the "replay" button.
167
-
168
- [![Django request log](https://user-images.githubusercontent.com/649496/213781414-417ad043-de67-4836-9ef1-2b91404336c3.png)](https://user-images.githubusercontent.com/649496/213781414-417ad043-de67-4836-9ef1-2b91404336c3.png)
169
-
170
- ## 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.
171
99
 
172
100
  ```python
173
- # settings.py
174
- INSTALLED_PACKAGES += [
175
- "plainrequestlog",
176
- ]
177
-
178
- MIDDLEWARE = MIDDLEWARE + [
179
- # ...
180
- "plainrequestlog.RequestLogMiddleware",
181
- ]
182
- ```
183
-
184
- The default settings can be customized if needed:
185
-
186
- ```python
187
- # settings.py
188
- DEV_REQUESTS_IGNORE_PATHS = [
189
- "/sw.js",
190
- "/favicon.ico",
191
- "/admin/jsi18n/",
192
- ]
193
- DEV_REQUESTS_MAX = 50
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
194
141
  ```
195
142
 
196
- ## Tailwind CSS
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.
197
144
 
198
- This package is styled with [Tailwind CSS](https://tailwindcss.com/),
199
- and pairs well with [`plain-tailwind`](https://github.com/plainpackages/plain-tailwind).
145
+ ## Admin cards
200
146
 
201
- If you are using your own Tailwind implementation,
202
- you can modify the "content" in your Tailwind config to include any Plain packages:
147
+ TODO
203
148
 
204
- ```js
205
- // tailwind.config.js
206
- module.exports = {
207
- content: [
208
- // ...
209
- ".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
210
- ],
211
- // ...
212
- }
213
- ```
149
+ ## Admin forms
214
150
 
215
- If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
151
+ TODO
216
152
 
217
- # plain.impersonate
153
+ ## Toolbar
218
154
 
219
- See what your users see.
155
+ TODO
220
156
 
221
- A key feature for providing customer support is to be able to view the site through their account.
222
- With `impersonate` installed, you can impersonate a user by finding them in the Django admin and clicking the "Impersonate" button.
223
-
224
- ![](/docs/img/impersonate-admin.png)
225
-
226
- Then with the [admin toolbar](/docs/plain-toolbar/) enabled, you'll get a notice of the impersonation and a button to exit:
227
-
228
- ![](/docs/img/impersonate-bar.png)
229
-
230
- ## Installation
231
-
232
- To impersonate users, you need the app, middleware, and URLs:
233
-
234
- ```python
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
- ```
157
+ ## Impersonate
252
158
 
253
- ## Settings
159
+ TODO
254
160
 
255
- By default, all admin users can impersonate other users.
161
+ ## Querystats
256
162
 
257
- ````python
258
- # settings.py
259
- IMPERSONATE_ALLOWED = lambda user: user.is_admin
260
- ``` -->
261
- ````
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", $this.data("column-autolink"));
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
@@ -6,7 +6,7 @@ from plain.packages import PackageConfig, packages_registry, register_config
6
6
 
7
7
  @register_config
8
8
  class Config(PackageConfig):
9
- label = "plainadmin"
9
+ package_label = "plainadmin"
10
10
 
11
11
  def ready(self):
12
12
  def _import_if_exists(module_name):
@@ -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"
@@ -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(status=204)
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 | None:
148
- return None
147
+ def get_list_url(self) -> str:
148
+ return ""
149
149
 
150
- def get_create_url(self) -> str | None:
151
- return None
150
+ def get_create_url(self) -> str:
151
+ return ""
152
152
 
153
- def get_detail_url(self, obj) -> str | None:
154
- return None
153
+ def get_detail_url(self, obj) -> str:
154
+ return ""
155
155
 
156
- def get_update_url(self, obj) -> str | None:
157
- return None
156
+ def get_update_url(self, obj) -> str:
157
+ return ""
158
158
 
159
- def get_delete_url(self, obj) -> str | None:
160
- return None
159
+ def get_delete_url(self, obj) -> str:
160
+ return ""
161
161
 
162
- def get_object_url(self, obj) -> str | None:
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 None
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 | None:
195
- return None
194
+ def get_list_url(self) -> str:
195
+ return ""
196
196
 
197
- def get_create_url(self) -> str | None:
198
- return None
197
+ def get_create_url(self) -> str:
198
+ return ""
199
199
 
200
- def get_detail_url(self, obj) -> str | None:
201
- return None
200
+ def get_detail_url(self, obj) -> str:
201
+ return ""
202
202
 
203
- def get_update_url(self, obj) -> str | None:
204
- return None
203
+ def get_update_url(self, obj) -> str:
204
+ return ""
205
205
 
206
- def get_delete_url(self, obj) -> str | None:
207
- return None
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 | None:
278
- return None
277
+ def get_list_url(self) -> str:
278
+ return ""
279
279
 
280
- def get_create_url(self) -> str | None:
281
- return None
280
+ def get_create_url(self) -> str:
281
+ return ""
282
282
 
283
- def get_detail_url(self, obj) -> str | None:
284
- return None
283
+ def get_detail_url(self, obj) -> str:
284
+ return ""
285
285
 
286
- def get_update_url(self, obj) -> str | None:
287
- return None
286
+ def get_update_url(self, obj) -> str:
287
+ return ""
288
288
 
289
- def get_delete_url(self, obj) -> str | None:
290
- return None
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 | None:
315
- return None
314
+ def get_list_url(self) -> str:
315
+ return ""
316
316
 
317
- def get_create_url(self) -> str | None:
318
- return None
317
+ def get_create_url(self) -> str:
318
+ return ""
319
319
 
320
- def get_detail_url(self, obj) -> str | None:
321
- return None
320
+ def get_detail_url(self, obj) -> str:
321
+ return ""
322
322
 
323
- def get_update_url(self, obj) -> str | None:
324
- return None
323
+ def get_update_url(self, obj) -> str:
324
+ return ""
325
325
 
326
- def get_delete_url(self, obj) -> str | None:
327
- return None
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 | None:
367
- return None
366
+ def get_list_url(self) -> str:
367
+ return ""
368
368
 
369
- def get_create_url(self) -> str | None:
370
- return None
369
+ def get_create_url(self) -> str:
370
+ return ""
371
371
 
372
- def get_detail_url(self, obj) -> str | None:
373
- return None
372
+ def get_detail_url(self, obj) -> str:
373
+ return ""
374
374
 
375
- def get_update_url(self, obj) -> str | None:
376
- return None
375
+ def get_update_url(self, obj) -> str:
376
+ return ""
377
377
 
378
- def get_delete_url(self, obj) -> str | None:
379
- return None
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
+ ![Plain Admin user example](https://assets.plainframework.com/docs/plain-pageviews-user.png)
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=GB0gTgSeMDt6jbQ9oH58xJNBJMP3iSPiVdF6KyX7vjQ,6390
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=FpuOFmMin5WM1jxxM8-cFLCzgcaWHU6_UaD3k0euLhc,668
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=AWD6UqPxGqJFaUhYTDWe4niTgk0thzU4gRl7qK41KNc,2759
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
@@ -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=gos3KBPvOovULG8xZYKOO4ENrQlUIGrlIkRPO99wkDw,7196
75
- plain/admin/views/objects.py,sha256=Wcw7_k4BDz4h49e4BGtq6g-3jo5-T0fJU9CfMcvMQK4,11319
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.22.1.dist-info/METADATA,sha256=OAsH8LlM_w0hE43s9cssSh4MoJvoUGPHU8GK-izJDjU,6827
80
- plain_admin-0.22.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
81
- plain_admin-0.22.1.dist-info/licenses/LICENSE,sha256=cvKM3OlqHx3ijD6e34zsSUkPvzl-ya3Dd63A6EHL94U,1500
82
- plain_admin-0.22.1.dist-info/RECORD,,
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,276 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: plain.admin
3
- Version: 0.22.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
- # 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
- ![Django query stats](https://user-images.githubusercontent.com/649496/213781593-54197bb6-36a8-4c9d-8294-5b43bd86a4c9.png)
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
- ![Plain Admin toolbar](https://user-images.githubusercontent.com/649496/213781915-a2094f54-99b8-4a05-a36e-dee107405229.png)
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
-
132
- {% load toolbar %}
133
-
134
- <!doctype html>
135
- <html lang="en">
136
- <head>
137
- ...
138
- </head>
139
- <body>
140
- {% toolbar %}
141
- ...
142
- </body>
143
- ```
144
-
145
- More specific settings can be found below.
146
-
147
- ## Tailwind CSS
148
-
149
- This package is styled with [Tailwind CSS](https://tailwindcss.com/),
150
- and pairs well with [`plain-tailwind`](https://github.com/plainpackages/plain-tailwind).
151
-
152
- If you are using your own Tailwind implementation,
153
- you can modify the "content" in your Tailwind config to include any Plain packages:
154
-
155
- ```js
156
- // tailwind.config.js
157
- module.exports = {
158
- content: [
159
- // ...
160
- ".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
161
- ],
162
- // ...
163
- }
164
- ```
165
-
166
- If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
167
-
168
- # plain.requestlog
169
-
170
- The request log stores a local history of HTTP requests and responses during `plain work` (Django runserver).
171
-
172
- The request history will make it easy to see redirects,
173
- 400 and 500 level errors,
174
- form submissions,
175
- API calls,
176
- webhooks,
177
- and more.
178
-
179
- [Watch on YouTube](https://www.youtube.com/watch?v=AwI7Pt5oZnM)
180
-
181
- Requests can be re-submitted by clicking the "replay" button.
182
-
183
- [![Django request log](https://user-images.githubusercontent.com/649496/213781414-417ad043-de67-4836-9ef1-2b91404336c3.png)](https://user-images.githubusercontent.com/649496/213781414-417ad043-de67-4836-9ef1-2b91404336c3.png)
184
-
185
- ## Installation
186
-
187
- ```python
188
- # settings.py
189
- INSTALLED_PACKAGES += [
190
- "plainrequestlog",
191
- ]
192
-
193
- MIDDLEWARE = MIDDLEWARE + [
194
- # ...
195
- "plainrequestlog.RequestLogMiddleware",
196
- ]
197
- ```
198
-
199
- The default settings can be customized if needed:
200
-
201
- ```python
202
- # settings.py
203
- DEV_REQUESTS_IGNORE_PATHS = [
204
- "/sw.js",
205
- "/favicon.ico",
206
- "/admin/jsi18n/",
207
- ]
208
- DEV_REQUESTS_MAX = 50
209
- ```
210
-
211
- ## Tailwind CSS
212
-
213
- This package is styled with [Tailwind CSS](https://tailwindcss.com/),
214
- and pairs well with [`plain-tailwind`](https://github.com/plainpackages/plain-tailwind).
215
-
216
- If you are using your own Tailwind implementation,
217
- you can modify the "content" in your Tailwind config to include any Plain packages:
218
-
219
- ```js
220
- // tailwind.config.js
221
- module.exports = {
222
- content: [
223
- // ...
224
- ".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
225
- ],
226
- // ...
227
- }
228
- ```
229
-
230
- If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
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
- ![](/docs/img/impersonate-admin.png)
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
- ![](/docs/img/impersonate-bar.png)
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
- ``` -->
276
- ````