Multiple ModelAdmins for one Wagtail model
Question:
Let’s say I have a model:
class BlogPost(Page):
date = models.DateField("Post date")
intro = models.CharField(max_length=250)
body = StreamField([
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
('gallery', CarouselBlock()),
('video', EmbedBlock()),
])
...
And I would like to create multiple pages in the sidebar that relate to this model. I tried an approach like this:
class BlogPostAdmin(ModelAdmin):
model = BlogPost
...
class DraftPostAdmin(ModelAdmin):
model = BlogPost
#query for drafts
...
class ScheduledPostAdmin(ModelAdmin):
model = BlogPost
#query for scheduled posts
...
class BlogGroup(ModelAdminGroup):
menu_label = 'Blog'
items = (BookAdmin, AuthorAdmin, GenreAdmin)
...
modeladmin_register(BlogGroup)
But the issue is that all of the pages show model instances that match the queryset for the first ModelAdmin. What is the best way to go about implementing multiple menu items to manage different aspects of one model in Wagtail?
Answers:
Use a proxy model and then define an appropriate manager for each proxy model. I have this working in an existing Wagtail-based application where I define proxy models for various states of Memberships in a membership application. In my case the base model is Member
, but then I have CurrentMember
, NonCurrentMember
, etc. This comment and related discussion might also be of interest.
Here is a slightly different approach, by overriding some of the methods on your BlogGroup
and using only a single BlogPostAdmin
you can get pretty far.
Code
from wagtail.admin.menu import MenuItem
from wagtail.contrib.modeladmin.options import (
ModelAdmin, ModelAdminGroup, modeladmin_register)
from bakerydemo.blog.models import BlogPage
class ModelAdminQueryMenuItem(MenuItem):
# based on the `ModelAdminMenuItem` but extends the Wagtail Admin `MenuItem` directly.
def __init__(self, model_admin, order, query, label_append=''):
self.model_admin = model_admin
url = model_admin.url_helper.index_url + "?" + query
classnames = 'icon icon-%s' % model_admin.get_menu_icon()
super().__init__(
label=model_admin.get_menu_label() + label_append,
url=url,
classnames=classnames,
order=order
)
def is_shown(self, request):
return self.model_admin.permission_helper.user_can_list(request.user)
class BlogPageAdmin(ModelAdmin):
model = BlogPage
def get_menu_items(self, order=None):
# new class method that builds a list of menu_item(s)
menu_items = []
## build 'live only' (no unpublished changes) items
live_menu_item = ModelAdminQueryMenuItem(self, order or self.get_menu_order(), query='has_unpublished_changes=False', label_append=' (Live)')
menu_items.append(live_menu_item)
## build 'draft' items
draft_menu_item = ModelAdminQueryMenuItem(self, order or self.get_menu_order(), query='has_unpublished_changes=True', label_append=' (Draft)')
menu_items.append(draft_menu_item)
return menu_items
def get_queryset(self, request):
qs = super().get_queryset(request)
## read the request and modify the qs as needed if query param does not work easily
return qs
class BlogGroup(ModelAdminGroup):
menu_label = 'Blog'
items = (BlogPageAdmin, )
def get_submenu_items(self):
menu_items = []
item_order = 1
for modeladmin in self.modeladmin_instances:
menu_items = menu_items + modeladmin.get_menu_items(item_order)
item_order = len(menu_items) + 1
return menu_items
modeladmin_register(BlogGroup)
Explanation
- Set up a single
BlogPageAdmin
class, this will have a new method on it get_menu_items
(plural), which will return a list of Wagtail Menu Items.
- We generate these menu items by overriding the Wagtail admin class
MenuItem
but we also add a user permissions helper, see the menus.py helper file within modeladmin. This provides us a way to customise the URL (adding url params) and the label, this can be modified as needed to get our sub-menu items presenting the way we want.
get_menu_items
manually builds up ALL the menu items we want, we can customise anything here, including label and icon to show what you want, essentially though this just provides a way to pass in a custom query params string to the url that gets built up.
- The query param can be anything we want, a search string and many of the fields on
Page
can be used without any code changes as they just get passed in as a Dict into our query. Below I have put all the possible values.
- For anything else more specific, we will need to override the method
get_queryset
and read the request params and update our query accordingly.
- You will need to customise the View’s page title but this can be done by reading the request in the view template customisation or by further customising the url that gets built up.
- Finally, in our
BlogGroup
we override the get_submenu_items
to use the custom method get_menu_items
. Note that this assumes EACH model passed in to this group will have that method.
Query String Fields
- Choices are: blog_person_relationship, body, content_type, content_type_id, date_published, depth, draft_title, expire_at, expired, first_published_at, formsubmission, go_live_at, group_permissions, has_unpublished_changes, id, image, image_id, introduction, last_published_at, latest_revision_created_at, live, live_revision, live_revision_id, locked, numchild, owner, owner_id, page_ptr, page_ptr_id, path, redirect, revisions, search_description, searchpromotion, seo_title, show_in_menus, sites_rooted_here, slug, subtitle, tagged_items, tags, title, url_path, view_restrictions
Agree that ProxyModels are the most obvious route…
ModelAdmin.get_queryset() can filter the listview and it’s referenced in some of the answers. adding a method like this to your modelAdmin
class DraftPostAdmin(ModelAdmin):
model = BlogPost
def get_queryset(self, request):
return super().get_queryset().filter(published_status='Draft')
Let’s say I have a model:
class BlogPost(Page):
date = models.DateField("Post date")
intro = models.CharField(max_length=250)
body = StreamField([
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
('gallery', CarouselBlock()),
('video', EmbedBlock()),
])
...
And I would like to create multiple pages in the sidebar that relate to this model. I tried an approach like this:
class BlogPostAdmin(ModelAdmin):
model = BlogPost
...
class DraftPostAdmin(ModelAdmin):
model = BlogPost
#query for drafts
...
class ScheduledPostAdmin(ModelAdmin):
model = BlogPost
#query for scheduled posts
...
class BlogGroup(ModelAdminGroup):
menu_label = 'Blog'
items = (BookAdmin, AuthorAdmin, GenreAdmin)
...
modeladmin_register(BlogGroup)
But the issue is that all of the pages show model instances that match the queryset for the first ModelAdmin. What is the best way to go about implementing multiple menu items to manage different aspects of one model in Wagtail?
Use a proxy model and then define an appropriate manager for each proxy model. I have this working in an existing Wagtail-based application where I define proxy models for various states of Memberships in a membership application. In my case the base model is Member
, but then I have CurrentMember
, NonCurrentMember
, etc. This comment and related discussion might also be of interest.
Here is a slightly different approach, by overriding some of the methods on your BlogGroup
and using only a single BlogPostAdmin
you can get pretty far.
Code
from wagtail.admin.menu import MenuItem
from wagtail.contrib.modeladmin.options import (
ModelAdmin, ModelAdminGroup, modeladmin_register)
from bakerydemo.blog.models import BlogPage
class ModelAdminQueryMenuItem(MenuItem):
# based on the `ModelAdminMenuItem` but extends the Wagtail Admin `MenuItem` directly.
def __init__(self, model_admin, order, query, label_append=''):
self.model_admin = model_admin
url = model_admin.url_helper.index_url + "?" + query
classnames = 'icon icon-%s' % model_admin.get_menu_icon()
super().__init__(
label=model_admin.get_menu_label() + label_append,
url=url,
classnames=classnames,
order=order
)
def is_shown(self, request):
return self.model_admin.permission_helper.user_can_list(request.user)
class BlogPageAdmin(ModelAdmin):
model = BlogPage
def get_menu_items(self, order=None):
# new class method that builds a list of menu_item(s)
menu_items = []
## build 'live only' (no unpublished changes) items
live_menu_item = ModelAdminQueryMenuItem(self, order or self.get_menu_order(), query='has_unpublished_changes=False', label_append=' (Live)')
menu_items.append(live_menu_item)
## build 'draft' items
draft_menu_item = ModelAdminQueryMenuItem(self, order or self.get_menu_order(), query='has_unpublished_changes=True', label_append=' (Draft)')
menu_items.append(draft_menu_item)
return menu_items
def get_queryset(self, request):
qs = super().get_queryset(request)
## read the request and modify the qs as needed if query param does not work easily
return qs
class BlogGroup(ModelAdminGroup):
menu_label = 'Blog'
items = (BlogPageAdmin, )
def get_submenu_items(self):
menu_items = []
item_order = 1
for modeladmin in self.modeladmin_instances:
menu_items = menu_items + modeladmin.get_menu_items(item_order)
item_order = len(menu_items) + 1
return menu_items
modeladmin_register(BlogGroup)
Explanation
- Set up a single
BlogPageAdmin
class, this will have a new method on itget_menu_items
(plural), which will return a list of Wagtail Menu Items. - We generate these menu items by overriding the Wagtail admin class
MenuItem
but we also add a user permissions helper, see the menus.py helper file within modeladmin. This provides us a way to customise the URL (adding url params) and the label, this can be modified as needed to get our sub-menu items presenting the way we want. get_menu_items
manually builds up ALL the menu items we want, we can customise anything here, including label and icon to show what you want, essentially though this just provides a way to pass in a custom query params string to the url that gets built up.- The query param can be anything we want, a search string and many of the fields on
Page
can be used without any code changes as they just get passed in as a Dict into our query. Below I have put all the possible values. - For anything else more specific, we will need to override the method
get_queryset
and read the request params and update our query accordingly. - You will need to customise the View’s page title but this can be done by reading the request in the view template customisation or by further customising the url that gets built up.
- Finally, in our
BlogGroup
we override theget_submenu_items
to use the custom methodget_menu_items
. Note that this assumes EACH model passed in to this group will have that method.
Query String Fields
- Choices are: blog_person_relationship, body, content_type, content_type_id, date_published, depth, draft_title, expire_at, expired, first_published_at, formsubmission, go_live_at, group_permissions, has_unpublished_changes, id, image, image_id, introduction, last_published_at, latest_revision_created_at, live, live_revision, live_revision_id, locked, numchild, owner, owner_id, page_ptr, page_ptr_id, path, redirect, revisions, search_description, searchpromotion, seo_title, show_in_menus, sites_rooted_here, slug, subtitle, tagged_items, tags, title, url_path, view_restrictions
Agree that ProxyModels are the most obvious route…
ModelAdmin.get_queryset() can filter the listview and it’s referenced in some of the answers. adding a method like this to your modelAdmin
class DraftPostAdmin(ModelAdmin):
model = BlogPost
def get_queryset(self, request):
return super().get_queryset().filter(published_status='Draft')