Skip to content

Commit c4a8fe2

Browse files
committed
Redo admin model viewsets with inner classes
1 parent 64e00a3 commit c4a8fe2

File tree

9 files changed

+167
-123
lines changed

9 files changed

+167
-123
lines changed

bolt-admin/bolt/admin/__init__.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1-
from .views.base import AdminListView, AdminPageView
2-
from .views.models import AdminModelViewset
1+
from .views.base import (
2+
AdminDeleteView,
3+
AdminDetailView,
4+
AdminListView,
5+
AdminPageView,
6+
AdminUpdateView,
7+
)
8+
from .views.models import (
9+
AdminModelDetailView,
10+
AdminModelListView,
11+
AdminModelUpdateView,
12+
AdminModelViewset,
13+
)
314
from .views.registry import (
415
register_dashboard,
516
register_view,
@@ -9,7 +20,13 @@
920
__all__ = [
1021
"AdminPageView",
1122
"AdminListView",
23+
"AdminDetailView",
24+
"AdminUpdateView",
25+
"AdminDeleteView",
1226
"AdminModelViewset",
27+
"AdminModelListView",
28+
"AdminModelDetailView",
29+
"AdminModelUpdateView",
1330
"register_viewset",
1431
"register_view",
1532
"register_dashboard",

bolt-admin/bolt/admin/assets/admin/admin.js

+4
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ jQuery(function($) {
1111
}
1212
});
1313
});
14+
15+
$("[data-autosubmit]").on("change", function(e) {
16+
$(this).closest("form").submit();
17+
});
1418
});

bolt-admin/bolt/admin/templates/admin/base.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
<div class="mt-4">
3030
{% for view in admin_registry.get_nav_views() %}
3131
<a class="flex items-center px-2 py-2 -mx-2 text-sm rounded hover:text-stone-300 text-stone-400 hover:bg-white/5" href="{{ view.get_absolute_url() }}">
32-
{{ view.title }}
32+
{{ view.get_title() }}
3333
</a>
3434
{% endfor %}
3535
</div>
@@ -39,7 +39,7 @@
3939
<div class="text-xs tracking-wide text-gray-500">Dashboards</div>
4040
{% for view in admin_registry.registered_dashboards %}
4141
<a class="flex items-center px-2 py-2 -mx-2 text-sm rounded hover:text-stone-300 text-stone-400 hover:bg-white/5" href="{{ view.get_absolute_url() }}">
42-
{{ view.title }}
42+
{{ view.get_title() }}
4343
</a>
4444
{% endfor %}
4545
</div>
@@ -75,7 +75,7 @@
7575
<a class="text-stone-500" href="{{ url ('admin:index') }}">Admin</a>
7676
{% for parent in parent_view_classes %}
7777
<span class="text-stone-400">/</span>
78-
<a class="text-stone-500" href="{{ parent.get_absolute_url() }}">{{ parent.title }}</a>
78+
<a class="text-stone-500" href="{{ parent.get_absolute_url() }}">{{ parent.get_title() }}</a>
7979
{% endfor %}
8080
<span class="text-stone-400">/</span>
8181
<a class="text-stone-600" href="{{ request.path }}">{{ title }}</a>

bolt-admin/bolt/admin/templates/admin/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ <h2>dashboards</h2>
88
<div class="grid grid-cols-4 gap-6 mt-4">
99
{% for view in dashboards %}
1010
<a class="p-4 border rounded" href="{{ view.get_absolute_url() }}">
11-
{{ view.title }}
11+
{{ view.get_title() }}
1212
</a>
1313
{% endfor %}
1414
</div>

bolt-admin/bolt/admin/templates/admin/list.html

+12-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,18 @@
1313
<div class="font-semibold">
1414
{{ title }}
1515
</div>
16-
<div>
16+
<div class="flex space-x-5">
17+
{% if list_filters %}
18+
<form method="GET" class="inline-flex">
19+
<select data-autosubmit name="filter" class="text-sm border-gray-200 rounded-md">
20+
<option value="">Filters</option>
21+
{% for filter in list_filters %}
22+
<option {% if filter == list_filter %}selected{% endif %}>{{ filter }}</option>
23+
{% endfor %}
24+
</select>
25+
</form>
26+
{% endif %}
27+
1728
{% if list_actions %}
1829
<form method="POST" data-actions-form>
1930
{{ csrf_input }}

bolt-admin/bolt/admin/views/base.py

+22-3
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class BaseAdminView(AuthViewMixin, TemplateView):
4242

4343
def get_context(self):
4444
context = super().get_context()
45-
context["title"] = self.title
45+
context["title"] = self.get_title()
4646
context["slug"] = self.get_slug()
4747
context["description"] = self.get_description()
4848
context["links"] = self.get_links()
@@ -53,9 +53,13 @@ def get_context(self):
5353
def view_name(cls) -> str:
5454
raise NotImplementedError
5555

