plain.admin 0.17.0__tar.gz → 0.19.0__tar.gz

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.
Files changed (92) hide show
  1. {plain_admin-0.17.0 → plain_admin-0.19.0}/PKG-INFO +1 -1
  2. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/assets/admin/admin.css +20 -0
  3. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/cards/base.py +3 -4
  4. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/base.html +24 -25
  5. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/detail.html +3 -3
  6. plain_admin-0.19.0/plain/admin/templates/admin/search.html +49 -0
  7. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/elements/admin/CheckboxField.html +1 -0
  8. plain_admin-0.19.0/plain/admin/templates/elements/admin/Help.html +1 -0
  9. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/elements/admin/InputField.html +1 -0
  10. plain_admin-0.19.0/plain/admin/templates/elements/admin/Label.html +4 -0
  11. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/elements/admin/SelectField.html +1 -0
  12. plain_admin-0.19.0/plain/admin/templates/elements/admin/Textarea.html +7 -0
  13. plain_admin-0.19.0/plain/admin/templates/elements/admin/TextareaField.html +6 -0
  14. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/toolbar/toolbar.html +3 -8
  15. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/urls.py +0 -2
  16. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/views/base.py +2 -20
  17. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/views/models.py +17 -18
  18. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/views/registry.py +18 -10
  19. {plain_admin-0.17.0 → plain_admin-0.19.0}/pyproject.toml +1 -1
  20. plain_admin-0.17.0/plain/admin/templates/admin/form.html +0 -13
  21. plain_admin-0.17.0/plain/admin/templates/admin/search.html +0 -27
  22. plain_admin-0.17.0/plain/admin/templates/elements/admin/Label.html +0 -3
  23. {plain_admin-0.17.0 → plain_admin-0.19.0}/.gitignore +0 -0
  24. {plain_admin-0.17.0 → plain_admin-0.19.0}/LICENSE +0 -0
  25. {plain_admin-0.17.0 → plain_admin-0.19.0}/README.md +0 -0
  26. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/README.md +0 -0
  27. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/__init__.py +0 -0
  28. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/assets/admin/admin.js +0 -0
  29. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/assets/admin/chart.js +0 -0
  30. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/assets/admin/jquery-3.6.1.slim.min.js +0 -0
  31. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/assets/admin/list.js +0 -0
  32. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/assets/admin/popper.min.js +0 -0
  33. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/assets/admin/tippy-bundle.umd.min.js +0 -0
  34. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/assets/toolbar/toolbar.js +0 -0
  35. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/cards/__init__.py +0 -0
  36. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/cards/charts.py +0 -0
  37. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/cards/tables.py +0 -0
  38. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/config.py +0 -0
  39. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/dates.py +0 -0
  40. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/default_settings.py +0 -0
  41. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/impersonate/README.md +0 -0
  42. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/impersonate/__init__.py +0 -0
  43. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/impersonate/middleware.py +0 -0
  44. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/impersonate/models.py +0 -0
  45. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/impersonate/permissions.py +0 -0
  46. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/impersonate/settings.py +0 -0
  47. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/impersonate/urls.py +0 -0
  48. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/impersonate/views.py +0 -0
  49. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/middleware.py +0 -0
  50. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/querystats/README.md +0 -0
  51. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/querystats/__init__.py +0 -0
  52. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/querystats/core.py +0 -0
  53. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/querystats/middleware.py +0 -0
  54. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/querystats/urls.py +0 -0
  55. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/querystats/views.py +0 -0
  56. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/cards/base.html +0 -0
  57. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/cards/card.html +0 -0
  58. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/cards/chart.html +0 -0
  59. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/cards/table.html +0 -0
  60. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/delete.html +0 -0
  61. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/index.html +0 -0
  62. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/list.html +0 -0
  63. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/page.html +0 -0
  64. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/values/UUID.html +0 -0
  65. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/values/bool.html +0 -0
  66. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/values/datetime.html +0 -0
  67. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/values/default.html +0 -0
  68. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/values/dict.html +0 -0
  69. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/values/get_display.html +0 -0
  70. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/values/img.html +0 -0
  71. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/values/list.html +0 -0
  72. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/values/model.html +0 -0
  73. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/admin/values/queryset.html +0 -0
  74. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/elements/admin/Checkbox.html +0 -0
  75. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/elements/admin/FieldErrors.html +0 -0
  76. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/elements/admin/Input.html +0 -0
  77. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/elements/admin/Select.html +0 -0
  78. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/elements/admin/Submit.html +0 -0
  79. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/querystats/querystats.html +0 -0
  80. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates/querystats/toolbar.html +0 -0
  81. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/templates.py +0 -0
  82. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/toolbar.py +0 -0
  83. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/views/__init__.py +0 -0
  84. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/views/objects.py +0 -0
  85. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/views/types.py +0 -0
  86. {plain_admin-0.17.0 → plain_admin-0.19.0}/plain/admin/views/viewsets.py +0 -0
  87. {plain_admin-0.17.0 → plain_admin-0.19.0}/tests/app/settings.py +0 -0
  88. {plain_admin-0.17.0 → plain_admin-0.19.0}/tests/app/urls.py +0 -0
  89. {plain_admin-0.17.0 → plain_admin-0.19.0}/tests/app/users/migrations/0001_initial.py +0 -0
  90. {plain_admin-0.17.0 → plain_admin-0.19.0}/tests/app/users/migrations/__init__.py +0 -0
  91. {plain_admin-0.17.0 → plain_admin-0.19.0}/tests/app/users/models.py +0 -0
  92. {plain_admin-0.17.0 → plain_admin-0.19.0}/tests/test_admin.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.admin
