feat: Implement Phase 2, Step 7 - WP Admin Interfaces

This commit is contained in:
Ruben Ramirez 2025-04-03 16:30:09 -05:00
parent 130b9eefb9
commit 00892b36c9
6 changed files with 793 additions and 45 deletions

View file

@ -120,8 +120,17 @@ function quiztech_init() {
// Initialize AJAX handler for assessment interactions
\Quiztech\AssessmentPlatform\Includes\Ajax\AssessmentAjaxHandler::init();
// Initialize Admin-specific features
if ( is_admin() ) {
$admin_list_tables = new \Quiztech\AssessmentPlatform\Admin\AdminListTables();
$admin_list_tables->register_hooks();
$settings_page = new \Quiztech\AssessmentPlatform\Admin\SettingsPage();
$settings_page->register_hooks();
}
// TODO: Instantiate other core classes and call their init methods here
// e.g., Admin menu handler, AJAX handlers, Shortcode handlers etc.
// e.g., Shortcode handlers etc.
}
add_action( 'plugins_loaded', 'quiztech_init' );

View file

@ -0,0 +1,282 @@
<?php
namespace Quiztech\AssessmentPlatform\Admin;
/**
* Customizes the WP Admin list tables for Quiztech CPTs.
*/
class AdminListTables {
/**
* Register hooks for customizing admin list tables.
*/
public function register_hooks() {
// Question CPT Columns
add_filter( 'manage_question_posts_columns', [ $this, 'filter_question_columns' ] );
add_action( 'manage_question_posts_custom_column', [ $this, 'render_question_custom_columns' ], 10, 2 );
// Assessment CPT Columns
add_filter( 'manage_assessment_posts_columns', [ $this, 'filter_assessment_columns' ] );
add_action( 'manage_assessment_posts_custom_column', [ $this, 'render_assessment_custom_columns' ], 10, 2 );
// Job CPT Columns
add_filter( 'manage_job_posts_columns', [ $this, 'filter_job_columns' ] );
add_action( 'manage_job_posts_custom_column', [ $this, 'render_job_custom_columns' ], 10, 2 );
// User Evaluation CPT Columns
add_filter( 'manage_user_evaluation_posts_columns', [ $this, 'filter_user_evaluation_columns' ] );
add_action( 'manage_user_evaluation_posts_custom_column', [ $this, 'render_user_evaluation_custom_columns' ], 10, 2 );
}
// --- Question CPT ---
/**
* Filters the columns for the Question CPT list table.
*
* @param array $columns Existing columns.
* @return array Modified columns.
*/
public function filter_question_columns( $columns ) {
// Add 'Question Type' column
// Keep 'Categories' (added by taxonomy registration)
// Example: Rearrange or add new columns
$new_columns = [];
foreach ($columns as $key => $title) {
$new_columns[$key] = $title;
if ($key === 'title') {
$new_columns['question_type'] = __( 'Question Type', 'quiztech' );
}
}
// If taxonomy column isn't added automatically, add it here:
// if (!isset($new_columns['taxonomy-quiztech_category'])) {
// $new_columns['taxonomy-quiztech_category'] = __('Categories', 'quiztech');
// }
return $new_columns;
}
/**
* Renders the content for custom columns in the Question CPT list table.
*
* @param string $column_name The name of the column to render.
* @param int $post_id The ID of the current post.
*/
public function render_question_custom_columns( $column_name, $post_id ) {
if ( 'question_type' === $column_name ) {
$question_type = get_post_meta( $post_id, '_quiztech_question_type', true );
echo esc_html( ucwords( str_replace( '-', ' ', $question_type ?: 'N/A' ) ) );
}
}
// --- Assessment CPT ---
/**
* Filters the columns for the Assessment CPT list table.
*
* @param array $columns Existing columns.
* @return array Modified columns.
*/
public function filter_assessment_columns( $columns ) {
$new_columns = [];
foreach ($columns as $key => $title) {
$new_columns[$key] = $title;
if ($key === 'title') {
$new_columns['num_questions'] = __( 'No. Questions', 'quiztech' );
$new_columns['credit_cost'] = __( 'Credit Cost', 'quiztech' );
}
}
// Ensure Categories column exists
// if (!isset($new_columns['taxonomy-quiztech_category'])) {
// $new_columns['taxonomy-quiztech_category'] = __('Categories', 'quiztech');
// }
return $new_columns;
}
/**
* Renders the content for custom columns in the Assessment CPT list table.
*
* @param string $column_name The name of the column to render.
* @param int $post_id The ID of the current post.
*/
public function render_assessment_custom_columns( $column_name, $post_id ) {
switch ( $column_name ) {
case 'num_questions':
// TODO: Implement logic to get question count (e.g., from '_quiztech_question_ids' meta)
$question_ids = get_post_meta( $post_id, '_quiztech_question_ids', true );
echo is_array( $question_ids ) ? count( $question_ids ) : '0';
break;
case 'credit_cost':
// TODO: Implement logic to calculate credit cost based on associated questions
echo 'N/A'; // Placeholder
break;
}
}
// --- Job CPT ---
/**
* Filters the columns for the Job CPT list table.
*
* @param array $columns Existing columns.
* @return array Modified columns.
*/
public function filter_job_columns( $columns ) {
$new_columns = [];
// Keep standard columns like title, date
foreach ($columns as $key => $title) {
if ($key === 'date') continue; // Move date to end
$new_columns[$key] = $title;
if ($key === 'title') {
$new_columns['associated_assessment'] = __( 'Assessment', 'quiztech' );
$new_columns['job_status'] = __( 'Status', 'quiztech' );
$new_columns['invitations_sent'] = __( 'Invites Sent', 'quiztech' );
$new_columns['evaluations_completed'] = __( 'Evaluations Completed', 'quiztech' );
}
}
$new_columns['date'] = $columns['date']; // Add date back at the end
return $new_columns;
}
/**
* Renders the content for custom columns in the Job CPT list table.
*
* @param string $column_name The name of the column to render.
* @param int $post_id The ID of the current post.
*/
public function render_job_custom_columns( $column_name, $post_id ) {
global $wpdb;
$invitation_table = $wpdb->prefix . 'quiztech_invitations';
switch ( $column_name ) {
case 'associated_assessment':
// TODO: Need meta field '_quiztech_associated_assessment_id'
$assessment_id = get_post_meta( $post_id, '_quiztech_associated_assessment_id', true );
if ( $assessment_id && $assessment_title = get_the_title( $assessment_id ) ) {
// Optional: Link to assessment edit screen
$edit_link = get_edit_post_link( $assessment_id );
if ($edit_link) {
echo '<a href="' . esc_url( $edit_link ) . '">' . esc_html( $assessment_title ) . '</a>';
} else {
echo esc_html( $assessment_title );
}
} else {
echo 'N/A';
}
break;
case 'job_status':
// TODO: Need meta field '_quiztech_job_status'
$status = get_post_meta( $post_id, '_quiztech_job_status', true );
echo esc_html( ucwords( $status ?: 'N/A' ) );
break;
case 'invitations_sent':
$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$invitation_table} WHERE job_id = %d", $post_id ) );
echo esc_html( $count ?: '0' );
break;
case 'evaluations_completed':
// This requires linking evaluations back to jobs, likely via the invitation ID stored in evaluation meta
$invitation_ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM {$invitation_table} WHERE job_id = %d", $post_id ) );
if (empty($invitation_ids)) {
echo '0';
break;
}
$args = [
'post_type' => 'user_evaluation',
'post_status' => 'completed', // Assuming 'completed' is the status set on final submit
'posts_per_page' => -1, // Count all
'meta_query' => [
[
'key' => 'quiztech_invitation_id', // Assumes this meta key links evaluation to invitation
'value' => $invitation_ids,
'compare' => 'IN',
]
],
'fields' => 'ids', // Only need count
];
$evaluation_query = new \WP_Query($args);
echo esc_html( $evaluation_query->post_count );
break;
}
}
// --- User Evaluation CPT ---
/**
* Filters the columns for the User Evaluation CPT list table.
*
* @param array $columns Existing columns.
* @return array Modified columns.
*/
public function filter_user_evaluation_columns( $columns ) {
// Remove 'title' maybe, replace with more useful info
unset($columns['title']);
$new_columns = [
'cb' => $columns['cb'], // Checkbox
'applicant_email' => __( 'Applicant Email', 'quiztech' ),
'job_title' => __( 'Job', 'quiztech' ),
'assessment_title' => __( 'Assessment', 'quiztech' ),
'status' => __( 'Status', 'quiztech' ),
'date_submitted' => __( 'Date Submitted', 'quiztech' ), // Use 'date' column key?
];
// Add back any other standard columns if needed, like 'date' if not used for 'date_submitted'
// $new_columns['date'] = $columns['date'];
return $new_columns;
}
/**
* Renders the content for custom columns in the User Evaluation CPT list table.
*
* @param string $column_name The name of the column to render.
* @param int $post_id The ID of the current post.
*/
public function render_user_evaluation_custom_columns( $column_name, $post_id ) {
global $wpdb;
$invitation_table = $wpdb->prefix . 'quiztech_invitations';
// Get linked invitation ID first, as it's needed for multiple columns
$invitation_id = get_post_meta( $post_id, 'quiztech_invitation_id', true );
$invitation_data = null;
if ($invitation_id) {
$invitation_data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$invitation_table} WHERE id = %d", $invitation_id ) );
}
switch ( $column_name ) {
case 'applicant_email':
echo $invitation_data ? esc_html( $invitation_data->applicant_email ) : 'N/A';
break;
case 'job_title':
if ($invitation_data && $invitation_data->job_id) {
$job_title = get_the_title($invitation_data->job_id);
$edit_link = get_edit_post_link( $invitation_data->job_id );
if ($edit_link) {
echo '<a href="' . esc_url( $edit_link ) . '">' . esc_html( $job_title ) . '</a>';
} else {
echo esc_html( $job_title );
}
} else {
echo 'N/A';
}
break;
case 'assessment_title':
if ($invitation_data && $invitation_data->assessment_id) {
$assessment_title = get_the_title($invitation_data->assessment_id);
$edit_link = get_edit_post_link( $invitation_data->assessment_id );
if ($edit_link) {
echo '<a href="' . esc_url( $edit_link ) . '">' . esc_html( $assessment_title ) . '</a>';
} else {
echo esc_html( $assessment_title );
}
} else {
echo 'N/A';
}
break;
case 'status':
$post_status_obj = get_post_status_object( get_post_status( $post_id ) );
echo esc_html( $post_status_obj ? $post_status_obj->label : get_post_status( $post_id ) );
break;
case 'date_submitted':
// Use the post date as submission date
echo esc_html( get_the_date( '', $post_id ) );
break;
}
}
}

