Report a problem on Plone content: on the content types where the behavior is enabled, a "Report a problem" button opens a form whose result is emailed to configured recipients.
It exists because visitors and editors often have no way to flag a problem on one specific page — a content error, unmasked personal data (GDPR), a missing annex, something published by mistake.
- A marker behavior,
imio.reportproblem.reportable, enabled per content type. - A
@@report-problemform, offered either in a modal window or as a full page. - A viewlet
report-problem-buttoninplone.belowcontentbody, with minimal markup that is easy to override (z3c.jbotcompatible). - A site-wide control panel: recipients, reasons, wording, display mode.
- Two extension points, so a consumer package can decide who receives a report and what happens after it is sent — see Extending.
- Optional hCaptcha protection and optional audit logging, both as extras.
- The add-on persists nothing: reports are emailed and announced through an event. Storing, ticketing or counting them is a consumer's business.
Important
Version 1.0 is Classic UI only. There is no Volto/React component and no
REST endpoint. The sending logic is deliberately isolated in
imio.reportproblem.report.send_problem_report(context, data) so that a REST
endpoint can be added in 1.1 without duplicating anything.
Plone 6.0, 6.1 and 6.2 on Python 3.10 to 3.13.
Install imio.reportproblem with pip:
pip install imio.reportproblemThen install the add-on in Site Setup, or apply the
imio.reportproblem:default GenericSetup profile.
The two optional dependencies are extras. Installing the package is what turns the corresponding feature on — there is no setting to flip.
| Extra | Pulls in | Effect |
|---|---|---|
captcha |
plone.formwidget.hcaptcha |
Adds an hCaptcha field to the form for anonymous users. |
audit |
collective.fingerpointing |
Logs every sent report to the audit log. |
all |
both of the above |
pip install "imio.reportproblem[all]"Warning
Without the captcha extra, the form is not offered to anonymous users at
all. This is deliberate — fail closed rather than expose an unprotected
anonymous form. A warning is logged at startup. So if citizens cannot see the
button, the captcha extra is missing. Authenticated users are unaffected:
they never get a captcha, because an authenticated account is already
accountable.
The four installation configurations (no extra, captcha, audit, all) are
each exercised in CI.
Enable the Report a problem behavior on the types you want, either in the Dexterity control panel or from your own GenericSetup profile:
<property name="behaviors" purge="false">
<element value="imio.reportproblem.reportable"/>
</property>Behavior marker interfaces are resolved dynamically by FTIAwareSpecification,
so existing content needs no migration when you enable it.
Site Setup → Report a problem (@@reportproblem-controlpanel), stored in
plone.app.registry under imio.reportproblem.interfaces.IReportProblemSettings:
| Record | Purpose |
|---|---|
recipients |
Default recipient addresses, validated one by one. |
reasons |
The reasons offered in the form. |
privacy_url |
Link to the privacy policy. |
subject_template |
Email subject template; ${title} is substituted. |
button_label |
Button text — also used as its aria-label. |
form_title |
Form title. |
form_intro |
Text shown above the form. |
confirmation_message |
Shown after a successful send. |
form_display_mode |
modal (default) or page. |
button_label, form_title, form_intro and confirmation_message ship
empty on purpose. While a field is empty the add-on shows its own label from
the i18n catalog, so it is translated. As soon as you type a value, that value
is used verbatim, in the one language you typed it in.
This is the opposite of reasons, which does ship defaults — an empty
vocabulary would make the form unusable.
The reasons are configurable through the web, so they inevitably escape the
package's .po files. The convention: a reason is translated when a matching
msgid exists in the imio.reportproblem domain, and shown exactly as typed
otherwise. The four shipped defaults are translated in FR and NL; a reason an
administrator adds by hand appears in the language it was typed in.
Form tokens are derived from the label with plone.i18n's idnormalizer, since
labels contain spaces and accents.
modal loads the form over AJAX with the Mockup pat-plone-modal pattern;
page renders it as a standalone page with a back link. It is the same view
either way — only the link changes, and a modal link whose pattern fails to
initialise stays an ordinary link, which is also the no-JavaScript fallback.
Switch to page if the captcha misbehaves inside the modal.
Resolves the applicable configuration for a given content:
class IProblemReportConfig(Interface):
def is_enabled(): ...
def get_recipients(): ...
def get_privacy_url(): ...The default adapter is registered on *, not on IProblemReportable. That
matters: register yours on a more specific interface — your own content type or
your own marker — and it wins the lookup naturally, with no overrides.zcml
and no dependency on ZCML load order.
The adapter is authoritative: it does not merge. The fallback chain (control
panel, then the portal's email_from_address) lives in the default adapter, so
subclass it and call super() to keep it:
from imio.reportproblem.adapters import ProblemReportConfig
from imio.reportproblem.interfaces import IProblemReportConfig
@implementer(IProblemReportConfig)
@adapter(IMyContent)
class MyReportConfig(ProblemReportConfig):
def get_recipients(self):
email = self._institution_email()
return [email] if email else super().get_recipients()If you do not call super(), your recipients are the only ones used. Merging
silently would ship citizens' reports to a global address your product never
asked for — bad for noise, worse for GDPR.
These are methods, not properties: a partial override reads unambiguously, and a subclass that forgets a decorator cannot silently mask a value.
is_enabled() returns True by default — no filtering on publication state,
type or role. Override it to restrict the button (hide it on drafts, limit it
to some categories, disable it for a period). Note the asymmetry: the full
display condition is is_enabled() and a resolvable recipient and, for
anonymous visitors, an available captcha. A product can restrict what is
shown; it cannot open what the guard rails close.
Resolve it through the single entry point, never by adapting by hand:
from imio.reportproblem.utils import get_report_config
config = get_report_config(context)An object event fired after a successful send. Hook onto it to open a ticket, build statistics, or store the report:
@adapter(IProblemReportedEvent)
def my_handler(event):
event.object # the content the report is about
event.data["reason"] # the reason *token*, not its translated titledata carries reason (token), review_state, userid (empty when
anonymous), name, email and message. The token is what lets you route or
count reports by reason without this package storing anything.
With the audit extra, the add-on ships one consumer of its own event: a
collective.fingerpointing subscriber.
Important
The audit line contains no personal data about the reporter — only the
content's UID and path, its portal_type, the reason token, and whether the
report was authenticated. An audit log is kept for a long time; writing a
citizen's name, email or message into it would create an unmanaged parallel
database. (collective.fingerpointing adds the acting user and IP itself, as
it always does.)
A site wanting reCAPTCHA or a honeypot overrides the form view on its own browser layer — a standard Plone mechanism that needs no extension point here.
This product has been translated into French and Dutch. Source labels are in English.
- An operating system that runs all the requirements mentioned.
- uv
- Make
- Git
- Docker (optional)
-
Clone this repository, then change your working directory.
git clone git@github.com:IMIO/imio.reportproblem.git cd imio.reportproblem -
Install this code base.
make install
Useful targets: make test, make test-coverage, make check (format then
lint), make i18n (update the catalogs), make start.
This package provides markers as strings (<!-- extra stuff goes here -->) that are compatible with plonecli and bobtemplates.plone.
These markers act as hooks to add all kinds of subtemplates, including behaviors, control panels, upgrade steps, or other subtemplates from plonecli.
To run plonecli with configuration to target this package, run the following command.
make add <template_name>For example, you can add a content type to your package with the following command.
make add content_typeYou can add a behavior with the following command.
make add behaviorYou can check the list of available subtemplates in the [`bobtemplates.plone` `README.md` file](https://github.com/plone/bobtemplates.plone/?tab=readme-ov-file#provided-subtemplates).
See also the documentation of [Mockup and Patternslib](https://6.docs.plone.org/classic-ui/mockup.html) for how to build the UI toolkit for Classic UI.
The project is licensed under GPLv2.
Generated using Cookieplone (1.0.0) and cookieplone-templates (200cfed) on 2026-07-28 15:05:54.907737. A special thanks to all contributors and supporters!