56+
@classmethod
57+
def get_title(cls) -> str:
58+
return cls.title
59+
5660
@classmethod
5761
def get_slug(cls) -> str:
58-
return cls.slug or slugify(cls.title)
62+
return cls.slug or slugify(cls.get_title())
5963

6064
@classmethod
6165
def get_description(cls) -> str:
@@ -125,17 +129,27 @@ class AdminListView(AdminPageView):
125129
template_name = "admin/list.html"
126130
list_fields: list
127131
list_actions: dict[str] = {}
132+
list_filters: list[str] = []
128133
page_size = 100
129134
show_search = False
130135

131136
def get_context(self):
132137
context = super().get_context()
133-
context["paginator"] = Paginator(self.get_objects(), self.page_size)
138+
139+
list_filter = self.request.GET.get("filter", "")
140+
141+
objects = self.get_objects()
142+
objects = self.filter_objects(list_filter, objects)
143+
144+
context["paginator"] = Paginator(objects, self.page_size)
134145
context["page"] = context["paginator"].get_page(self.request.GET.get("page", 1))
135146
context["objects"] = context["page"] # alias
136147
context["list_fields"] = self.list_fields
137148
context["list_actions"] = self.list_actions
138149

150+
context["list_filters"] = self.list_filters
151+
context["list_filter"] = list_filter
152+
139153
# Implement search yourself in get_objects
140154
context["search_query"] = self.request.GET.get("search", "")
141155
context["show_search"] = self.show_search
@@ -162,6 +176,10 @@ def post(self) -> HttpResponse:
162176
def get_objects(self) -> list:
163177
return []
164178

179+
def filter_objects(self, filter_name: str, objects: list):
180+
"""Implement custom object filters here by looking at filter name"""
181+
return objects
182+
165183
def get_object_field(self, obj, field: str):
166184
# Try basic dict lookup first
167185
if field in obj:
@@ -200,6 +218,7 @@ def get_context(self):
200218
return context
201219

