wagtail-nest / wagtail-modeladmin

Add any model in your project to the Wagtail admin. Formerly wagtail.contrib.modeladmin.
Other
22 stars 8 forks source link

Custom buttons and views for objects in `ModelAdmin` #25

Open donhauser opened 3 years ago

donhauser commented 3 years ago

Is your proposal related to a problem?

Example: Imagine you have a model like "Organisation" consisting of attributes like name, employees and so on and you want to create multiple views for it (through the admin panel), e.g. a "list of employees", "organisation overview". Now, since you are using ModelAdmin, you would like to have a "employee list" and "overview" button beneath each organisation object in the index view, so you can easily access both object-views.

Note: You could use inspect_view for one action, but not for multiple actions.

Currently it is quite tricky implement new action-buttons with separate views because:

This problem is also discussed in wagtail/wagtail-modeladmin#7 and wagtail/wagtail-modeladmin#10

Describe the solution you'd like

In contrast to wagtail/wagtail-modeladmin#7 and wagtail/wagtail-modeladmin#10, I am proposing a very minimal change in ButtonHelper and ModelAdmin only. The goal is to simplify the steps above without breaking any backwards compatibility in Wagtail, so the feature is lightweight but effective:

A scratch code can be seen below.

Describe alternatives you've considered

One alternative would be to rewrite ButtonHelper as mentioned in wagtail/wagtail-modeladmin#10. By changing ButtonHelper heavily, old Wagtail-Projects might be broken, so this is rather a long term goal.

Additional context

Changes to Wagtail:

# Changes to ButtonHelper
class ButtonHelper:

    # Default button generator for custom/extra buttons (structured like add_button)
    def extra_button(self, url=None, label='', title=None, classnames_add=None, classnames_exclude=None):
        if classnames_add is None:
            classnames_add = []
        if classnames_exclude is None:
            classnames_exclude = []

        cn = self.finalise_classname(classnames_add, classnames_exclude)

        if not title:
            title = label

        return {
            'url': url,
            'label': label,
            'classname': cn,
            'title': title,
        }

    def get_buttons_for_obj(self, obj, exclude=None, classnames_add=None,
                            classnames_exclude=None):

        # OLD CODE HERE

        # New code

        # Check if the custom data structure is present
        if hasattr(self, "custom_object_buttons"):
             button_list = self.custom_object_buttons

            for action, kw in button_list:

                # TODO unite kw['classnames_add'] and classnames_add
                # TODO unite kw['classnames_exclude'] and classnames_exclude

                # create and append the button using the regular url pattern
                button = self.extra_button(self.url_helper.get_action_url(action, pk), **kw)
                btns.append(button)

        return btns

class ModelAdmin:

    def get_admin_urls_for_registration(self):        

        # OLD CODE HERE

        # New code
        # create url pattern for each custom view, just like for "add", "edit", ...
        urls = urls + tuple(
            re_path(
                self.url_helper.get_action_url_pattern(action),
                view,
                name=self.url_helper.get_action_url_name(action))
            for action, view in self.get_custom_object_views()
        )

        return urls

    # fallback method
    def get_custom_object_views(self):
        return []

Adding Buttons would now be much easier:


class MyButtonHelper(ButtonHelper):

    # custom definitions
    # (action, attributes)
    custom_object_buttons = [
        ("empolyees", {"label": 'Employee List', "add_class":["some_class"]}),
        ("overview", {"label": 'Overview', "title": 'Show a detailed overview table'}),
        ..
    ]

class MyModelWagtailAdmin(ModelAdmin):

    button_helper_class = MyButtonHelper

    def empolyees_view(self, request, instance_pk):
        # some call to View.as_view(..)

    def overview_view(self, request, instance_pk):
        # some call to OtherView.as_view(..)

    # Define custom object views
    #   This is a function because self is needed
    #   It would be even nicer to have only a list of actions
    #   and to automatically return self.{action}_view
    def get_custom_object_views(self):
        return [
            # (action, view)
            ("empolyees", self.empolyees_view),
            ("overview", self.overview_view),
        ]

If you like this suggestion, I will create a pull request for this feature