Full Review List (classic) and folder_buttons Action category

This page appears to be broken and I'm not sure what exactly it should do. You can see the issue by going to demo.plone.org and putting some content in the Pending state. If you also add a Review Portlet you will see a link to Full Review List, or just go to demo.plone.org

Despite having checkboxes for each item, there are no buttons by default. This is because it is looking for actions in the folder_buttons section. If you have a migrated site you will get these, but a new 6 site does not have this category created. The only button that appears to work on migrated 5 sites is the Change Workflow one. Cut/copy/paste/delete have actions string:folder_copy:method (etc) and return a 404.

I think this form should probably not rely on folder_buttons, or those actions should be created in a default 6 site. Is there even a valid action for cut/copy/paste/delete we can change too?

1 Like

I ran into this in my 5.1 and 5.2 sites, where I needed a new DCWorkflow and a way to bulk review my content. After unsuccessfully trying to patch Products.CMFPlone.WorkflowTool listActions / listActionInfos I ended up vibe coding a dashboard with help from zai-org/GLM-5.2-FP8 · Hugging Face

Turns out, I discovered a reusable pattern for my dashboards.

Maybe this is of help to someone... Use a named vocabulary as a filter.

(Edit: added screenshot at bottom.)

./browser/dashboard_product_review.py

# -*- coding: utf-8 -*-
import logging

from plone import api
from plone.app.z3cform.widget import AjaxSelectFieldWidget
from plone.autoform import directives
from plone.autoform.form import AutoExtensibleForm
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from z3c.form import button
from z3c.form import form
from zope import schema
from zope.interface import Interface

log = logging.getLogger(__name__)


class IDashboardViewForm(Interface):
    """Schema for the filter form"""

    brand_path = schema.Choice(
        title=u"Optional: Brand Filter",
        description=u"Select a brand to filter the product list",
        vocabulary='mdb_theme.BrandPathsVocabulary',
        required=False,
    )
    directives.widget('brand_path', AjaxSelectFieldWidget)


class DashboardView(AutoExtensibleForm, form.Form):
    """A hybrid view: z3c.form for filter, standard HTML for batch table"""

    schema = IDashboardViewForm
    ignoreContext = True
    label = u"Product Review Dashboard"
    description = u"Review and batch transition products."

    # Use a custom template so we can inject the table below the form
    template = ViewPageTemplateFile('templates/dashboard_product_review.pt')

    def update(self):
        """Intercept the batch action POST before z3c.form takes over"""
        if 'apply' in self.request.form:
            self.handle_batch_action()

        super(DashboardView, self).update()

    def handle_batch_action(self):
        """Process the selected UIDs and apply the workflow transition"""

        selected_uids = self.request.form.get('selected_uids', [])
        workflow_action = self.request.form.get('workflow_action')

        if not selected_uids:
            self.status = u"Please select at least one product."
            return

        if not workflow_action:
            self.status = u"Please select a workflow transition."
            return

        count = 0
        errors = 0

        for uid in selected_uids:
            obj = api.content.get(UID=uid)
            if obj:
                try:
                    api.content.transition(obj=obj, transition=workflow_action)
                    count += 1
                except Exception as e:
                    log.error(u"Transition failed for %s: %s" % (obj.absolute_url(), e))
                    errors += 1

        self.status = u"Processed %s products. Errors: %s" % (count, errors)

    @button.buttonAndHandler(u'Filter', name='filter')
    def handleFilter(self, action):
        """Handle the filter button click"""
        data, errors = self.extractData()
        if errors:
            self.status = self.formErrorsMessage
            return
        # The form will re-render, and get_products() will read the request.

    def get_products(self):
        """Query the catalog for products to display"""
        # The AjaxSelectFieldWidget stores the value in the request under this key
        brand_path = self.request.form.get('form.widgets.brand_path')

        # Handle the case where AjaxSelect returns a list
        if isinstance(brand_path, list):
            brand_path = brand_path[0] if brand_path else None

        catalog = api.portal.get_tool('portal_catalog')
        query = {
            'portal_type': 'produkte',
            'review_state': ['internal', 'pending'],
            'sort_on': 'sortable_title',
        }

        if brand_path and brand_path != '--NOVALUE--':
            query['kundenname_verweis'] = brand_path

        brains = catalog.searchResults(query)

        return brains

./browser/configure.zcml

<browser:page
    for="Products.CMFCore.interfaces.ISiteRoot"
    name="dashboard-product-review"
    class="mdb_theme.browser.dashboard_product_review.DashboardView"
    permission="zope2.View"
    />
./browser/templates/dashboard_product_review.pt

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:tal="http://xml.zope.org/namespaces/tal"
      xmlns:metal="http://xml.zope.org/namespaces/metal"
      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
      metal:use-macro="context/main_template/macros/master"
      i18n:domain="mdb_theme">
    <body>
    <metal:content fill-slot="main">

    <h1 tal:content="view/label">Title</h1>

    <tal:status tal:condition="view/status">
        <div class="portalMessage info">
            <strong tal:content="view/status">Status</strong>
        </div>
    </tal:status>


        <!-- this renders the filter -->
        <metal:content-core fill-slot="content-core">
            <metal:content-core define-macro="content-core">
                <metal:block use-macro="context/@@ploneform-macros/titlelessform" />
            </metal:content-core>
        </metal:content-core>

    <!-- The Batch Action Table (Standard HTML Form) -->
    <form action="" method="post" id="batch-action-form">
        <table class="listing">
            <thead>
                <tr>
                    <th>Select</th>
                    <th>Product Title</th>
                    <th>Brand Path</th>
                    <th>Current State</th>
                </tr>
            </thead>
            <tbody>
                <tal:products repeat="brain view/get_products">
                    <tr>
                        <td>
                            <input type="checkbox" 
                                   name="selected_uids:list" 
                                   tal:attributes="value brain/UID" />
                        </td>
                        <td>
                            <a tal:attributes="href brain/getURL" 
                               tal:content="brain/Title">Product Title</a>
                        </td>
                        <td tal:content="brain/kundenname_verweis">Brand Path</td>
                        <td tal:content="brain/review_state">State</td>
                    </tr>
                </tal:products>
            </tbody>
        </table>
        
        <div class="batch-action-controls" style="margin-top: 1em;">
            <select name="workflow_action">
                <option value="">-- Select Action --</option>
                <option value="publish">Publish</option>
                <option value="reject">Reject</option>
                <option value="retract">Retract</option>
            </select>
            <input type="submit" name="apply" value="Apply to Selected" class="context" />
        </div>
    </form>

</metal:content>

    </body>
</html>