Changelog#
Changelog#
1.0b4 (unreleased)#
Render the surveys in the page font stack: the SJS2 default theme sets
--sjs2-typography-font-family-textto a bareOpen Sanswithout any fallback, and no Open Sans webfont ships with the add-on, so every client without that font installed locally (iOS, Android, most Linux desktops) rendered the whole form in the browser default font, i.e. Times/serif.viewer.cssnow sets the variable tovar(--bs-font-sans-serif, ...)on.sd-root-modern.sd-theme-root(a plain class selector is required — the library rule is wrapped in:where()and would win an equal-specificity override).Admission control for public submissions (security review findings 3–6).
@@save-pollnow consumes a per-survey, per-client bucket before any parsing and answers429 rate_limited(withRetry-After) over the limit and503 rate_limit_unavailablewhen the bucket store is unavailable (fail closed). New settingssubmission_rate_limit_enabled/_per_minute/_per_hour/_trust_proxyin the “Security” fieldset, profile version 1004 with a registry upgrade step (upgrades.to_1004). Tests:test_ratelimit.py,test_coverage_views.py::SavePollRateLimitTests,test_integration_views.py::test_save_poll_rate_limit_*.Bind direct-embed tokens to their survey:
@@save-pollrejects a token whosesubclaim is not the target survey’s UID (survey_mismatch) and refuses embed submissions for surveys that are not indirectmode (direct_embedding_not_enabled); both share the token issuance helpers (embed_security.get_survey_uid/is_direct_embedding_mode). Tests:test_integration_views.py::test_save_poll_rejects_embed_token_issued_for_another_surveyand neighbours.Sanitize generated result HTML with a parser-based allow list (
converters/sanitize.py), closing the stored, click-triggered XSS in the result detail view, the HTML/PDF exports and the mail body. Values are no longer trusted just because they passed the validator. Tests:test_converters.py::test_build_html_neutralises_click_triggered_javascript_urlsand thetest_sanitize_html_*cases.Prevent server-side requests during PDF rendering:
write_pdf()applies the allow-list sanitizer in data-images-only mode before calling WeasyPrint, removing network, file and relative image sources while preserving inline images. Test:test_converters.py::test_write_pdf_drops_non_inline_images_but_keeps_inline_images.Neutralise spreadsheet formula injection in the CSV/XLSX exports (
converters/spreadsheet.py): formula-leading values are written as text inconverters/csv_export.py,converters/xlsx_export.pyand the CSV download of the dashboard. Tests:test_converters.py::test_write_csv_escapes_formula_leading_valuesandtest_write_xlsx_stores_formula_leading_values_as_text.Remove the
authenticity_token_cache_pathsetting (UI “Token cache path”) and thelegacy_diskcache_pathargument ofkv.get_configured_kv_store(). The authenticity-token cache location is derived solely fromkv_cache_directory(<directory>/auth) or therdbmscaching backend;docs/security.rstanddocs/global-options.rstdescribe the current behaviour.Wire the previously inert AI prompt settings: “Prompt before” and “Prompt after” now wrap the user prompt for prompt-based creation and refinement (
browser/services/ai.py::apply_prompt_wrapper()), and “Default prompt” prefills the prompt field of an empty AI workspace (browser/ai.pt). Tests:test_ai_settings.py::ApplyPromptWrapperTests,LoadPromptSettingsTestsandAIPromptWiringTests.Replace the dead
PDFFormExtractorcall path with the currentprivacyforms.pdfAPI (PDFFormService,PDFField.layout/choices/field_flags).PDFFormExtractordoes not exist in any publishedprivacyforms.pdf>= 0.2.0, so field extraction ran through the inline pypdf implementation and the AI-upload PDF detection always failed. The inline duplicate (204 lines) and thefitzimport alias are gone;test_fillable_pdf_view.py::PDFFieldExtractionTestscovers the extraction against a real PDF.Remove dead code:
views._find_sample_forms_dir(),views._extract_json_object()/_parse_json_loose()(the tolerant extractor lives injson_extract.py),monitoring._get_cache_dir(),storage._sqlite_path_to_uri(),SQLiteResultStorage,FormsSettingsEditForm/FormsSettingsControlPanel,OriginNotAllowedError,PDFValidationError,ISurveyMonitorView,migrate_zodb_results_to_sqlite()and the@@get-polls-jsonendpoint (v1, no caller — the dashboard uses@@get-polls-json2).Remove the remaining compatibility shims: the AI provider derivation from “legacy populated fields” (
load_ai_settings(),FormsSettingsView._effective_ai_provider()), the registry-record creation in_save_to_registry(), thehasattrmail-settings branch inutils.resolve_mail_settings(), theComponentLookupErrorfallback inssrf.get_post_endpoint_policy()and the optionalprivacyforms_aiimports ofvocabularies/ai_models.py,chatbot/engine.pyandsetuphandlers._prebuild_deno_binary().Remove test and repository leftovers:
tests/legacy_survey_template.py, the emptiedtests/integration/directory, seven permanently skipped tests intest_integration_views.py, the root directoriessample_forms/,fillable_forms/,embedding_demo1/anduv-plone62/, plusFilledForm.pdf,bobtemplate.cfgandmprocs.yaml..webui_secret_keyis untracked and gitignored, andAGENTS.mdno longer names the removedtest_plone52.cfg.Remove the orphaned
@@pfs-theme-editorview (browser/pdf_theme_editor.py, its template and the two static assets). It was registered for the site root but referenced by no template, JavaScript, test or documentation; the survey theme system lives in@@theme-manager/@@theme-editor.Remove the
@@demo-contentview (browser/demo_content.py, 1446 lines). The demo content is provided by thezopyx.surveyjs:demoGenericSetup profile;scripts/init_plone.pyno longer invokes the view and the endpoint is gone fromdocs/views.rst.Remove the unused command-line surface of the result converter (
converters/cli.py): theargparseentry point (main(),parse_args(),data_default(),form_default(),parse_formats()), the PEP 723 script header and the CLI-only path constants. The module keeps the export/mail engine (SurveyConverter) used bysubscribers.pyandbrowser/survey_results.py, plusslugify()andload_dotenv();converters.__init__no longer re-exports the argparse helpers.Remove the unused Robot Framework scaffold:
tests/test_robot.py, the twotests/robot/*.robotfiles (plone/Dexterity boilerplate), theZOPYX_SURVEYJS_ACCEPTANCE_TESTINGlayer with itsplone.app.robotframework/zserverfixture bases and therobotbuildout part. No workflow, Makefile target or documentation ever used them. Theplone.app.robotframework[debug]test dependency stays: it is imported byplone.app.contenttypes.testing, whichtesting.pybuilds on.Document the transparency obligations of Article 50 of the EU AI Act for the AI features (
docs/ai.rst→ “AI transparency (EU AI Act, Article 50)”,docs/limitations.rst). The section maps Art. 50(1), (2), (4) and (5) onto@@ai,@@chatbotand the export pipeline, states that the open-source carve-out of Art. 2(12) does not apply to systems falling under Art. 5 or Art. 50, and records that the add-on implements neither the interaction disclosure nor the marking of AI-generated output, so the duty remains with the operator. Documentation only, no code change.
1.0b3 (released 2026-09-21)#
Add configurable outbound POST endpoint policies under the Forms control panel’s Security fieldset. The default public-endpoint policy blocks private and special-use destinations; strict allowlist mode accepts exact hostnames and
*.suffixpatterns only. Redirects are disabled and outbound POSTs use a bounded timeout.Return HTTP 405 for unsupported methods on the public form JSON and submission endpoints instead of exposing 500
AttributeErrorresponses.Fix the ZODB conflict → replay 403 cascade in
@@save-poll(issue #35). Thereceived:auth-token replay marker is written when the token is accepted, before the submission is stored. A ZODBConflictErrormakes Zope abort the attempt and re-run the whole request with the same form data, so the retried attempt found its own marker and rejected the submission withauth_token_replay(403): every conflict surfaced as a client-visible auth failure instead of a clean retry. The marker is now released again when the attempt’s transaction aborts (kv.release_key_on_abort()), which keeps the fail-closed “consumed on receipt” behaviour for a committed submission while letting the retry pass its own token check. Tests:test_kv.py::ReleaseKeyOnAbortTests,test_auth_services.py::AuthServiceTests::test_replay_marker_is_released_when_the_attempt_abortsandtest_integration_views.py::SurveyViewIntegrationTests::test_save_poll_releases_the_replay_marker_when_the_attempt_aborts.Document the fixed retry behaviour in
docs/security.rst(replay protection) anddocs/load-testing.rst(the conflict hotspot is no longer an auth failure, but the conflict rate remains the scaling ceiling of the write path).
1.0b2 (released 2026-09-13)#
Set the package version to
1.0b2.Add the optional GenericSetup profile
zopyx.surveyjs:demo(profiles/demo/, handlers indemo_profile.py). It seeds the four SurveyJS theme presets (light,dark,light-no-panels,dark-no-panels) and creates a publisheddemo-formsfolder with three complex English example forms, one per scope and each using a different theme: employee onboarding, customer satisfaction and conference registration. Both import steps are idempotent, and a form gets a new version only when its shipped definition changed, so re-applying the profile after an upgrade refreshes the demo content without duplicating anything.Add the Plone site distribution
surveyjs(distributions.zcmlplusdistributions/surveyjs/). It creates a Classic UI site with this add-on installed and uses the demo profile as its example content, so the themes and thedemo-formsfolder are created when a site is created with example content (setup_content). Documented indocs/distribution.rst.Add seven diagrams to the documentation (
docs/_static/diagrams/): the component overview and the submission lifecycle, the validation pipeline, the deployment topology, the token lifecycle, the export pipeline and the storage backends. Every raster image links to its interactive HTML artifact (zoom, pan, light/dark themes, guided views, search), the generator specifications ship alongside so the diagrams stay reproducible, and the images are embedded inoverview.rst,actions.rst,validation.rst,storage.rst,security.rst,exports.rstandinstallation.rst.Fix
browser/ai.py:_to_jsonable()logged from itsexceptbranches through a module logger that was never defined, so a serializer method that raised turned the whole call into aNameErrorinstead of falling through to the next strategy (vars(value)). The logger is defined now, andtests/test_ai.py::AIViewTests::test_to_jsonable_survives_failing_serializer_methodscovers the fallback.Make the repository pass
ruffcompletely:ruff formatandruff check --fixreformatted 41 files (docstring whitespace, blank lines after the import block, import grouping, comment indentation) without any behaviour change, and the ten findings that survived the automatic fixes were fixed by hand — the missing logger above, an unusedPDFFormNotFoundErrorre-export inbrowser/fillable_pdf.py, and unused locals in the token-store and theme-manager tests, which now assert the behaviour their comments only described.RELEASE.rstrequires a cleanruff check/ruff format --checkrun as part of the release gate and no longer tolerates pre-existing findings.
1.0b1 (released 2026-09-12)#
First beta release. The functional changes since the last published version on
PyPI (1.0a4) are described in the 1.0a5–1.0a7 sections below; the
entries here are the release-preparation changes of the beta itself.
Set the package version to
1.0b1.Declare the runtime dependencies that previously existed only in this repository’s buildout:
privacyforms.ai(imported by the AI generator, the AI service and the model vocabulary) andprivacyforms.pdf(the fillable-PDF workflow). PDF filling needs PyMuPDF, which is shipped as the optional extrazopyx.surveyjs[pdf]instead of a hard requirement because it is dual-licensed (AGPL-3.0 or a commercial Artifex licence).Import
privacyforms_ailazily (browser/services/ai.py:get_ai_helper()) instead of at module level inbrowser/ai.py. A deployment without the helper now fails the single AI feature with an actionable message instead of failing while Zope configures the add-on (ZCML resolves.ai.AIView).Remove unreachable PDF code:
browser/services/pdf.py(no caller at all, itsextract_mode="llm"branch imported the deletedai_generatormodule) andpdf_forms.py(the unwired pdfcpu pipeline, reachable only from the removed module). The supported paths are the AI generator (@@ai-upload) and the fillable-PDF workflow.Documentation corrections: the fillable-PDF pages no longer claim that survey data is merged into a PDF template (values come from the fill form, keyed by raw PDF field names; signature fields are never written), the
todopage is replaced bylimitations.rstdocumenting the known gaps (rate limiting, automatic PDF merge, security items tracked in SECURITY.md, multi-server KV behaviour), the stale test-baseline numbers inSECURITY.mdanddocs/security.rstare refreshed to the measured 475 Plone tests / 115 pytest tests, anddocs/old/carries a README marking its files as historical, non-authoritative notes.Add
RELEASE.rstwith the release checklist, and use Python 3.14 in the PyPI/TestPyPI publish workflows so the publishing interpreter matches the one the test suite runs on.Enable the
pdfextra in this repository’s buildout (base.cfg) so the documented “Download Filled PDF” workflow actually works in the dev and demo instance; PyMuPDF was previously missing there, so the feature failed with “PyMuPDF is required”. PyMuPDF stays an extra because of its dual licence (AGPL-3.0 or commercial).Remove the pdfcpu binary from the container image and delete the unreachable pdfcpu CLI wrapper
pdf_form_extract.pytogether with its tests. Withpdf_forms.pygone, nothing used the binary any more; the supported PDF extraction path isprivacyforms.pdf.Remove the remaining add-on-template leftovers:
constraints.txt(it only contained-c constraints_plone52.txt),.travis.ymland.gitlab-ci.yml(both pinned topython:2.7). GitHub Actions is the CI.Ignore the local buildout variant
test-6.2.x.cfgand the localuv.lockin.gitignore(requirements.txtpins the buildout toolchain).SECURITY.mdnow itemises the unfixed security findings with location, impact and recommendation — SSRF viapost_endpoint_url, missing rate limiting, CSV/XLSX formula injection, unaudited exports, token-store atomicity, key length/rotation, missing token/session binding, container hardening, dependency pinning, bot controls, output encoding and CSRF — instead of naming them in prose only.Add
scripts/invalidate_tokens.pyto invalidate the tokens of a leaked CSV export across all surveys (dry run by default,--applyto write).
1.0a7 (unreleased)#
Update the vendored SurveyJS browser bundles, translations, and server-side validator to SurveyJS 3.0.4.
Build the validator without Deno’s 24-hour minimum-dependency-age window, so a deliberately pinned
survey-corerelease compiles immediately instead of failing the CI validator job and install-time builds for a day after each upstream release.Remove a timing race from the KV store TTL expiry test that failed intermittently on loaded CI runners.
Correct the package metadata: the PyPI project URLs now point at the actual repository (
zopyx/zopyx.surveyjs) instead of the never-existingcollective/zopyx.surveyjs, and they document where the documentation and the changelog live. The classifiers now list only the supported platform (Plone 6.2, replacing the obsolete Plone 5.2 entry) and the Python versions that CI actually exercises (3.12, 3.13, 3.14).Add a CI job that installs the built distribution and runs the Plone-free test subset (converters, schema, validator wrapper) on Python 3.12, 3.13 and 3.14, so the advertised Python support is verified rather than assumed.
Remove the legacy Plone 5.2 buildout configuration (
test_plone52.cfg) and thetox.iniinherited from the add-on template, which still targeted Python 2.7/3.7 and Plone 4.3–5.2 and referred to a constraints file that no longer exists.Exclude local-only documentation from the source distribution:
docs/olddescribes removed features,docs/html/docs/_buildare artifacts of a localmake docsrun.Tag pre-releases (
a/b/rc) are published as GitHub pre-releases instead of full releases, so a beta is never offered as the latest release.Ignore generated security-review artifacts and local scratch files (
tokens.csv, the vulnerability report snapshots,tmp/, local agent/tool directories) so they cannot be committed accidentally.
1.0a6 (unreleased)#
Add per-survey SurveyJS theme selection with default theme support.
Add the Theme Manager configlet, theme creation/upload actions, and improved upload and creation button styling with icons.
Improve the theme editor toolbar with semantic action colors and a restore version action icon.
Apply panelless SurveyJS themes correctly in the editor preview.
Show version numbers in the theme history and restore-version dialog.
Update the vendored SurveyJS browser bundles, translations, and server-side validator to SurveyJS 3.0.2.
Fix CI buildout action paths and use Python 3.14 for the Plone environment.
1.0a5 (unreleased)#
Version bump to 1.0a5 to trigger a fresh CI run of all workflows.
1.0a4 (released 2026-08-11)#
Fix: the generated demo site (
scripts/init_plone.py) showed up without any theme.addPloneSitewas called with an invaliddistributionkeyword (the parameter isdistribution_namesince Plone 6.1), so the classic distribution never ran and the Plone Classic theme (Barceloneta) was never applied. Usedistribution_nameand enable thebarcelonetatheme instead ofprivacyforms.theme.
1.0a3 (released 2026-08-11)#
Nothing changed yet.
1.0a2 (released 2026-08-11)#
Nothing changed yet.
1.0a1 (unreleased)#
Initial release. [zopyx]