170
src/Admin/SettingsPage.php Normal file
View file

@ -0,0 +1,170 @@
<?php
namespace Quiztech\AssessmentPlatform\Admin;
/**
* Handles the Quiztech plugin settings page in WP Admin.
*/
class SettingsPage {
/**
* Option group name.
* @var string
*/
private $option_group = 'quiztech_settings';
/**
* Option name in wp_options table.
* @var string
*/
private $option_name = 'quiztech_settings';
/**
* Settings page slug.
* @var string
*/
private $page_slug = 'quiztech-settings-page';
/**
* Register hooks for the settings page.
*/
public function register_hooks() {
add_action( 'admin_menu', [ $this, 'add_admin_page' ] );
add_action( 'admin_init', [ $this, 'register_settings' ] );
}
/**
* Adds the submenu page under the main Settings menu.
*/
public function add_admin_page() {
add_options_page(
__( 'Quiztech Settings', 'quiztech' ), // Page Title
__( 'Quiztech', 'quiztech' ), // Menu Title
'manage_options', // Capability Required
$this->page_slug, // Menu Slug
[ $this, 'render_settings_page' ] // Callback function to render the page
);
}
/**
* Registers settings, sections, and fields using the Settings API.
*/
public function register_settings() {
register_setting(
$this->option_group, // Option group
$this->option_name, // Option name
[ $this, 'sanitize_settings' ] // Sanitization callback
);
// Stripe Section
add_settings_section(
'quiztech_stripe_section', // Section ID
__( 'Stripe API Keys', 'quiztech' ), // Section Title
'__return_false', // Section callback (optional description)
$this->page_slug // Page slug where section appears
);
// Stripe Public Key Field
add_settings_field(
'quiztech_stripe_public_key', // Field ID
__( 'Stripe Public Key', 'quiztech' ), // Field Title
[ $this, 'render_text_field' ], // Field render callback
$this->page_slug, // Page slug
'quiztech_stripe_section', // Section ID
[ // Arguments for callback
'id' => 'quiztech_stripe_public_key',
'option_name' => $this->option_name,
'key' => 'stripe_public_key',
'description' => __( 'Enter your Stripe publishable API key.', 'quiztech' )
]
);
// Stripe Secret Key Field
add_settings_field(
'quiztech_stripe_secret_key', // Field ID
__( 'Stripe Secret Key', 'quiztech' ), // Field Title
[ $this, 'render_text_field' ], // Field render callback
$this->page_slug, // Page slug
'quiztech_stripe_section', // Section ID
[ // Arguments for callback
'id' => 'quiztech_stripe_secret_key',
'option_name' => $this->option_name,
'key' => 'stripe_secret_key',
'type' => 'password', // Mask the input
'description' => __( 'Enter your Stripe secret API key. This is kept confidential.', 'quiztech' )
]
);
// TODO: Add more sections/fields as needed (e.g., email settings, default credit costs)
}
/**
* Renders the main settings page container and form.
*/
public function render_settings_page() {
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form action="options.php" method="post">
<?php
// Output security fields for the registered setting group
settings_fields( $this->option_group );
// Output setting sections and their fields
do_settings_sections( $this->page_slug );
// Output save settings button
submit_button( __( 'Save Settings', 'quiztech' ) );
?>
</form>
</div>
<?php
}
/**
* Renders a standard text input field for a setting.
* Expects args: 'id', 'option_name', 'key', 'description' (optional), 'type' (optional, default 'text')
*
* @param array $args Arguments passed from add_settings_field.
*/
public function render_text_field( $args ) {
$options = get_option( $args['option_name'], [] ); // Get all settings or default to empty array
$value = isset( $options[ $args['key'] ] ) ? $options[ $args['key'] ] : '';
$type = isset( $args['type'] ) ? $args['type'] : 'text';
?>
<input
type="<?php echo esc_attr( $type ); ?>"
id="<?php echo esc_attr( $args['id'] ); ?>"
name="<?php echo esc_attr( $args['option_name'] . '[' . $args['key'] . ']' ); ?>"
value="<?php echo esc_attr( $value ); ?>"
class="regular-text"
/>
<?php if ( isset( $args['description'] ) ) : ?>
<p class="description"><?php echo esc_html( $args['description'] ); ?></p>
<?php endif; ?>
<?php
}
/**
* Sanitizes the settings array before saving.
*
* @param array $input The raw input array from the form.
* @return array The sanitized array.
*/
public function sanitize_settings( $input ) {
$sanitized_input = [];
if ( isset( $input['stripe_public_key'] ) ) {
// Basic sanitization, might need stricter validation (e.g., regex for pk_live_/pk_test_)
$sanitized_input['stripe_public_key'] = sanitize_text_field( $input['stripe_public_key'] );
}
if ( isset( $input['stripe_secret_key'] ) ) {
// Basic sanitization, might need stricter validation (e.g., regex for sk_live_/sk_test_)
$sanitized_input['stripe_secret_key'] = sanitize_text_field( $input['stripe_secret_key'] );
}
// TODO: Sanitize other settings as they are added
return $sanitized_input;
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace Quiztech\\AssessmentPlatform\\Includes\\Ajax;
namespace Quiztech\AssessmentPlatform\Includes\Ajax;
/**
* Handles AJAX requests related to the front-end assessment process.
@ -24,6 +24,67 @@ class AssessmentAjaxHandler {
new self();
}
/**
* Helper method to find an existing user_evaluation CPT by invitation ID
* or create a new one if it doesn't exist.
*
* @param int $invitation_id The database ID of the invitation record.
* @return int The post ID of the user_evaluation CPT, or 0 on failure.
*/
private function get_or_create_user_evaluation(int $invitation_id): int {
if ( ! $invitation_id ) {
error_log("Quiztech AJAX Error: get_or_create_user_evaluation called with invalid invitation ID: " . $invitation_id);
return 0;
}
$args = [
'post_type' => 'user_evaluation',
'post_status' => 'any', // Find it regardless of status initially
'meta_query' => [
[
'key' => 'quiztech_invitation_id',
'value' => $invitation_id,
'compare' => '=',
]
],
'posts_per_page' => 1,
'fields' => 'ids', // Only get the ID
];
$evaluation_posts = get_posts($args);
if ( ! empty( $evaluation_posts ) ) {
// Found existing evaluation
return $evaluation_posts[0];
} else {
// Not found, create a new one
$post_data = [
'post_type' => 'user_evaluation',
'post_status' => 'publish', // Start as published (or maybe 'pending'/'in-progress' if custom statuses are added)
'post_title' => sprintf( __( 'Evaluation for Invitation #%d', 'quiztech' ), $invitation_id ),
'post_content' => '', // No content needed initially
// 'post_author' => ?? // Assign to an admin or system user? Default is current user (likely none in AJAX)
];
$evaluation_id = wp_insert_post( $post_data, true ); // Pass true for WP_Error on failure
if ( is_wp_error( $evaluation_id ) ) {
error_log("Quiztech AJAX Error: Failed to create user_evaluation CPT for invitation ID {$invitation_id}: " . $evaluation_id->get_error_message());
return 0;
}
// Add the linking meta field
$meta_updated = update_post_meta( $evaluation_id, 'quiztech_invitation_id', $invitation_id );
if ( ! $meta_updated ) {
// Log error, but maybe don't fail the whole request? Or should we delete the post?
error_log("Quiztech AJAX Warning: Failed to add quiztech_invitation_id meta to new evaluation ID {$evaluation_id} for invitation ID {$invitation_id}.");
// Depending on requirements, might return 0 here or proceed. Let's proceed for now.
}
error_log("Quiztech AJAX Info: Created new user_evaluation CPT ID {$evaluation_id} for invitation ID {$invitation_id}.");
return $evaluation_id;
}
}
// AJAX handler methods will be added below:
// - handle_submit_prescreening()
// - handle_save_answer()
@ -43,32 +104,14 @@ class AssessmentAjaxHandler {
wp_send_json_error(['message' => __('Missing invitation ID.', 'quiztech')], 400);
}
// --- TODO: Ensure user_evaluation CPT is created earlier (e.g., in FrontendHandler) ---
// For now, query for it based on invitation ID (assuming meta field 'quiztech_invitation_id' exists on evaluation)
$args = [
'post_type' => 'user_evaluation',
'post_status' => 'any', // Find it regardless of status
'meta_query' => [
[
'key' => 'quiztech_invitation_id', // Assumed meta key linking evaluation to invitation
'value' => $invitation_id,
'compare' => '=',
]
],
'posts_per_page' => 1,
'fields' => 'ids', // Only get the ID
];
$evaluation_posts = get_posts($args);
$evaluation_id = !empty($evaluation_posts) ? $evaluation_posts[0] : 0;
// 3. Get or Create User Evaluation Record
$evaluation_id = $this->get_or_create_user_evaluation($invitation_id);
if ( ! $evaluation_id ) {
// If not found, something is wrong with the flow (should have been created earlier)
error_log("Quiztech AJAX Error: Could not find user_evaluation CPT for invitation ID: " . $invitation_id);
wp_send_json_error(['message' => __('Could not find associated evaluation record.', 'quiztech')], 404);
error_log("Quiztech AJAX Error: Failed to get or create user_evaluation for invitation ID: " . $invitation_id);
wp_send_json_error(['message' => __('Could not process evaluation record.', 'quiztech')], 500);
}
// --- End TODO section ---
// 3. Sanitize Submitted Answers
// 4. Sanitize Submitted Answers
$submitted_answers = isset($_POST['pre_screen_answer']) && is_array($_POST['pre_screen_answer']) ? $_POST['pre_screen_answer'] : [];
$sanitized_answers = [];
foreach ($submitted_answers as $index => $answer) {
@ -87,7 +130,9 @@ class AssessmentAjaxHandler {
// 5. Update Invitation Status
try {
$invitations = new \Quiztech\AssessmentPlatform\Includes\Invitations();
// TODO: Create the update_status method in Invitations class
// Note: The update_status method expects the invitation *record ID*, not the token.
// We need to retrieve the invitation ID based on the token if we only have the token here.
// Assuming $invitation_id passed in POST *is* the record ID for now. If it's the token, this needs adjustment.
$updated = $invitations->update_status($invitation_id, 'pre-screening-complete');
if (!$updated) {
error_log("Quiztech AJAX Error: Failed to update invitation status for ID: " . $invitation_id);
@ -119,16 +164,62 @@ class AssessmentAjaxHandler {
$question_id = isset($_POST['question_id']) ? absint($_POST['question_id']) : 0;
$answer = isset($_POST['answer']) ? wp_unslash($_POST['answer']) : ''; // Sanitize based on question type later
// TODO: Get the associated user_evaluation CPT ID based on invitation_id (similar to pre-screening handler)
$evaluation_id = 0; // Placeholder
if ( ! $invitation_id || ! $question_id || ! $evaluation_id ) {
wp_send_json_error(['message' => __('Invalid request data for saving answer.', 'quiztech')], 400);
// Basic validation for required IDs before querying
if ( ! $invitation_id || ! $question_id ) {
wp_send_json_error(['message' => __('Missing required data for saving answer.', 'quiztech')], 400);
}
// 3. Get or Create User Evaluation Record
$evaluation_id = $this->get_or_create_user_evaluation($invitation_id);
if ( ! $evaluation_id ) {
error_log("Quiztech AJAX Error: Failed to get or create user_evaluation for invitation ID: " . $invitation_id . " during answer save.");
wp_send_json_error(['message' => __('Could not process evaluation record for saving answer.', 'quiztech')], 500);
}
// 4. Fetch the question type meta for the given question ID
$question_type = \get_post_meta($question_id, '_quiztech_question_type', true);
if ( ! $question_type ) {
// Log if type is missing, but proceed with default sanitization
error_log("Quiztech AJAX Warning: Missing question type meta for question ID: " . $question_id);
$question_type = 'text'; // Default to text if not set
}
// Sanitize the answer based on question type
$sanitized_answer = ''; // Initialize
if (is_array($answer)) {
// Handle array answers (likely checkboxes)
if ('checkbox' === $question_type) {
// Sanitize each value in the array
$sanitized_answer = array_map('sanitize_text_field', $answer);
// Note: update_post_meta can handle arrays directly, storing them serialized.
} else {
// Unexpected array answer for this question type
error_log("Quiztech AJAX Error: Received array answer for non-checkbox question ID: " . $question_id);
// Sanitize by joining elements (simple approach, might need refinement)
$sanitized_answer = sanitize_text_field(implode(', ', $answer));
}
} else {
// Handle string/scalar answers
switch ($question_type) {
case 'textarea':
$sanitized_answer = sanitize_textarea_field($answer);
break;
case 'numeric':
// Allow integers and potentially floats. Use floatval for broader acceptance.
// Ensure it's actually numeric before casting to avoid warnings/errors.
$sanitized_answer = is_numeric($answer) ? floatval($answer) : 0;
break;
case 'multiple-choice': // Assuming the value is a simple key/identifier
$sanitized_answer = sanitize_key($answer);
break;
case 'text':
default: // Default to sanitize_text_field for 'text' or unknown/missing types
$sanitized_answer = sanitize_text_field($answer);
break;
}
}
// TODO: Fetch question type for $question_id to apply appropriate sanitization to $answer
// Example: if type is 'text', use sanitize_textarea_field; if 'multiple-choice', check against allowed values.
$sanitized_answer = sanitize_text_field($answer); // Basic sanitization for now
// 3. Save Answer (as user_evaluation CPT meta)
// Use a meta key structure like 'quiztech_answer_{question_id}' or store in a single array meta field.
@ -153,19 +244,47 @@ class AssessmentAjaxHandler {
// 2. Get Data
$invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0;
// TODO: Get the associated user_evaluation CPT ID based on invitation_id (similar to other handlers)
$evaluation_id = 0; // Placeholder
if ( ! $invitation_id || ! $evaluation_id ) {
wp_send_json_error(['message' => __('Invalid request data for final submission.', 'quiztech')], 400);
if ( ! $invitation_id ) {
wp_send_json_error(['message' => __('Missing invitation ID.', 'quiztech')], 400);
}
// 3. Update Invitation Status
// TODO: Call Invitations->update_status($invitation_id, 'assessment-complete');
// 3. Get or Create User Evaluation Record
$evaluation_id = $this->get_or_create_user_evaluation($invitation_id);
if ( ! $evaluation_id ) {
error_log("Quiztech AJAX Error: Failed to get or create user_evaluation for invitation ID: " . $invitation_id . " during final submission.");
wp_send_json_error(['message' => __('Could not process evaluation record for submission.', 'quiztech')], 500);
}
// 4. Update Invitation Status
try {
$invitations = new \Quiztech\AssessmentPlatform\Includes\Invitations();
// Note: The update_status method expects the invitation *record ID*, not the token.
// We need to retrieve the invitation ID based on the token if we only have the token here.
// Assuming $invitation_id passed in POST *is* the record ID for now. If it's the token, this needs adjustment.
$updated = $invitations->update_status($invitation_id, 'assessment-complete');
if (!$updated) {
error_log("Quiztech AJAX Error: Failed to update invitation status to complete for ID: " . $invitation_id);
// Decide if this should be a user-facing error or just logged
}
} catch (\Exception $e) {
error_log("Quiztech AJAX Error: Exception updating invitation status to complete: " . $e->getMessage());
// Decide if this should be a user-facing error
}
// 4. Update User Evaluation CPT Status to 'completed'
$post_update_data = [
'ID' => $evaluation_id,
'post_status' => 'completed', // Use a custom status if needed, but 'completed' seems appropriate
];
$post_updated = wp_update_post($post_update_data, true); // Pass true for WP_Error object on failure
if (is_wp_error($post_updated)) {
error_log("Quiztech AJAX Error: Failed to update user_evaluation CPT status for ID {$evaluation_id}: " . $post_updated->get_error_message());
// Decide if this should be a user-facing error
// wp_send_json_error(['message' => __('Failed to finalize assessment record.', 'quiztech')], 500);
}
// 4. Update User Evaluation CPT Status
// TODO: Update post status of $evaluation_id to 'completed' using wp_update_post()
// 5. Send Response
// TODO: Consider adding a redirect URL or specific completion message to the response data

View file

@ -133,4 +133,60 @@ class Invitations {
return $invitation; // Return the invitation data object if valid
}
/**
* Update the status of an invitation record.
*
* @param int $invitation_id The ID of the invitation record to update.
* @param string $new_status The new status to set (e.g., 'pre-screening-complete', 'assessment-complete', 'expired').
* @return bool True on successful update, false on failure or invalid input.
*/
public function update_status( $invitation_id, $new_status ) {
global $wpdb;
$table_name = $wpdb->prefix . 'quiztech_invitations';
// Validate input
$invitation_id = absint( $invitation_id );
$new_status = sanitize_text_field( $new_status ); // Basic sanitization
if ( ! $invitation_id || empty( $new_status ) ) {
\error_log( 'Quiztech Error: Invalid input provided to update_status.' );
return false;
}
// Define allowed statuses to prevent arbitrary values
$allowed_statuses = [
'pending',
'viewed', // Optional status if needed
'pre-screening-complete',
'assessment-started', // Optional status
'assessment-complete',
'expired',
'cancelled', // Optional status
];
if ( ! in_array( $new_status, $allowed_statuses, true ) ) {
\error_log( "Quiztech Error: Invalid status '{$new_status}' provided to update_status for invitation ID {$invitation_id}." );
return false;
}
// Prepare data and format for update
$data = [ 'status' => $new_status ];
$where = [ 'id' => $invitation_id ]; // Assuming 'id' is the primary key column name
$format = [ '%s' ]; // Format for data
$where_format = [ '%d' ]; // Format for where clause
$updated = $wpdb->update( $table_name, $data, $where, $format, $where_format );
if ( false === $updated ) {
\error_log( "Quiztech Error: Failed to update invitation status for ID {$invitation_id}. DB Error: " . $wpdb->last_error );
return false;
}
// $updated contains the number of rows affected.
// Return true if one or more rows were updated (or potentially 0 if the status was already set to the new value).
// We consider 0 rows affected as success in the case the status was already correct.
return true;
}
}

View file

@ -10,7 +10,7 @@ namespace Quiztech\AssessmentPlatform\Includes;
// If this file is called directly, abort.
if ( ! \defined( 'WPINC' ) ) {
\die;
die;
}
/**
@ -172,4 +172,116 @@ function quiztech_register_post_types() {
}
\add_action( 'init', __NAMESPACE__ . '\quiztech_register_post_types' );
// --- Meta Box for Question Type ---
/**
* Adds the meta box container for Question Type.
*
* @param string $post_type The post type slug.
*/
function quiztech_add_question_meta_boxes( $post_type ) {
// Limit meta box to specific post type
if ( 'question' === $post_type ) {
\add_meta_box(
'quiztech_question_type_metabox', // ID
\__( 'Question Type', 'quiztech' ), // Title
__NAMESPACE__ . '\quiztech_render_question_type_metabox', // Callback function
'question', // Post type
'side', // Context (normal, side, advanced)
'high' // Priority (high, core, default, low)
);
}
}
\add_action( 'add_meta_boxes', __NAMESPACE__ . '\quiztech_add_question_meta_boxes' );
/**
* Renders the meta box content for Question Type.
*
* @param \WP_Post $post The post object.
*/
function quiztech_render_question_type_metabox( $post ) {
// Add a nonce field so we can check for it later.
\wp_nonce_field( 'quiztech_save_question_type_meta', 'quiztech_question_type_nonce' );
// Use get_post_meta to retrieve an existing value from the database.
$value = \get_post_meta( $post->ID, '_quiztech_question_type', true );
// Define the available question types
$question_types = [
'text' => \__( 'Text (Single Line)', 'quiztech' ),
'textarea' => \__( 'Text Area (Multi-line)', 'quiztech' ),
'multiple-choice' => \__( 'Multiple Choice (Single Answer)', 'quiztech' ),
'checkbox' => \__( 'Checkboxes (Multiple Answers)', 'quiztech' ),
'numeric' => \__( 'Numeric', 'quiztech' ),
// Add more types as needed
];
// Display the form field.
echo '<label for="quiztech_question_type_field">';
\esc_html_e( 'Select the type of question:', 'quiztech' );
echo '</label> ';
echo '<select name="quiztech_question_type_field" id="quiztech_question_type_field" class="postbox">';
echo '<option value="">' . \esc_html__( '-- Select Type --', 'quiztech' ) . '</option>'; // Default empty option
foreach ( $question_types as $type_key => $type_label ) {
echo '<option value="' . \esc_attr( $type_key ) . '" ' . \selected( $value, $type_key, false ) . '>' . \esc_html( $type_label ) . '</option>';
}
echo '</select>';
}
/**
* Saves the meta box data for Question Type.
*
* @param int $post_id The ID of the post being saved.
*/
function quiztech_save_question_type_meta( $post_id ) {
// Check if our nonce is set.
if ( ! isset( $_POST['quiztech_question_type_nonce'] ) ) {
return;
}
// Verify that the nonce is valid.
if ( ! \wp_verify_nonce( \sanitize_key( $_POST['quiztech_question_type_nonce'] ), 'quiztech_save_question_type_meta' ) ) {
return;
}
// If this is an autosave, our form has not been submitted, so we don't want to do anything.
if ( \defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// Check the user's permissions.
if ( isset( $_POST['post_type'] ) && 'question' === $_POST['post_type'] ) {
if ( ! \current_user_can( 'edit_post', $post_id ) ) {
return;
}
} else {
// Assuming other post types don't use this meta box
return;
}
// Make sure that the field is set.
if ( ! isset( $_POST['quiztech_question_type_field'] ) ) {
return;
}
// Sanitize user input.
$new_meta_value = \sanitize_text_field( \wp_unslash( $_POST['quiztech_question_type_field'] ) );
// Define allowed types again for validation
$allowed_types = ['text', 'textarea', 'multiple-choice', 'checkbox', 'numeric']; // Keep this in sync with the render function
// Update the meta field in the database if the value is allowed or empty.
if ( in_array( $new_meta_value, $allowed_types, true ) || '' === $new_meta_value ) {
\update_post_meta( $post_id, '_quiztech_question_type', $new_meta_value );
} else {
// Optionally delete meta if invalid value submitted, or log an error
\delete_post_meta( $post_id, '_quiztech_question_type' );
}
}
// Hook into the 'save_post' action specifically for the 'question' post type
\add_action( 'save_post_question', __NAMESPACE__ . '\quiztech_save_question_type_meta' );
?>