202220
def get_template_names(self) -> list[str]:
221+
# TODO move these to model views
203222
if not self.template_name and isinstance(self.object, models.Model):
204223
object_meta = self.object._meta
205224
return [

bolt-admin/bolt/admin/views/models.py

+69-93
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ class AdminModelListView(AdminListView):
4343
list_order = []
4444
search_fields: list = ["pk"]
4545

46+
@classmethod
47+
def get_title(cls) -> str:
48+
return cls.model._meta.verbose_name_plural.capitalize()
49+
50+
@classmethod
51+
def get_slug(cls) -> str:
52+
return cls.model._meta.model_name
53+
4654
def get_context(self):
4755
context = super().get_context()
4856

@@ -92,122 +100,90 @@ def get_object_field(self, object, field: str):
92100
return get_model_field(object, field)
93101

94102

95-
class AdminModelViewset:
103+
class AdminModelDetailView(AdminDetailView):
96104
model: "models.Model"
97-
list_description = ""
98-
list_fields: list = ["pk"]
99-
list_order = []
100-
detail_fields: list = []
101-
search_fields = ["pk"]
102-
103-
form_class = None # TODO type annotation
104-
105-
list_cards = []
106-
form_cards = []
105+
fields: list = []
107106

108107
@classmethod
109-
def get_list_view(cls) -> AdminModelListView:
110-
class V(AdminModelListView):
111-
model = cls.model
112-
title = cls.model._meta.verbose_name_plural.capitalize()
113-
description = cls.list_description
114-
slug = cls.model._meta.model_name
115-
list_fields = cls.list_fields
116-
list_order = cls.list_order
117-
cards = cls.list_cards
118-
search_fields = cls.search_fields
108+
def get_title(cls) -> str:
109+
return cls.model._meta.verbose_name.capitalize()
119110

120-
def get_update_url(self, object):
121-
update_view = cls.get_update_view()
111+
@classmethod
112+
def get_slug(cls) -> str:
113+
return f"{cls.model._meta.model_name}_detail"
122114

123-
if not update_view:
124-
return None
115+
@classmethod
116+
def get_path(cls) -> str:
117+
return f"{cls.model._meta.model_name}/<int:pk>/"
125118

126-
# TODO a way to do this without explicit namespace?
127-
return reverse_lazy(
128-
URL_NAMESPACE + ":" + update_view.view_name(),
129-
kwargs={"pk": object.pk},
130-
)
119+
def get_context(self):
120+
context = super().get_context()
121+
context["fields"] = self.fields or ["pk"] + [
122+
f.name for f in self.object._meta.get_fields() if not f.remote_field
123+
]
124+
return context
131125

132-
def get_detail_url(self, object):
133-
detail_view = cls.get_detail_view()
126+
def get_object_field(self, object, field: str):
127+
return get_model_field(object, field)
134128

135-
if not detail_view:
136-
return None
129+
def get_object(self):
130+
return self.model.objects.get(pk=self.url_kwargs["pk"])
137131

138-
return reverse_lazy(
139-
URL_NAMESPACE + ":" + detail_view.view_name(),
140-
kwargs={"pk": object.pk},
141-
)
132+
def get_template_names(self) -> list[str]:
133+
return super().get_template_names() + [
134+
"admin/detail.html",
135+
]
142136

143-
def get_initial_queryset(self):
144-
return cls.get_list_queryset(self)
145137

146-
return V
138+
class AdminModelUpdateView(AdminUpdateView):
139+
model: "models.Model"
140+
form_class = None # TODO type annotation
141+
success_url = "." # Redirect back to the same update page by default
147142

148143
@classmethod
149-
def get_update_view(cls) -> AdminUpdateView | None:
150-
if not cls.form_class:
151-
return None
144+
def get_title(cls) -> str:
145+
return f"Update {cls.model._meta.verbose_name}"
152146

153-
class V(AdminUpdateView):
154-
title = f"Update {cls.model._meta.verbose_name}"
155-
slug = f"{cls.model._meta.model_name}_update"
156-
form_class = cls.form_class
157-
path = f"{cls.model._meta.model_name}/<int:pk>/update/"
158-
cards = cls.form_cards
159-
success_url = "." # Redirect back to the same update page by default
160-
parent_view_class = cls.get_list_view()
147+
@classmethod
148+
def get_slug(cls) -> str:
149+
return f"{cls.model._meta.model_name}_update"
161150

162-
def get_object(self):
163-
return cls.model.objects.get(pk=self.url_kwargs["pk"])
151+
@classmethod
152+
def get_path(cls) -> str:
153+
return f"{cls.model._meta.model_name}/<int:pk>/update/"
164154

165-
return V
155+
def get_object(self):
156+
return self.model.objects.get(pk=self.url_kwargs["pk"])
166157

167-
@classmethod
168-
def get_detail_view(cls) -> AdminDetailView | None:
169-
class V(AdminDetailView):
170-
title = cls.model._meta.verbose_name.capitalize()
171-
slug = f"{cls.model._meta.model_name}_detail"
172-
path = f"{cls.model._meta.model_name}/<int:pk>/"
173-
parent_view_class = cls.get_list_view()
174-
fields = cls.detail_fields
175-
176-
def get_context(self):
177-
context = super().get_context()
178-
context["fields"] = self.fields or ["pk"] + [
179-
f.name for f in self.object._meta.get_fields() if not f.remote_field
180-
]
181-
return context
182-
183-
def get_object(self):
184-
return cls.model.objects.get(pk=self.url_kwargs["pk"])
185-
186-
def get_template_names(self) -> list[str]:
187-
return super().get_template_names() + [
188-
"admin/detail.html",
189-
]
190-
191-
def get_object_field(self, object, field: str):
192-
return get_model_field(object, field)
193-
194-
return V
195158

159+
class AdminModelViewset:
196160
@classmethod
197161
def get_views(cls) -> list["View"]:
198162
views = []
199163

200-
if list_view := cls.get_list_view():
201-
views.append(list_view)
164+
if hasattr(cls, "ListView") and hasattr(cls, "DetailView"):
165+
cls.ListView.get_detail_url = lambda self, obj: reverse_lazy(
166+
f"{URL_NAMESPACE}:{cls.DetailView.view_name()}",
167+
kwargs={"pk": obj.pk},
168+
)
202169

203-
if update_view := cls.get_update_view():
204-
views.append(update_view)
170+
cls.DetailView.parent_view_class = cls.ListView
205171

206-
if detail_view := cls.get_detail_view():
207-
views.append(detail_view)
172+
if hasattr(cls, "ListView") and hasattr(cls, "UpdateView"):
173+
cls.ListView.get_update_url = lambda self, obj: reverse_lazy(
174+
f"{URL_NAMESPACE}:{cls.UpdateView.view_name()}",
175+
kwargs={"pk": obj.pk},
176+
)
208177

209-
return views
178+
cls.UpdateView.parent_view_class = cls.ListView
210179

211-
def get_list_queryset(self):
212-
# Can't use super() with this the way it works now
213-
return self.model.objects.all()
180+
if hasattr(cls, "ListView"):
181+
views.append(cls.ListView)
182+
183+
if hasattr(cls, "DetailView"):
184+
views.append(cls.DetailView)
185+
186+
if hasattr(cls, "UpdateView"):
187+
views.append(cls.UpdateView)
188+
189+
return views

bolt-admin/bolt/admin/views/registry.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def get_nav_views(self):
4848

4949
sorted_views = sorted(
5050
[view for view in self.registered_views if view.should_show_in_nav()],
51-
key=lambda v: v.title,
51+
key=lambda v: v.get_title(),
5252
)
5353

5454
return sorted_views

0 commit comments

Comments
 (0)