quiztech-assessment-platform/src/Includes/Ajax/AssessmentAjaxHandler.php

304 lines
No EOL
12 KiB
PHP

<?php
namespace Quiztech\AssessmentPlatform\Includes\Ajax;
use Quiztech\AssessmentPlatform\Includes\Invitations; // Added use statement
/**
* Handles AJAX requests related to the front-end assessment process.
*/
class AssessmentAjaxHandler {
/**
* Constructor. Registers AJAX hooks.
*/
public function __construct() {
add_action('wp_ajax_quiztech_submit_prescreening', [$this, 'handle_submit_prescreening']);
add_action('wp_ajax_quiztech_save_answer', [$this, 'handle_save_answer']);
add_action('wp_ajax_quiztech_submit_assessment', [$this, 'handle_submit_assessment']);
add_action('wp_ajax_nopriv_quiztech_save_answer', [$this, 'handle_save_answer']);
add_action('wp_ajax_nopriv_quiztech_submit_assessment', [$this, 'handle_submit_assessment']);
}
/**
* Initialize the handler.
* Static method to instantiate the class and register hooks.
*/
public static function init() {
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;
}
}
/**
* Handles the AJAX submission of the pre-screening form.
* Expects 'nonce', 'invitation_id', and 'pre_screen_answer' array in $_POST.
*/
public function handle_submit_prescreening() {
// 1. Verify Nonce
check_ajax_referer('quiztech_prescreening_nonce', 'nonce');
// 2. Get and Sanitize Core Data
$invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0;
if ( ! $invitation_id ) {
wp_send_json_error(['message' => __('Missing invitation ID.', '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);
wp_send_json_error(['message' => __('Could not process evaluation record.', 'quiztech')], 500);
}
// 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) {
// Use sanitize_textarea_field as pre-screening questions are currently textareas
$sanitized_answers[sanitize_key($index)] = sanitize_textarea_field(wp_unslash($answer));
}
// 4. Save Answers (as user_evaluation CPT meta)
if (!empty($sanitized_answers)) {
update_post_meta($evaluation_id, 'quiztech_prescreening_answers', $sanitized_answers);
} else {
// Handle case where no answers were submitted? Or rely on form 'required' attribute?
// For now, proceed even if empty.
}
// 5. Update Invitation Status
try {
$invitations = new Invitations();
$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);
// Decide if this should be a user-facing error or just logged
}
} catch (\Exception $e) {
error_log("Quiztech AJAX Error: Exception updating invitation status: " . $e->getMessage());
// Decide if this should be a user-facing error
}
// 6. Send Response
wp_send_json_success(['message' => __('Pre-screening submitted successfully. Starting assessment...', 'quiztech')]);
// Ensure script execution stops
wp_die();
}
/**
* Handles the AJAX auto-save of a single assessment answer.
* Expects 'nonce', 'invitation_id', 'question_id', and 'answer' in $_POST.
*/
public function handle_save_answer() {
// 1. Verify Nonce
check_ajax_referer('quiztech_assessment_nonce', 'nonce');
// 2. Get and Sanitize Data
$invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0;
$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
// 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;
}
}
// 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.
// Using individual meta keys might be simpler for querying later if needed.
$meta_key = 'quiztech_answer_' . $question_id;
update_post_meta($evaluation_id, $meta_key, $sanitized_answer);
// 4. Send Response
wp_send_json_success(['message' => __('Answer saved.', 'quiztech')]);
// Ensure script execution stops
wp_die();
}
/**
* Handles the final AJAX submission of the assessment.
* Expects 'nonce' and 'invitation_id' in $_POST.
*/
public function handle_submit_assessment() {
// 1. Verify Nonce
check_ajax_referer('quiztech_assessment_nonce', 'nonce'); // Reuse assessment nonce
// 2. Get Data
$invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0;
if ( ! $invitation_id ) {
wp_send_json_error(['message' => __('Missing invitation ID.', '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 final submission.");
wp_send_json_error(['message' => __('Could not process evaluation record for submission.', 'quiztech')], 500);
}
// 4. Update Invitation Status
try {
$invitations = new Invitations();
// Assuming $invitation_id passed in POST *is* the record ID.
$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
}
// 5. 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);
}
// 6. Get the Assessment ID associated with the invitation
// This requires a method in Invitations class to get the full record by ID
$invitation_record = $invitations->get_invitation_by_id($invitation_id); // Assuming this method exists or will be added
$assessment_id = $invitation_record ? $invitation_record->assessment_id : 0;
// 7. Get the custom completion message
$completion_message = '';
if ($assessment_id) {
$completion_message = \get_post_meta($assessment_id, '_quiztech_completion_message', true);
}
// Use a default message if the custom one is empty
if (empty($completion_message)) {
$completion_message = __('Assessment submitted successfully!', 'quiztech');
}
// 8. Send Response
wp_send_json_success(['completionMessage' => $completion_message]); // Send completion message
// Ensure script execution stops
wp_die();
}
}