Compare commits
2 commits
d63aa8d409
...
00892b36c9
| Author | SHA1 | Date | |
|---|---|---|---|
| 00892b36c9 | |||
| 130b9eefb9 |
9 changed files with 1144 additions and 11 deletions
163
public/js/assessment.js
Normal file
163
public/js/assessment.js
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* Quiztech Assessment Interaction Script
|
||||
*
|
||||
* Handles AJAX submissions for pre-screening, answer auto-save,
|
||||
* and final assessment submission.
|
||||
*/
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
$(function() {
|
||||
console.log('Assessment script loaded.');
|
||||
|
||||
// Check if localized data is available
|
||||
if (typeof quiztech_assessment_vars === 'undefined') {
|
||||
console.error('Quiztech Assessment Error: Localized variables not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
var $prescreeningForm = $('#quiztech-prescreening-form');
|
||||
var $prescreeningSection = $('#quiztech-prescreening-section');
|
||||
var $assessmentSection = $('#quiztech-assessment-section');
|
||||
var $submitButton = $prescreeningForm.find('button[type="submit"]');
|
||||
var $formMessages = $('<div class="form-messages"></div>').insertBefore($submitButton); // Area for messages
|
||||
|
||||
// --- Pre-Screening Form Handling ---
|
||||
$prescreeningForm.on('submit', function(event) {
|
||||
event.preventDefault(); // Stop traditional form submission
|
||||
|
||||
$formMessages.empty().removeClass('error success'); // Clear previous messages
|
||||
$submitButton.prop('disabled', true).text('Submitting...'); // Disable button
|
||||
|
||||
var formData = $(this).serialize(); // Get form data
|
||||
|
||||
// Add required AJAX parameters
|
||||
formData += '&action=quiztech_submit_prescreening';
|
||||
formData += '&nonce=' + quiztech_assessment_vars.prescreening_nonce;
|
||||
formData += '&invitation_id=' + quiztech_assessment_vars.invitation_id;
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: quiztech_assessment_vars.ajax_url,
|
||||
data: formData,
|
||||
dataType: 'json', // Expect JSON response from server
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Success! Hide pre-screening, show assessment
|
||||
$formMessages.addClass('success').text(response.data.message || 'Success!'); // Show success message briefly
|
||||
$prescreeningSection.slideUp();
|
||||
$assessmentSection.slideDown();
|
||||
// No need to re-enable button as the form is gone
|
||||
} else {
|
||||
// Handle WP JSON error
|
||||
$formMessages.addClass('error').text(response.data.message || 'An error occurred.');
|
||||
$submitButton.prop('disabled', false).text('Submit Pre-Screening & Start Assessment'); // Re-enable button
|
||||
}
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
// Handle general AJAX error
|
||||
console.error("AJAX Error:", textStatus, errorThrown);
|
||||
$formMessages.addClass('error').text('A network error occurred. Please try again.');
|
||||
$submitButton.prop('disabled', false).text('Submit Pre-Screening & Start Assessment'); // Re-enable button
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- Assessment Answer Auto-Save ---
|
||||
var $assessmentForm = $('#quiztech-assessment-form');
|
||||
var autoSaveTimeout; // To debounce requests
|
||||
|
||||
// Target input/textarea/select elements within the assessment form for auto-save
|
||||
$assessmentForm.on('change blur', 'input, textarea, select', function() {
|
||||
clearTimeout(autoSaveTimeout); // Clear previous timeout if exists
|
||||
|
||||
var $input = $(this);
|
||||
var $questionGroup = $input.closest('.question-group');
|
||||
var questionId = $questionGroup.data('question-id');
|
||||
var answer = $input.val();
|
||||
|
||||
// Add a small visual indicator within the question group
|
||||
var $indicator = $questionGroup.find('.save-indicator');
|
||||
if ($indicator.length === 0) {
|
||||
$indicator = $('<span class="save-indicator" style="margin-left: 10px; font-size: 0.8em; color: grey;"></span>').appendTo($questionGroup.find('label:first'));
|
||||
}
|
||||
$indicator.text('Saving...');
|
||||
|
||||
// Debounce the AJAX request slightly
|
||||
autoSaveTimeout = setTimeout(function() {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: quiztech_assessment_vars.ajax_url,
|
||||
data: {
|
||||
action: 'quiztech_save_answer',
|
||||
nonce: quiztech_assessment_vars.assessment_nonce, // Use the correct nonce
|
||||
invitation_id: quiztech_assessment_vars.invitation_id,
|
||||
question_id: questionId,
|
||||
answer: answer
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$indicator.text('Saved ✓').css('color', 'green');
|
||||
// Optionally fade out the indicator after a delay
|
||||
setTimeout(function() { $indicator.fadeOut().remove(); }, 2000);
|
||||
} else {
|
||||
$indicator.text('Error!').css('color', 'red');
|
||||
console.error("Auto-save error:", response.data.message);
|
||||
// Consider more prominent error display if needed
|
||||
}
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
$indicator.text('Network Error!').css('color', 'red');
|
||||
console.error("AJAX Error:", textStatus, errorThrown);
|
||||
}
|
||||
});
|
||||
}, 500); // Wait 500ms after the last change/blur before sending
|
||||
});
|
||||
|
||||
|
||||
// --- Final Assessment Submission ---
|
||||
var $submitAssessmentButton = $('#quiztech-submit-assessment');
|
||||
var $assessmentFormMessages = $('<div class="form-messages"></div>').insertAfter($submitAssessmentButton); // Area for messages
|
||||
|
||||
$submitAssessmentButton.on('click', function(event) {
|
||||
event.preventDefault(); // Stop traditional form submission (though AJAX auto-save handles data)
|
||||
|
||||
if (!confirm('Are you sure you want to submit your assessment?')) {
|
||||
return; // User cancelled
|
||||
}
|
||||
|
||||
$assessmentFormMessages.empty().removeClass('error success');
|
||||
$submitAssessmentButton.prop('disabled', true).text('Submitting...');
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: quiztech_assessment_vars.ajax_url,
|
||||
data: {
|
||||
action: 'quiztech_submit_assessment',
|
||||
nonce: quiztech_assessment_vars.assessment_nonce, // Reuse assessment nonce
|
||||
invitation_id: quiztech_assessment_vars.invitation_id
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Success! Display message and potentially hide the form/button
|
||||
$assessmentFormMessages.addClass('success').text(response.data.message || 'Assessment Submitted Successfully!');
|
||||
$assessmentForm.hide(); // Hide the form after successful submission
|
||||
// Optionally redirect: window.location.href = response.data.redirect_url;
|
||||
} else {
|
||||
$assessmentFormMessages.addClass('error').text(response.data.message || 'An error occurred during submission.');
|
||||
$submitAssessmentButton.prop('disabled', false).text('Submit Assessment'); // Re-enable button
|
||||
}
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
console.error("AJAX Error:", textStatus, errorThrown);
|
||||
$assessmentFormMessages.addClass('error').text('A network error occurred. Please try again.');
|
||||
$submitAssessmentButton.prop('disabled', false).text('Submit Assessment'); // Re-enable button
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}); // End document ready
|
||||
|
||||
})(jQuery);
|
||||
|
|
@ -56,15 +56,17 @@ if ( ! $invitation_data || ! $current_step ) {
|
|||
<h1><?php esc_html_e( 'Assessment Invitation', 'quiztech' ); ?></h1>
|
||||
|
||||
<?php if ( 'pre_screening' === $current_step ) : ?>
|
||||
|
||||
<div id="quiztech-prescreening-section"> <?php // Added ID for JS targeting ?>
|
||||
<div class="step-info">
|
||||
<h2><?php esc_html_e( 'Step 1: Pre-Screening Questions', 'quiztech' ); ?></h2>
|
||||
<p><?php esc_html_e( 'Please answer the following questions before starting the assessment.', 'quiztech' ); ?></p>
|
||||
</div>
|
||||
|
||||
<form method="post" action=""> <?php // TODO: Add action URL and nonce for processing submission ?>
|
||||
<?php wp_nonce_field( 'quiztech_submit_prescreening_' . $invitation_data->token, 'quiztech_prescreening_nonce' ); ?>
|
||||
<input type="hidden" name="quiztech_invitation_token" value="<?php echo esc_attr( $invitation_data->token ); ?>">
|
||||
<form id="quiztech-prescreening-form" method="post" action=""> <?php // Action handled by AJAX ?>
|
||||
<?php // Nonce is checked via AJAX, but good to have in form too for non-JS fallback (if implemented) ?>
|
||||
<?php // wp_nonce_field( 'quiztech_submit_prescreening_' . $invitation_data->token, 'quiztech_prescreening_nonce' ); ?>
|
||||
<?php // Token is passed via localized script vars ?>
|
||||
<?php // <input type="hidden" name="quiztech_invitation_token" value="<?php echo esc_attr( $invitation_data->token ); ?>"> ?>
|
||||
<input type="hidden" name="action" value="quiztech_submit_prescreening">
|
||||
|
||||
<?php if ( is_array( $pre_screening_questions ) && ! empty( $pre_screening_questions ) ) : ?>
|
||||
|
|
@ -90,9 +92,10 @@ if ( ! $invitation_data || ! $current_step ) {
|
|||
<?php // Render questions here - Removed, handled above ?>
|
||||
<button type="submit"><?php esc_html_e( 'Submit Pre-Screening & Start Assessment', 'quiztech' ); ?></button>
|
||||
</form>
|
||||
</div> <!-- #quiztech-prescreening-section -->
|
||||
|
||||
<?php elseif ( 'assessment' === $current_step ) : ?>
|
||||
|
||||
<div id="quiztech-assessment-section" style="display: none;"> <?php // Added ID and initially hidden ?>
|
||||
<div class="step-info">
|
||||
<h2><?php esc_html_e( 'Step 2: Assessment', 'quiztech' ); ?></h2>
|
||||
<p><?php printf( esc_html__( 'You are about to begin Assessment ID: %d', 'quiztech' ), absint( $invitation_data->assessment_id ) ); ?></p>
|
||||
|
|
@ -132,10 +135,25 @@ if ( ! $invitation_data || ! $current_step ) {
|
|||
<?php break; ?>
|
||||
|
||||
<?php case 'multiple-choice': ?>
|
||||
<?php // TODO: Fetch actual choices from post meta ?>
|
||||
<label><input type="radio" name="assessment_answer[<?php echo esc_attr( $question_id ); ?>]" value="opt1" required> Option 1</label><br>
|
||||
<label><input type="radio" name="assessment_answer[<?php echo esc_attr( $question_id ); ?>]" value="opt2"> Option 2</label><br>
|
||||
<label><input type="radio" name="assessment_answer[<?php echo esc_attr( $question_id ); ?>]" value="opt3"> Option 3</label>
|
||||
<?php
|
||||
$choices = get_post_meta( $question_id, 'question_choices', true );
|
||||
if ( is_array( $choices ) && ! empty( $choices ) ) :
|
||||
foreach ( $choices as $choice_index => $choice_text ) :
|
||||
$choice_value = esc_attr( $choice_text ); // Use text as value for simplicity
|
||||
$choice_id = 'choice_' . esc_attr( $question_id ) . '_' . esc_attr( $choice_index );
|
||||
?>
|
||||
<label for="<?php echo $choice_id; ?>">
|
||||
<input type="radio"
|
||||
id="<?php echo $choice_id; ?>"
|
||||
name="assessment_answer[<?php echo esc_attr( $question_id ); ?>]"
|
||||
value="<?php echo $choice_value; ?>"
|
||||
required>
|
||||
<?php echo esc_html( $choice_text ); ?>
|
||||
</label><br>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
<p class="error"><?php esc_html_e( 'Error: Choices not found for this question.', 'quiztech' ); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php break; ?>
|
||||
|
||||
<?php case 'true-false': ?>
|
||||
|
|
@ -160,6 +178,7 @@ if ( ! $invitation_data || ! $current_step ) {
|
|||
endif;
|
||||
?>
|
||||
<?php // Removed the simple "Start Assessment" button, replaced by question rendering logic ?>
|
||||
</div> <!-- #quiztech-assessment-section -->
|
||||
|
||||
<?php else : ?>
|
||||
<p class="error"><?php esc_html_e( 'An unexpected error occurred. Invalid assessment step.', 'quiztech' ); ?></p>
|
||||
|
|
|
|||
|
|
@ -116,8 +116,21 @@ function quiztech_init() {
|
|||
$frontend_handler = new \Quiztech\AssessmentPlatform\Includes\FrontendHandler();
|
||||
$frontend_handler->init_hooks();
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
296
src/Includes/Ajax/AssessmentAjaxHandler.php
Normal file
296
src/Includes/Ajax/AssessmentAjaxHandler.php
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
<?php
|
||||
|
||||
namespace Quiztech\AssessmentPlatform\Includes\Ajax;
|
||||
|
||||
/**
|
||||
* 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']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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');
|
||||
|
||||
// 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 \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
|
||||
}
|
||||
|
||||
|
||||
// 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 \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);
|
||||
}
|
||||
|
||||
|
||||
// 5. Send Response
|
||||
// TODO: Consider adding a redirect URL or specific completion message to the response data
|
||||
wp_send_json_success(['message' => __('Assessment submitted successfully!', 'quiztech')]);
|
||||
|
||||
// Ensure script execution stops
|
||||
wp_die();
|
||||
}
|
||||
}
|
||||
|
|
@ -65,6 +65,28 @@ class FrontendHandler {
|
|||
// This template should be located within the plugin or theme.
|
||||
$template_path = QUIZTECH_PLUGIN_DIR . 'public/templates/assessment-shell.php'; // Example path
|
||||
|
||||
|
||||
// Enqueue and localize the assessment script
|
||||
wp_enqueue_script(
|
||||
'quiztech-assessment-script',
|
||||
QUIZTECH_PLUGIN_URL . 'public/js/assessment.js',
|
||||
['jquery'],
|
||||
QUIZTECH_VERSION,
|
||||
true // Load in footer
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'quiztech-assessment-script',
|
||||
'quiztech_assessment_vars',
|
||||
[
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'prescreening_nonce' => wp_create_nonce('quiztech_prescreening_nonce'),
|
||||
'assessment_nonce' => wp_create_nonce('quiztech_assessment_nonce'),
|
||||
'invitation_id' => $invitation_data->id, // Pass invitation ID for context
|
||||
// Add other necessary vars here, e.g., evaluation ID if available
|
||||
]
|
||||
);
|
||||
|
||||
if ( file_exists( $template_path ) ) {
|
||||
include( $template_path );
|
||||
exit; // Important: Stop WordPress from loading the default theme template
|
||||
|
|
|
|||
|
|
@ -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