feat: Implement Phase 2, Step 7 - WP Admin Interfaces
This commit is contained in:
parent
130b9eefb9
commit
00892b36c9
6 changed files with 793 additions and 45 deletions
|
|
@ -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' );
|
||||
|
||||
|
|
|
|||
282
src/Admin/AdminListTables.php
Normal file
282
src/Admin/AdminListTables.php
Normal 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
170
src/Admin/SettingsPage.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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' );
|
||||
|
||||
?>
|
||||
Loading…
Add table
Reference in a new issue