Updated for version 0.9.82+
The Quizzes/Surveys module provides a comprehensive API for quiz and survey authoring, attempt management, grading, analytics, assignment, question pools, and full Bricks Builder integration. Surveys can be decoupled from progress tracking and support optional anonymous responses. This reference documents all public APIs and extension points.
Table of Contents
- Checking Module Status
- Core Services
- Public Helper Functions
- AJAX Endpoints
- Event Hooks
- Bricks Integrations
- JavaScript API
- Database Schema
- Security and Architecture Notes
Checking Module Status
use BaselMedia\BricksMembers\Core\ModuleRegistry;
if ( ModuleRegistry::is_active( 'quiz_system' ) ) {
// Quiz functionality is available.
}
Core Services
QuizService
Namespace: BaselMedia\BricksMembers\Modules\Quiz\QuizService
The main service for quiz operations, grading, and attempt management.
$service = \BaselMedia\BricksMembers\Modules\Quiz\QuizService::get_instance();
Quiz Data Methods
// Get complete quiz payload
$quiz = $service->get_quiz( $quiz_id, $include_correct = true );
// Returns: [ 'id', 'title', 'questions', 'settings', 'question_count' ]
// Get questions only
$questions = $service->get_questions( $quiz_id, $include_correct = true );
// Save questions (sanitizes automatically)
$saved = $service->save_questions( $quiz_id, $questions_array );
// Get/save settings
$settings = $service->get_settings( $quiz_id );
$saved = $service->save_settings( $quiz_id, $settings_array );
// Survey mode helpers
$is_survey = $service->is_survey_mode( $quiz_id ); // true if is_survey enabled
$allow_anon = $service->survey_allows_anonymous( $quiz_id ); // true if allow_anonymous (surveys only)
// Sanitize question payload
$sanitized = $service->sanitize_questions( $questions_array );
Quiz Settings Schema
$default_settings = array(
'is_survey' => false, // Survey mode: decoupled from progress
'allow_anonymous' => false, // Anonymous responses (surveys only)
'passing_score' => 70.0, // 0-100 percentage
'max_attempts' => 0, // 0 = unlimited
'time_limit_seconds' => 0, // 0 = no limit
'randomize_questions' => false,
'randomize_answers' => false,
'show_correct_answers' => 'after_submit', // after_submit|after_pass|never
'question_count' => 0, // 0 = all questions
'question_pool_id' => 0, // 0 = inline questions
'allow_retake_after_pass'=> true,
'allow_retake' => true, // alias
'display_mode' => 'all_at_once', // all_at_once|one_at_a_time
'require_answer_before_next' => false, // one_at_a_time only; gates Next until answered
'auto_progress_on_pass' => true, // mark assigned posts complete (ignored in survey mode)
);
Survey mode: When is_survey is true, the quiz skips assignment-based access checks, does not integrate with progress tracking, and can be placed anywhere. With allow_anonymous, start_attempt() and submit_attempt() accept user_id = 0.
Attempt Lifecycle Methods
// Start or resume an attempt
$start = $service->start_attempt( $user_id, $quiz_id );
// Returns: [ 'attempt_id', 'questions', 'time_limit_seconds', 'time_started_ts', 'status' ]
// Returns: WP_Error if cannot take quiz
// For surveys with allow_anonymous: user_id can be 0
// Submit an attempt
$result = $service->submit_attempt(
$user_id,
$quiz_id,
$attempt_id,
$answers, // array keyed by question_id
$auto_submitted = false
);
// For surveys with allow_anonymous: user_id can be 0
// Returns: [ 'success', 'passed', 'score_percent', 'score_earned', 'score_total',
// 'results', 'pending_review', 'redirect_url' ]
// Check if user can take quiz
$can_take = $service->can_take_quiz( $user_id, $quiz_id );
// Returns: true or WP_Error with reason
// Get computed status
$status = $service->get_status( $user_id, $quiz_id );
// Returns: [ 'status', 'attempt_count', 'attempts_remaining', 'best_score',
// 'last_score', 'passed', 'current_attempt_id' ]
Answer Payload Formats
$answers = array(
// Multiple choice / true-false
'q_abc123' => 'a_2',
// Multiple answer (array of selected IDs)
'q_multi_1' => array( 'a_1', 'a_3' ),
// Matching (keyed pairs)
'q_match_1' => array(
'left_a' => 'right_2',
'left_b' => 'right_1',
),
// Sorting/ordering (ordered array)
'q_sort_1' => array( 'item_2', 'item_1', 'item_3' ),
// Essay/short answer (text)
'q_essay_9' => 'My long-form answer...',
// Fill in the blank
'q_blank_1' => 'photosynthesis',
);
Manual Grading
$graded = $service->grade_manual_answer(
$attempt_id,
$grader_id, // user ID of grader
'q_essay_9', // question ID
7.5, // earned points
'Strong analysis, but needs more citations.' // feedback
);
// Automatically recalculates totals and emits events when all pending items graded
Attempt Query Methods
// Single attempt
$attempt = $service->get_attempt( $attempt_id );
$latest = $service->get_latest_attempt( $user_id, $quiz_id );
$best = $service->get_best_attempt( $user_id, $quiz_id );
// All attempts
$attempts = $service->get_attempts( $user_id, $quiz_id );
// Counts and scores
$count = $service->get_attempt_count( $user_id, $quiz_id );
$best_score = $service->get_best_score( $user_id, $quiz_id );
$has_passed = $service->has_passed_quiz( $user_id, $quiz_id );
// Admin: all attempts for a quiz
$rows = $service->get_quiz_attempts( $quiz_id, $limit = 250 );
// Analytics
$analytics = $service->get_quiz_analytics( $quiz_id );
// Returns: [ 'total_attempts', 'pass_rate', 'avg_score', 'question_stats' => [...] ]
// Management
$reset = $service->reset_attempts( $user_id, $quiz_id );
$deleted = $service->delete_attempt( $attempt_id );
QuizAssignmentService
Namespace: BaselMedia\BricksMembers\Modules\Quiz\QuizAssignmentService
Manages quiz-to-post assignments (both meta-based and legacy rules).
$assign = \BaselMedia\BricksMembers\Modules\Quiz\QuizAssignmentService::get_instance();
Meta-Based Assignment (Recommended)
// Assign quiz to post (use quiz_id=0 to unassign)
$result = $assign->assign_quiz_to_post( $post_id, $quiz_id );
// Get quiz assigned to a post
$quiz_id = $assign->get_assigned_quiz_from_meta( $post_id );
// Get posts assigned to a quiz
$post_ids = $assign->get_assigned_posts_from_meta( $quiz_id );
Combined Lookups (Meta + Legacy Rules)
// Get quiz for post (checks meta first, then rules)
$quiz_id = $assign->get_quiz_for_post( $post_id );
// Get required rule for post
$rule = $assign->get_required_rule_for_post( $post_id );
// Get all posts for a quiz
$post_ids = $assign->get_posts_for_quiz( $quiz_id, $required_only = false );
// Get all rules targeting a post
$rules = $assign->get_rules_for_post( $post_id );
Legacy Rules System (Deprecated for New Code)
$rules = $assign->get_rules();
$saved = $assign->save_rules( $rules_array );
Legacy Rule Schema
array(
'id' => 'qr_abc123',
'quiz_id' => 321,
'target_post_id' => 456,
'post_ids' => array( 456, 457 ),
'trigger' => 'on_completion', // on_completion|on_progress_percent|manual|on_button_click
'trigger_value' => null,
'display_mode' => 'popup', // popup|redirect|inline
'popup_template_id' => 0,
'required' => true,
'mode' => 'required',
'enabled' => true,
)
QuizBankService
Namespace: BaselMedia\BricksMembers\Modules\Quiz\QuizBankService
Manages reusable question pools stored as taxonomy terms.
$bank = \BaselMedia\BricksMembers\Modules\Quiz\QuizBankService::get_instance();
Pool Management
// List all pools
$pools = $bank->get_pools();
// Returns: [ [ 'id', 'name', 'description', 'question_count' ], ... ]
// Get single pool
$pool = $bank->get_pool( $pool_id );
// Create pool
$pool_id = $bank->create_pool( 'Math Pool', 'Reusable arithmetic questions' );
// Delete pool
$deleted = $bank->delete_pool( $pool_id );
Pool Questions
// Save questions to pool
$saved = $bank->save_pool_questions( $pool_id, $questions_array );
// Get all questions from pool
$questions = $bank->get_pool_questions( $pool_id );
// Get questions with optional limit and randomization
$questions = $bank->get_questions_from_pool( $pool_id, $count = 10, $randomize = true );
// Draw random questions (alias)
$questions = $bank->draw_questions( $pool_id, $count );
// Get count
$count = $bank->get_pool_question_count( $pool_id );
QuizSystem
Namespace: BaselMedia\BricksMembers\Modules\Quiz\QuizSystem
Module bootstrap and progress integration. You typically don’t interact with this directly.
// The system hooks into progress filters (no-op when quiz is in survey mode):
// - brm_is_post_truly_completed - requires quiz pass for completion
// - brm_adjacent_structure_item - inserts quiz as next item when not passed
// - brm_toggle_progress_response - injects quiz trigger on completion attempt
// - brm_event_quiz_passed - auto-marks assigned posts complete
Public Helper Functions
Stable public procedural wrappers live in brm-public-api.php.
Quiz Data
brm_get_quiz( int $quiz_id, bool $include_correct = false ): ?array
brm_get_quiz_questions( int $quiz_id, bool $include_correct = false ): array
brm_get_quiz_settings( int $quiz_id ): array
User Status
brm_can_take_quiz( int $user_id, int $quiz_id ): bool|\WP_Error
brm_get_user_quiz_status( int $user_id, int $quiz_id ): array
Attempts
brm_get_quiz_attempts( int $user_id, int $quiz_id ): array
brm_get_latest_quiz_attempt( int $user_id, int $quiz_id ): ?array
Assignment
Assignment is intentionally owned internally by QuizAssignmentService and the admin AJAX handlers. It is not part of the current stable procedural public API.
Admin UI Integration Points
Quiz assignment is available in three places in the admin:
- Quiz Admin Page: Settings tab → Assigned Posts section (assign multiple posts to one quiz)
- Post Editor Meta Box: BricksMembers meta box → Assigned Quiz field (assign one quiz to a post)
- Structure Interface Quick Edit: Quick Edit modal → Assigned Quiz field (fast assignment while managing content structure)
All three methods write through the same QuizAssignmentService owner and store the relationship in the _brm_assigned_quiz_id post meta.
Question Pools
Question pool reads and draws are owned internally by QuizBankService. They are not currently exposed as stable procedural wrappers.
AJAX Endpoints
Quiz-specific endpoints are registered by \BaselMedia\BricksMembers\Ajax\QuizActions::register(). The quiz assignment search UI (post editor meta box and Structure Quick Edit modal) uses the shared search endpoint:
Quiz Search (Admin Assignment UI)
Endpoint: brm_search_posts (from \BaselMedia\BricksMembers\Ajax\SearchActions)
Used by the Assigned Quiz field in the post editor meta box and Structure Quick Edit modal. Type 2+ characters to search quizzes by title.
// GET request
action=brm_search_posts
post_type=brm_quiz
q=search_term
nonce=...
Response: { success: true, data: [ { id, title, label, type } ] }
Returns published and draft quizzes. The UI uses the same result markup as Protected Downloads and Categories & Tags (.brm-search-result, .brm-search-result__title, .brm-search-result__meta).
Frontend Endpoints
Require: logged-in user + BRM_NONCE_FRONTEND (surveys with allow_anonymous accept unauthenticated requests for start/submit)
| Action | Parameters | Returns |
|---|---|---|
brm_quiz_start |
quiz_id |
Attempt payload with questions |
brm_quiz_submit |
quiz_id, attempt_id, answers (JSON or array), auto_submitted (optional) |
Grading result |
brm_quiz_get_status |
quiz_id |
Status object |
brm_refresh_quiz_element |
quiz_id, element_id |
Updated HTML for conditional elements |
Admin Endpoints
Require: manage_options capability + BRM_NONCE_ADMIN or BRM_NONCE_AJAX
| Action | Parameters | Description |
|---|---|---|
brm_quiz_save_questions |
quiz_id, questions (JSON) |
Save quiz questions |
brm_quiz_save_settings |
quiz_id, settings (JSON) |
Save quiz settings |
brm_quiz_save_rules |
rules (JSON) |
Save assignment rules (legacy) |
brm_quiz_assign_to_post |
post_id, quiz_id |
Assign/unassign quiz (quiz_id=0 to unassign) |
brm_quiz_grade_answer |
attempt_id, question_id, earned_points, feedback |
Manually grade a question |
brm_quiz_reset_attempts |
quiz_id, user_id |
Reset all attempts for user+quiz |
brm_quiz_delete_attempt |
attempt_id |
Delete single attempt |
brm_quiz_create_pool |
name, description |
Create question pool |
brm_quiz_delete_pool |
pool_id |
Delete pool |
brm_quiz_get_pool_questions |
pool_id |
Get pool questions for editing |
brm_quiz_save_pool_questions |
pool_id, questions (JSON) |
Save pool questions |
brm_quiz_update_post |
quiz_id, title, status |
Update quiz post |
brm_quiz_duplicate |
quiz_id |
Duplicate quiz, returns new_quiz_id |
brm_quiz_delete |
quiz_id |
Delete quiz |
brm_quiz_get_questions |
quiz_id |
Get questions (for import/export) |
Event Hooks
Quiz events are dispatched as \BaselMedia\BricksMembers\Core\Event objects.
Available Events
| Hook | When Fired | Context Keys |
|---|---|---|
brm_event_quiz_submitted |
After submit_attempt() | quiz_id, attempt_id, score_percent, passed |
brm_event_quiz_passed |
After submit or grading when passed | quiz_id, attempt_id, score_percent, passed=true |
brm_event_quiz_failed |
After submit or grading when failed | quiz_id, attempt_id, score_percent, passed=false |
brm_event_quiz_graded |
After manual grading completes | quiz_id, attempt_id, score_percent, passed, grader_id |
Example: Listen for Quiz Pass
add_action( 'brm_event_quiz_passed', function( \BaselMedia\BricksMembers\Core\Event $event ) {
$user_id = $event->get_user_id();
$quiz_id = (int) $event->get_context_value( 'quiz_id', $event->get_resource_id() );
$score = (float) $event->get_context_value( 'score_percent', 0 );
// Custom automation: CRM sync, badge award, analytics, etc.
error_log( "User $user_id passed quiz $quiz_id with score $score%" );
}, 10, 1 );
Bricks Integrations
Query Types
BRM: Quiz Questions (brm_quiz_questions)
Loops through questions for a quiz.
Controls:
brmQuizQueryId– Quiz ID (supports{post_id})brmQuizQueryLimit– Max questions (0 = all)brmQuizIncludeCorrect– Include correct answers (builder only)
Loop Object:
[
'id' => 'q_abc123',
'type' => 'multiple_choice',
'text' => 'Question prompt text',
'points' => 1,
'answers' => [ ... ],
'explanation' => 'Optional explanation',
'media_id' => 0,
'order' => 0,
'quiz_id' => 123,
'current_index' => 1,
'question_count' => 10,
]
BRM: Quiz Answers (brm_quiz_answers)
Nested loop for answer options. Must be inside a brm_quiz_questions loop. Works for choice, multiple_choice, multiple_answer, true_false, and image_choice question types.
Loop Object:
[
'id' => 'a_xyz789',
'text' => 'Answer text',
'is_correct' => true, // redacted on frontend
'order' => 0,
'index' => 1,
'total' => 4,
'question_id' => 'q_abc123',
'quiz_id' => 123,
'question_type' => 'multiple_choice',
'is_multiple' => false,
'input_type' => 'radio',
'match_target' => '', // for matching questions
'image_id' => 456, // answer image attachment ID
'image_url' => 'https://...', // answer image URL (medium size)
'question_context' => [ ... ], // parent question payload for nested tag resolution
]
Inside nested answer rows, parent question tags like {brm_quiz_q:id} still resolve correctly.
BRM: Quiz Pairs (brm_quiz_pairs)
Nested loop for matching question pairs. Must be inside a brm_quiz_questions loop with a matching type question.
Loop Object:
[
'left' => 'Left side text',
'left_id' => 'left_abc123',
'left_image_id' => 789,
'left_image_url' => 'https://...',
'right' => 'Right side text',
'right_id' => 'right_xyz456',
'right_image_id' => 790,
'right_image_url' => 'https://...',
'order' => 0,
'index' => 1,
'total' => 4,
'question_id' => 'q_abc123',
'quiz_id' => 123,
'question_context' => [ ... ],
]
Inside nested pair rows, parent question tags such as {brm_quiz_q:id} and {brm_quiz_q:text} stay available.
BRM: Quiz Items (brm_quiz_items)
Nested loop for ordering/sorting question items. Must be inside a brm_quiz_questions loop with an ordering or sorting type question.
Loop Object:
[
'id' => 'item_abc123',
'text' => 'Item text',
'correct_position' => 1, // redacted on frontend
'order' => 0, // current/shuffled position
'index' => 1, // 1-based display index
'total' => 5,
'question_id' => 'q_abc123',
'quiz_id' => 123,
'image_id' => 456,
'image_url' => 'https://...',
'question_context' => [ ... ],
]
Inside nested item rows, parent question tags such as {brm_quiz_q:id} stay available.
Bricks Elements
| Element | Name | Purpose |
|---|---|---|
| BRM Quiz | brm-quiz |
Complete quiz with presets, timer, navigation |
| BRM Quiz Answer Selector | brm-quiz-answer-selector |
Clickable answer wrapper for custom designs |
| BRM Quiz Text Answer | brm-quiz-text-answer |
Text input for essay/short answer |
| BRM Quiz Answer Input | brm-quiz-answer-input |
Legacy fixed-style inputs |
Dynamic Tags
Quiz-Level Tags
| Tag | Description |
|---|---|
{brm_quiz_title} | Quiz title |
{brm_quiz_questions} | Questions JSON (for advanced use) |
{brm_quiz_time_limit} | Time limit in seconds |
{brm_quiz_passing_score} | Required passing percentage |
{brm_quiz_max_attempts} | Maximum attempts |
{brm_quiz_score_percent} | User’s best score % |
{brm_quiz_best_score} | User’s best raw score |
{brm_quiz_last_score} | User’s most recent score |
{brm_quiz_passed} | 1 if passed, 0 if not |
{brm_quiz_pass_fail} | passed / failed / not_attempted |
{brm_quiz_status} | Current status string |
{brm_quiz_attempts_count} | Number of attempts made |
{brm_quiz_attempts_remaining} | Remaining attempts (-1 = unlimited) |
{brm_quiz_question_count} | Total question count |
{brm_quiz_current_question_index} | Current question position |
Question Loop Tags (brm_quiz_q:)
| Tag | Description |
|---|---|
{brm_quiz_q:id} | Question ID |
{brm_quiz_q:text} | Question text/prompt |
{brm_quiz_q:type} | Question type |
{brm_quiz_q:points} | Points value |
{brm_quiz_q:media} | Attached media URL |
{brm_quiz_q:index} | 1-based position |
{brm_quiz_q:total} | Total questions |
{brm_quiz_q:explanation} | Answer explanation |
{brm_quiz_q:answer_count} | Number of answers |
{brm_quiz_q:is_multiple} | yes/no if multiple answers allowed |
{brm_quiz_q:is_text_input} | yes/no if text input required |
{brm_quiz_q:input_type} | radio/checkbox/text/textarea/special |
Answer Loop Tags (brm_quiz_a:)
| Tag | Description |
|---|---|
{brm_quiz_a:id} | Answer ID |
{brm_quiz_a:text} | Answer text |
{brm_quiz_a:index} | 1-based position |
{brm_quiz_a:total} | Total answers |
{brm_quiz_a:question_id} | Parent question ID |
{brm_quiz_a:is_correct} | yes/no (builder only) |
{brm_quiz_a:input_name} | Form input name |
{brm_quiz_a:input_value} | Form input value |
{brm_quiz_a:input_type} | HTML input type |
{brm_quiz_a:match_target} | Match target (for matching questions) |
{brm_quiz_a:image} | Answer image URL (medium size) |
{brm_quiz_a:image_id} | Answer image attachment ID |
Pair Loop Tags (brm_quiz_pair:)
| Tag | Description |
|---|---|
{brm_quiz_pair:left} | Left side text |
{brm_quiz_pair:left_id} | Left side ID (for drag/drop) |
{brm_quiz_pair:left_image} | Left side image URL |
{brm_quiz_pair:left_image_id} | Left side image attachment ID |
{brm_quiz_pair:right} | Right side text |
{brm_quiz_pair:right_id} | Right side ID (for drag/drop) |
{brm_quiz_pair:right_image} | Right side image URL |
{brm_quiz_pair:right_image_id} | Right side image attachment ID |
{brm_quiz_pair:index} | 1-based position |
{brm_quiz_pair:total} | Total pairs |
Item Loop Tags (brm_quiz_item:)
| Tag | Description |
|---|---|
{brm_quiz_item:id} | Item ID (for drag/drop) |
{brm_quiz_item:text} | Item text |
{brm_quiz_item:image} | Item image URL |
{brm_quiz_item:image_id} | Item image attachment ID |
{brm_quiz_item:index} | 1-based position |
{brm_quiz_item:total} | Total items |
Bricks Conditions
| Condition Key | Compare | Value Options |
|---|---|---|
brm_quiz_passed | is | Yes / No |
brm_quiz_can_take | is | Yes / No |
brm_quiz_status | is / is not | not_started, in_progress, pending_review, passed, failed, exhausted |
brm_quiz_score_above | is at least | number |
brm_quiz_score | comparisons | number |
brm_quiz_attempted | is | Yes / No |
Form Action
Action: Submit Quiz (BRM) – brm_quiz_submit
Controls:
brmQuizId– Quiz post ID (supports{post_id})brmQuizAttemptField– Hidden field ID for attempt ID (optional)brmQuizAnswersField– Field ID for JSON answers map (optional)
If no answers field is configured, fields prefixed with q_ are automatically collected.
Injected Native Controls
Button Controls:
brmQuizSubmit– Enable quiz submitbrmQuizConfirmMessage– Confirm before submitbrmQuizPassedRedirect– Pass redirect URLbrmQuizFailedRedirect– Fail redirect URL
Div Controls (Timer):
brmQuizTimer/brmQuizTimerEnabled– Enable timerbrmQuizTimerFormat– mm:ss, hh:mm:ss, countdown_textbrmQuizTimerWarning– Warning threshold (seconds)brmQuizTimerAutoSubmit– Auto-submit on timeout
JavaScript API
Global Objects
window.brmQuiz // Core quiz functions
window.brmQuizActions // Alias for brmQuiz
Quiz Control Functions
brmQuizActions.startQuiz(quizIdOrRoot);
brmQuizActions.submitQuiz(quizIdOrRoot);
brmQuizActions.advanceToNextQuestion(quizIdOrRoot);
brmQuizActions.goToPreviousQuestion(quizIdOrRoot);
brmQuizActions.goToQuestion(questionIndex, quizIdOrRoot);
In custom/query-loop builds, these functions are intended to be called from normal Bricks controls (buttons, divs, links, slider navigation wrappers). BRM owns the quiz state and validation; Bricks owns the markup and styling.
Answer Management Functions
brmQuizActions.selectAnswer(questionId, answerId);
brmQuizActions.deselectAnswer(questionId, answerId);
brmQuizActions.toggleAnswer(questionId, answerId);
brmQuizActions.getSelectedAnswers(questionId);
brmQuizActions.getAllSelections();
brmQuizActions.clearSelections(questionId);
brmQuizActions.clearAllSelections();
brmQuizActions.isAnswerSelected(questionId, answerId);
Text Answer Functions
brmQuizActions.getTextAnswer(questionId);
brmQuizActions.setTextAnswer(questionId, value);
brmQuizActions.clearTextAnswer(questionId);
State Functions
brmQuizActions.getQuizState(quizIdOrRoot);
brmQuizActions.setAnswer(questionId, value, quizIdOrRoot);
brmQuizActions.getAnswer(questionId, quizIdOrRoot);
brmQuizActions.getAllAnswers(quizIdOrRoot);
Custom Events
Dispatched on document:
| Event | Detail | When |
|---|---|---|
brm:quiz:started / brm:quizStarted | { quizId, attemptId } | Quiz started |
brm:quiz:submitted / brm:quizSubmitted | { quizId, result } | Quiz submitted |
brm:quiz:passed / brm:quizPassed | { quizId, score } | Quiz passed |
brm:quiz:failed / brm:quizFailed | { quizId, score } | Quiz failed |
brm:quiz:timeout / brm:quizTimeout | { quizId } | Timer expired |
brm:quiz:question-changed | { quizId, index } | Question navigation |
brm:quiz:answer:selected | { questionId, answerId, isMultiple } | Answer selected |
brm:quiz:answer:deselected | { questionId, answerId } | Answer deselected |
brm:quiz:text:changed | { questionId, value } | Text answer changed |
brm:quiz:advance:requested | { quizId } | Next requested |
brm:quiz:previous:requested | { quizId } | Previous requested |
brm:quiz:submit:requested | { quizId, selections } | Submit requested |
brm:quiz:frontend:ready | {} | Frontend initialized |
brm:quiz:error | { quizId, error } | Error occurred |
brm:quiz:trigger | { quiz_id, display_mode, popup_id } | Quiz triggered (from progress) |
brm:quiz:required | { quiz_id } | Quiz required (inline mode) |
brm:quiz:order:changed | { questionId, order } | Ordering items reordered |
brm:quiz:match:made | { questionId, itemId, targetId } | Matching pair matched |
Drag-Drop for Matching and Ordering Questions
The quiz frontend provides native drag-drop support for matching and ordering questions. This works on both desktop (mouse) and mobile (touch) without any external libraries.
Required HTML Structure
For matching questions:
<div class="brm-quiz-matching">
<div class="brm-quiz-matching-left">
<div class="brm-quiz-drag-item" draggable="true" data-item-id="item1">Item 1</div>
<div class="brm-quiz-drag-item" draggable="true" data-item-id="item2">Item 2</div>
</div>
<div class="brm-quiz-matching-right">
<div class="brm-quiz-drop-zone" data-question-id="q1" data-target-id="target1">Target 1</div>
<div class="brm-quiz-drop-zone" data-question-id="q1" data-target-id="target2">Target 2</div>
</div>
</div>
For ordering questions:
<div class="brm-quiz-sorting-container" data-question-id="q2">
<div class="brm-quiz-drag-item" draggable="true" data-item-id="item1">First</div>
<div class="brm-quiz-drag-item" draggable="true" data-item-id="item2">Second</div>
<div class="brm-quiz-drag-item" draggable="true" data-item-id="item3">Third</div>
</div>
A direct-child layout is the simplest option, but custom/query-loop markup may also wrap each .brm-quiz-drag-item in a single outer container. BRM ordering now resolves the sortable node from the sorting container branch instead of requiring perfectly flat markup.
CSS Classes for Styling
| Class | Applied When |
|---|---|
.brm-quiz-drag-item--dragging | Item is being dragged |
.brm-quiz-drag-item--drop-target | Item is a valid drop target (ordering) |
.brm-quiz-drag-item--placed | Item has been matched (matching) |
.brm-quiz-drop-zone--over | Drop zone is being hovered |
.brm-quiz-placed-item | Placed item clone inside drop zone |
Custom Events
| Event | Detail | When |
|---|---|---|
brm:quiz:order:changed | { questionId, order } | Ordering items reordered |
brm:quiz:match:made | { questionId, itemId, targetId } | Matching pair matched |
Using with Bricks Interactions
All quiz navigation and actions are built using native Bricks Interactions. This is the primary integration point for building custom quiz UIs.
How to Set Up an Interaction
- Select any element (Button, Div, etc.) in Bricks Builder
- Click the Interactions icon (lightning bolt) in the panel header
- Click + to add a new interaction
- Configure:
- Trigger: Click (or other event)
- Action: JavaScript (Function)
- Function name:
brmQuizActions.submitQuiz(no parentheses)
- Preview on frontend (interactions don’t run in the builder)
Function Reference for Interactions
| Purpose | Function Name | Arguments |
|---|---|---|
| Start Quiz | brmQuizActions.startQuiz | None (auto-detects quiz) |
| Submit Quiz | brmQuizActions.submitQuiz | None (auto-detects quiz) |
| Next Question | brmQuizActions.advanceToNextQuestion | None |
| Previous Question | brmQuizActions.goToPreviousQuestion | None |
| Jump to Question | brmQuizActions.goToQuestion | Question index (use Arguments repeater) |
| Set Answer | brmQuizActions.setAnswer | questionId, value |
| Open Quiz Popup | brmQuizActions.openPopup | Popup ID or Quiz ID (optional) |
| Close Quiz Popup | brmQuizActions.closePopup | Popup ID or Quiz ID (optional) |
| Get Quiz State | brmQuizActions.getQuizState | Quiz ID (optional) |
| Get Answer | brmQuizActions.getAnswer | questionId, quizId (optional) |
| Get All Answers | brmQuizActions.getAllAnswers | Quiz ID (optional) |
Quiz Root Container Requirements
Every custom quiz uses a root container marked with data-brm-quiz-root (the BRM Quiz element adds this automatically). Quiz context is auto-resolved from the current page: no manual quiz ID is required.
| Attribute | Required | Description |
|---|---|---|
data-brm-quiz-root | Yes | Marks the quiz root (added by BRM Quiz element) |
data-brm-quiz-state | No | Initial state: not_started, in_progress, passed, failed |
data-brm-quiz-display-mode | No | all_at_once or one_at_a_time |
data-brm-quiz-time-limit | No | Time limit in seconds (0 for no limit) |
data-brm-quiz-attempt-id | No | Current attempt ID (auto-populated) |
data-brm-quiz-popup | No | Quiz ID for popup targeting (for popup templates) |
Popup Integration
The quiz system integrates with Bricks’ native popup system:
// Open popup programmatically
brmQuizActions.openPopup(popupId); // Opens specific popup by template ID
brmQuizActions.openPopup(quizId); // Opens popup marked with data-brm-quiz-popup="{quizId}"
brmQuizActions.openPopup(); // Opens any popup with data-brm-quiz-popup attribute
// Close popup
brmQuizActions.closePopup(popupId);
brmQuizActions.closePopup(); // Closes currently open quiz popup
// These map to Bricks' bricksOpenPopup() and bricksClosePopup()
For quiz popups triggered from progress checkbox, mark your popup template’s quiz root with:
<div data-brm-quiz-root data-brm-quiz-popup="{post_id}">
...quiz content...
</div>
Quiz Trigger Flow
When a progress checkbox triggers a required quiz:
- AJAX handler returns
quiz_triggerpayload with display_mode and popup_id - Frontend dispatches
brm:quiz:triggerevent - Based on display_mode:
- popup: Calls
bricksOpenPopup()with popup_id or searches for matching popup - redirect: Navigates to quiz_url
- inline: Scrolls to quiz element and dispatches
brm:quiz:required
- popup: Calls
// Quiz trigger payload structure
{
trigger: 'quiz',
quiz_id: 123,
post_id: 456, // Lesson ID
quiz_url: '/quiz/...',
required: true,
display_mode: 'popup', // popup, redirect, or inline
popup_id: 789, // Bricks popup template ID (for popup mode)
rule_id: 'rule_abc',
status: 'not_started',
passed: false
}
Passing Arguments
For functions requiring arguments, use the Arguments repeater in the Interaction panel. Each argument can be a static value or a dynamic tag like {brm_quiz_q:index}.
For complex cases, create a wrapper function:
window.goToSpecificQuestion = function(brxParam) {
// brxParam.target gives you the clicked element
const index = brxParam.target.dataset.questionIndex;
brmQuizActions.goToQuestion(parseInt(index, 10));
};
Combining with Other Actions
Interactions can chain multiple actions. Common pattern:
Interaction 1:
Trigger: Click
Action: Set attribute (class: is-loading)
Target: Self
Interaction 2:
Trigger: Click
Action: JavaScript (Function)
Function: brmQuizActions.submitQuiz
Listening to Quiz Events
React to quiz events in custom JavaScript:
document.addEventListener('brm:quiz:answer:selected', function(e) {
console.log('Selected:', e.detail.answerId, 'for', e.detail.questionId);
});
document.addEventListener('brm:quiz:submitted', function(e) {
if (e.detail.result.passed) {
// Custom celebration
}
});
Database Schema
Table: {prefix}brm_quiz_attempts
| Column | Type | Description |
|---|---|---|
id | BIGINT UNSIGNED | Primary key |
quiz_id | BIGINT UNSIGNED | Quiz post ID |
user_id | BIGINT UNSIGNED | WordPress user ID |
attempt_no | INT UNSIGNED | Attempt number (1-based) |
attempt_number | INT UNSIGNED | Alias for attempt_no |
status | VARCHAR(20) | in_progress / submitted |
grading_status | VARCHAR(20) | graded / pending_review |
passed | TINYINT(1) | 0 or 1 |
score_earned | DECIMAL(10,2) | Points earned |
score_total | DECIMAL(10,2) | Total possible points |
score_percent | DECIMAL(5,2) | Percentage score |
passing_score | DECIMAL(5,2) | Required passing % |
answers | LONGTEXT | JSON of submitted answers |
results | LONGTEXT | JSON of per-question results |
metadata | LONGTEXT | JSON of additional data |
started_at | DATETIME | Attempt start time |
submitted_at | DATETIME | Submission time |
graded_at | DATETIME | Final grading time |
time_started | INT UNSIGNED | Unix timestamp (compat) |
time_finished | INT UNSIGNED | Unix timestamp (compat) |
time_elapsed_seconds | INT UNSIGNED | Duration in seconds |
Indexes
idx_quiz_user– (quiz_id, user_id)idx_user_quiz_status– (user_id, quiz_id, status)idx_quiz_passed– (quiz_id, passed)idx_grading_status– (grading_status)idx_time_finished– (time_finished)
Post Meta
_brm_quiz_questions– Quiz questions (JSON)_brm_quiz_settings– Quiz settings (JSON)_brm_assigned_quiz_id– Quiz ID assigned to a post_brm_quiz_assigned_posts– Post IDs assigned to a quiz (array)
Taxonomy
brm_question_pool– Question pool taxonomy (attached tobrm_quiz)- Term meta:
_brm_pool_questions– Questions JSON
Security and Architecture Notes
- All quiz mutations flow through namespaced services in
src/Modules/Quiz/ - Public procedural helpers are thin wrappers only
- AJAX handlers use
AjaxHandlers::verify_ajax_request()with rate limiting - Input parsing uses
Security::input_*andSecurity::json_decode_post() - Mutation paths invalidate caches and call
MutationContracthooks - Correct answers are redacted from frontend payloads by default
- Admin endpoints require
manage_optionscapability