feat: Implement Phase 3 Applicant Assessment Experience (Items 15-17)
This commit is contained in:
parent
2bbe7efdfe
commit
742630778c
6 changed files with 734 additions and 564 deletions
|
|
@ -5,159 +5,224 @@
|
||||||
* and final assessment submission.
|
* and final assessment submission.
|
||||||
*/
|
*/
|
||||||
(function($) {
|
(function($) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
console.log('Assessment script loaded.');
|
console.log('Assessment script loaded.');
|
||||||
|
|
||||||
// Check if localized data is available
|
// Check if localized data is available
|
||||||
if (typeof quiztech_assessment_vars === 'undefined') {
|
if (typeof quiztech_assessment_vars === 'undefined') {
|
||||||
console.error('Quiztech Assessment Error: Localized variables not found.');
|
console.error('Quiztech Assessment Error: Localized variables not found.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var $prescreeningForm = $('#quiztech-prescreening-form');
|
var $prescreeningForm = $('#quiztech-prescreening-form');
|
||||||
var $prescreeningSection = $('#quiztech-prescreening-section');
|
var $prescreeningSection = $('#quiztech-prescreening-section');
|
||||||
var $assessmentSection = $('#quiztech-assessment-section');
|
var $assessmentSection = $('#quiztech-assessment-section');
|
||||||
var $submitButton = $prescreeningForm.find('button[type="submit"]');
|
var $submitButton = $prescreeningForm.find('button[type="submit"]');
|
||||||
var $formMessages = $('<div class="form-messages"></div>').insertBefore($submitButton); // Area for messages
|
var $preScreenFormMessages = $('<div class="form-messages"></div>').insertBefore($submitButton); // Area for messages
|
||||||
|
|
||||||
// --- Pre-Screening Form Handling ---
|
// Assessment specific elements
|
||||||
$prescreeningForm.on('submit', function(event) {
|
var $assessmentForm = $('#quiztech-assessment-form');
|
||||||
event.preventDefault(); // Stop traditional form submission
|
var $questionsContainer = $('#quiztech-questions-container');
|
||||||
|
var $questionContainers = $questionsContainer.find('.quiztech-question-container');
|
||||||
|
var $timerDisplay = $('#quiztech-timer');
|
||||||
|
var $nextButton = $('#quiztech-next-question');
|
||||||
|
var $submitAssessmentButton = $('#quiztech-submit-assessment');
|
||||||
|
var $completionMessage = $('#quiztech-completion-message');
|
||||||
|
|
||||||
$formMessages.empty().removeClass('error success'); // Clear previous messages
|
// State variables
|
||||||
$submitButton.prop('disabled', true).text('Submitting...'); // Disable button
|
var currentQuestionIndex = 0;
|
||||||
|
var totalQuestions = $questionContainers.length;
|
||||||
|
var timerInterval;
|
||||||
|
var timerSeconds = 0;
|
||||||
|
|
||||||
var formData = $(this).serialize(); // Get form data
|
// --- Pre-Screening Form Handling ---
|
||||||
|
$prescreeningForm.on('submit', function(event) {
|
||||||
|
event.preventDefault(); // Stop traditional form submission
|
||||||
|
|
||||||
// Add required AJAX parameters
|
// Use $preScreenFormMessages here
|
||||||
formData += '&action=quiztech_submit_prescreening';
|
$preScreenFormMessages.empty().removeClass('error success'); // Clear previous messages
|
||||||
formData += '&nonce=' + quiztech_assessment_vars.prescreening_nonce;
|
$submitButton.prop('disabled', true).text('Submitting...'); // Disable button
|
||||||
formData += '&invitation_id=' + quiztech_assessment_vars.invitation_id;
|
|
||||||
|
|
||||||
$.ajax({
|
var formData = $(this).serialize(); // Get form data
|
||||||
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 ---
|
// Add required AJAX parameters
|
||||||
var $assessmentForm = $('#quiztech-assessment-form');
|
formData += '&action=quiztech_submit_prescreening';
|
||||||
var autoSaveTimeout; // To debounce requests
|
formData += '&nonce=' + quiztech_assessment_vars.prescreening_nonce;
|
||||||
|
formData += '&invitation_id=' + quiztech_assessment_vars.invitation_id;
|
||||||
|
|
||||||
// Target input/textarea/select elements within the assessment form for auto-save
|
$.ajax({
|
||||||
$assessmentForm.on('change blur', 'input, textarea, select', function() {
|
type: 'POST',
|
||||||
clearTimeout(autoSaveTimeout); // Clear previous timeout if exists
|
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
|
||||||
|
$preScreenFormMessages.addClass('success').text(response.data.message || 'Success!'); // Show success message briefly & Use correct variable
|
||||||
|
$prescreeningSection.slideUp();
|
||||||
|
$assessmentSection.slideDown();
|
||||||
|
startTimer(); // Start the assessment timer
|
||||||
|
updateNavigationButtons(); // Show initial nav state
|
||||||
|
// No need to re-enable button as the form is gone
|
||||||
|
} else {
|
||||||
|
// Handle WP JSON error
|
||||||
|
$preScreenFormMessages.addClass('error').text(response.data.message || 'An error occurred.'); // Use correct variable
|
||||||
|
$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);
|
||||||
|
$preScreenFormMessages.addClass('error').text('A network error occurred. Please try again.'); // Use correct variable
|
||||||
|
$submitButton.prop('disabled', false).text('Submit Pre-Screening & Start Assessment'); // Re-enable button
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
var $input = $(this);
|
// --- Assessment Answer Auto-Save ---
|
||||||
var $questionGroup = $input.closest('.question-group');
|
// var $assessmentForm = $('#quiztech-assessment-form'); // Already defined above
|
||||||
var questionId = $questionGroup.data('question-id');
|
var autoSaveTimeout; // To debounce requests
|
||||||
var answer = $input.val();
|
|
||||||
|
|
||||||
// Add a small visual indicator within the question group
|
// Target input/textarea/select elements within the assessment form for auto-save
|
||||||
var $indicator = $questionGroup.find('.save-indicator');
|
$assessmentForm.on('change blur', 'input, textarea, select', function() {
|
||||||
if ($indicator.length === 0) {
|
clearTimeout(autoSaveTimeout); // Clear previous timeout if exists
|
||||||
$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
|
var $input = $(this);
|
||||||
autoSaveTimeout = setTimeout(function() {
|
var $questionGroup = $input.closest('.quiztech-question-container'); // Updated selector
|
||||||
$.ajax({
|
var questionId = $questionGroup.data('question-id');
|
||||||
type: 'POST',
|
var answer = $input.val();
|
||||||
url: quiztech_assessment_vars.ajax_url,
|
|
||||||
data: {
|
// Add a small visual indicator within the question group
|
||||||
action: 'quiztech_save_answer',
|
var $indicator = $questionGroup.find('.save-indicator');
|
||||||
nonce: quiztech_assessment_vars.assessment_nonce, // Use the correct nonce
|
if ($indicator.length === 0) {
|
||||||
invitation_id: quiztech_assessment_vars.invitation_id,
|
$indicator = $('<span class="save-indicator" style="margin-left: 10px; font-size: 0.8em; color: grey;"></span>').appendTo($questionGroup.find('label:first'));
|
||||||
question_id: questionId,
|
}
|
||||||
answer: answer
|
$indicator.text('Saving...');
|
||||||
},
|
|
||||||
dataType: 'json',
|
// Debounce the AJAX request slightly
|
||||||
success: function(response) {
|
autoSaveTimeout = setTimeout(function() {
|
||||||
if (response.success) {
|
$.ajax({
|
||||||
$indicator.text('Saved ✓').css('color', 'green');
|
type: 'POST',
|
||||||
// Optionally fade out the indicator after a delay
|
url: quiztech_assessment_vars.ajax_url,
|
||||||
setTimeout(function() { $indicator.fadeOut().remove(); }, 2000);
|
data: {
|
||||||
} else {
|
action: 'quiztech_save_answer',
|
||||||
$indicator.text('Error!').css('color', 'red');
|
nonce: quiztech_assessment_vars.assessment_nonce, // Use the correct nonce
|
||||||
console.error("Auto-save error:", response.data.message);
|
invitation_id: quiztech_assessment_vars.invitation_id,
|
||||||
// Consider more prominent error display if needed
|
question_id: questionId,
|
||||||
}
|
answer: answer
|
||||||
},
|
},
|
||||||
error: function(jqXHR, textStatus, errorThrown) {
|
dataType: 'json',
|
||||||
$indicator.text('Network Error!').css('color', 'red');
|
success: function(response) {
|
||||||
console.error("AJAX Error:", textStatus, errorThrown);
|
if (response.success) {
|
||||||
}
|
$indicator.text('Saved ✓').css('color', 'green');
|
||||||
});
|
// Optionally fade out the indicator after a delay
|
||||||
}, 500); // Wait 500ms after the last change/blur before sending
|
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 ---
|
// --- Assessment Navigation ---
|
||||||
var $submitAssessmentButton = $('#quiztech-submit-assessment');
|
$nextButton.on('click', function() {
|
||||||
var $assessmentFormMessages = $('<div class="form-messages"></div>').insertAfter($submitAssessmentButton); // Area for messages
|
if (currentQuestionIndex < totalQuestions - 1) {
|
||||||
|
// Hide current, show next
|
||||||
|
$questionContainers.eq(currentQuestionIndex).addClass('quiztech-question-hidden');
|
||||||
|
currentQuestionIndex++;
|
||||||
|
$questionContainers.eq(currentQuestionIndex).removeClass('quiztech-question-hidden');
|
||||||
|
updateNavigationButtons();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$submitAssessmentButton.on('click', function(event) {
|
function updateNavigationButtons() {
|
||||||
event.preventDefault(); // Stop traditional form submission (though AJAX auto-save handles data)
|
if (currentQuestionIndex >= totalQuestions - 1) {
|
||||||
|
// Last question
|
||||||
|
$nextButton.hide();
|
||||||
|
$submitAssessmentButton.show();
|
||||||
|
} else {
|
||||||
|
$nextButton.show();
|
||||||
|
$submitAssessmentButton.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!confirm('Are you sure you want to submit your assessment?')) {
|
// --- Timer Functions ---
|
||||||
return; // User cancelled
|
function startTimer() {
|
||||||
}
|
if (timerInterval) clearInterval(timerInterval); // Clear existing if any
|
||||||
|
timerSeconds = 0; // Reset timer
|
||||||
|
$timerDisplay.text(formatTime(timerSeconds)); // Initial display
|
||||||
|
|
||||||
$assessmentFormMessages.empty().removeClass('error success');
|
timerInterval = setInterval(function() {
|
||||||
$submitAssessmentButton.prop('disabled', true).text('Submitting...');
|
timerSeconds++;
|
||||||
|
$timerDisplay.text(formatTime(timerSeconds));
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
$.ajax({
|
function formatTime(totalSeconds) {
|
||||||
type: 'POST',
|
var hours = Math.floor(totalSeconds / 3600);
|
||||||
url: quiztech_assessment_vars.ajax_url,
|
var minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
data: {
|
var seconds = totalSeconds % 60;
|
||||||
action: 'quiztech_submit_assessment',
|
// Pad with leading zeros
|
||||||
nonce: quiztech_assessment_vars.assessment_nonce, // Reuse assessment nonce
|
minutes = String(minutes).padStart(2, '0');
|
||||||
invitation_id: quiztech_assessment_vars.invitation_id
|
seconds = String(seconds).padStart(2, '0');
|
||||||
},
|
return (hours > 0 ? String(hours).padStart(2, '0') + ':' : '') + minutes + ':' + seconds;
|
||||||
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
|
// --- Final Assessment Submission ---
|
||||||
|
// var $submitAssessmentButton = $('#quiztech-submit-assessment'); // Already defined above
|
||||||
|
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 completion message and hide form/timer
|
||||||
|
// Use .html() to render potential basic HTML in the message
|
||||||
|
$completionMessage.html(response.data.completionMessage || 'Assessment Submitted Successfully!').show();
|
||||||
|
$assessmentForm.hide(); // Hide the form elements (questions, nav)
|
||||||
|
$timerDisplay.hide(); // Hide timer
|
||||||
|
clearInterval(timerInterval); // Stop timer
|
||||||
|
$assessmentFormMessages.remove(); // Remove the temporary message area if not needed
|
||||||
|
} 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);
|
})(jQuery);
|
||||||
|
|
@ -48,6 +48,13 @@ if ( ! $invitation_data || ! $current_step ) {
|
||||||
.form-group textarea { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; min-height: 80px; }
|
.form-group textarea { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; min-height: 80px; }
|
||||||
button[type="submit"] { padding: 10px 20px; background-color: #0073aa; color: #fff; border: none; border-radius: 3px; cursor: pointer; }
|
button[type="submit"] { padding: 10px 20px; background-color: #0073aa; color: #fff; border: none; border-radius: 3px; cursor: pointer; }
|
||||||
button[type="submit"]:hover { background-color: #005a87; }
|
button[type="submit"]:hover { background-color: #005a87; }
|
||||||
|
.quiztech-timer { font-size: 1.2em; font-weight: bold; text-align: right; margin-bottom: 15px; padding: 5px; background-color: #f0f0f0; border-radius: 3px; }
|
||||||
|
.quiztech-question-container { border: 1px solid #eee; padding: 20px; margin-bottom: 20px; }
|
||||||
|
.quiztech-question-hidden { display: none; }
|
||||||
|
.quiztech-navigation { margin-top: 20px; text-align: right; }
|
||||||
|
.quiztech-navigation button { margin-left: 10px; padding: 10px 20px; background-color: #0073aa; color: #fff; border: none; border-radius: 3px; cursor: pointer; }
|
||||||
|
.quiztech-navigation button:hover { background-color: #005a87; }
|
||||||
|
#quiztech-completion-message { margin-top: 20px; padding: 15px; background-color: #dff0d8; border: 1px solid #d6e9c6; color: #3c763d; border-radius: 4px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -101,22 +108,25 @@ if ( ! $invitation_data || ! $current_step ) {
|
||||||
<p><?php printf( esc_html__( 'You are about to begin Assessment ID: %d', 'quiztech' ), absint( $invitation_data->assessment_id ) ); ?></p>
|
<p><?php printf( esc_html__( 'You are about to begin Assessment ID: %d', 'quiztech' ), absint( $invitation_data->assessment_id ) ); ?></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="quiztech-timer" class="quiztech-timer">00:00</div> <?php // Timer placeholder ?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$assessment_id = $invitation_data->assessment_id;
|
$assessment_id = $invitation_data->assessment_id;
|
||||||
// Fetch question IDs associated with the assessment (assuming stored in 'question_ids' meta field)
|
// Fetch question IDs associated with the assessment (assuming stored in 'question_ids' meta field)
|
||||||
$question_ids = get_post_meta( $assessment_id, 'question_ids', true );
|
$question_ids = get_post_meta( $assessment_id, '_quiztech_question_ids', true ); // Corrected meta key
|
||||||
|
|
||||||
if ( is_array( $question_ids ) && ! empty( $question_ids ) ) :
|
if ( is_array( $question_ids ) && ! empty( $question_ids ) ) :
|
||||||
?>
|
?>
|
||||||
<form id="quiztech-assessment-form" method="post" action=""> <?php // Action handled by AJAX ?>
|
<form id="quiztech-assessment-form" method="post" action=""> <?php // Action handled by AJAX ?>
|
||||||
<?php wp_nonce_field( 'quiztech_submit_assessment_' . $invitation_data->token, 'quiztech_assessment_nonce' ); ?>
|
<?php // Nonce is checked via AJAX, hidden inputs might not be needed if not submitting traditionally ?>
|
||||||
<input type="hidden" name="quiztech_invitation_token" value="<?php echo esc_attr( $invitation_data->token ); ?>">
|
<?php // wp_nonce_field( 'quiztech_submit_assessment_' . $invitation_data->token, 'quiztech_assessment_nonce' ); ?>
|
||||||
<input type="hidden" name="quiztech_assessment_id" value="<?php echo esc_attr( $assessment_id ); ?>">
|
<?php // <input type="hidden" name="quiztech_invitation_token" value="<?php echo esc_attr( $invitation_data->token ); ?>"> ?>
|
||||||
<input type="hidden" name="action" value="quiztech_submit_assessment">
|
<?php // <input type="hidden" name="quiztech_assessment_id" value="<?php echo esc_attr( $assessment_id ); ?>"> ?>
|
||||||
|
<?php // <input type="hidden" name="action" value="quiztech_submit_assessment"> ?>
|
||||||
|
|
||||||
<h4><?php esc_html_e( 'Questions:', 'quiztech' ); ?></h4>
|
<div id="quiztech-questions-container">
|
||||||
|
|
||||||
<?php foreach ( $question_ids as $question_id ) : ?>
|
<?php foreach ( $question_ids as $index => $question_id ) : ?>
|
||||||
<?php
|
<?php
|
||||||
$question_post = get_post( $question_id );
|
$question_post = get_post( $question_id );
|
||||||
if ( ! $question_post || 'question' !== $question_post->post_type ) {
|
if ( ! $question_post || 'question' !== $question_post->post_type ) {
|
||||||
|
|
@ -125,7 +135,11 @@ if ( ! $invitation_data || ! $current_step ) {
|
||||||
$question_title = get_the_title( $question_post );
|
$question_title = get_the_title( $question_post );
|
||||||
$question_type = get_post_meta( $question_id, 'question_type', true );
|
$question_type = get_post_meta( $question_id, 'question_type', true );
|
||||||
?>
|
?>
|
||||||
<div class="form-group question-group" data-question-id="<?php echo esc_attr( $question_id ); ?>">
|
<div class="quiztech-question-container <?php echo $is_first_question ? '' : 'quiztech-question-hidden'; ?>"
|
||||||
|
data-question-id="<?php echo esc_attr( $question_id ); ?>"
|
||||||
|
data-question-index="<?php echo esc_attr( $index ); ?>">
|
||||||
|
|
||||||
|
<h4><?php printf( esc_html__( 'Question %d of %d', 'quiztech' ), $index + 1, count( $question_ids ) ); ?></h4>
|
||||||
<label><strong><?php echo esc_html( $question_title ); ?></strong></label>
|
<label><strong><?php echo esc_html( $question_title ); ?></strong></label>
|
||||||
|
|
||||||
<?php // Render input based on question type ?>
|
<?php // Render input based on question type ?>
|
||||||
|
|
@ -165,11 +179,21 @@ if ( ! $invitation_data || ! $current_step ) {
|
||||||
<p class="error"><?php esc_html_e( 'Unsupported question type.', 'quiztech' ); ?></p>
|
<p class="error"><?php esc_html_e( 'Unsupported question type.', 'quiztech' ); ?></p>
|
||||||
<?php endswitch; ?>
|
<?php endswitch; ?>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<?php // Removed <hr> as container provides separation ?>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
</div> <!-- #quiztech-questions-container -->
|
||||||
|
|
||||||
|
<div class="quiztech-navigation">
|
||||||
|
<button type="button" id="quiztech-next-question"><?php esc_html_e( 'Next Question', 'quiztech' ); ?></button>
|
||||||
|
<button type="button" id="quiztech-submit-assessment" style="display: none;"><?php esc_html_e( 'Submit Assessment', 'quiztech' ); ?></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" id="quiztech-submit-assessment"><?php esc_html_e( 'Submit Assessment', 'quiztech' ); ?></button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div id="quiztech-completion-message" style="display: none;">
|
||||||
|
<?php // Completion message will be inserted here by JS ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
else :
|
else :
|
||||||
echo '<p class="error">' . esc_html__( 'Could not load questions for this assessment.', 'quiztech' ) . '</p>';
|
echo '<p class="error">' . esc_html__( 'Could not load questions for this assessment.', 'quiztech' ) . '</p>';
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class AssessmentMetaboxes {
|
||||||
public function register_hooks() {
|
public function register_hooks() {
|
||||||
add_action( 'add_meta_boxes', [ $this, 'add_assessment_metaboxes' ] );
|
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_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' ),
|
\__( 'Linked Questions', 'quiztech' ),
|
||||||
[ $this, 'render_linked_questions_metabox' ],
|
[ $this, 'render_linked_questions_metabox' ],
|
||||||
'assessment',
|
'assessment',
|
||||||
'normal', // Context below editor
|
'normal', // Context
|
||||||
'high'
|
'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 ( ! 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 ( \defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; }
|
||||||
if ( ! \current_user_can( 'edit_post', $post_id ) ) { 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
|
// Process submitted IDs
|
||||||
$submitted_ids = [];
|
$submitted_ids = [];
|
||||||
|
|
@ -106,4 +115,45 @@ class AssessmentMetaboxes {
|
||||||
// Update meta (even if empty array, to clear previous selections)
|
// Update meta (even if empty array, to clear previous selections)
|
||||||
\update_post_meta( $post_id, '_quiztech_linked_question_ids', $submitted_ids );
|
\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 '<p>' . \esc_html__( 'Enter the message to display to applicants after they successfully submit this assessment. Basic HTML is allowed.', 'quiztech' ) . '</p>';
|
||||||
|
echo '<textarea id="quiztech_completion_message_field" name="quiztech_completion_message_field" rows="5" style="width:100%;">' . \esc_textarea( $completion_message ) . '</textarea>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,296 +2,301 @@
|
||||||
|
|
||||||
namespace Quiztech\AssessmentPlatform\Includes\Ajax;
|
namespace Quiztech\AssessmentPlatform\Includes\Ajax;
|
||||||
|
|
||||||
|
use Quiztech\AssessmentPlatform\Includes\Invitations; // Added use statement
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles AJAX requests related to the front-end assessment process.
|
* Handles AJAX requests related to the front-end assessment process.
|
||||||
*/
|
*/
|
||||||
class AssessmentAjaxHandler {
|
class AssessmentAjaxHandler {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor. Registers AJAX hooks.
|
* Constructor. Registers AJAX hooks.
|
||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
add_action('wp_ajax_quiztech_submit_prescreening', [$this, 'handle_submit_prescreening']);
|
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_save_answer', [$this, 'handle_save_answer']);
|
||||||
add_action('wp_ajax_quiztech_submit_assessment', [$this, 'handle_submit_assessment']);
|
add_action('wp_ajax_quiztech_submit_assessment', [$this, 'handle_submit_assessment']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the handler.
|
* Initialize the handler.
|
||||||
* Static method to instantiate the class and register hooks.
|
* Static method to instantiate the class and register hooks.
|
||||||
*/
|
*/
|
||||||
public static function init() {
|
public static function init() {
|
||||||
new self();
|
new self();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to find an existing user_evaluation CPT by invitation ID
|
* Helper method to find an existing user_evaluation CPT by invitation ID
|
||||||
* or create a new one if it doesn't exist.
|
* or create a new one if it doesn't exist.
|
||||||
*
|
*
|
||||||
* @param int $invitation_id The database ID of the invitation record.
|
* @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.
|
* @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 {
|
private function get_or_create_user_evaluation(int $invitation_id): int {
|
||||||
if ( ! $invitation_id ) {
|
if ( ! $invitation_id ) {
|
||||||
error_log("Quiztech AJAX Error: get_or_create_user_evaluation called with invalid invitation ID: " . $invitation_id);
|
error_log("Quiztech AJAX Error: get_or_create_user_evaluation called with invalid invitation ID: " . $invitation_id);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$args = [
|
$args = [
|
||||||
'post_type' => 'user_evaluation',
|
'post_type' => 'user_evaluation',
|
||||||
'post_status' => 'any', // Find it regardless of status initially
|
'post_status' => 'any', // Find it regardless of status initially
|
||||||
'meta_query' => [
|
'meta_query' => [
|
||||||
[
|
[
|
||||||
'key' => 'quiztech_invitation_id',
|
'key' => 'quiztech_invitation_id',
|
||||||
'value' => $invitation_id,
|
'value' => $invitation_id,
|
||||||
'compare' => '=',
|
'compare' => '=',
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'posts_per_page' => 1,
|
'posts_per_page' => 1,
|
||||||
'fields' => 'ids', // Only get the ID
|
'fields' => 'ids', // Only get the ID
|
||||||
];
|
];
|
||||||
$evaluation_posts = get_posts($args);
|
$evaluation_posts = get_posts($args);
|
||||||
|
|
||||||
if ( ! empty( $evaluation_posts ) ) {
|
if ( ! empty( $evaluation_posts ) ) {
|
||||||
// Found existing evaluation
|
// Found existing evaluation
|
||||||
return $evaluation_posts[0];
|
return $evaluation_posts[0];
|
||||||
} else {
|
} else {
|
||||||
// Not found, create a new one
|
// Not found, create a new one
|
||||||
$post_data = [
|
$post_data = [
|
||||||
'post_type' => 'user_evaluation',
|
'post_type' => 'user_evaluation',
|
||||||
'post_status' => 'publish', // Start as published (or maybe 'pending'/'in-progress' if custom statuses are added)
|
'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_title' => sprintf( __( 'Evaluation for Invitation #%d', 'quiztech' ), $invitation_id ),
|
||||||
'post_content' => '', // No content needed initially
|
'post_content' => '', // No content needed initially
|
||||||
// 'post_author' => ?? // Assign to an admin or system user? Default is current user (likely none in AJAX)
|
// '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
|
$evaluation_id = wp_insert_post( $post_data, true ); // Pass true for WP_Error on failure
|
||||||
|
|
||||||
if ( is_wp_error( $evaluation_id ) ) {
|
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());
|
error_log("Quiztech AJAX Error: Failed to create user_evaluation CPT for invitation ID {$invitation_id}: " . $evaluation_id->get_error_message());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the linking meta field
|
// Add the linking meta field
|
||||||
$meta_updated = update_post_meta( $evaluation_id, 'quiztech_invitation_id', $invitation_id );
|
$meta_updated = update_post_meta( $evaluation_id, 'quiztech_invitation_id', $invitation_id );
|
||||||
if ( ! $meta_updated ) {
|
if ( ! $meta_updated ) {
|
||||||
// Log error, but maybe don't fail the whole request? Or should we delete the post?
|
// 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}.");
|
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.
|
// 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}.");
|
error_log("Quiztech AJAX Info: Created new user_evaluation CPT ID {$evaluation_id} for invitation ID {$invitation_id}.");
|
||||||
return $evaluation_id;
|
return $evaluation_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// AJAX handler methods will be added below:
|
/**
|
||||||
// - handle_submit_prescreening()
|
* Handles the AJAX submission of the pre-screening form.
|
||||||
// - handle_save_answer()
|
* Expects 'nonce', 'invitation_id', and 'pre_screen_answer' array in $_POST.
|
||||||
// - handle_submit_assessment()
|
*/
|
||||||
|
public function handle_submit_prescreening() {
|
||||||
|
// 1. Verify Nonce
|
||||||
|
check_ajax_referer('quiztech_prescreening_nonce', 'nonce');
|
||||||
|
|
||||||
/**
|
// 2. Get and Sanitize Core Data
|
||||||
* Handles the AJAX submission of the pre-screening form.
|
$invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0;
|
||||||
* Expects 'nonce', 'invitation_id', and 'pre_screen_answer' array in $_POST.
|
if ( ! $invitation_id ) {
|
||||||
*/
|
wp_send_json_error(['message' => __('Missing invitation ID.', 'quiztech')], 400);
|
||||||
public function handle_submit_prescreening() {
|
}
|
||||||
// 1. Verify Nonce
|
|
||||||
check_ajax_referer('quiztech_prescreening_nonce', 'nonce');
|
|
||||||
|
|
||||||
// 2. Get and Sanitize Core Data
|
// 3. Get or Create User Evaluation Record
|
||||||
$invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0;
|
$evaluation_id = $this->get_or_create_user_evaluation($invitation_id);
|
||||||
if ( ! $invitation_id ) {
|
if ( ! $evaluation_id ) {
|
||||||
wp_send_json_error(['message' => __('Missing invitation ID.', 'quiztech')], 400);
|
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
|
// 4. Sanitize Submitted Answers
|
||||||
$evaluation_id = $this->get_or_create_user_evaluation($invitation_id);
|
$submitted_answers = isset($_POST['pre_screen_answer']) && is_array($_POST['pre_screen_answer']) ? $_POST['pre_screen_answer'] : [];
|
||||||
if ( ! $evaluation_id ) {
|
$sanitized_answers = [];
|
||||||
error_log("Quiztech AJAX Error: Failed to get or create user_evaluation for invitation ID: " . $invitation_id);
|
foreach ($submitted_answers as $index => $answer) {
|
||||||
wp_send_json_error(['message' => __('Could not process evaluation record.', 'quiztech')], 500);
|
// 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
|
// 4. Save Answers (as user_evaluation CPT meta)
|
||||||
$submitted_answers = isset($_POST['pre_screen_answer']) && is_array($_POST['pre_screen_answer']) ? $_POST['pre_screen_answer'] : [];
|
if (!empty($sanitized_answers)) {
|
||||||
$sanitized_answers = [];
|
update_post_meta($evaluation_id, 'quiztech_prescreening_answers', $sanitized_answers);
|
||||||
foreach ($submitted_answers as $index => $answer) {
|
} else {
|
||||||
// Use sanitize_textarea_field as pre-screening questions are currently textareas
|
// Handle case where no answers were submitted? Or rely on form 'required' attribute?
|
||||||
$sanitized_answers[sanitize_key($index)] = sanitize_textarea_field(wp_unslash($answer));
|
// For now, proceed even if empty.
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Save Answers (as user_evaluation CPT meta)
|
// 5. Update Invitation Status
|
||||||
if (!empty($sanitized_answers)) {
|
try {
|
||||||
update_post_meta($evaluation_id, 'quiztech_prescreening_answers', $sanitized_answers);
|
$invitations = new Invitations();
|
||||||
} else {
|
$updated = $invitations->update_status($invitation_id, 'pre-screening-complete');
|
||||||
// Handle case where no answers were submitted? Or rely on form 'required' attribute?
|
if (!$updated) {
|
||||||
// For now, proceed even if empty.
|
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
|
||||||
|
}
|
||||||
// 5. Update Invitation Status
|
} catch (\Exception $e) {
|
||||||
try {
|
error_log("Quiztech AJAX Error: Exception updating invitation status: " . $e->getMessage());
|
||||||
$invitations = new \Quiztech\AssessmentPlatform\Includes\Invitations();
|
// Decide if this should be a user-facing error
|
||||||
// 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
|
// 6. Send Response
|
||||||
wp_send_json_success(['message' => __('Pre-screening submitted successfully. Starting assessment...', 'quiztech')]);
|
wp_send_json_success(['message' => __('Pre-screening submitted successfully. Starting assessment...', 'quiztech')]);
|
||||||
|
|
||||||
// Ensure script execution stops
|
// Ensure script execution stops
|
||||||
wp_die();
|
wp_die();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the AJAX auto-save of a single assessment answer.
|
* Handles the AJAX auto-save of a single assessment answer.
|
||||||
* Expects 'nonce', 'invitation_id', 'question_id', and 'answer' in $_POST.
|
* Expects 'nonce', 'invitation_id', 'question_id', and 'answer' in $_POST.
|
||||||
*/
|
*/
|
||||||
public function handle_save_answer() {
|
public function handle_save_answer() {
|
||||||
// 1. Verify Nonce
|
// 1. Verify Nonce
|
||||||
check_ajax_referer('quiztech_assessment_nonce', 'nonce');
|
check_ajax_referer('quiztech_assessment_nonce', 'nonce');
|
||||||
|
|
||||||
// 2. Get and Sanitize Data
|
// 2. Get and Sanitize Data
|
||||||
$invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0;
|
$invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0;
|
||||||
$question_id = isset($_POST['question_id']) ? absint($_POST['question_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
|
$answer = isset($_POST['answer']) ? wp_unslash($_POST['answer']) : ''; // Sanitize based on question type later
|
||||||
|
|
||||||
// Basic validation for required IDs before querying
|
// Basic validation for required IDs before querying
|
||||||
if ( ! $invitation_id || ! $question_id ) {
|
if ( ! $invitation_id || ! $question_id ) {
|
||||||
wp_send_json_error(['message' => __('Missing required data for saving answer.', 'quiztech')], 400);
|
wp_send_json_error(['message' => __('Missing required data for saving answer.', 'quiztech')], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Get or Create User Evaluation Record
|
// 3. Get or Create User Evaluation Record
|
||||||
$evaluation_id = $this->get_or_create_user_evaluation($invitation_id);
|
$evaluation_id = $this->get_or_create_user_evaluation($invitation_id);
|
||||||
if ( ! $evaluation_id ) {
|
if ( ! $evaluation_id ) {
|
||||||
error_log("Quiztech AJAX Error: Failed to get or create user_evaluation for invitation ID: " . $invitation_id . " during answer save.");
|
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);
|
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
|
// 4. Fetch the question type meta for the given question ID
|
||||||
$question_type = \get_post_meta($question_id, '_quiztech_question_type', true);
|
$question_type = \get_post_meta($question_id, '_quiztech_question_type', true);
|
||||||
if ( ! $question_type ) {
|
if ( ! $question_type ) {
|
||||||
// Log if type is missing, but proceed with default sanitization
|
// Log if type is missing, but proceed with default sanitization
|
||||||
error_log("Quiztech AJAX Warning: Missing question type meta for question ID: " . $question_id);
|
error_log("Quiztech AJAX Warning: Missing question type meta for question ID: " . $question_id);
|
||||||
$question_type = 'text'; // Default to text if not set
|
$question_type = 'text'; // Default to text if not set
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize the answer based on question type
|
// Sanitize the answer based on question type
|
||||||
$sanitized_answer = ''; // Initialize
|
$sanitized_answer = ''; // Initialize
|
||||||
|
|
||||||
if (is_array($answer)) {
|
if (is_array($answer)) {
|
||||||
// Handle array answers (likely checkboxes)
|
// Handle array answers (likely checkboxes)
|
||||||
if ('checkbox' === $question_type) {
|
if ('checkbox' === $question_type) {
|
||||||
// Sanitize each value in the array
|
// Sanitize each value in the array
|
||||||
$sanitized_answer = array_map('sanitize_text_field', $answer);
|
$sanitized_answer = array_map('sanitize_text_field', $answer);
|
||||||
// Note: update_post_meta can handle arrays directly, storing them serialized.
|
// Note: update_post_meta can handle arrays directly, storing them serialized.
|
||||||
} else {
|
} else {
|
||||||
// Unexpected array answer for this question type
|
// Unexpected array answer for this question type
|
||||||
error_log("Quiztech AJAX Error: Received array answer for non-checkbox question ID: " . $question_id);
|
error_log("Quiztech AJAX Error: Received array answer for non-checkbox question ID: " . $question_id);
|
||||||
// Sanitize by joining elements (simple approach, might need refinement)
|
// Sanitize by joining elements (simple approach, might need refinement)
|
||||||
$sanitized_answer = sanitize_text_field(implode(', ', $answer));
|
$sanitized_answer = sanitize_text_field(implode(', ', $answer));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle string/scalar answers
|
// Handle string/scalar answers
|
||||||
switch ($question_type) {
|
switch ($question_type) {
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
$sanitized_answer = sanitize_textarea_field($answer);
|
$sanitized_answer = sanitize_textarea_field($answer);
|
||||||
break;
|
break;
|
||||||
case 'numeric':
|
case 'numeric':
|
||||||
// Allow integers and potentially floats. Use floatval for broader acceptance.
|
// Allow integers and potentially floats. Use floatval for broader acceptance.
|
||||||
// Ensure it's actually numeric before casting to avoid warnings/errors.
|
// Ensure it's actually numeric before casting to avoid warnings/errors.
|
||||||
$sanitized_answer = is_numeric($answer) ? floatval($answer) : 0;
|
$sanitized_answer = is_numeric($answer) ? floatval($answer) : 0;
|
||||||
break;
|
break;
|
||||||
case 'multiple-choice': // Assuming the value is a simple key/identifier
|
case 'multiple-choice': // Assuming the value is a simple key/identifier
|
||||||
$sanitized_answer = sanitize_key($answer);
|
$sanitized_answer = sanitize_key($answer);
|
||||||
break;
|
break;
|
||||||
case 'text':
|
case 'text':
|
||||||
default: // Default to sanitize_text_field for 'text' or unknown/missing types
|
default: // Default to sanitize_text_field for 'text' or unknown/missing types
|
||||||
$sanitized_answer = sanitize_text_field($answer);
|
$sanitized_answer = sanitize_text_field($answer);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 3. Save Answer (as user_evaluation CPT meta)
|
// 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.
|
// 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.
|
// Using individual meta keys might be simpler for querying later if needed.
|
||||||
$meta_key = 'quiztech_answer_' . $question_id;
|
$meta_key = 'quiztech_answer_' . $question_id;
|
||||||
update_post_meta($evaluation_id, $meta_key, $sanitized_answer);
|
update_post_meta($evaluation_id, $meta_key, $sanitized_answer);
|
||||||
|
|
||||||
// 4. Send Response
|
// 4. Send Response
|
||||||
wp_send_json_success(['message' => __('Answer saved.', 'quiztech')]);
|
wp_send_json_success(['message' => __('Answer saved.', 'quiztech')]);
|
||||||
|
|
||||||
// Ensure script execution stops
|
// Ensure script execution stops
|
||||||
wp_die();
|
wp_die();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the final AJAX submission of the assessment.
|
* Handles the final AJAX submission of the assessment.
|
||||||
* Expects 'nonce' and 'invitation_id' in $_POST.
|
* Expects 'nonce' and 'invitation_id' in $_POST.
|
||||||
*/
|
*/
|
||||||
public function handle_submit_assessment() {
|
public function handle_submit_assessment() {
|
||||||
// 1. Verify Nonce
|
// 1. Verify Nonce
|
||||||
check_ajax_referer('quiztech_assessment_nonce', 'nonce'); // Reuse assessment nonce
|
check_ajax_referer('quiztech_assessment_nonce', 'nonce'); // Reuse assessment nonce
|
||||||
|
|
||||||
// 2. Get Data
|
// 2. Get Data
|
||||||
$invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0;
|
$invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0;
|
||||||
if ( ! $invitation_id ) {
|
if ( ! $invitation_id ) {
|
||||||
wp_send_json_error(['message' => __('Missing invitation ID.', 'quiztech')], 400);
|
wp_send_json_error(['message' => __('Missing invitation ID.', 'quiztech')], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Get or Create User Evaluation Record
|
// 3. Get or Create User Evaluation Record
|
||||||
$evaluation_id = $this->get_or_create_user_evaluation($invitation_id);
|
$evaluation_id = $this->get_or_create_user_evaluation($invitation_id);
|
||||||
if ( ! $evaluation_id ) {
|
if ( ! $evaluation_id ) {
|
||||||
error_log("Quiztech AJAX Error: Failed to get or create user_evaluation for invitation ID: " . $invitation_id . " during final submission.");
|
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);
|
wp_send_json_error(['message' => __('Could not process evaluation record for submission.', 'quiztech')], 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Update Invitation Status
|
// 4. Update Invitation Status
|
||||||
try {
|
try {
|
||||||
$invitations = new \Quiztech\AssessmentPlatform\Includes\Invitations();
|
$invitations = new Invitations();
|
||||||
// Note: The update_status method expects the invitation *record ID*, not the token.
|
// Assuming $invitation_id passed in POST *is* the record ID.
|
||||||
// We need to retrieve the invitation ID based on the token if we only have the token here.
|
$updated = $invitations->update_status($invitation_id, 'assessment-complete');
|
||||||
// Assuming $invitation_id passed in POST *is* the record ID for now. If it's the token, this needs adjustment.
|
if (!$updated) {
|
||||||
$updated = $invitations->update_status($invitation_id, 'assessment-complete');
|
error_log("Quiztech AJAX Error: Failed to update invitation status to complete for ID: " . $invitation_id);
|
||||||
if (!$updated) {
|
// Decide if this should be a user-facing error or just logged
|
||||||
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());
|
||||||
} catch (\Exception $e) {
|
// Decide if this should be a user-facing error
|
||||||
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'
|
// 5. Update User Evaluation CPT Status to 'completed'
|
||||||
$post_update_data = [
|
$post_update_data = [
|
||||||
'ID' => $evaluation_id,
|
'ID' => $evaluation_id,
|
||||||
'post_status' => 'completed', // Use a custom status if needed, but 'completed' seems appropriate
|
'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
|
$post_updated = wp_update_post($post_update_data, true); // Pass true for WP_Error object on failure
|
||||||
|
|
||||||
if (is_wp_error($post_updated)) {
|
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());
|
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
|
// Decide if this should be a user-facing error
|
||||||
// wp_send_json_error(['message' => __('Failed to finalize assessment record.', 'quiztech')], 500);
|
// wp_send_json_error(['message' => __('Failed to finalize assessment record.', 'quiztech')], 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 5. Send Response
|
// 6. Get the Assessment ID associated with the invitation
|
||||||
// Future Enhancement: Consider adding a redirect URL or specific completion message/HTML
|
// This requires a method in Invitations class to get the full record by ID
|
||||||
// to the response data based on plugin settings or other logic.
|
$invitation_record = $invitations->get_invitation_by_id($invitation_id); // Assuming this method exists or will be added
|
||||||
wp_send_json_success(['message' => __('Assessment submitted successfully!', 'quiztech')]);
|
$assessment_id = $invitation_record ? $invitation_record->assessment_id : 0;
|
||||||
|
|
||||||
// Ensure script execution stops
|
// 7. Get the custom completion message
|
||||||
wp_die();
|
$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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -52,9 +52,9 @@ class FrontendHandler {
|
||||||
|
|
||||||
// Determine the current step (pre-screening or assessment)
|
// Determine the current step (pre-screening or assessment)
|
||||||
$current_step = 'assessment'; // Default to 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.
|
// If pre-screening questions exist AND the invitation status is still 'sent' (the initial state), show pre-screening.
|
||||||
// Assumes 'pending' is the initial status before viewing/pre-screening. Adjust if needed.
|
// Once pre-screening is submitted, the status should change (e.g., to 'viewed', 'pre-screening complete').
|
||||||
if ( ! empty( $pre_screening_questions ) && $invitation_data->status === 'pending' ) {
|
if ( ! empty( $pre_screening_questions ) && $invitation_data->status === 'sent' ) {
|
||||||
$current_step = 'pre_screening';
|
$current_step = 'pre_screening';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,185 +6,211 @@ namespace Quiztech\AssessmentPlatform\Includes;
|
||||||
*/
|
*/
|
||||||
class Invitations {
|
class Invitations {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a cryptographically secure unique token for an invitation.
|
* Generate a cryptographically secure unique token for an invitation.
|
||||||
*
|
*
|
||||||
* @return string The generated unique token.
|
* @return string The generated unique token.
|
||||||
*/
|
*/
|
||||||
public function generate_unique_token() {
|
public function generate_unique_token() {
|
||||||
// Placeholder for token generation logic
|
// Placeholder for token generation logic
|
||||||
// Consider using wp_generate_password() or random_bytes()
|
// Consider using wp_generate_password() or random_bytes()
|
||||||
\error_log('Invitation Token Generation Called - Placeholder');
|
\error_log('Invitation Token Generation Called - Placeholder');
|
||||||
return bin2hex(random_bytes(16)); // Example placeholder
|
return bin2hex(random_bytes(16)); // Example placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and store an invitation record.
|
* Create and store an invitation record.
|
||||||
*
|
*
|
||||||
* @param int $job_id The ID of the job associated with the invitation.
|
* @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 int $assessment_id The ID of the assessment associated with the invitation.
|
||||||
* @param string $applicant_email The email address of the applicant being invited.
|
* @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.
|
* @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 ) {
|
public function create_invitation( $job_id, $assessment_id, $applicant_email ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$table_name = $wpdb->prefix . 'quiztech_invitations';
|
$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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
$token = $this->generate_unique_token();
|
||||||
* 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().
|
|
||||||
|
|
||||||
\error_log('Send Invitation Email Called - Placeholder');
|
$data = [
|
||||||
$invite_url = \site_url('/assessment-invite/?token=' . $token); // Corrected line 71
|
'token' => $token,
|
||||||
$subject = 'You are invited to take an assessment';
|
'job_id' => absint( $job_id ),
|
||||||
$message = "Please click the following link to take your assessment:\n\n" . $invite_url;
|
'assessment_id' => absint( $assessment_id ),
|
||||||
// $headers = ['Content-Type: text/html; charset=UTF-8']; // If sending HTML email
|
'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
|
$format = [
|
||||||
return $sent;
|
'%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 );
|
||||||
* 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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if ( false === $inserted ) {
|
||||||
* Update the status of an invitation record.
|
\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 ] );
|
||||||
* @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
|
// Return the DB ID of the inserted record, not the token, as ID is used elsewhere
|
||||||
$invitation_id = absint( $invitation_id );
|
return $wpdb->insert_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.' );
|
* Send the invitation email to the applicant.
|
||||||
return false;
|
*
|
||||||
}
|
* @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
|
\error_log('Send Invitation Email Called - Placeholder');
|
||||||
$allowed_statuses = [
|
$invite_url = \site_url('/assessment-invite/?token=' . $token); // Corrected line 71
|
||||||
'pending',
|
$subject = 'You are invited to take an assessment';
|
||||||
'viewed', // Optional status if needed
|
$message = "Please click the following link to take your assessment:\n\n" . $invite_url;
|
||||||
'pre-screening-complete',
|
// $headers = ['Content-Type: text/html; charset=UTF-8']; // If sending HTML email
|
||||||
'assessment-started', // Optional status
|
|
||||||
'assessment-complete',
|
|
||||||
'expired',
|
|
||||||
'cancelled', // Optional status
|
|
||||||
];
|
|
||||||
|
|
||||||
if ( ! in_array( $new_status, $allowed_statuses, true ) ) {
|
$sent = \wp_mail($applicant_email, $subject, $message); // Corrected line 76
|
||||||
\error_log( "Quiztech Error: Invalid status '{$new_status}' provided to update_status for invitation ID {$invitation_id}." );
|
return $sent;
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare data and format for update
|
/**
|
||||||
$data = [ 'status' => $new_status ];
|
* Validate an incoming invitation token.
|
||||||
$where = [ 'id' => $invitation_id ]; // Assuming 'id' is the primary key column name
|
* Checks if token exists and is in 'sent' or 'pre-screening-complete' status.
|
||||||
$format = [ '%s' ]; // Format for data
|
*
|
||||||
$where_format = [ '%d' ]; // Format for where clause
|
* @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 ) {
|
$invitation = $wpdb->get_row(
|
||||||
\error_log( "Quiztech Error: Failed to update invitation status for ID {$invitation_id}. DB Error: " . $wpdb->last_error );
|
$wpdb->prepare(
|
||||||
return false;
|
"SELECT * FROM $table_name WHERE token = %s",
|
||||||
}
|
$token
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// $updated contains the number of rows affected.
|
if ( ! $invitation ) {
|
||||||
// Return true if one or more rows were updated (or potentially 0 if the status was already set to the new value).
|
\error_log( "Quiztech Info: Invitation token not found: $token" );
|
||||||
// We consider 0 rows affected as success in the case the status was already correct.
|
return null; // Token doesn't exist
|
||||||
return true;
|
}
|
||||||
}
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue