Quizzes API

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

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)

ActionParametersReturns
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

ActionParametersDescription
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

HookWhen FiredContext 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

ElementNamePurpose
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

TagDescription
{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:)

TagDescription
{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:)

TagDescription
{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:)

TagDescription
{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:)

TagDescription
{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 KeyCompareValue Options
brm_quiz_passedisYes / No
brm_quiz_can_takeisYes / No
brm_quiz_statusis / is notnot_started, in_progress, pending_review, passed, failed, exhausted
brm_quiz_score_aboveis at leastnumber
brm_quiz_scorecomparisonsnumber
brm_quiz_attemptedisYes / 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 submit
  • brmQuizConfirmMessage – Confirm before submit
  • brmQuizPassedRedirect – Pass redirect URL
  • brmQuizFailedRedirect – Fail redirect URL

Div Controls (Timer):

  • brmQuizTimer / brmQuizTimerEnabled – Enable timer
  • brmQuizTimerFormat – mm:ss, hh:mm:ss, countdown_text
  • brmQuizTimerWarning – 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:

EventDetailWhen
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

ClassApplied When
.brm-quiz-drag-item--draggingItem is being dragged
.brm-quiz-drag-item--drop-targetItem is a valid drop target (ordering)
.brm-quiz-drag-item--placedItem has been matched (matching)
.brm-quiz-drop-zone--overDrop zone is being hovered
.brm-quiz-placed-itemPlaced item clone inside drop zone

Custom Events

EventDetailWhen
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

  1. Select any element (Button, Div, etc.) in Bricks Builder
  2. Click the Interactions icon (lightning bolt) in the panel header
  3. Click + to add a new interaction
  4. Configure:
    • Trigger: Click (or other event)
    • Action: JavaScript (Function)
    • Function name: brmQuizActions.submitQuiz (no parentheses)
  5. Preview on frontend (interactions don’t run in the builder)

Function Reference for Interactions

PurposeFunction NameArguments
Start QuizbrmQuizActions.startQuizNone (auto-detects quiz)
Submit QuizbrmQuizActions.submitQuizNone (auto-detects quiz)
Next QuestionbrmQuizActions.advanceToNextQuestionNone
Previous QuestionbrmQuizActions.goToPreviousQuestionNone
Jump to QuestionbrmQuizActions.goToQuestionQuestion index (use Arguments repeater)
Set AnswerbrmQuizActions.setAnswerquestionId, value
Open Quiz PopupbrmQuizActions.openPopupPopup ID or Quiz ID (optional)
Close Quiz PopupbrmQuizActions.closePopupPopup ID or Quiz ID (optional)
Get Quiz StatebrmQuizActions.getQuizStateQuiz ID (optional)
Get AnswerbrmQuizActions.getAnswerquestionId, quizId (optional)
Get All AnswersbrmQuizActions.getAllAnswersQuiz 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.

AttributeRequiredDescription
data-brm-quiz-rootYesMarks the quiz root (added by BRM Quiz element)
data-brm-quiz-stateNoInitial state: not_started, in_progress, passed, failed
data-brm-quiz-display-modeNoall_at_once or one_at_a_time
data-brm-quiz-time-limitNoTime limit in seconds (0 for no limit)
data-brm-quiz-attempt-idNoCurrent attempt ID (auto-populated)
data-brm-quiz-popupNoQuiz 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:

  1. AJAX handler returns quiz_trigger payload with display_mode and popup_id
  2. Frontend dispatches brm:quiz:trigger event
  3. 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
// 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

ColumnTypeDescription
idBIGINT UNSIGNEDPrimary key
quiz_idBIGINT UNSIGNEDQuiz post ID
user_idBIGINT UNSIGNEDWordPress user ID
attempt_noINT UNSIGNEDAttempt number (1-based)
attempt_numberINT UNSIGNEDAlias for attempt_no
statusVARCHAR(20)in_progress / submitted
grading_statusVARCHAR(20)graded / pending_review
passedTINYINT(1)0 or 1
score_earnedDECIMAL(10,2)Points earned
score_totalDECIMAL(10,2)Total possible points
score_percentDECIMAL(5,2)Percentage score
passing_scoreDECIMAL(5,2)Required passing %
answersLONGTEXTJSON of submitted answers
resultsLONGTEXTJSON of per-question results
metadataLONGTEXTJSON of additional data
started_atDATETIMEAttempt start time
submitted_atDATETIMESubmission time
graded_atDATETIMEFinal grading time
time_startedINT UNSIGNEDUnix timestamp (compat)
time_finishedINT UNSIGNEDUnix timestamp (compat)
time_elapsed_secondsINT UNSIGNEDDuration 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 to brm_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_* and Security::json_decode_post()
  • Mutation paths invalidate caches and call MutationContract hooks
  • Correct answers are redacted from frontend payloads by default
  • Admin endpoints require manage_options capability

Related Documentation

Early Bird Deal

Start Building Your Membership Site Today

Create, sell, and manage your content without limits. BricksMembers gives you everything you need to build membership and LMS sites with Bricks Builder.

Lifetime updates & bug fixes • Premium support • 0% transaction fees • 60-day money-back guarantee