3
- Version: 0.17.0
3
+ Version: 0.19.0
4
4
  Summary: Admin dashboard and tools for Plain.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-Expression: BSD-3-Clause
@@ -61,6 +61,12 @@ main a:hover {
61
61
  border-radius: 6px;
62
62
  transition: background-color 0.2s, border-color 0.2s, transform 0.2s;
63
63
  cursor: pointer;
64
+ flex-shrink: 0;
65
+
66
+ @media (max-width: 640px) {
67
+ font-size: 12px;
68
+ padding: 6px 12px;
69
+ }
64
70
 
65
71
  &:hover {
66
72
  background-color: #2a2928;
@@ -80,6 +86,20 @@ main a:hover {
80
86
  }
81
87
  }
82
88
 
89
+ main button[type="submit"] {
90
+ background-color: #2563eb;
91
+ border-color: #2563eb;
92
+
93
+ &:hover {
94
+ background-color: #1d4ed8;
95
+ border-color: #1d4ed8;
96
+ }
97
+ &:focus {
98
+ outline: 2px solid #2563eb;
99
+ outline-offset: 2px;
100
+ }
101
+ }
102
+
83
103
  /* Cards use these? */
84
104
  section {
85
105
  border: rgba(255, 255, 255, 0.1) 1px solid;
@@ -2,7 +2,6 @@ from enum import Enum
2
2
 
3
3
  from plain.http import HttpRequest
4
4
  from plain.templates import Template
5
- from plain.utils.text import slugify
6
5
  from plain.views import View
7
6
 
8
7
 
@@ -22,7 +21,6 @@ class Card:
22
21
  title: str
23
22
 
24
23
  # Optional fields
25
- slug: str = ""
26
24
  description: str = ""
27
25
  text: str = ""
28
26
  link: str = ""
@@ -60,8 +58,9 @@ class Card:
60
58
  def get_title(self) -> str:
61
59
  return self.title
62
60
 
63
- def get_slug(self) -> str:
64
- return self.slug or slugify(self.title)
61
+ @classmethod
62
+ def get_slug(cls) -> str:
63
+ return f"{cls.__module__}.{cls.__name__}".lower().replace(".", "_")
65
64
 
66
65
  def get_description(self) -> str:
67
66
  return self.description
@@ -22,9 +22,9 @@
22
22
  </head>
23
23
  <body class="flex min-h-screen bg-stone-950">
24
24
 
25
- <nav class="fixed top-0 left-0 right-0 h-14 px-4 py-2 flex items-center justify-evenly text-sm text-white/70">
25
+ <nav class="fixed top-0 left-0 right-0 h-14 px-4 py-2 flex items-center justify-between sm:justify-evenly text-sm text-white/70 space-x-3">
26
26
  <div class="flex items-center space-x-2">
27
- <button type="button" data-toggle="#admin-sidebar,#admin-content" class="mr-1 lg:hidden">
27
+ <button type="button" data-toggle="#admin-sidebar,#admin-content" class="lg:hidden">
28
28
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-5 h-5 bi bi-list" viewBox="0 0 16 16">
29
29
  <path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5"/>
30
30
  </svg>
@@ -32,11 +32,6 @@
32
32
  </button>
33
33
 
34
34
  <a class="inline-flex items-center text-stone-300" href="{{ url('admin:index') }}">
35
- <svg class="w-5 h-5 mr-2" width="160" height="125" viewBox="0 0 160 125" fill="none" xmlns="http://www.w3.org/2000/svg">
36
- <rect x="4.78467" y="4.79785" width="150.978" height="115.404" rx="5" stroke="#ffffff" stroke-width="8"/>
37
- <path d="M151.762 60.3705C99.2596 39.3233 80.202 66.8232 8.78467 60.3705V116.2H151.762V60.3705Z" fill="#ffffff"/>
38
- <path d="M51.104 8.08887H108.179V10.7668C108.179 12.6998 106.612 14.2668 104.679 14.2668H54.604C52.671 14.2668 51.104 12.6998 51.104 10.7668V8.08887Z" fill="#ffffff" stroke="#ffffff"/>
39
- </svg>
40
35
  <span class="text-stone-400">Admin</span>
41
36
  </a>
42
37
 
@@ -50,14 +45,14 @@
50
45
  #}
51
46
  </div>
52
47
 
53
- <form method="GET" action="{{ url('admin:search') }}" class="flex flex-1 justify-center">
54
- <div class="relative max-w-xs">
48
+ <form method="GET" action="{{ url('admin:search') }}" class="hidden sm:flex flex-1 justify-center">
49
+ <div class="relative max-w-xs flex flex-1">
55
50
  <label for="query" class="sr-only">Search</label>
56
51
  <input
57
52
  type="text"
58
53
  name="query"
59
54
  id="query"
60
- class="block w-full pr-10 pl-10 placeholder:text-center text-sm border-gray-200/10 text-white rounded-md focus:border-blue-500 focus:ring-blue-500 bg-white/5 py-1"
55
+ class="block w-full pr-10 pl-10 placeholder:text-white/30 placeholder:text-center text-sm border-gray-200/10 text-white rounded-md focus:border-blue-500 focus:ring-blue-500 bg-white/5 py-1"
61
56
  placeholder="Search everything"
62
57
  value="{{ global_search_query|default('') }}"
63
58
  >
@@ -99,23 +94,26 @@
99
94
  <div class="text-xs tracking-wide text-stone-500">Recent</div>
100
95
  </div> -->
101
96
  </div>
102
- <div class="flex items-center justify-between mt-8 text-sm pb-3 pt-3 text-stone-400 sticky bottom-0 bg-stone-950">
103
- <div class="flex items-center truncate">
104
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-6 h-6 mr-1.5 bi bi-person-circle" viewBox="0 0 16 16">
105
- <path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
106
- <path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
107
- </svg>
108
- <span class="truncate">
109
- {{ request.user }}
110
- </span>
97
+ <div class="mt-8 flex flex-col text-sm pb-3 pt-3 text-stone-400 sticky bottom-0 bg-stone-950">
98
+ <a class="sm:hidden py-1" href="{{ url('admin:search') }}">Global search</a>
99
+ <div class="flex items-center justify-between">
100
+ <div class="flex items-center truncate">
101
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-6 h-6 mr-1.5 bi bi-person-circle" viewBox="0 0 16 16">
102
+ <path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
103
+ <path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
104
+ </svg>
105
+ <span class="truncate">
106
+ {{ request.user }}
107
+ </span>
108
+ </div>
109
+ <a class="ml-2 hover:text-white hover:underline flex-shrink-0" href="{{ url('logout') }}">Log out</a>
111
110
  </div>
112
- <a class="ml-2 hover:text-white hover:underline flex-shrink-0" href="{{ url('logout') }}">Log out</a>
113
111
  </div>
114
112
  </aside>
115
113
 
116
114
  <div id="admin-content" data-toggle-class="x" class="absolute top-0 bottom-0 right-0 left-0 lg:left-52 bg-stone-900 border border-white/5 text-white rounded-lg overflow-auto">
117
- <div class="flex items-center justify-between px-4 lg:px-8 sticky z-10 top-0 bg-stone-900 border-b border-white/10 py-2 lg:py-3">
118
- <div>
115
+ <div class="flex space-x-3 items-center justify-between px-4 lg:px-8 sticky z-10 top-0 left-0 right-0 bg-stone-900 border-b border-white/10 py-2 lg:py-3">
116
+ <div class="flex-shrink-0">
119
117
  {% block header %}
120
118
  <div class="flex items-center">
121
119
  {% block image %}
@@ -124,7 +122,7 @@
124
122
  {% endif %}
125
123
  {% endblock %}
126
124
  <div class="max-w-prose break-all">
127
- <h1 class="text-xl text-white/90">
125
+ <h1 class="sm:text-xl text-white/90">
128
126
  {% block title %}{{ title }}{% endblock %}
129
127
  </h1>
130
128
  {% if description %}<p class="mt-1 text-sm text-gray-500">{{ description }}</p>{% endif %}
@@ -132,7 +130,8 @@
132
130
  </div>
133
131
  {% endblock %}
134
132
  </div>
135
- <div class="flex space-x-2 text-sm actions">
133
+ <div class="flex space-x-2 text-sm actions overflow-auto p-1">
134
+ {# Styled with admin.css for easier adding of elements #}
136
135
  {% block actions %}{% endblock %}
137
136
  {% for link, url in links.items() %}
138
137
  <a href="{{ url }}">{{ link }}</a>
@@ -150,7 +149,7 @@
150
149
  </div>
151
150
  {% endif %}
152
151
 
153
- <main class="px-4 py-6 lg:px-8 text-white/70">{% block content %}{% endblock %}</main>
152
+ <main class="px-4 py-6 lg:px-8 text-white/80">{% block content %}{% endblock %}</main>
154
153
  </div>
155
154
 
156
155
  </div>
@@ -9,10 +9,10 @@
9
9
  Toggle raw values
10
10
  </button>
11
11
 
12
- <dl class="text-sm mt-3 w-full grid grid-cols-[max-content,1fr] gap-y-2 gap-x-8" style="grid-template-columns: max-content 1fr;">
12
+ <dl class="text-sm mt-3 w-full grid !grid-cols-1 sm:!grid-cols-[max-content,1fr] sm:gap-y-2 gap-x-8" style="grid-template-columns: max-content 1fr;">
13
13
  {% for field in fields %}
14
- <dt class="font-semibold"><code>{{ field }}</code></dt>
15
- <dd class="flex items-center">
14
+ <dt class="font-medium mt-4 sm:mt-0"><code>{{ field }}</code></dt>
15
+ <dd class="flex items-center mt-1 sm:mt-0">
16
16
  {% with value=get_field_value(object, field) %}
17
17
  <div class="raw-value hidden"><code>{{ value }}</code></div>
18
18
  <div class="pretty-value">{% include get_field_value_template(object, field, value) with context %}</div>
@@ -0,0 +1,49 @@
1
+ {% extends "admin/base.html" %}
2
+
3
+ {% block title %}
4
+ {%- if global_search_query -%}
5
+ Search results for "{{ global_search_query }}"
6
+ {%- else -%}
7
+ Search
8
+ {%- endif -%}
9
+ {% endblock %}
10
+
11
+ {% block content %}
12
+
13
+ {# Mobile search bar #}
14
+ <div class="sm:hidden">
15
+ <form method="GET" action="{{ url('admin:search') }}" class="">
16
+ <div class="relative">
17
+ <label for="query" class="sr-only">Search</label>
18
+ <input
19
+ type="text"
20
+ name="query"
21
+ id="query"
22
+ class="block w-full pr-10 pl-10 placeholder:text-center border-gray-200/10 text-white rounded-md focus:border-blue-500 focus:ring-blue-500 bg-white/5 py-1"
23
+ placeholder="Search everything"
24
+ value="{{ global_search_query|default('') }}"
25
+ >
26
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
27
+ <svg class="h-3.5 w-3.5 text-gray-400" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
28
+ <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"></path>
29
+ </svg>
30
+ </div>
31
+ </div>
32
+ </form>
33
+ </div>
34
+
35
+ {% if global_search_query %}
36
+ <div class="*:mt-14 *:empty:mt-0">
37
+ {% for view in searchable_views %}
38
+ <div
39
+ hx-get="{{ view.get_view_url() }}?search={{ global_search_query }}&page_size=5"
40
+ hx-trigger="plainhtmx:load from:body"
41
+ plain-hx-fragment="list">
42
+ </div>
43
+ {% endfor %}
44
+ </div>
45
+ {% else %}
46
+ <p class="text-stone-500 hidden sm:inline">Enter a search query in the top bar</p>
47
+ {% endif %}
48
+
49
+ {% endblock %}
@@ -2,6 +2,7 @@
2
2
  <div class="flex items-center space-x-2">
3
3
  <admin.Checkbox field=field />
4
4
  <admin.Label field=field>{{ label }}</admin.Label>
5
+ {% if help is defined %}<admin.Help help=help />{% endif %}
5
6
  </div>
6
7
  <admin.FieldErrors field=field />
7
8
  </div>
@@ -0,0 +1 @@
1
+ <p class="mt-2 text-sm text-gray-500">{{ help }}</p>
@@ -1,5 +1,6 @@
1
1
  <div class="space-y-1">
2
2
  <admin.Label field=field>{{ label }}</admin.Label>
3
3
  <admin.Input field=field />
4
+ {% if help is defined %}<admin.Help help=help />{% endif %}
4
5
  <admin.FieldErrors field=field />
5
6
  </div>
@@ -0,0 +1,4 @@
1
+ <label for="{{ field.html_id }}" class="block flex items-baseline justify-between">
2
+ <div class="text-sm font-medium">{{ caller() }}</div>
3
+ {% if not field.field.required %}<div class="text-white/60 text-xs">Optional</div>{% endif %}
4
+ </label>
@@ -1,5 +1,6 @@
1
1
  <div class="space-y-1">
2
2
  <admin.Label field=field>{{ label }}</admin.Label>
3
3
  <admin.Select field=field />
4
+ {% if help is defined %}<admin.Help help=help />{% endif %}
4
5
  <admin.FieldErrors field=field />
5
6
  </div>
@@ -0,0 +1,7 @@
1
+ <textarea
2
+ id="{{ field.html_id }}"
3
+ name="{{ field.html_name }}"
4
+ class="block w-full text-sm border rounded border-white/10 bg-white/5"
5
+ rows="{{ rows|default(3) }}"
6
+ {% if field.field.required %}required{% endif %}
7
+ >{{ field.value() or "" }}</textarea>
@@ -0,0 +1,6 @@
1
+ <div class="space-y-1">
2
+ <admin.Label field=field>{{ label }}</admin.Label>
3
+ <admin.Textarea field=field />
4
+ {% if help is defined %}<admin.Help help=help />{% endif %}
5
+ <admin.FieldErrors field=field />
6
+ </div>
@@ -1,15 +1,10 @@
1
1
  {% if toolbar.should_render() %}
2
2
  {% set exception=toolbar.request_exception() %}
3
3
  <script src="{{ asset('toolbar/toolbar.js') }}"></script>
4
- <div id="plaintoolbar" class="print:hidden text-sm py-1.5 text-stone-300 bg-stone-950 fixed bottom-3 mx-3 max-w-full drop-shadow-md z-30 rounded-2xl lg:flex lg:flex-col -translate-x-1/2 left-1/2 max-h-[90vh]">
5
- <div class="flex justify-between px-4 mx-auto space-x-4">
4
+ <div id="plaintoolbar" class="print:hidden text-sm py-1.5 text-stone-300 fixed bottom-3 mx-3 max-w-full drop-shadow-sm z-30 ring-1 ring-stone-200/5 rounded-2xl lg:flex lg:flex-col -translate-x-1/2 left-1/2 max-h-[90vh] bg-gradient-to-b from-stone-950/90 to-stone-950/95 backdrop-blur-sm">
5
+ <div class="flex justify-between px-3 mx-auto space-x-4">
6
6
  <div class="flex items-center">
7
- <svg class="h-4 w-4" width="160" height="125" viewBox="0 0 160 125" fill="none" xmlns="http://www.w3.org/2000/svg">
8
- <rect x="4.78467" y="4.79785" width="150.978" height="115.404" rx="5" stroke="#ffffff" stroke-width="8"/>
9
- <path d="M151.762 60.3705C99.2596 39.3233 80.202 66.8232 8.78467 60.3705V116.2H151.762V60.3705Z" fill="#ffffff"/>
10
- <path d="M51.104 8.08887H108.179V10.7668C108.179 12.6998 106.612 14.2668 104.679 14.2668H54.604C52.671 14.2668 51.104 12.6998 51.104 10.7668V8.08887Z" fill="#ffffff" stroke="#ffffff"/>
11
- </svg>
12
- <code class="ml-2 text-xs whitespace-nowrap text-mono">{{ toolbar.version }}</code>
7
+ <code class="ml-1.5 text-xs whitespace-nowrap text-mono">{{ toolbar.version }}</code>
13
8
 
14
9
  {% if request.impersonator is defined %}
15
10
  <div class="flex items-center ml-1 font-light">
@@ -10,7 +10,6 @@ from .views.registry import registry
10
10
  class AdminIndexView(AdminView):
11
11
  template_name = "admin/index.html"
12
12
  title = "Dashboard"
13
- slug = ""
14
13
 
15
14
  def get(self):
16
15
  # Slight hack to redirect to the first view that doesn't
@@ -24,7 +23,6 @@ class AdminIndexView(AdminView):
24
23
  class AdminSearchView(AdminView):
25
24
  template_name = "admin/search.html"
26
25
  title = "Search"
27
- slug = "search"
28
26
 
29
27
  def get_template_context(self):
30
28
  context = super().get_template_context()
@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
3
3
  from plain.auth.views import AuthViewMixin
4
4
  from plain.urls import reverse
5
5
  from plain.utils import timezone
6
- from plain.utils.text import slugify
7
6
  from plain.views import (
8
7
  TemplateView,
9
8
  )
@@ -22,7 +21,6 @@ class AdminView(AuthViewMixin, TemplateView):
22
21
  admin_required = True
23
22
 
24
23
  title: str = ""
25
- slug: str = ""
26
24
  path: str = ""
27
25
  description: str = ""
28
26
  image: Img | None = None
@@ -69,15 +67,7 @@ class AdminView(AuthViewMixin, TemplateView):
69
67
 
70
68
  @classmethod
71
69
  def get_slug(cls) -> str:
72
- if cls.slug:
73
- return cls.slug
74
-
75
- if cls.title:
76
- return slugify(cls.title)
77
-
78
- raise NotImplementedError(
79
- f"Please set a slug on the {cls} class or implement get_slug()."
80
- )
70
+ return f"{cls.__module__}.{cls.__name__}".lower().replace(".", "_")
81
71
 
82
72
  # Can actually use @classmethod, @staticmethod or regular method for these?
83
73
  def get_title(self) -> str:
@@ -91,15 +81,7 @@ class AdminView(AuthViewMixin, TemplateView):
91
81
 
92
82
  @classmethod
93
83
  def get_path(cls) -> str:
94
- if cls.path:
95
- return cls.path
96
-
97
- if slug := cls.get_slug():
98
- return slug
99
-
100
- raise NotImplementedError(
101
- f"Please set a path on the {cls} class or implement get_slug() or get_path()."
102
- )
84
+ return cls.path
103
85
 
104
86
  @classmethod
105
87
  def get_parent_view_classes(cls) -> list["AdminView"]:
@@ -68,8 +68,11 @@ class AdminModelListView(AdminListView):
68
68
  return cls.model._meta.model_name.capitalize() + "s"
69
69
 
70
70
  @classmethod
71
- def get_slug(cls) -> str:
72
- return cls.model._meta.model_name
71
+ def get_path(cls) -> str:
72
+ if path := super().get_path():
73
+ return path
74
+
75
+ return f"{cls.model._meta.model_name}/"
73
76
 
74
77
  def get_template_context(self):
75
78
  context = super().get_template_context()
@@ -137,12 +140,11 @@ class AdminModelDetailView(AdminDetailView):
137
140
  def get_title(self) -> str:
138
141
  return str(self.object)
139
142
 
140
- @classmethod
141
- def get_slug(cls) -> str:
142
- return f"{cls.model._meta.model_name}_detail"
143
-
144
143
  @classmethod
145
144
  def get_path(cls) -> str:
145
+ if path := super().get_path():
146
+ return path
147
+
146
148
  return f"{cls.model._meta.model_name}/<int:pk>/"
147
149
 
148
150
  def get_fields(self):
@@ -182,12 +184,11 @@ class AdminModelCreateView(AdminCreateView):
182
184
 
183
185
  return f"New {self.model._meta.model_name}"
184
186
 
185
- @classmethod
186
- def get_slug(cls) -> str:
187
- return f"{cls.model._meta.model_name}_create"
188
-
189
187
  @classmethod
190
188
  def get_path(cls) -> str:
189
+ if path := super().get_path():
190
+ return path
191
+
191
192
  return f"{cls.model._meta.model_name}/create/"
192
193
 
193
194
  def get_template_names(self):
@@ -213,12 +214,11 @@ class AdminModelUpdateView(AdminUpdateView):
213
214
 
214
215
  return f"Update {self.object}"
215
216
 
216
- @classmethod
217
- def get_slug(cls) -> str:
218
- return f"{cls.model._meta.model_name}_update"
219
-
220
217
  @classmethod
221
218
  def get_path(cls) -> str:
219
+ if path := super().get_path():
220
+ return path
221
+
222
222
  return f"{cls.model._meta.model_name}/<int:pk>/update/"
223
223
 
224
224
  def get_object(self):
@@ -242,12 +242,11 @@ class AdminModelDeleteView(AdminDeleteView):
242
242
  def get_title(self) -> str:
243
243
  return f"Delete {self.object}"
244
244
 
245
- @classmethod
246
- def get_slug(cls) -> str:
247
- return f"{cls.model._meta.model_name}_delete"
248
-
249
245
  @classmethod
250
246
  def get_path(cls) -> str:
247
+ if path := super().get_path():
248
+ return path
249
+
251
250
  return f"{cls.model._meta.model_name}/<int:pk>/delete/"
252
251
 
253
252
  def get_object(self):
@@ -67,18 +67,26 @@ class AdminViewRegistry:
67
67
  def get_urls(self):
68
68
  urls = []
69
69
 
70
- paths_seen = set()
71
-
72
- def add_view_path(view, _path):
73
- if _path in paths_seen:
74
- raise ValueError(f"Path {_path} already registered")
75
- paths_seen.add(_path)
76
- if not _path.endswith("/"):
77
- _path += "/"
78
- urls.append(path(_path, view, name=view.view_name()))
70
+ paths_seen = {}
79
71
 
80
72
  for view in self.registered_views:
81
- add_view_path(view, f"p/{view.get_path()}")
73
+ view_path = view.get_path()
74
+
75
+ if not view_path:
76
+ raise ValueError(f"Path for {view} is empty")
77
+
78
+ if existing_view := paths_seen.get(view_path, None):
79
+ raise ValueError(
80
+ f"Duplicate admin path {view_path}\n{existing_view}\n{view}"
81
+ )
82
+
83
+ paths_seen[view_path] = view
84
+
85
+ # Append trailing slashes automatically
86
+ if not view_path.endswith("/"):
87
+ view_path += "/"
88
+
89
+ urls.append(path(f"p/{view_path}", view, name=view.view_name()))
82
90
 
83
91
  return urls
84
92
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain.admin"
3
- version = "0.17.0"
3
+ version = "0.19.0"
4
4
  description = "Admin dashboard and tools for Plain."
5
5
  authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
6
  license = "BSD-3-Clause"
@@ -1,13 +0,0 @@
1
- {% extends "admin/base.html" %}
2
-
3
- {% block content %}
4
-
5
- <form method="post">
6
- {{ csrf_input }}
7
- {% block form_content %}{% endblock %}
8
- <button class="px-5 py-2 mt-6 text-white bg-blue-600 rounded hover:bg-blue-700" type="submit">
9
- Save
10
- </button>
11
- </form>
12
-
13
- {% endblock %}
@@ -1,27 +0,0 @@
1
- {% extends "admin/base.html" %}
2
-
3
- {% block title %}
4
- {%- if global_search_query -%}
5
- Search results for "{{ global_search_query }}"
6
- {%- else -%}
7
- Search
8
- {%- endif -%}
9
- {% endblock %}
10
-
11
- {% block content %}
12
-
13
- {% if global_search_query %}
14
- <div class="*:mt-14 *:empty:mt-0">
15
- {% for view in searchable_views %}
16
- <div
17
- hx-get="{{ view.get_view_url() }}?search={{ global_search_query }}&page_size=5"
18
- hx-trigger="plainhtmx:load from:body"
19
- plain-hx-fragment="list">
20
- </div>
21
- {% endfor %}
22
- </div>
23
- {% else %}
24
- <p class="text-stone-500">Enter a search query in the top bar</p>
25
- {% endif %}
26
-
27
- {% endblock %}
@@ -1,3 +0,0 @@
1
- <label for="{{ field.html_id }}" class="block text-sm font-medium">
2
- {{ caller() }}
3
- </label>
File without changes
File without changes
File without changes