Files
imks/models/message_poll.rs
T
zhenyi 821537186e refactor(tests): reformat code and update dependency management
- Reorganized import statements in adapter tests for better readability
- Replaced or_insert_with(Vec::new) with or_default() in test closures
- Updated Cargo.lock with new dependency versions and checksums
- Added TLS features to tonic dependency configuration
- Included sqlx, chrono, and uuid dependencies with specific features
- Added jsonwebtoken and arc-swap as project dependencies
- Reformatted assertion statements to comply with line length limits
- Adjusted base64 import order in engine codec module
- Updated protobuf include statement formatting
2026-06-11 12:11:05 +08:00

171 lines
4.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Poll on a message — new tables `message_poll`, `message_poll_option`,
//! `message_poll_vote`.
//!
//! Discord-style polls: attached to a message, with multiple options,
//! optional multi-vote, and an expiry time.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// The poll itself (one per message, optional).
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct MessagePoll {
pub id: Uuid,
pub message_id: Uuid,
/// The question displayed to voters.
pub question: String,
/// Whether users can select multiple options.
pub allow_multiselect: bool,
/// Maximum number of options a user can select (NULL = unlimited when multiselect).
pub max_selections: Option<i32>,
/// When voting closes (NULL = no expiry).
pub expires_at: Option<DateTime<Utc>>,
/// Total number of votes cast (denormalized for fast reads).
pub total_votes: i64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// A single selectable option within a poll.
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct MessagePollOption {
pub id: Uuid,
pub poll_id: Uuid,
/// Display text for this option.
pub text: String,
/// Optional emoji prefix (Discord-style).
pub emoji: Option<String>,
/// Number of votes this option received (denormalized).
pub vote_count: i64,
/// Display order.
pub position: i32,
}
/// A single vote cast by a user.
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct MessagePollVote {
pub id: Uuid,
pub poll_id: Uuid,
pub option_id: Uuid,
pub user_id: Uuid,
pub created_at: DateTime<Utc>,
}
/// Aggregated poll results for API responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PollResult {
pub poll: MessagePoll,
pub options: Vec<PollOptionResult>,
/// Which options the current user voted for (empty if not voted).
pub my_votes: Vec<Uuid>,
/// Whether the poll has expired.
pub is_expired: bool,
}
/// Option with its vote count and percentage.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PollOptionResult {
#[serde(flatten)]
pub option: MessagePollOption,
/// Percentage of total votes (0.0100.0), rounded to 1 decimal.
pub percentage: f64,
}
impl PollResult {
/// Compute percentages from total_votes.
pub fn from_poll(
poll: MessagePoll,
options: Vec<MessagePollOption>,
my_votes: Vec<Uuid>,
) -> Self {
let total = poll.total_votes.max(1) as f64;
let now = Utc::now();
let is_expired = poll.expires_at.is_some_and(|exp| now >= exp);
let options = options
.into_iter()
.map(|opt| {
let pct = if poll.total_votes > 0 {
(opt.vote_count as f64 / total * 100.0 * 10.0).round() / 10.0
} else {
0.0
};
PollOptionResult {
option: opt,
percentage: pct,
}
})
.collect();
Self {
poll,
options,
my_votes,
is_expired,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_poll_result_percentages() {
let poll = MessagePoll {
id: Uuid::now_v7(),
message_id: Uuid::now_v7(),
question: "Best language?".to_string(),
allow_multiselect: false,
max_selections: None,
expires_at: None,
total_votes: 10,
created_at: Utc::now(),
updated_at: Utc::now(),
};
let options = vec![
MessagePollOption {
id: Uuid::now_v7(),
poll_id: poll.id,
text: "Rust".to_string(),
emoji: None,
vote_count: 7,
position: 0,
},
MessagePollOption {
id: Uuid::now_v7(),
poll_id: poll.id,
text: "Go".to_string(),
emoji: None,
vote_count: 3,
position: 1,
},
];
let result = PollResult::from_poll(poll, options, vec![]);
assert!(!result.is_expired);
assert_eq!(result.options[0].percentage, 70.0);
assert_eq!(result.options[1].percentage, 30.0);
}
#[test]
fn test_poll_result_zero_votes() {
let poll = MessagePoll {
id: Uuid::now_v7(),
message_id: Uuid::now_v7(),
question: "Empty poll".to_string(),
allow_multiselect: false,
max_selections: None,
expires_at: None,
total_votes: 0,
created_at: Utc::now(),
updated_at: Utc::now(),
};
let result = PollResult::from_poll(poll, vec![], vec![]);
assert!(result.options.is_empty());
}
}