PRC Quiz Builder
A block-first, interactive quiz system for the PRC platform. Authors compose quizzes using Gutenberg blocks powered by the WordPress Interactivity API. Supports classic quizzes, typology clustering, embeddable usage, and community group results.
by Seth Rubenstein · github.com/pewresearch/prc-quiz-builder · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/pewresearch/prc-quiz-builder/archive/refs/heads/trunk.zipReadme
PRC Quiz Builder
Canonical docs: docs/plugins/prc-quiz-builder/
An interactive, block-based quiz system for the PRC platform.
Overview
Quiz Builder provides a custom quiz post type and a suite of Gutenberg blocks backed by the WordPress Interactivity API. Authors compose quizzes in the block editor using a Controller → Pages → Page → Question → Answer hierarchy; the Controller block injects all runtime state into the Interactivity API context and coordinates submission, scoring, and results display on the frontend. The plugin supports three quiz modes (scored quiz, typology/clustering, freeform), three display types (paged, scrollable, and fluid), community group results, and iframe-embeddable views.
Firebase Realtime Database is the persistence layer for archetype (result) records and community groups. WordPress post meta tracks submission analytics.
Dependencies
- Upstream:
prc-platform-core(provides\PRC\Platform\Firebaseand theprc_api_endpoints,prc_platform_on_post_init, andprc_iframe_contenthooks),prc-research-teams(optional — registers team-prefixed quiz URLs viaprc_research_teams_rewrite_config), WP Consent API (wp_add_cookie_info) - Downstream: Internal analytics tooling queries the
_submissionsREST field on thequizpost type; any page embedding a quiz via/embed/or/iframe/URLs
Architecture
The plugin bootstraps through includes/class-plugin.php, which loads all dependencies, registers the quiz CPT, sets up rewrite rules, and instantiates each block class. Blocks are loaded from build/ (production) or src/ (local/dev), controlled by wp_get_environment_type().
Archetype lookups are cached in the WordPress object cache (prc_quiz_builder_archetypes group, 1-day TTL) and backed by Firebase. Community groups are stored only in Firebase; Groups::get_group() reads from Firebase.
The Controller block's render_callback is the key server/client bridge: it writes all runtime context (quizId, quizType, displayType, configuredDisplayType, allowSubmissions, groupId, archetype, etc.) into data-wp-context and wires up all Interactivity API directives. configuredDisplayType is an immutable copy of the author's displayType setting; displayType may be rewritten at runtime (e.g. fluid → scrollable on narrow viewports). Core/buttons blocks that carry specific CSS classes (prc-quiz-start-button, prc-quiz-next-page-button, prc-quiz-submit-button, etc.) have data-wp-on--click attributes injected server-side via WP_HTML_Tag_Processor.
Key Files
| Path | Purpose |
|---|---|
prc-quiz-builder.php |
Plugin entry point; defines PRC_QUIZ_FILE, PRC_QUIZ_DIR, PRC_QUIZ_VERSION constants |
includes/class-plugin.php |
Core orchestrator — loads deps, registers CPT, rewrite rules, query vars, cookies, and all blocks |
includes/class-archetypes.php |
Firebase CRUD for archetype (result hash) records; object-cache layer |
includes/class-groups.php |
Firebase CRUD for community groups |
includes/class-rest-api.php |
REST endpoint registration and handlers; contains the $rest_disabled kill switch |
includes/class-analytics.php |
_report post meta schema and submission counter; exposes _submissions REST field |
includes/class-ability-categories.php |
Registers the quiz WP Abilities category for MCP discovery |
includes/class-ability.php |
WP Abilities API prc-quiz-builder/get-analytics tool (submissions + groups; MCP + REST) |
includes/class-cli-report.php |
WP-CLI wp prc quiz report — ad hoc read/update of _report meta |
includes/class-cli-build-audience.php |
WP-CLI wp prc quiz build-group-owners-audience — thin wrapper around Audience_Service |
includes/class-audience-service.php |
Shared build / list / delete for quiz group-owners audiences (CLI + REST) |
includes/class-loader.php |
Hook registration queue |
includes/class-block-supports.php |
CPT-scoped inserter filtering (allowed_block_types_all), Quiz Builder category, editor-support enqueue |
includes/editor-support/ |
Unregisters quiz core block variations outside the quiz CPT editor |
includes/inspector-sidebar-panel/ |
Block editor plugin: quiz analytics, group analytics, and group-creators audience panel |
src/controller/class-controller.php |
Controller block — server render, Interactivity API context injection, button directive patching |
src/controller/view.js |
Controller Interactivity API store — display-type resolution, submission, page visibility, navigation |
src/results/class-results.php |
Results block server render |
src/group-results/ |
Group results block (view script + create-group action) |
src/embeddable/ |
Embeddable block for cross-site reuse |
build/ |
Compiled JS/CSS/asset manifests for all blocks |
Blocks
| Block | Namespace | Role |
|---|---|---|
| Controller | prc-quiz/controller |
Root block; owns all Interactivity API state and submission flow |
| Pages | prc-quiz/pages |
Wrapper for a multi-page quiz |
| Page | prc-quiz/page |
A single page; contains questions and arbitrary content |
| Question | prc-quiz/question |
Single-choice, multiple-choice, or thermometer; supports randomization |
| Answer | prc-quiz/answer |
Answer choice with optional correctness, points, and label |
| Results | prc-quiz/results |
Container rendered after quiz completion |
| Result Score | prc-quiz/result-score |
Displays the participant's score |
| Result Table | prc-quiz/result-table |
Tabular results view; supports demographic breaks |
| Result Histogram | prc-quiz/result-histogram |
Score distribution histogram |
| Group Results | prc-quiz/group-results |
Community group aggregate results; required to enable group creation |
| Progress Bar | prc-quiz/progress-bar |
Linear bar or per-question circles showing completion and outcomes |
| Embeddable | prc-quiz/embeddable |
Reuse a quiz across other posts; edits propagate to all embeds |
Quiz blocks appear in the block inserter only when editing the quiz post type, grouped under the Quiz Builder category (prc-quiz slug). The Quiz embeddable block (prc-quiz/embeddable) remains available on other post types for synced cross-post reuse.
Knowledge quiz features (type: quiz)
These options apply only when the Controller type is quiz (not typology or freeform).
Live feedback and answer lock
Enable Live Feedback on the Controller block. After a participant selects an answer on a single-choice or thermometer question, the UI shows whether the choice is correct or incorrect and locks further changes on that question. Multiple-choice questions stay open so participants can select every required answer before locking.
Live feedback is implemented in src/answer/view.js via context.liveFeedback and state.isQuestionLocked.
Answer correctness tri-state
Knowledge-quiz answers use a tri-state correct attribute:
| Value | Meaning | Toolbar label |
|---|---|---|
true |
Correct answer | Correct Answer |
null |
Neutral / "Not sure" | Not Sure |
false |
Incorrect answer | Incorrect Answer |
The toolbar control lives in src/answer/correct-toggle.js. Freeform quizzes hide the control.
Question outcome
getQuestionOutcome() (src/controller/question-outcome.js) resolves each question to correct, incorrect, unsure, or unanswered:
- Correct — selected UUIDs exactly match every answer marked
correct: true - Unsure — only
correct: nullanswers are selected (no wrong or right answers) - Incorrect — any other non-empty selection
- Unanswered — no selection
The Results block exposes a Correct / Incorrect block bit (prc-quiz-builder/question-outcome-label) that prints the active question outcome from context. Labels come from the Quiz Controller (Correct / Incorrect / Not sure outcome label settings) and can be overridden per bit in the bit insert/edit popover. Empty bit fields inherit the quiz-wide defaults.
Any core/paragraph or core/heading that contains this bit receives the class has-prc-quiz-question-outcome and the interactive class is-awaiting-selection while the question is unanswered, so the whole host block stays hidden until the user has a selection.
Progress Circles use the same outcome logic for per-question marks.
Exclusive score buckets
The Controller Score Buckets panel stores a JSON catalog of named, non-overlapping closed ranges ([min, max] inclusive; shared endpoints count as overlap). Validation runs in the editor via validateExclusiveBuckets() (src/controller/score-buckets.js).
At results time, matchScoreBucket() returns the first bucket containing the participant's score. Insert the Matching Score Bucket block bit (prc-quiz-builder/matching-score-bucket) in the Results block to print the matched label.
Progress bar variations
prc-quiz/progress-bar has two block variations:
| Variation | Class | Behavior |
|---|---|---|
| Progress Bar (default) | (none) | Linear fill showing percent of questions answered |
| Progress Circles | is-style-circles |
One circle per question — check, x, or question mark by outcome |
Circles reflect live outcomes when Live Feedback is enabled; otherwise they show answered vs unanswered.
Display Types and Frontend Behavior
The Controller block's displayType attribute accepts paged, scrollable, or fluid. Runtime behavior is implemented in src/controller/view.js and src/page/view.js.
Fluid resolution
A fluid quiz resolves to a concrete display mode by viewport width (782px breakpoint, matching the block editor's mobile preview). Resolution runs on init and re-runs on window resize via actions.applyDisplayType (data-wp-on-async-window--resize on the Controller when displayType is fluid):
| Viewport | Resolved displayType |
Notes |
|---|---|---|
< 782px |
scrollable |
Next-page button wrappers hidden within the quiz container |
>= 782px |
paged |
Next-page button wrappers shown; one page at a time |
configuredDisplayType retains the original fluid value so client logic can distinguish a fluid quiz that resolved to scrollable from a natively scrollable quiz, and so resize handlers can re-resolve without losing the fluid configuration.
Page visibility (displayPages)
The Pages block binds hidden to !state.displayPages (src/pages/class-pages.php). Visibility rules:
| Configuration | During quiz | Results or group-results URL |
|---|---|---|
paged |
Pages visible | Pages hidden |
fluid → paged (desktop) |
Pages visible | Pages hidden |
scrollable (native) |
Pages always visible | Pages always visible |
fluid → scrollable (mobile) |
Pages visible | Pages hidden (matches paged) |
Native scrollable quizzes keep pages visible on results URLs so inline, submit-as-you-go results can render below the questions. Fluid quizzes on mobile hide pages when landing on a results URL so users are not dropped at the top of the question stack.