The destination is a local survey-question catalog that can replace the Roper iPoll API. This plugin still serves search and item views from Roper. Every successful API response also upserts a private WordPress post so the catalog fills as people search.
Phases
- Fill (this release).
Roper_Apistays the live client. After a successful decode it firesprc_roper_questions_receivedorprc_roper_question_received. Missing ids enqueueprc_roper_hydrate_question. Complete payloads upsert immediately. - Read-through (later).
Store::get()andto_item_view()/to_search_hit()are the seam. A later change can prefer a complete local row and fall back to Roper on miss. - Cut over (later). Stop calling Roper when the local corpus and a local search index can stand alone.
WP_Queryon title is not Roper keyword search.
Fill phase
Data shape
One post per Roper question UUID. Completeness is stub (search hit) or complete (question endpoint). Upsert never downgrades complete to stub.
Identity is post_name plus _roper_id. _roper_payload holds the raw JSON for that source so later readers do not depend on a second mapped copy.
Post meta
-
_roper_idstring required -
Exact Roper UUID.
-
_roper_completenessenum required -
stub or complete.
-
_roper_payloadstring required -
Raw Roper JSON for the search hit or full question object.
-
_roper_sourceenum required -
Which endpoint last wrote the row.
-
_roper_fetched_atdate required -
ISO-8601 timestamp of the last write.
-
_roper_study_titlestring -
Study title from the payload.
-
_roper_start_datestring -
Interview start from the API, not display format.
-
_roper_end_datestring -
Interview end from the API, not display format.
-
_roper_expand_countnumber -
Public Roper Item expand hits. Incremented by POST /roper/expand. Never created from a miss.
No fields match this filter.
The CPT slug is survey-question. It is not public, not queryable, not in REST, and has no rewrite rules. show_ui is on for staff with manage_options. All Survey Questions uses the shared prc-wp-admin-dataview shell (Survey_Question_List). The edit screen shows a read-only Roper data metabox from Survey_Question_Store::from_post_id(). Custom Fields support is off because the stored keys are protected (_roper_*). The datasets taxonomy can apply to these posts. Survey_Question_Type adds the CPT through prc_platform__datasets_enabled_post_types. The main front-end datasets term query drops survey-question from the post types. REST and secondary queries keep the requested types so Dataset Options can list catalog rows. If that leaves no types, the query uses sentinel prc-roper-excluded so WordPress does not query every type. The edit screen does not show the hierarchical datasets metabox. Ingest owns the relationship.
Dataset_Resolver maps a question to an existing datasets term. It never creates terms. Slug order is /dataset/{slug} from sourceURL (including /{team}/dataset/{slug}/), then sanitize_title() of the stripped study title. /datasets/ (the CPT archive) is not a term slug. A miss is allowed. wp_set_object_terms() receives an integer term_id only. When a term matches and the question has an archive id, ingest writes _roper_archive_id onto the related dataset post from Term Data Store. It does not create dataset posts. Dataset Options lists those questions through GET /roper/dataset/{id}/questions (a Fill in prc-datasets/options-after).
to_search_hit() and to_item_view() add database (term name, or the stripped study title) and database_link (term link, else study.doi, else https://doi.org/10.25940/ROPER-{archiveId}). Item view also adds url from sourceURL. Stored terms win when post_id is set. Live mapping still returns database and database_link without a store row.
Store API
Survey_Question_Store is the future read API. Callers do not write post meta themselves.
get( string $id ): ?Survey_Questionfind_many( string[] $ids ): array<string, Survey_Question>from_post_id( int $post_id ): ?Survey_Questionupsert( Survey_Question $question ): intrecord_expand( string $id ): int— increment_roper_expand_count. A miss orroper-results-previewis a no-op and returns0.
Survey_Question::from_search_hit() and from_question() parse at the store boundary, including archive id, source URL, and DOI. to_search_hit() and to_item_view() return the arrays the blocks already consume from Roper_Api.
Ingest
Search does not insert 10–20 posts during page render. One find_many() runs on the request. Missing and stub ids enqueue unique Action Scheduler jobs in group prc-roper. Each job writes the search stub if needed, then calls get_question(), which fires prc_roper_question_received and upserts complete. That action does not enqueue more work. The cached search hit is optional. A missing cache still stores a stub keyed by id.
If Action Scheduler is missing, ingest writes the search stub only and skips the extra HTTP on the request.
Public Roper Item expand (actions.openQuestion when context.open is false) POSTs /prc-api/v3/roper/expand. Collapse, editor preview, and non-collapsible items do not increment the count.
-
class-roper-api.php -
class-survey-question.php -
class-dataset-resolver.php -
class-survey-question-type.php -
class-survey-question-store.php -
class-survey-question-ingest.php -
class-survey-question-list.php -
inspector-sidebar-panel/class-inspector-sidebar-panel.php
Related
- User guide
- REST API — live Roper proxy routes and
POST /roper/expand. The CPT is not a public REST type.