Added in version 4.1.

How to create an admin class for a grouper model

What is a grouper model?

It’s an reusable abstract structural pattern, that is in django CMS used to separate language independent and language specific content.

django CMS defines grouper-content structure for Page-PageContent as follows:

  • The Page is the grouper model which represents base unit, that can have multiple content objects attached

  • The PageContent is the content model which represents page content that can be different for its grouping field - language in our case. It also includes the placeholders for the frontend editor.

This mechanism ensures that language-independent properties of a page, such as position in the page tree or permissions, are collected at the grouper model while language-specific content is collected in the content model.

Note

This pattern is relevant for django CMS Versioning since it versions the content objects and not the grouper objects.

To this end, if you want to create models that should be versionable like the PageContent of a Page objects you need to define a grouper and a content model.

Extra grouping fields define fields of the content model by which they are grouped: PageContent uses language as an extra grouping field. This means that one Page object can have multiple PageContent objects assign to which differ in their language.

If not extra grouping fields are given each grouper object can have at most one content object assigmed to it.

The language field is a typical (but not necessary) extra grouping field.

Administrating grouper models

To simplify creation of grouper content models, django CMS provides support for both the model admin class of the grouper model and the change and add forms of the content model.

In this scenario you will register a model admin for the grouper model and it will provide the user with the ability to view, change and add content objects, too. You will not necessarily need to add a model admin class for the content model at all (with the possible exception of a redirecting stub to allow third party apps to reverse admin views for the content model, too, see below).

To create a model admin class for a grouper model put the following code in your admin.py:

from cms.admin.utils import GrouperModelAdmin


class MyGrouperAdmin(GrouperModelAdmin):
    # Declare content model
    content_model = MyContent
    # Add language tabs to change and add views
    extra_grouping_fields = ("language",)
    # Add grouper and content fields to change list view
    # Add preview and settings action to change list view
    list_display = (
        "field_in_grouper_model",
        "content__field_in_content_model",
        "admin_list_actions",
    )

The property content_model defines which model is used as the content model. If you do not specify a content_model, django CMS will look for a model named like the grouper model but with “Content” appended. The default content model for Post would be PostContent.

The content model needs to have a foreign key pointing to the grouper model. The first foreign key found is assumed to be the field by which the content objects are assigned to their grouper objects. If you have multiple foreign keys to the grouper model, please specify content_related_field.

For this example there is only language as extra grouping field declared. You only have to proviude tuple of extra_grouping_fields if you have any.

Note

All fields serving as extra grouping fields must be part of the admin’s fieldsets setting for GrouperModelAdmin to work properly. In the change form the fields will be invisible.

Change list view

For the list display GrouperModelAdmin provides additional fields from the content model: content__{content_model_field_name}. Those fields can be used in list_display just as grouper model fields and will automatically show the content of the currently selected grouping fields.

Finally, GrouperModelAdmin provides two action buttons for each entry in the change list view:

  • to preview the content model in the frontend editor

  • to change the settings (i.e., go to the change view of the grouper object)

These are for convenience and appear as soon as admin_list_actions is added to the list_display attribute.

Example

This is an example (taken from django CMS alias) on how a grouper admin might look like:

from cms.admin.utils import GrouperModelAdmin


@admin.register(Alias)
class AliasAdmin(GrouperModelAdmin):
    list_display = ["content__name", "category", "admin_list_actions"]
    list_display_links = None  # With action buttons a link is not needed
    list_filter = (
        SiteFilter,
        CategoryFilter,
    )  # Custom filters
    fields = (
        "content__name",
        "category",
        "site",
        "content__language",
    )  # feeds into fieldsets
    readonly_fields = ("static_code",)
    form = AliasGrouperAdminForm  # Custom admin form
    extra_grouping_fields = ("language",)  # Language as grouping field
    EMPTY_CONTENT_VALUE = mark_safe(
        _("<i>Missing language</i>")
    )  # Label for missing content objects

Other extra grouping fields (besides language)

The standard templates of django CMS will work with language as an extra grouping field out of the box:

  • It creates a dropdown to switch languages for the admin’s change list view.

  • It creates tabs to switch languages for the admin’s change and add views.

To use other grouping fields you will have to do two things:

  1. You will need to supply templates for the change list view and the change and add views that render corresponding dropdowns or other ways of selecting which content is currently being viewed.

  2. You will need to provide context for the templates to render the valid choices.

Providing your own templates

To show a selector for your additional grouping field you need to overwrite both the change_list_template and change_form_template. Your templates can extend the default templates. Let’s say you have “region” as an additional grouping field. For the change list template this might look like this:

{% extends "admin/cms/grouper/change_list.html" %}
{% block language_tabs %}
    {# Here goes the region mark-up #}
    {% if region_dropdown %}
        <div class="region-selector">
            ...
        </div>
    {% endif %}
    {{ block.super }}
{% endblock %}

For the change form template this might look like this:

{% extends "admin/cms/grouper/change_form.html" %}
{% block search %}
    {# Here goes the region mark-up #}
    {% if "region" in cl.model_admin.extra_grouping_fields %}
        <div class="region-selector">
            ...
        </div>
    {% endif %}
    {{ block.super }}
{% endblock %}

Providing the required context

To provide the required context for your additional grouping model, you will have to implement two methods in your grouper model admin.

from cms.admin.utils import GrouperModelAdmin


class MyGrouperAdmin(GrouperModelAdmin):
    model = MyModel
    extra_grouping_fields = ("region",)

    ...

    def changelist_view(request, extra_context=None):
        """Extra context for changelist_view"""
        my_context = {...}  # Add context on region grouper
        return super().changelist_view(
            request, extra_context={**(extra_context or {}), **my_context}
        )

    def get_extra_context(self, request, obj_id=None):
        """Extra context for add_view and change_view"""
        my_context = {...}  # Add context on region grouper
        return {
            **super().get_extra_context(request, obj_id),
            **my_context,
        }

Consider that the context will require a set of values your additional grouping field can take. In the region example this might be all_regions = {"americas": _("Americas"), "europe": _("Europe"), ...}.