447 lines
14 KiB
PHP
447 lines
14 KiB
PHP
<?php
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
class Beep_Polls {
|
|
|
|
const TABLE_NAME = 'beep_polls';
|
|
const VOTES_TABLE = 'beep_poll_votes';
|
|
|
|
public static function init() {
|
|
add_action('rest_api_init', [__CLASS__, 'register_routes']);
|
|
}
|
|
|
|
public static function table_name() {
|
|
global $wpdb;
|
|
return $wpdb->prefix . self::TABLE_NAME;
|
|
}
|
|
|
|
public static function votes_table_name() {
|
|
global $wpdb;
|
|
return $wpdb->prefix . self::VOTES_TABLE;
|
|
}
|
|
|
|
/**
|
|
* Create the polls and poll_votes tables.
|
|
*/
|
|
public static function create_tables() {
|
|
global $wpdb;
|
|
$polls = self::table_name();
|
|
$votes = self::votes_table_name();
|
|
$charset = $wpdb->get_charset_collate();
|
|
|
|
$polls_sql = "CREATE TABLE $polls (
|
|
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
beep_id BIGINT(20) UNSIGNED NOT NULL,
|
|
question TEXT NOT NULL,
|
|
options TEXT NOT NULL,
|
|
votes TEXT NOT NULL DEFAULT '[]',
|
|
total_votes BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
|
|
ends_at DATETIME NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id),
|
|
KEY beep_id (beep_id)
|
|
) $charset;";
|
|
|
|
$votes_sql = "CREATE TABLE $votes (
|
|
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
poll_id BIGINT(20) UNSIGNED NOT NULL,
|
|
user_id BIGINT(20) UNSIGNED NOT NULL,
|
|
option_index INT NOT NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY poll_user (poll_id, user_id),
|
|
KEY poll_id (poll_id),
|
|
KEY user_id (user_id)
|
|
) $charset;";
|
|
|
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
|
dbDelta($polls_sql);
|
|
dbDelta($votes_sql);
|
|
}
|
|
|
|
public static function register_routes() {
|
|
// Create a poll for a beep
|
|
register_rest_route('beep/v1', '/poll/(?P<beep_id>\d+)', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'create_poll'],
|
|
'permission_callback' => [__CLASS__, 'can_create_poll'],
|
|
]);
|
|
|
|
// Get poll data for a beep
|
|
register_rest_route('beep/v1', '/poll/(?P<beep_id>\d+)', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_poll'],
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
|
|
// Vote on a poll
|
|
register_rest_route('beep/v1', '/poll/(?P<beep_id>\d+)/vote', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'vote'],
|
|
'permission_callback' => [__CLASS__, 'can_vote'],
|
|
]);
|
|
}
|
|
|
|
public static function can_create_poll() {
|
|
return is_user_logged_in() && !beep_user_is_banned(get_current_user_id());
|
|
}
|
|
|
|
public static function can_vote() {
|
|
return is_user_logged_in() && !beep_user_is_banned(get_current_user_id());
|
|
}
|
|
|
|
/**
|
|
* Create a poll for a beep post.
|
|
* Expects JSON body: { "question": "...", "options": ["a", "b", ...], "ends_at": "YYYY-MM-DDTHH:MM:SS" }
|
|
*/
|
|
public static function create_poll($request) {
|
|
$beep_id = (int) $request['beep_id'];
|
|
$post = get_post($beep_id);
|
|
|
|
if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) {
|
|
return new WP_Error('not_found', 'Beep not found.', ['status' => 404]);
|
|
}
|
|
|
|
if (!current_user_can('edit_post', $beep_id)) {
|
|
return new WP_Error('forbidden', 'Cannot create poll for this beep.', ['status' => 403]);
|
|
}
|
|
|
|
// Check if poll already exists
|
|
if (self::poll_exists($beep_id)) {
|
|
return new WP_Error('poll_exists', 'A poll already exists for this beep.', ['status' => 400]);
|
|
}
|
|
|
|
$body = $request->get_json_params();
|
|
$question = sanitize_text_field($body['question'] ?? '');
|
|
$options = $body['options'] ?? [];
|
|
$ends_at = isset($body['ends_at']) ? sanitize_text_field($body['ends_at']) : null;
|
|
|
|
if (empty($question)) {
|
|
return new WP_Error('missing_question', 'Poll question is required.', ['status' => 400]);
|
|
}
|
|
|
|
if (count($options) < 2) {
|
|
return new WP_Error('too_few_options', 'Poll must have at least 2 options.', ['status' => 400]);
|
|
}
|
|
|
|
if (count($options) > 6) {
|
|
return new WP_Error('too_many_options', 'Poll can have at most 6 options.', ['status' => 400]);
|
|
}
|
|
|
|
// Sanitize options
|
|
$options = array_map('sanitize_text_field', $options);
|
|
$votes = array_fill(0, count($options), 0);
|
|
|
|
global $wpdb;
|
|
$table = self::table_name();
|
|
|
|
$result = $wpdb->insert($table, [
|
|
'beep_id' => $beep_id,
|
|
'question' => $question,
|
|
'options' => json_encode($options),
|
|
'votes' => json_encode($votes),
|
|
'total_votes' => 0,
|
|
'ends_at' => $ends_at ? date('Y-m-d H:i:s', strtotime($ends_at)) : null,
|
|
'created_at' => current_time('mysql'),
|
|
], ['%d', '%s', '%s', '%s', '%d', '%s', '%s']);
|
|
|
|
if ($result === false) {
|
|
return new WP_Error('db_error', 'Failed to create poll.', ['status' => 500]);
|
|
}
|
|
|
|
$poll_id = $wpdb->insert_id;
|
|
|
|
// Store poll_id in post meta for easy lookup
|
|
update_post_meta($beep_id, 'beep_poll_id', $poll_id);
|
|
|
|
return rest_ensure_response([
|
|
'id' => $poll_id,
|
|
'beep_id' => $beep_id,
|
|
'question' => $question,
|
|
'options' => $options,
|
|
'votes' => $votes,
|
|
'total_votes' => 0,
|
|
'ends_at' => $ends_at,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get poll data for a beep.
|
|
*/
|
|
public static function get_poll($request) {
|
|
$beep_id = (int) $request['beep_id'];
|
|
$poll = self::get_poll_by_beep_id($beep_id);
|
|
|
|
if (!$poll) {
|
|
return new WP_Error('not_found', 'Poll not found.', ['status' => 404]);
|
|
}
|
|
|
|
$user_id = get_current_user_id();
|
|
$user_vote = null;
|
|
if ($user_id) {
|
|
$user_vote = self::get_user_vote($poll['id'], $user_id);
|
|
}
|
|
|
|
return rest_ensure_response([
|
|
'id' => $poll['id'],
|
|
'beep_id' => $poll['beep_id'],
|
|
'question' => $poll['question'],
|
|
'options' => $poll['options'],
|
|
'votes' => $poll['votes'],
|
|
'total_votes' => $poll['total_votes'],
|
|
'ends_at' => $poll['ends_at'],
|
|
'has_ended' => $poll['ends_at'] ? (strtotime($poll['ends_at']) < time()) : false,
|
|
'user_vote' => $user_vote,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Vote on a poll.
|
|
* Expects JSON body: { "option_index": 0 }
|
|
*/
|
|
public static function vote($request) {
|
|
$beep_id = (int) $request['beep_id'];
|
|
$poll = self::get_poll_by_beep_id($beep_id);
|
|
|
|
if (!$poll) {
|
|
return new WP_Error('not_found', 'Poll not found.', ['status' => 404]);
|
|
}
|
|
|
|
// Check if poll has ended
|
|
if ($poll['ends_at'] && strtotime($poll['ends_at']) < time()) {
|
|
return new WP_Error('poll_ended', 'This poll has ended.', ['status' => 400]);
|
|
}
|
|
|
|
$body = $request->get_json_params();
|
|
$option_index = (int) ($body['option_index'] ?? -1);
|
|
|
|
if ($option_index < 0 || $option_index >= count($poll['options'])) {
|
|
return new WP_Error('invalid_option', 'Invalid option index.', ['status' => 400]);
|
|
}
|
|
|
|
$user_id = get_current_user_id();
|
|
|
|
// Check if user already voted
|
|
$existing_vote = self::get_user_vote($poll['id'], $user_id);
|
|
if ($existing_vote !== null) {
|
|
// If same option, un-vote (toggle)
|
|
if ($existing_vote === $option_index) {
|
|
return rest_ensure_response(self::remove_vote($poll['id'], $user_id, $option_index, $beep_id));
|
|
}
|
|
// Different option - change vote
|
|
return rest_ensure_response(self::change_vote($poll['id'], $user_id, $existing_vote, $option_index, $beep_id));
|
|
}
|
|
|
|
// Cast new vote
|
|
return rest_ensure_response(self::cast_vote($poll['id'], $user_id, $option_index, $beep_id));
|
|
}
|
|
|
|
private static function cast_vote($poll_id, $user_id, $option_index, $beep_id) {
|
|
global $wpdb;
|
|
$polls_table = self::table_name();
|
|
$votes_table = self::votes_table_name();
|
|
|
|
// Insert vote
|
|
$wpdb->insert($votes_table, [
|
|
'poll_id' => $poll_id,
|
|
'user_id' => $user_id,
|
|
'option_index' => $option_index,
|
|
'created_at' => current_time('mysql'),
|
|
], ['%d', '%d', '%d', '%s']);
|
|
|
|
// Update poll votes and total
|
|
$poll = self::get_poll_by_id($poll_id);
|
|
$votes = $poll['votes'];
|
|
$votes[$option_index]++;
|
|
$total_votes = $poll['total_votes'] + 1;
|
|
|
|
$wpdb->update(
|
|
$polls_table,
|
|
['votes' => json_encode($votes), 'total_votes' => $total_votes],
|
|
['id' => $poll_id],
|
|
['%s', '%d'],
|
|
['%d']
|
|
);
|
|
|
|
return [
|
|
'voted' => true,
|
|
'option_index' => $option_index,
|
|
'votes' => $votes,
|
|
'total_votes' => $total_votes,
|
|
'user_vote' => $option_index,
|
|
];
|
|
}
|
|
|
|
private static function remove_vote($poll_id, $user_id, $option_index, $beep_id) {
|
|
global $wpdb;
|
|
$polls_table = self::table_name();
|
|
$votes_table = self::votes_table_name();
|
|
|
|
// Delete vote
|
|
$wpdb->delete($votes_table, [
|
|
'poll_id' => $poll_id,
|
|
'user_id' => $user_id,
|
|
], ['%d', '%d']);
|
|
|
|
// Update poll votes and total
|
|
$poll = self::get_poll_by_id($poll_id);
|
|
$votes = $poll['votes'];
|
|
$votes[$option_index] = max(0, $votes[$option_index] - 1);
|
|
$total_votes = max(0, $poll['total_votes'] - 1);
|
|
|
|
$wpdb->update(
|
|
$polls_table,
|
|
['votes' => json_encode($votes), 'total_votes' => $total_votes],
|
|
['id' => $poll_id],
|
|
['%s', '%d'],
|
|
['%d']
|
|
);
|
|
|
|
return [
|
|
'voted' => false,
|
|
'option_index' => null,
|
|
'votes' => $votes,
|
|
'total_votes' => $total_votes,
|
|
'user_vote' => null,
|
|
];
|
|
}
|
|
|
|
private static function change_vote($poll_id, $user_id, $old_index, $new_index, $beep_id) {
|
|
global $wpdb;
|
|
$polls_table = self::table_name();
|
|
$votes_table = self::votes_table_name();
|
|
|
|
// Update vote
|
|
$wpdb->update(
|
|
$votes_table,
|
|
['option_index' => $new_index],
|
|
['poll_id' => $poll_id, 'user_id' => $user_id],
|
|
['%d'],
|
|
['%d', '%d']
|
|
);
|
|
|
|
// Update poll votes
|
|
$poll = self::get_poll_by_id($poll_id);
|
|
$votes = $poll['votes'];
|
|
$votes[$old_index] = max(0, $votes[$old_index] - 1);
|
|
$votes[$new_index]++;
|
|
|
|
$wpdb->update(
|
|
$polls_table,
|
|
['votes' => json_encode($votes)],
|
|
['id' => $poll_id],
|
|
['%s'],
|
|
['%d']
|
|
);
|
|
|
|
return [
|
|
'voted' => true,
|
|
'option_index' => $new_index,
|
|
'votes' => $votes,
|
|
'total_votes' => $poll['total_votes'],
|
|
'user_vote' => $new_index,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get poll by beep ID.
|
|
*/
|
|
public static function get_poll_by_beep_id($beep_id) {
|
|
// First check post meta for poll_id
|
|
$poll_id = get_post_meta($beep_id, 'beep_poll_id', true);
|
|
if ($poll_id) {
|
|
return self::get_poll_by_id($poll_id);
|
|
}
|
|
|
|
// Fallback to table lookup
|
|
global $wpdb;
|
|
$table = self::table_name();
|
|
$row = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $table WHERE beep_id = %d",
|
|
$beep_id
|
|
), ARRAY_A);
|
|
|
|
if (!$row) {
|
|
return null;
|
|
}
|
|
|
|
return self::format_poll($row);
|
|
}
|
|
|
|
/**
|
|
* Get poll by ID.
|
|
*/
|
|
public static function get_poll_by_id($poll_id) {
|
|
global $wpdb;
|
|
$table = self::table_name();
|
|
$row = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $table WHERE id = %d",
|
|
$poll_id
|
|
), ARRAY_A);
|
|
|
|
if (!$row) {
|
|
return null;
|
|
}
|
|
|
|
return self::format_poll($row);
|
|
}
|
|
|
|
/**
|
|
* Check if poll exists for a beep.
|
|
*/
|
|
public static function poll_exists($beep_id) {
|
|
return self::get_poll_by_beep_id($beep_id) !== null;
|
|
}
|
|
|
|
/**
|
|
* Get user's vote for a poll.
|
|
*/
|
|
public static function get_user_vote($poll_id, $user_id) {
|
|
global $wpdb;
|
|
$table = self::votes_table_name();
|
|
$row = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT option_index FROM $table WHERE poll_id = %d AND user_id = %d",
|
|
$poll_id, $user_id
|
|
));
|
|
|
|
return $row ? (int) $row->option_index : null;
|
|
}
|
|
|
|
/**
|
|
* Format poll row from database.
|
|
*/
|
|
private static function format_poll($row) {
|
|
return [
|
|
'id' => (int) $row['id'],
|
|
'beep_id' => (int) $row['beep_id'],
|
|
'question' => $row['question'],
|
|
'options' => json_decode($row['options'], true),
|
|
'votes' => json_decode($row['votes'], true),
|
|
'total_votes' => (int) $row['total_votes'],
|
|
'ends_at' => $row['ends_at'],
|
|
'created_at' => $row['created_at'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get poll results with percentages (for display).
|
|
*/
|
|
public static function get_results_with_percentages($poll) {
|
|
$total = $poll['total_votes'];
|
|
$results = [];
|
|
foreach ($poll['options'] as $i => $option) {
|
|
$votes = $poll['votes'][$i] ?? 0;
|
|
$percentage = $total > 0 ? round(($votes / $total) * 100) : 0;
|
|
$results[] = [
|
|
'index' => $i,
|
|
'option' => $option,
|
|
'votes' => $votes,
|
|
'percentage' => $percentage,
|
|
];
|
|
}
|
|
return $results;
|
|
}
|
|
}
|