-
+ $question_id ) : ?>
post_type ) {
@@ -125,7 +135,11 @@ if ( ! $invitation_data || ! $current_step ) {
$question_title = get_the_title( $question_post );
$question_type = get_post_meta( $question_id, 'question_type', true );
?>
-
+
+
+
+
+
-
+
+
+
+
+
' . esc_html__( 'Could not load questions for this assessment.', 'quiztech' ) . '';
diff --git a/src/Admin/AssessmentMetaboxes.php b/src/Admin/AssessmentMetaboxes.php
index 27216bd..051c345 100644
--- a/src/Admin/AssessmentMetaboxes.php
+++ b/src/Admin/AssessmentMetaboxes.php
@@ -18,6 +18,7 @@ class AssessmentMetaboxes {
public function register_hooks() {
add_action( 'add_meta_boxes', [ $this, 'add_assessment_metaboxes' ] );
add_action( 'save_post_assessment', [ $this, 'save_linked_questions_meta' ] );
+ add_action( 'save_post_assessment', [ $this, 'save_completion_message_meta' ] ); // Added hook for completion message save
}
/**
@@ -32,9 +33,18 @@ class AssessmentMetaboxes {
\__( 'Linked Questions', 'quiztech' ),
[ $this, 'render_linked_questions_metabox' ],
'assessment',
- 'normal', // Context below editor
+ 'normal', // Context
'high'
);
+ // Added metabox for completion message
+ \add_meta_box(
+ 'quiztech_completion_message_metabox',
+ \__( 'Completion Message', 'quiztech' ),
+ [ $this, 'render_completion_message_metabox' ],
+ 'assessment',
+ 'normal', // Context
+ 'low' // Priority
+ );
}
}
@@ -94,7 +104,6 @@ class AssessmentMetaboxes {
if ( ! isset( $_POST['quiztech_linked_questions_nonce'] ) || ! \wp_verify_nonce( \sanitize_key( $_POST['quiztech_linked_questions_nonce'] ), 'quiztech_save_linked_questions_meta' ) ) { return; }
if ( \defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; }
if ( ! \current_user_can( 'edit_post', $post_id ) ) { return; }
- // No need to check post type here, as the action is specific ('save_post_assessment')
// Process submitted IDs
$submitted_ids = [];
@@ -106,4 +115,45 @@ class AssessmentMetaboxes {
// Update meta (even if empty array, to clear previous selections)
\update_post_meta( $post_id, '_quiztech_linked_question_ids', $submitted_ids );
}
+
+ /**
+ * Renders the meta box content for the completion message.
+ *
+ * @param \WP_Post $post The post object.
+ */
+ public function render_completion_message_metabox( $post ) {
+ \wp_nonce_field( 'quiztech_save_completion_message_meta', 'quiztech_completion_message_nonce' );
+ $completion_message = \get_post_meta( $post->ID, '_quiztech_completion_message', true );
+
+ echo '
' . \esc_html__( 'Enter the message to display to applicants after they successfully submit this assessment. Basic HTML is allowed.', 'quiztech' ) . '
';
+ echo '
';
+ }
+
+ /**
+ * Saves the meta box data for the completion message.
+ *
+ * @param int $post_id The ID of the post being saved.
+ */
+ public function save_completion_message_meta( $post_id ) {
+ // Basic checks (nonce, autosave, permissions)
+ if ( ! isset( $_POST['quiztech_completion_message_nonce'] ) || ! \wp_verify_nonce( \sanitize_key( $_POST['quiztech_completion_message_nonce'] ), 'quiztech_save_completion_message_meta' ) ) { return; }
+ if ( \defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; }
+ if ( ! \current_user_can( 'edit_post', $post_id ) ) { return; }
+
+ // Process submitted message
+ $new_message = '';
+ if ( isset( $_POST['quiztech_completion_message_field'] ) ) {
+ // Allow basic HTML like links, paragraphs, bold, italics
+ $allowed_html = [
+ 'a' => [ 'href' => [], 'title' => [], 'target' => [] ],
+ 'br' => [],
+ 'em' => [],
+ 'strong' => [],
+ 'p' => [],
+ ];
+ $new_message = \wp_kses( wp_unslash( $_POST['quiztech_completion_message_field'] ), $allowed_html );
+ }
+
+ \update_post_meta( $post_id, '_quiztech_completion_message', $new_message );
+ }
}
\ No newline at end of file
diff --git a/src/Includes/Ajax/AssessmentAjaxHandler.php b/src/Includes/Ajax/AssessmentAjaxHandler.php
index 2268691..3a909a4 100644
--- a/src/Includes/Ajax/AssessmentAjaxHandler.php
+++ b/src/Includes/Ajax/AssessmentAjaxHandler.php
@@ -2,296 +2,301 @@
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']);
- }
+ /**
+ * 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']);
+ }
- /**
- * Initialize the handler.
- * Static method to instantiate the class and register hooks.
- */
- public static function init() {
- new self();
- }
+ /**
+ * 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;
- }
+ /**
+ * 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);
+ $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 ( ! 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;
- }
+ 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.
- }
+ // 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;
- }
- }
+ 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()
- // - handle_submit_assessment()
+ /**
+ * 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');
- /**
- * 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);
+ }
- // 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);
+ }
- // 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. 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.
+ }
- // 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 \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, '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
- }
+ // 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')]);
+ // 6. Send Response
+ wp_send_json_success(['message' => __('Pre-screening submitted successfully. Starting assessment...', 'quiztech')]);
- // Ensure script execution stops
- wp_die();
- }
+ // 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');
+ /**
+ * 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
+ // 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);
- }
+ // 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);
- }
+ // 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
- }
+ // 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
+ // 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;
- }
- }
+ 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);
+ // 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')]);
+ // 4. Send Response
+ wp_send_json_success(['message' => __('Answer saved.', 'quiztech')]);
- // Ensure script execution stops
- wp_die();
- }
+ // 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
+ /**
+ * 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);
- }
+ // 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);
- }
+ // 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 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
+ }
- // 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
+ // 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);
- }
+ 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);
+ }
- // 5. Send Response
- // Future Enhancement: Consider adding a redirect URL or specific completion message/HTML
- // to the response data based on plugin settings or other logic.
- wp_send_json_success(['message' => __('Assessment submitted successfully!', 'quiztech')]);
+ // 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;
- // Ensure script execution stops
- wp_die();
- }
+ // 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();
+ }
}
\ No newline at end of file
diff --git a/src/Includes/FrontendHandler.php b/src/Includes/FrontendHandler.php
index 10cd1cf..9f92620 100644
--- a/src/Includes/FrontendHandler.php
+++ b/src/Includes/FrontendHandler.php
@@ -52,9 +52,9 @@ class FrontendHandler {
// Determine the current step (pre-screening or assessment)
$current_step = 'assessment'; // Default to assessment
- // If pre-screening questions exist AND the invitation status is still 'pending' (or similar initial state), show pre-screening.
- // Assumes 'pending' is the initial status before viewing/pre-screening. Adjust if needed.
- if ( ! empty( $pre_screening_questions ) && $invitation_data->status === 'pending' ) {
+ // If pre-screening questions exist AND the invitation status is still 'sent' (the initial state), show pre-screening.
+ // Once pre-screening is submitted, the status should change (e.g., to 'viewed', 'pre-screening complete').
+ if ( ! empty( $pre_screening_questions ) && $invitation_data->status === 'sent' ) {
$current_step = 'pre_screening';
}
diff --git a/src/Includes/Invitations.php b/src/Includes/Invitations.php
index f833506..519d994 100644
--- a/src/Includes/Invitations.php
+++ b/src/Includes/Invitations.php
@@ -6,185 +6,211 @@ namespace Quiztech\AssessmentPlatform\Includes;
*/
class Invitations {
- /**
- * Generate a cryptographically secure unique token for an invitation.
- *
- * @return string The generated unique token.
- */
- public function generate_unique_token() {
- // Placeholder for token generation logic
- // Consider using wp_generate_password() or random_bytes()
- \error_log('Invitation Token Generation Called - Placeholder');
- return bin2hex(random_bytes(16)); // Example placeholder
- }
+ /**
+ * Generate a cryptographically secure unique token for an invitation.
+ *
+ * @return string The generated unique token.
+ */
+ public function generate_unique_token() {
+ // Placeholder for token generation logic
+ // Consider using wp_generate_password() or random_bytes()
+ \error_log('Invitation Token Generation Called - Placeholder');
+ return bin2hex(random_bytes(16)); // Example placeholder
+ }
- /**
- * Create and store an invitation record.
- *
- * @param int $job_id The ID of the job associated with the invitation.
- * @param int $assessment_id The ID of the assessment associated with the invitation.
- * @param string $applicant_email The email address of the applicant being invited.
- * @return string|\WP_Error The generated token on success, or \WP_Error on failure.
- */
- public function create_invitation( $job_id, $assessment_id, $applicant_email ) {
- global $wpdb;
- $table_name = $wpdb->prefix . 'quiztech_invitations';
-
- $token = $this->generate_unique_token();
-
- $data = [
- 'token' => $token,
- 'job_id' => absint( $job_id ),
- 'assessment_id' => absint( $assessment_id ),
- 'applicant_email' => sanitize_email( $applicant_email ),
- 'status' => 'pending',
- 'created_timestamp' => current_time( 'mysql', 1 ), // GMT time
- // 'expiry_timestamp' => null, // Set if expiry is needed
- ];
-
- $format = [
- '%s', // token
- '%d', // job_id
- '%d', // assessment_id
- '%s', // applicant_email
- '%s', // status
- '%s', // created_timestamp
- // '%s', // expiry_timestamp
- ];
-
- $inserted = $wpdb->insert( $table_name, $data, $format );
-
- if ( false === $inserted ) {
- \error_log( 'Quiztech Error: Failed to insert invitation record. DB Error: ' . $wpdb->last_error );
- return new \WP_Error( 'invitation_db_error', __( 'Could not save the invitation record.', 'quiztech' ), [ 'status' => 500 ] );
- }
-
- return $token;
- }
+ /**
+ * Create and store an invitation record.
+ *
+ * @param int $job_id The ID of the job associated with the invitation.
+ * @param int $assessment_id The ID of the assessment associated with the invitation.
+ * @param string $applicant_email The email address of the applicant being invited.
+ * @return int|\WP_Error The database ID of the new invitation record on success, or \WP_Error on failure.
+ */
+ public function create_invitation( $job_id, $assessment_id, $applicant_email ) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'quiztech_invitations';
- /**
- * Send the invitation email to the applicant.
- *
- * @param string $applicant_email The recipient's email address.
- * @param string $token The unique invitation token.
- * @param array $job_details Optional details about the job for the email body.
- * @return bool True on success, false on failure.
- */
- public function send_invitation_email( $applicant_email, $token, $job_details = [] ) {
- // Placeholder for email sending logic
- // 1. Construct the invitation URL (e.g., \site_url('/assessment-invite/?token=' . $token))
- // 2. Create email subject and body (using $job_details if provided).
- // 3. Use \wp_mail() to send the email.
- // 4. Handle success/failure of \wp_mail().
+ $token = $this->generate_unique_token();
- \error_log('Send Invitation Email Called - Placeholder');
- $invite_url = \site_url('/assessment-invite/?token=' . $token); // Corrected line 71
- $subject = 'You are invited to take an assessment';
- $message = "Please click the following link to take your assessment:\n\n" . $invite_url;
- // $headers = ['Content-Type: text/html; charset=UTF-8']; // If sending HTML email
+ $data = [
+ 'token' => $token,
+ 'job_id' => absint( $job_id ),
+ 'assessment_id' => absint( $assessment_id ),
+ 'applicant_email' => sanitize_email( $applicant_email ),
+ 'status' => 'sent', // Updated initial status based on user feedback
+ 'created_timestamp' => current_time( 'mysql', 1 ), // GMT time
+ // 'expiry_timestamp' => null, // Set if expiry is needed
+ ];
- $sent = \wp_mail($applicant_email, $subject, $message); // Corrected line 76
- return $sent;
- }
+ $format = [
+ '%s', // token
+ '%d', // job_id
+ '%d', // assessment_id
+ '%s', // applicant_email
+ '%s', // status
+ '%s', // created_timestamp
+ // '%s', // expiry_timestamp
+ ];
- /**
- * Validate an incoming invitation token.
- *
- * @param string $token The token to validate.
- * @return bool|\WP_Error True if valid, false if invalid/expired/used, \WP_Error on error.
- */
- public function validate_token( $token ) {
- global $wpdb;
- $table_name = $wpdb->prefix . 'quiztech_invitations';
-
- // Basic sanitization - ensure it looks like our expected token format (32 hex chars)
- if ( ! preg_match( '/^[a-f0-9]{32}$/', $token ) ) {
- return false; // Invalid token format
- }
-
- $invitation = $wpdb->get_row(
- $wpdb->prepare(
- "SELECT * FROM $table_name WHERE token = %s",
- $token
- )
- );
-
- if ( ! $invitation ) {
- \error_log( "Quiztech Info: Invitation token not found: $token" );
- return false; // Token doesn't exist
- }
-
- if ( 'pending' !== $invitation->status ) {
- \error_log( "Quiztech Info: Invitation token already used or expired: $token (Status: $invitation->status)" );
- return false; // Token not in pending state (already used, completed, expired etc.)
- }
-
- // Optional: Check expiry_timestamp if implemented
- // if ( $invitation->expiry_timestamp && strtotime( $invitation->expiry_timestamp ) < time() ) {
- // // Optionally update status to 'expired' here
- // return false; // Token expired
- // }
-
- // Token is valid and pending
- // Optionally update status to 'viewed' here if needed
- // $wpdb->update($table_name, ['status' => 'viewed'], ['id' => $invitation->id], ['%s'], ['%d']);
-
- return $invitation; // Return the invitation data object if valid
- }
+ $inserted = $wpdb->insert( $table_name, $data, $format );
- /**
- * 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';
+ if ( false === $inserted ) {
+ \error_log( 'Quiztech Error: Failed to insert invitation record. DB Error: ' . $wpdb->last_error );
+ return new \WP_Error( 'invitation_db_error', __( 'Could not save the invitation record.', 'quiztech' ), [ 'status' => 500 ] );
+ }
- // Validate input
- $invitation_id = absint( $invitation_id );
- $new_status = sanitize_text_field( $new_status ); // Basic sanitization
+ // Return the DB ID of the inserted record, not the token, as ID is used elsewhere
+ return $wpdb->insert_id;
+ }
- if ( ! $invitation_id || empty( $new_status ) ) {
- \error_log( 'Quiztech Error: Invalid input provided to update_status.' );
- return false;
- }
+ /**
+ * Send the invitation email to the applicant.
+ *
+ * @param string $applicant_email The recipient's email address.
+ * @param string $token The unique invitation token.
+ * @param array $job_details Optional details about the job for the email body.
+ * @return bool True on success, false on failure.
+ */
+ public function send_invitation_email( $applicant_email, $token, $job_details = [] ) {
+ // Placeholder for email sending logic
+ // 1. Construct the invitation URL (e.g., \site_url('/assessment-invite/?token=' . $token))
+ // 2. Create email subject and body (using $job_details if provided).
+ // 3. Use \wp_mail() to send the email.
+ // 4. Handle success/failure of \wp_mail().
- // 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
- ];
+ \error_log('Send Invitation Email Called - Placeholder');
+ $invite_url = \site_url('/assessment-invite/?token=' . $token); // Corrected line 71
+ $subject = 'You are invited to take an assessment';
+ $message = "Please click the following link to take your assessment:\n\n" . $invite_url;
+ // $headers = ['Content-Type: text/html; charset=UTF-8']; // If sending HTML email
- 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;
- }
+ $sent = \wp_mail($applicant_email, $subject, $message); // Corrected line 76
+ return $sent;
+ }
- // 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
+ /**
+ * Validate an incoming invitation token.
+ * Checks if token exists and is in 'sent' or 'pre-screening-complete' status.
+ *
+ * @param string $token The token to validate.
+ * @return object|null The invitation data object if valid and ready for assessment, null otherwise.
+ */
+ public function validate_token( $token ) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'quiztech_invitations';
- $updated = $wpdb->update( $table_name, $data, $where, $format, $where_format );
+ // Basic sanitization - ensure it looks like our expected token format (32 hex chars)
+ if ( ! preg_match( '/^[a-f0-9]{32}$/', $token ) ) {
+ return null; // Invalid token format
+ }
- if ( false === $updated ) {
- \error_log( "Quiztech Error: Failed to update invitation status for ID {$invitation_id}. DB Error: " . $wpdb->last_error );
- return false;
- }
+ $invitation = $wpdb->get_row(
+ $wpdb->prepare(
+ "SELECT * FROM $table_name WHERE token = %s",
+ $token
+ )
+ );
- // $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;
- }
+ if ( ! $invitation ) {
+ \error_log( "Quiztech Info: Invitation token not found: $token" );
+ return null; // Token doesn't exist
+ }
+
+ // Allow access if status is 'sent' (initial) or 'pre-screening-complete'
+ $allowed_statuses_for_access = ['sent', 'pre-screening-complete'];
+ if ( ! in_array($invitation->status, $allowed_statuses_for_access, true) ) {
+ \error_log( "Quiztech Info: Invitation token not in an accessible state: $token (Status: $invitation->status)" );
+ return null; // Token not in an accessible state (already used, completed, expired etc.)
+ }
+
+ // Optional: Check expiry_timestamp if implemented
+ // if ( $invitation->expiry_timestamp && strtotime( $invitation->expiry_timestamp ) < time() ) {
+ // // Optionally update status to 'expired' here
+ // return null; // Token expired
+ // }
+
+ // Token is valid and in an accessible state
+ return $invitation; // Return the invitation data object
+ }
+
+ /**
+ * Retrieve an invitation record by its database ID.
+ *
+ * @param int $invitation_id The ID of the invitation record.
+ * @return object|null The invitation data object if found, null otherwise.
+ */
+ public function get_invitation_by_id( int $invitation_id ) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'quiztech_invitations';
+
+ if ( $invitation_id <= 0 ) {
+ return null;
+ }
+
+ $invitation = $wpdb->get_row(
+ $wpdb->prepare(
+ "SELECT * FROM $table_name WHERE id = %d",
+ $invitation_id
+ )
+ );
+
+ return $invitation; // Returns object or null if not found
+ }
+
+
+ /**
+ * 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 = [
+ 'sent', // Changed from '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;
+ }
}
\ No newline at end of file