How to customize listings using adapters


#1

Quite often, in a given listing (e.g. list of Samples) one would want to display more columns, add a new filter at the top, filter items differently, or even add a new “custom” Add button.

This recipe explains how to customize and/or change the behavior of listings by using your own add-on, without the need of modifying senaite’s code base.

Listings can be customized by using subscriber adapters. This functionality was introduced in https://github.com/senaite/senaite.core.listing/pull/2. A basic use-case is explained in that same Pull Request. Also, a discussion about this topic can be found in Gitter archives: https://gitter.im/senaite/Lobby?at=5d3b209dca086f6739e1c38e

In this recipe, we will explain how to add a new column in samples listing first and how to add a new “status” filter button at the top of the list.

Creation of the adapter and registration

First, you need to create the adapter class in your add-on:

from bika.lims import api
from senaite.core.listing.interfaces import IListingView
from senaite.core.listing.interfaces import IListingViewAdapter

class MyOwnSamplesListingAdapter(object):
    adapts(IListingView)
    implements(IListingViewAdapter)

    # Order of priority of this subscriber adapter over others
    priority_order = 1000

    def __init__(self, listing, context):
        self.listing = listing
        self.context = context

    def before_render(self):
        # Do your own stuff here. E.g., add search criteria in `contentFilter` variable
        return

    def folder_item(self, obj, item, index):
        # Do your own stuff here. E.g., set the value to display for a given column
        return item

Note this class adapts IListingView in first place, which means that this adapter will “adapt” (customize, change the behavior of) objects from IListingView type, the type that all senaite listings implement. This adapter implements IListingViewAdapter. While rendering any listing, senaite will look for existing adapters that implement this type for the given listing. If an adapter is found, the functions before_render and folder_item will be called accordingly during the listing rendering life-cycle.

At this point, we’ve created the basic adapter, but we haven’t yet registered the adapter. Register the adapter by adding the following snippet in your configure.zcml file (located in same package where you added the python class), or create a new one:

  <!--  Samples listing with additional filters and columns  -->
<subscriber
    for="bika.lims.browser.analysisrequest.AnalysisRequestsView
         bika.lims.interfaces.IAnalysisRequestsFolder"
    provides="senaite.core.listing.interfaces.IListingViewAdapter"
    factory=".MyOwnSamplesListingAdapter" />

Note the following parameters:

  • for : we tell the system which is the listing that needs to be adapted (first line) and in which context this listing has to be adapted. In this case, we tell the system that we want to adapt the samples listing (aka analysis requests), but only when the context is the main listing folder. If we wanted the adapter to work for all listings regardless of the context we could replace bika.lims.interfaces.IAnalysisRequestsFolder by *.
  • provides: The type of adapter provided
  • factory: relative or full canonical path to your new adapter class

At this point, your adapter will be called each time the main samples listing is called. Now is time to make the adapter do what we want:

Add a new status filter button and column in your listing

As you’ve noticed, there is a function in the adapter called before_render. As the name states, this function is called just before the listing gets rendered. Thus, is the right place to apply wide-listing modifications, such as adding new columns, modifying the search criteria, adding new filter buttons, etc. In our example, we will add a new filter button and a new column. Add the following code inside before_render:

    def before_render(self):
        # Add a new filter status
        draft_status = {
            "id": "draft",
            "title": "Draft",
            "contentFilter": {
                "review_state": "sample_draft",
                "sort_on": "created",
                "sort_order": "descending",
            },
            "columns": self.listing.columns.keys(),
        }
        self.listing.review_states.append(draft_status)

        # Add the column
        self.listing.columns["MyColumn"] = {
            "title": "My column",
            "sortable": False,
            "toggle": True,
        }

        # Make the new column visible for all filter statuses
        for filter in self.listing.review_states:
            filter.update({"columns": self.listing.columns.keys()})

Next step is to “display” the value in the listing while rendering a given item (in this case, a sample). Add the following code in folder_item function:

    def folder_item(self, obj, item, index):
        sample = api.get_object(obj)
        item["MyColumn"] = obj.getField("MyCustomAttribute").get(sample) or "Empty value"
        return item

Note that here we’ve assumed that you added a new Schema field “MyCustomAttribute” for the content type “AnalysisRequest”.

Further reading: