Skip to content

feat(quant_webform): Quant Forms for Webform, managed in Drupal (2.3.0) - #264

Merged
stooit merged 68 commits into
2.xfrom
feat/webform-quant-forms
Nov 9, 2026
Merged

stooit merged 68 commits into
2.xfrom
feat/webform-quant-forms

Conversation

@stooit

@stooit stooit commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Part of 2.3.0. Based on #263 (feat/oauth-connect), which includes #262; merge those first.

Quant Forms for Webform: an opted-in Webform takes submissions on the static site through Quant Forms (multi-page wizards, conditions, composites, files), with its notifications and spam settings managed in Drupal. Opt-in is per Webform; nothing changes on upgrade.

Changes from the staging rehearsal

  • Saved Quant Forms settings failed config validation. The schema used webform.webform.third_party.quant_webform; Webform reads webform.settings.third_party.*.
  • Configuration > Quant > Forms explains the opt-in first, lists each difference a form has on the static site (not only "Works, with 3 differences"), and uses plain plurals.
  • sendFormConfig() catches every Guzzle error, like the rest of the client.
  • Release notes updated (RELEASE-NOTES-2.3.0.md).
  • The export token is bound to its page. One signed token no longer switches on the static form render on every path until it expires; sites in a subdirectory are handled.
  • Notes of a form's differences render as markup, not escaped text.

How it was tested

  • Staging: three Webforms of the demo site (a 4-step wizard among them) registered; their pages published with the form config; the dashboard lists them and shows them as managed in Drupal.
  • Not passed yet: a real submission through the staging edge returns 500 after validation (missing fields correctly return the 400 message). This looks like the staging edge, not the module; it is being checked separately. Please do not release until a submission passes on staging.
  • Whole module suite: 212 pass, 1 skipped; phpcs clean on changed files.

stooit added 30 commits November 8, 2026 14:02
Adds quant_api.credentials (settings.php override, then the OAuth connection,
then the typed token), the platform and auth_method settings with schema, and
an update hook that sets the platform from an existing endpoint. The client,
quant_search and the seed form no longer read api_token from config.
The editor approves the site in the Quant dashboard (client drupal-quant, S256
PKCE, state bound to the session and user) and picks a project. The module
limits the token to that project, reads the project's write token and keeps
both outside config. Change project starts a new login; the old project stays
until the new choice. Cron refreshes the token with the portal's rotation
rules; a database copy never refreshes the original site's tokens. Multi-domain
sites stay on token connect. Adds drush quant:auth-status, kernel tests for the
refresh rules and a functional test against a fake dashboard.
…ort fetches

The content push passes form_enabled and form_config from the event metadata,
and the client gets sendFormConfig() for POST /v1/form. The export fetch now
always sends the signed quant-token (before, only with content drafts on), so
a render can tell that Quant is exporting it.
A page that shows a Webform publishes with its Quant Forms config: mandatory
fields that are always required, Drupal's own fields removed, honeypot and
reCAPTCHA, the confirmation message without tokens, the first email handler
and an optional Slack webhook kept outside config. Saving a Webform publishes
its pages; closing or deleting it turns Quant Forms off. Adds a per-form
Quant Forms setting with a support report, a Forms settings page with
Register all forms, drush quant:webform-register and the support matrix.
…atic site

On a Quant export render every wizard page is in the page. The script shows
one step at a time, checks each step (HTML validation, required checkbox
groups, #states), builds the review step, and posts once to Quant Forms with
Accept: application/json. Repeated multipart names are renamed so that every
file of a multi-file field is kept. Confirmations show in place; url
confirmations redirect. Without JavaScript the form posts as one page.
An unconnected site sent requests with empty credentials, for example a
PATCH /v1/unpublish when a search page was deleted. Every call now fails
closed before any request, with one log line that says why.

(cherry picked from commit 7c2edb2)
An unconnected site sent requests with empty credentials, for example a
PATCH /v1/unpublish when a search page was deleted. Every call now fails
closed before any request, with one log line that says why.
A newsletter Webform in the footer was on every page, and a page with two
Webforms registered only the first, so the second form's submissions went
to the wrong config. On an export render a Webform with its own page now
posts there from any page that embeds it, and Quant Forms is registered only
on the page a form posts to.
…d endpoint, and a rotated write token recovers

- A copy of the database (fingerprint mismatch) gets no write token in
  login mode, and the settings page says to log in on the copy.
- The platform chosen for a login is saved only when the login completes,
  so a cancelled login never re-points a working token connection.
- In login mode the endpoint is the platform's own host; a custom endpoint
  (a settings.php override aside) no longer receives the hidden token.
- Cron re-reads the project write token with the login after a refresh and
  at least daily, so a token rotated in the dashboard recovers.
…ache context

The static form behaviour switched on whenever the quant-token header was
present, and with content drafts off that header is never validated. It now
needs a token this site signed that has not expired (TokenManager::isGenuine).
The render cache varies by a two-value context (quant_export: export or live)
instead of the header value, which changed on every fetch. The purger records
no traffic with content drafts off, as before the export fetch always sent
the token.
- Opt-in: every Webform is off until ticked (update hook changes nothing),
  so an upgrade never replaces settings made by hand in the dashboard.
  Register the ticked forms acts only on ticked forms.
- A Quant Forms section in each Webform's settings: notification email
  (from the handler, set in Drupal, or none), Slack with a per-form webhook,
  a honeypot field, reCAPTCHA v2, the spam check, country and IP blocklists,
  and what shows after a submit. Secrets are in KeyValue, masked, never in
  config. Features Quant cannot do are listed greyed out with the reason.
  The pushed config carries notification_settings.managed_by = drupal.
- mandatory_fields lists only element types that post their own key and
  that anonymous visitors see (not likert, datetime, email_confirm,
  select_other, radios_other, #private, #access_create_roles).
- The module turns Quant Forms off on a page it registered when the page
  loses its form, the form is opted out, unsupported, closed (also by its
  schedule, on cron) or deleted, using a map of the pages it registered.
  Pages that never had a ticked form are left alone.
- Release notes for 2.3.0 with the upgrade note.
Found on the demo site: "[webform:title]: [webform_submission:values:topic]"
became ": {{topic}}".
…rite token

- A copy (fingerprint mismatch) logs an error that names the differing
  components, shows on the status report and the settings page, and offers
  "This is the same site" (configure quant), which stores the fingerprint
  again so the site publishes. The fingerprint docblock is corrected.
- A 401 from the content API re-reads the project write token with the
  login, at most once every 15 minutes, so a token rotated in the dashboard
  recovers on the next publish.
…and tighten secrets

- N2: turning Quant Forms off sends a config without managed_by (released_by
  drupal), so the dashboard can manage the page again.
- N4: the open state is stored when a page registers; cron turns off every
  registered form that is closed, opted out or deleted, and a failed
  turn-off stays registered and runs again on the next cron.
- N5: reCAPTCHA cannot be on without a secret key; the report says so.
- N6: deleting a Webform deletes its secrets.
- N7: secret changes are held from validation and stored in presave, so only
  a successful save stores them; Remove works on an unticked form.
- N8: only a genuine signed token skips the page cache.
…ver-side dashboard

- $settings['quant_api_endpoint_url'] names the content API endpoint in
  every mode (for example a stage host behind basic auth). It is never
  editable in the UI; the settings page and drush show it without its
  userinfo, and an HTTP middleware moves userinfo from any request URI into
  an Authorization header before the request is sent, so it never reaches a
  message or a log.
- $settings['quant_api_dashboard_internal_url'] is the dashboard address for
  server-to-server calls (token, refresh, projects, limit, write token); the
  browser redirect keeps quant_api_dashboard_url. Default: the same.
The dashboard token response names the user but not the organisations, so a
real login found no projects. The test dashboard now answers like the real
one. A dashboard fixed in settings.php also sets the starting platform.
Without the Redirect module, a changed alias (for example Pathauto after a
title change) left a 404 at the old URL on the static site. The old alias
now gets a 301 to the new one, for published nodes and terms only. With the
Redirect module on, its own redirect entity is pushed as before.
Installing the module failed strict config schema checks for
views.view.quant_metadata_node.
…leted

A deleted published node or term sent its internal-path redirect again,
because the entity is still published during delete. /node/N kept a 301
to a page that no longer exists. Deleted content now unpublishes its
internal paths (the prefixed one only, for one deleted translation).
The file_link formatter settings of views.view.quant_metadata_file carried
a copy of Views UI options, which fails strict config schema checks.
Log out deleted the token only in Drupal, so it stayed valid on the portal
until it expired. The module now calls POST /api/oauth/revoke first (best
effort: an older dashboard or a failed call is logged, and the local
connection is deleted in every case).
stooit added 29 commits November 9, 2026 09:15
The core schema fixes from fix/content-paths make the shipped views valid.
…s; review fixes

From the release review:
- Log out (or a token save) on a copy of another site's database revoked the
  original site's login. A copy now only forgets the login locally.
- Change project and a failed project pick now end the login they replace
  or discard, instead of leaving it valid at the dashboard.
- An expired access token with a refresh token stored no longer shows
  "The Quant login needs renewing": the next request renews it.
- The settings page decides on its success and error messages from a session
  flag and the client's own unreachable flag, not from other queued
  messages, so a token save shows "Connected" again.
- The endpoint override warning no longer shows credentials in the URL.
isGenuine() accepted a signed export token on any path, so one token switched
on the static-form render of every page until it expired. It now needs the
path that the token was made for, like validate() in strict mode.
With a dashboard override outside quantgov.cloud (for example a local
QuantGov dashboard), the platform radio defaulted to QuantCDN while the
saved platform was QuantGov, so saving the settings form for another reason
logged the site out.
Pushes run after the response is sent (and from the queue on cron), so a
failed push was only in the log. Each failure is now kept for the site, with
a short reason and never the error text, until a later push of the same page
succeeds. Editors (configure quant or access content overview) see a warning
on their next page view, and the status report lists the pages. Also: a
blank or invalid pagination selector no longer raises PHP warnings.
From review: a cached editor page did not show a new failure (the list now
has a cache tag, invalidated when it changes); a file that Quant already had
(MD5 matches) never cleared its failure; concurrent queue workers could lose
entries (the list changes under a lock); the message pointed editors without
access to the status report.
The export fetch signs the route with the base path (/drupal/contact) while
the request path is /contact, so a site in a subdirectory lost the static
form render after the token was bound to its path.
…ts in CI

CI (Drupal 11.1) failed: config/install/quant.settings.yml held 0/1 for keys
the schema types as boolean. Drupal 11.1 installs module config as trusted
data, without casting it, so the strict schema check saw integers. Drupal
11.4 casts on save, which is why a local 11.4 run passed. The install config
now holds true/false, and update 9009 casts integers that existing sites
stored.

CI now starts a PHP web server and sets SIMPLETEST_BASE_URL, so the
functional tests run instead of failing on the missing base URL.
CI (Drupal 11.1, which installs module config without casting it) still
failed on quant.token_settings. A new kernel test checks every config/install
and config/optional file of quant and its submodules against its schema, as
the files are on disk, and lists every mismatch in one run. It found:
- quant.token_settings disable/strict and the Quant Search entity settings
  stored 0/1 for booleans: now true/false; update 9009 casts them on
  existing sites.
- The quant_metadata Views field option kept 0 for each unticked key (from
  checkboxes), against its list-of-strings schema: the shipped views keep
  only the ticked keys, the field drops unticked keys on save, and update
  9010 filters saved views.
CI also installs drupal/webform, so the Quant Forms tests can run.
… save happened

The settings test loaded the webform through the entity cache of the test
process, so after the browser saved it, the second load could return the
copy from before the save (seen on Drupal 11.1 in CI: no 'enabled' key).
The test now resets that cache before each load and first checks the
"has been saved" message, so a failed save and a stale read are told
apart.
@stooit
stooit merged commit 90947bc into 2.x Nov 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant