The most common use for adding custom views to the Wagtail admin is to provide an interface for managing a Django model. Using [](snippets), Wagtail provides ready-made views for listing, creating, and editing Django models with minimal configuration.
For other kinds of admin views that don't fit this pattern, you can write your own Django views and register them as part of the Wagtail admin through [hooks](admin_hooks). In this example, we'll implement a view that displays a calendar for the current year, using [the calendar module](https://docs.python.org/3/library/calendar.html) from Python's standard library.
Within a Wagtail project, create a new `wagtailcalendar` app with `./manage.py startapp wagtailcalendar` and add it to your project's `INSTALLED_APPS`. (In this case, we're using the name 'wagtailcalendar' to avoid clashing with the standard library's `calendar` module - in general, there is no need to use a 'wagtail' prefix.)
At this point, the standard practice for a Django project would be to add a URL route for this view to your project's top-level URL config module. However, in this case, we want the view to only be available to logged-in users, and to appear within the `/admin/` URL namespace which is managed by Wagtail. This is done through the [Register Admin URLs](register_admin_urls) hook.
On startup, Wagtail looks for a `wagtail_hooks` submodule within each installed app. In this submodule, you can define functions to be run at various points in Wagtail's operation, such as building the URL config for the admin and constructing the main menu.
Currently, this view is outputting a plain HTML fragment. Let's insert this into the usual Wagtail admin page furniture, by creating a template that extends Wagtail's base template `"wagtailadmin/base.html"`.
{% include "wagtailadmin/shared/header.html" with title="Calendar" icon="date" %}
<divclass="nice-padding">
{{ calendar_html|safe }}
</div>
{% endblock %}
```
Here we are overriding three of the blocks defined in the base template: `titletag` (which sets the content of the HTML `<title>` tag), `extra_css` (which allows us to provide additional CSS styles specific to this page), and `content` (for the main content area of the page). We're also including the standard header bar component, and setting a title and icon. For a list of the recognised icon identifiers, see the [style guide](styleguide).
Revisiting `/admin/calendar/` will now show the calendar within the Wagtail admin page furniture.
![A calendar, shown within the Wagtail admin interface](../_static/images/adminviews_calendar_template.png)
## Adding a menu item
Our calendar view is now complete, but there's no way to reach it from the rest of the admin backend. To add an item to the sidebar menu, we'll use another hook, [Register Admin Menu Item](register_admin_menu_item). Update `wagtail_hooks.py` as follows:
Finally we can alter our `wagtail_hooks.py` to include a group of custom menu items. This is similar to adding a single item but involves importing two more classes, `Menu` and `SubmenuMenuItem`.
![Wagtail admin sidebar menu, showing an expanded "Calendar" group menu item with a date icon, showing two child menu items, 'Calendar' and 'Month'.](../_static/images/adminviews_menu_group_expanded.png)
Wagtail provides a {class}`~wagtail.admin.viewsets.base.ViewSet` class that combines the registration of views and the associated menu item into a single class. For example, you can group the calendar views from the previous example into a single menu item by creating a `ViewSet` subclass in `views.py`:
```{code-block} python
from wagtail.admin.viewsets.base import ViewSet
...
class CalendarViewSet(ViewSet):
add_to_admin_menu = True
menu_label = "Calendar"
icon = "date"
# The `name` will be used for both the URL prefix and the URL namespace.
# They can be customised individually via `url_prefix` and `url_namespace`.
name = "calendar"
def get_urlpatterns(self):
return [
# This can be accessed at `/admin/calendar/`
# and reverse-resolved with the name `calendar:index`.
# This first URL will be used for the menu item, but it can be
# customised by overriding the `menu_url` property.
path('', index, name='index'),
# This can be accessed at `/admin/calendar/month/`
# and reverse-resolved with the name `calendar:month`.
path('month/', month, name='month'),
]
```
Then, remove the `register_admin_urls` and `register_admin_menu_item` hooks in `wagtail_hooks.py` in favor of registering the `ViewSet` subclass with the [`register_admin_viewset`](register_admin_viewset) hook:
```{code-block} python
from .views import CalendarViewSet
@hooks.register("register_admin_viewset")
def register_viewset():
return CalendarViewSet()
```
Compared to the previous example with the two separate hooks, this will result in a single menu item "Calendar" that takes you to the `/admin/calendar/` URL. The second URL will not have its own menu item, but it will still be accessible at `/admin/calendar/month/`. This is useful for grouping related views together, that may not necessarily need their own menu items.
For further customisations, refer to the {class}`~wagtail.admin.viewsets.base.ViewSet` documentation.
```{versionadded} 5.2
Support for registering a menu item in the base `ViewSet` class was added.
```
(using_base_viewsetgroup)=
## Combining multiple `ViewSet`s using a `ViewSetGroup`
The {class}`~wagtail.admin.viewsets.base.ViewSetGroup` class can be used to group multiple `ViewSet`s inside a top-level menu item. For example, if you have a different viewset e.g. `EventViewSet` that you want to group with the `CalendarViewSet` from the previous example, you can do so by creating a `ViewSetGroup` subclass in `views.py`:
```{code-block} python
from wagtail.admin.viewsets.base import ViewSetGroup
...
class AgendaViewSetGroup(ViewSetGroup):
menu_label = "Agenda"
menu_icon = "table"
# You can specify instances or subclasses of `ViewSet` in `items`.
items = (CalendarViewSet(), EventViewSet)
```
Then, remove `add_to_admin_menu` from the viewsets and update the `register_admin_viewset` hook in `wagtail_hooks.py` to register the `ViewSetGroup` instead of the individual viewsets:
```{code-block} python
from .views import AgendaViewSetGroup
@hooks.register("register_admin_viewset")
def register_viewset():
return AgendaViewSetGroup()
```
This will result in a top-level menu item "Agenda" with the two viewsets' menu items as sub-items, e.g. "Calendar" and "Events".
For further customisations, refer to the {class}`~wagtail.admin.viewsets.base.ViewSetGroup` documentation.