- Moved governance models (`Vote`, `VoteType`, `VotingResults`) from `models/governance.rs` to `controllers/governance.rs` for better organization and to avoid circular dependencies. This improves maintainability and reduces complexity. - Updated governance views to use the new model locations. - Added a limit to the number of recent activities displayed on the dashboard for performance optimization.
993 lines
36 KiB
Rust
993 lines
36 KiB
Rust
use crate::db::governance::{
|
|
self, create_activity, get_all_activities, get_proposal_by_id, get_proposals,
|
|
get_recent_activities,
|
|
};
|
|
// Note: Now using heromodels directly instead of local governance models
|
|
use crate::utils::render_template;
|
|
use actix_session::Session;
|
|
use actix_web::{HttpResponse, Responder, Result, web};
|
|
use chrono::{Duration, Utc};
|
|
use heromodels::models::ActivityType;
|
|
use heromodels::models::governance::{Proposal, ProposalStatus};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
use tera::Tera;
|
|
|
|
use chrono::prelude::*;
|
|
|
|
/// Simple vote type for UI display
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub enum VoteType {
|
|
Yes,
|
|
No,
|
|
Abstain,
|
|
}
|
|
|
|
/// Simple vote structure for UI display
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Vote {
|
|
pub id: String,
|
|
pub proposal_id: String,
|
|
pub voter_id: i32,
|
|
pub voter_name: String,
|
|
pub vote_type: VoteType,
|
|
pub comment: Option<String>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl Vote {
|
|
pub fn new(
|
|
proposal_id: String,
|
|
voter_id: i32,
|
|
voter_name: String,
|
|
vote_type: VoteType,
|
|
comment: Option<String>,
|
|
) -> Self {
|
|
let now = Utc::now();
|
|
Self {
|
|
id: uuid::Uuid::new_v4().to_string(),
|
|
proposal_id,
|
|
voter_id,
|
|
voter_name,
|
|
vote_type,
|
|
comment,
|
|
created_at: now,
|
|
updated_at: now,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Simple voting results structure for UI display
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct VotingResults {
|
|
pub proposal_id: String,
|
|
pub yes_count: usize,
|
|
pub no_count: usize,
|
|
pub abstain_count: usize,
|
|
pub total_votes: usize,
|
|
}
|
|
|
|
impl VotingResults {
|
|
pub fn new(proposal_id: String) -> Self {
|
|
Self {
|
|
proposal_id,
|
|
yes_count: 0,
|
|
no_count: 0,
|
|
abstain_count: 0,
|
|
total_votes: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Controller for handling governance-related routes
|
|
pub struct GovernanceController;
|
|
|
|
#[allow(dead_code)]
|
|
impl GovernanceController {
|
|
/// Helper function to get user from session
|
|
/// For testing purposes, this will always return a mock user
|
|
fn get_user_from_session(session: &Session) -> Option<Value> {
|
|
// Try to get user from session first
|
|
let session_user = session
|
|
.get::<String>("user")
|
|
.ok()
|
|
.flatten()
|
|
.and_then(|user_json| serde_json::from_str(&user_json).ok());
|
|
|
|
// If user is not in session, return a mock user for testing
|
|
session_user.or_else(|| {
|
|
// Create a mock user
|
|
let mock_user = serde_json::json!({
|
|
"id": 1,
|
|
"username": "test_user",
|
|
"email": "test@example.com",
|
|
"name": "Test User",
|
|
"role": "member"
|
|
});
|
|
Some(mock_user)
|
|
})
|
|
}
|
|
|
|
/// Calculate statistics from the database
|
|
fn calculate_statistics_from_database(proposals: &[Proposal]) -> GovernanceStats {
|
|
let mut stats = GovernanceStats {
|
|
total_proposals: proposals.len(),
|
|
active_proposals: 0,
|
|
approved_proposals: 0,
|
|
rejected_proposals: 0,
|
|
draft_proposals: 0,
|
|
total_votes: 0,
|
|
participation_rate: 0.0,
|
|
};
|
|
|
|
// Count proposals by status
|
|
for proposal in proposals {
|
|
match proposal.status {
|
|
ProposalStatus::Active => stats.active_proposals += 1,
|
|
ProposalStatus::Approved => stats.approved_proposals += 1,
|
|
ProposalStatus::Rejected => stats.rejected_proposals += 1,
|
|
ProposalStatus::Draft => stats.draft_proposals += 1,
|
|
_ => {} // Handle other statuses if needed
|
|
}
|
|
|
|
// Count total votes
|
|
stats.total_votes += proposal.ballots.len();
|
|
}
|
|
|
|
// Calculate participation rate (if there are any proposals)
|
|
if stats.total_proposals > 0 {
|
|
// This is a simplified calculation - in a real application, you would
|
|
// calculate this based on the number of eligible voters
|
|
stats.participation_rate =
|
|
(stats.total_votes as f64 / stats.total_proposals as f64) * 100.0;
|
|
}
|
|
|
|
stats
|
|
}
|
|
|
|
/// Handles the governance dashboard page route
|
|
pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
|
|
let mut ctx = tera::Context::new();
|
|
ctx.insert("active_page", "governance");
|
|
ctx.insert("active_tab", "dashboard");
|
|
|
|
// Header data
|
|
ctx.insert("page_title", "Governance Dashboard");
|
|
ctx.insert(
|
|
"page_description",
|
|
"Participate in community decision-making",
|
|
);
|
|
ctx.insert("show_create_button", &false);
|
|
|
|
// Add user to context (will always be available with our mock user)
|
|
let user = Self::get_user_from_session(&session).unwrap();
|
|
ctx.insert("user", &user);
|
|
|
|
// Get proposals from the database
|
|
let proposals = match crate::db::governance::get_proposals() {
|
|
Ok(props) => {
|
|
// println!(
|
|
// "📋 Proposals list page: Successfully loaded {} proposals from database",
|
|
// props.len()
|
|
// );
|
|
for (i, proposal) in props.iter().enumerate() {
|
|
println!(
|
|
" Proposal {}: ID={}, title={:?}, status={:?}",
|
|
i + 1,
|
|
proposal.base_data.id,
|
|
proposal.title,
|
|
proposal.status
|
|
);
|
|
}
|
|
props
|
|
}
|
|
Err(e) => {
|
|
println!("❌ Proposals list page: Failed to load proposals: {}", e);
|
|
ctx.insert("error", &format!("Failed to load proposals: {}", e));
|
|
vec![]
|
|
}
|
|
};
|
|
|
|
// Make a copy of proposals for statistics
|
|
let proposals_for_stats = proposals.clone();
|
|
|
|
// Filter for active proposals only
|
|
let active_proposals: Vec<heromodels::models::Proposal> = proposals
|
|
.into_iter()
|
|
.filter(|p| p.status == heromodels::models::ProposalStatus::Active)
|
|
.collect();
|
|
|
|
// Sort active proposals by voting end date (ascending)
|
|
let mut sorted_active_proposals = active_proposals.clone();
|
|
sorted_active_proposals.sort_by(|a, b| a.vote_start_date.cmp(&b.vote_end_date));
|
|
|
|
ctx.insert("proposals", &sorted_active_proposals);
|
|
|
|
// Get the nearest deadline proposal for the voting pane
|
|
if let Some(nearest_proposal) = sorted_active_proposals.first() {
|
|
// Calculate voting results for the nearest proposal
|
|
let results = Self::calculate_voting_results_from_proposal(nearest_proposal);
|
|
|
|
// Add both the proposal and its results to the context
|
|
ctx.insert("nearest_proposal", nearest_proposal);
|
|
ctx.insert("nearest_proposal_results", &results);
|
|
}
|
|
|
|
// Calculate statistics from the database
|
|
let stats = Self::calculate_statistics_from_database(&proposals_for_stats);
|
|
ctx.insert("stats", &stats);
|
|
|
|
// Get recent governance activities from our tracker (limit to 4 for dashboard)
|
|
let recent_activity = match Self::get_recent_governance_activities() {
|
|
Ok(activities) => activities.into_iter().take(4).collect::<Vec<_>>(),
|
|
Err(e) => {
|
|
eprintln!("Failed to load recent activities: {}", e);
|
|
Vec::new()
|
|
}
|
|
};
|
|
ctx.insert("recent_activity", &recent_activity);
|
|
|
|
render_template(&tmpl, "governance/index.html", &ctx)
|
|
}
|
|
|
|
/// Handles the proposal list page route
|
|
pub async fn proposals(
|
|
query: web::Query<ProposalQuery>,
|
|
tmpl: web::Data<Tera>,
|
|
session: Session,
|
|
) -> Result<impl Responder> {
|
|
let mut ctx = tera::Context::new();
|
|
ctx.insert("active_page", "governance");
|
|
ctx.insert("active_tab", "proposals");
|
|
|
|
// Header data
|
|
ctx.insert("page_title", "All Proposals");
|
|
ctx.insert(
|
|
"page_description",
|
|
"Browse and filter all governance proposals",
|
|
);
|
|
ctx.insert("show_create_button", &false);
|
|
|
|
// Add user to context if available
|
|
if let Some(user) = Self::get_user_from_session(&session) {
|
|
ctx.insert("user", &user);
|
|
}
|
|
|
|
// Get proposals from the database
|
|
let mut proposals = match get_proposals() {
|
|
Ok(props) => props,
|
|
Err(e) => {
|
|
ctx.insert("error", &format!("Failed to load proposals: {}", e));
|
|
vec![]
|
|
}
|
|
};
|
|
|
|
// Filter proposals by status if provided
|
|
if let Some(status_filter) = &query.status {
|
|
if !status_filter.is_empty() {
|
|
proposals = proposals
|
|
.into_iter()
|
|
.filter(|p| {
|
|
let proposal_status = format!("{:?}", p.status);
|
|
proposal_status == *status_filter
|
|
})
|
|
.collect();
|
|
}
|
|
}
|
|
|
|
// Filter by search term if provided (title or description)
|
|
if let Some(search_term) = &query.search {
|
|
if !search_term.is_empty() {
|
|
let search_term = search_term.to_lowercase();
|
|
proposals = proposals
|
|
.into_iter()
|
|
.filter(|p| {
|
|
p.title.to_lowercase().contains(&search_term)
|
|
|| p.description.to_lowercase().contains(&search_term)
|
|
})
|
|
.collect();
|
|
}
|
|
}
|
|
|
|
// Add the filtered proposals to the context
|
|
ctx.insert("proposals", &proposals);
|
|
|
|
// Add the filter values back to the context for form persistence
|
|
ctx.insert("status_filter", &query.status);
|
|
ctx.insert("search_filter", &query.search);
|
|
|
|
render_template(&tmpl, "governance/proposals.html", &ctx)
|
|
}
|
|
|
|
/// Handles the proposal detail page route
|
|
pub async fn proposal_detail(
|
|
path: web::Path<String>,
|
|
req: actix_web::HttpRequest,
|
|
tmpl: web::Data<Tera>,
|
|
session: Session,
|
|
) -> Result<impl Responder> {
|
|
// Extract query parameters from the request
|
|
let query_str = req.query_string();
|
|
let vote_success = query_str.contains("vote_success=true");
|
|
let proposal_id = path.into_inner();
|
|
let mut ctx = tera::Context::new();
|
|
ctx.insert("active_page", "governance");
|
|
ctx.insert("active_tab", "proposals");
|
|
|
|
// Header data
|
|
ctx.insert("page_title", "Proposal Details");
|
|
ctx.insert(
|
|
"page_description",
|
|
"View proposal information and cast your vote",
|
|
);
|
|
ctx.insert("show_create_button", &false);
|
|
|
|
// Add user to context if available
|
|
if let Some(user) = Self::get_user_from_session(&session) {
|
|
ctx.insert("user", &user);
|
|
}
|
|
|
|
// Get mock proposal detail
|
|
let proposal = get_proposal_by_id(proposal_id.parse().unwrap());
|
|
if let Ok(Some(proposal)) = proposal {
|
|
ctx.insert("proposal", &proposal);
|
|
|
|
// Extract votes directly from the proposal
|
|
let votes = Self::extract_votes_from_proposal(&proposal);
|
|
ctx.insert("votes", &votes);
|
|
|
|
// Calculate voting results directly from the proposal
|
|
let results = Self::calculate_voting_results_from_proposal(&proposal);
|
|
ctx.insert("results", &results);
|
|
|
|
// Check if vote_success parameter is present and add success message
|
|
if vote_success {
|
|
ctx.insert("success", "Your vote has been successfully recorded!");
|
|
}
|
|
|
|
render_template(&tmpl, "governance/proposal_detail.html", &ctx)
|
|
} else {
|
|
// Proposal not found
|
|
ctx.insert("error", "Proposal not found");
|
|
// For the error page, we'll use a special case to set the status code to 404
|
|
match tmpl.render("error.html", &ctx) {
|
|
Ok(content) => Ok(HttpResponse::NotFound()
|
|
.content_type("text/html")
|
|
.body(content)),
|
|
Err(e) => {
|
|
eprintln!("Error rendering error template: {}", e);
|
|
Err(actix_web::error::ErrorInternalServerError(format!(
|
|
"Error: {}",
|
|
e
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Handles the create proposal page route
|
|
pub async fn create_proposal_form(
|
|
tmpl: web::Data<Tera>,
|
|
session: Session,
|
|
) -> Result<impl Responder> {
|
|
let mut ctx = tera::Context::new();
|
|
ctx.insert("active_page", "governance");
|
|
ctx.insert("active_tab", "create");
|
|
|
|
// Header data
|
|
ctx.insert("page_title", "Create Proposal");
|
|
ctx.insert(
|
|
"page_description",
|
|
"Submit a new proposal for community voting",
|
|
);
|
|
ctx.insert("show_create_button", &false);
|
|
|
|
// Add user to context (will always be available with our mock user)
|
|
let user = Self::get_user_from_session(&session).unwrap();
|
|
ctx.insert("user", &user);
|
|
|
|
render_template(&tmpl, "governance/create_proposal.html", &ctx)
|
|
}
|
|
|
|
/// Handles the submission of a new proposal
|
|
pub async fn submit_proposal(
|
|
_form: web::Form<ProposalForm>,
|
|
tmpl: web::Data<Tera>,
|
|
session: Session,
|
|
) -> Result<impl Responder> {
|
|
let mut ctx = tera::Context::new();
|
|
ctx.insert("active_page", "governance");
|
|
|
|
// Add user to context (will always be available with our mock user)
|
|
let user = Self::get_user_from_session(&session).unwrap();
|
|
ctx.insert("user", &user);
|
|
|
|
let proposal_title = &_form.title;
|
|
let proposal_description = &_form.description;
|
|
|
|
// Use the DB-backed proposal creation
|
|
// Parse voting_start_date and voting_end_date from the form (YYYY-MM-DD expected)
|
|
let voting_start_date = _form.voting_start_date.as_ref().and_then(|s| {
|
|
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
|
|
.ok()
|
|
.and_then(|d| d.and_hms_opt(0, 0, 0))
|
|
.map(|naive| chrono::Utc.from_utc_datetime(&naive))
|
|
});
|
|
let voting_end_date = _form.voting_end_date.as_ref().and_then(|s| {
|
|
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
|
|
.ok()
|
|
.and_then(|d| d.and_hms_opt(23, 59, 59))
|
|
.map(|naive| chrono::Utc.from_utc_datetime(&naive))
|
|
});
|
|
|
|
// Extract user id and name from serde_json::Value
|
|
let user_id = user
|
|
.get("id")
|
|
.and_then(|v| v.as_i64())
|
|
.unwrap_or(1)
|
|
.to_string();
|
|
|
|
let user_name = user
|
|
.get("username")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("Test User")
|
|
.to_string();
|
|
|
|
let is_draft = _form.draft.is_some();
|
|
let status = if is_draft {
|
|
ProposalStatus::Draft
|
|
} else {
|
|
ProposalStatus::Active
|
|
};
|
|
match governance::create_new_proposal(
|
|
&user_id,
|
|
&user_name,
|
|
proposal_title,
|
|
proposal_description,
|
|
status,
|
|
voting_start_date,
|
|
voting_end_date,
|
|
) {
|
|
Ok((proposal_id, saved_proposal)) => {
|
|
println!(
|
|
"Proposal saved to DB: ID={}, title={:?}",
|
|
proposal_id, saved_proposal.title
|
|
);
|
|
|
|
// Track the proposal creation activity
|
|
let _ = create_activity(
|
|
proposal_id,
|
|
&saved_proposal.title,
|
|
&user_name,
|
|
&ActivityType::ProposalCreated,
|
|
);
|
|
|
|
ctx.insert("success", "Proposal created successfully!");
|
|
}
|
|
Err(err) => {
|
|
println!("Failed to save proposal: {err}");
|
|
ctx.insert("error", &format!("Failed to save proposal: {err}"));
|
|
}
|
|
}
|
|
|
|
// For now, we'll just redirect to the proposals page with a success message
|
|
|
|
// Get proposals from the database
|
|
let proposals = match crate::db::governance::get_proposals() {
|
|
Ok(props) => {
|
|
println!(
|
|
"✅ Successfully loaded {} proposals from database",
|
|
props.len()
|
|
);
|
|
for (i, proposal) in props.iter().enumerate() {
|
|
println!(
|
|
" Proposal {}: ID={}, title={:?}, status={:?}",
|
|
i + 1,
|
|
proposal.base_data.id,
|
|
proposal.title,
|
|
proposal.status
|
|
);
|
|
}
|
|
props
|
|
}
|
|
Err(e) => {
|
|
println!("❌ Failed to load proposals: {}", e);
|
|
ctx.insert("error", &format!("Failed to load proposals: {}", e));
|
|
vec![]
|
|
}
|
|
};
|
|
ctx.insert("proposals", &proposals);
|
|
|
|
// Add the required context variables for the proposals template
|
|
ctx.insert("active_tab", "proposals");
|
|
ctx.insert("status_filter", &None::<String>);
|
|
ctx.insert("search_filter", &None::<String>);
|
|
|
|
// Header data (required by _header.html template)
|
|
ctx.insert("page_title", "All Proposals");
|
|
ctx.insert(
|
|
"page_description",
|
|
"Browse and filter all governance proposals",
|
|
);
|
|
ctx.insert("show_create_button", &false);
|
|
|
|
render_template(&tmpl, "governance/proposals.html", &ctx)
|
|
}
|
|
|
|
/// Handles the submission of a vote on a proposal
|
|
pub async fn submit_vote(
|
|
path: web::Path<String>,
|
|
form: web::Form<VoteForm>,
|
|
tmpl: web::Data<Tera>,
|
|
session: Session,
|
|
) -> Result<impl Responder> {
|
|
let proposal_id = path.into_inner();
|
|
let mut ctx = tera::Context::new();
|
|
ctx.insert("active_page", "governance");
|
|
|
|
// Check if user is logged in
|
|
let user = match Self::get_user_from_session(&session) {
|
|
Some(user) => user,
|
|
None => {
|
|
return Ok(HttpResponse::Found()
|
|
.append_header(("Location", "/login"))
|
|
.finish());
|
|
}
|
|
};
|
|
ctx.insert("user", &user);
|
|
|
|
// Extract user ID
|
|
let user_id = user.get("id").and_then(|v| v.as_i64()).unwrap_or(1) as i32;
|
|
|
|
// Parse proposal ID
|
|
let proposal_id_u32 = match proposal_id.parse::<u32>() {
|
|
Ok(id) => id,
|
|
Err(_) => {
|
|
ctx.insert("error", "Invalid proposal ID");
|
|
return render_template(&tmpl, "error.html", &ctx);
|
|
}
|
|
};
|
|
|
|
// Submit the vote
|
|
match crate::db::governance::submit_vote_on_proposal(
|
|
proposal_id_u32,
|
|
user_id,
|
|
&form.vote_type,
|
|
1, // Default to 1 share
|
|
form.comment.as_ref().map(|s| s.to_string()), // Pass the comment from the form
|
|
) {
|
|
Ok(_) => {
|
|
// Record the vote activity
|
|
let user_name = user
|
|
.get("username")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("Unknown User");
|
|
|
|
// Track the vote cast activity
|
|
if let Ok(Some(proposal)) = get_proposal_by_id(proposal_id_u32) {
|
|
let _ = create_activity(
|
|
proposal_id_u32,
|
|
&proposal.title,
|
|
user_name,
|
|
&ActivityType::VoteCast,
|
|
);
|
|
}
|
|
|
|
// Redirect to the proposal detail page with a success message
|
|
return Ok(HttpResponse::Found()
|
|
.append_header((
|
|
"Location",
|
|
format!("/governance/proposals/{}?vote_success=true", proposal_id),
|
|
))
|
|
.finish());
|
|
}
|
|
Err(e) => {
|
|
ctx.insert("error", &format!("Failed to submit vote: {}", e));
|
|
render_template(&tmpl, "error.html", &ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Handles the my votes page route
|
|
pub async fn my_votes(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
|
|
let mut ctx = tera::Context::new();
|
|
ctx.insert("active_page", "governance");
|
|
ctx.insert("active_tab", "my_votes");
|
|
|
|
// Header data
|
|
ctx.insert("page_title", "My Votes");
|
|
ctx.insert(
|
|
"page_description",
|
|
"View your voting history and participation",
|
|
);
|
|
ctx.insert("show_create_button", &false);
|
|
|
|
// Add user to context (will always be available with our mock user)
|
|
let user = Self::get_user_from_session(&session).unwrap();
|
|
ctx.insert("user", &user);
|
|
|
|
// Extract user ID
|
|
let user_id = user.get("id").and_then(|v| v.as_i64()).unwrap_or(1) as i32;
|
|
|
|
// Get all proposals from the database
|
|
let proposals = match crate::db::governance::get_proposals() {
|
|
Ok(props) => props,
|
|
Err(e) => {
|
|
ctx.insert("error", &format!("Failed to load proposals: {}", e));
|
|
vec![]
|
|
}
|
|
};
|
|
|
|
// Extract votes for this user from all proposals
|
|
let mut user_votes = Vec::new();
|
|
for proposal in &proposals {
|
|
// Extract votes from this proposal
|
|
let votes = Self::extract_votes_from_proposal(proposal);
|
|
|
|
// Filter votes for this user
|
|
for vote in votes {
|
|
if vote.voter_id == user_id {
|
|
user_votes.push((vote, proposal.clone()));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate total vote counts for all proposals
|
|
let total_vote_counts = Self::calculate_total_vote_counts(&proposals);
|
|
ctx.insert("total_yes_votes", &total_vote_counts.0);
|
|
ctx.insert("total_no_votes", &total_vote_counts.1);
|
|
ctx.insert("total_abstain_votes", &total_vote_counts.2);
|
|
|
|
ctx.insert("votes", &user_votes);
|
|
|
|
render_template(&tmpl, "governance/my_votes.html", &ctx)
|
|
}
|
|
|
|
/// Handles the all activities page route
|
|
pub async fn all_activities(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
|
|
let mut ctx = tera::Context::new();
|
|
ctx.insert("active_page", "governance");
|
|
ctx.insert("active_tab", "activities");
|
|
|
|
// Header data
|
|
ctx.insert("page_title", "All Governance Activities");
|
|
ctx.insert(
|
|
"page_description",
|
|
"Complete history of governance actions and events",
|
|
);
|
|
ctx.insert("show_create_button", &false);
|
|
|
|
// Add user to context (will always be available with our mock user)
|
|
let user = Self::get_user_from_session(&session).unwrap();
|
|
ctx.insert("user", &user);
|
|
|
|
// Get all governance activities from the database
|
|
let activities = match Self::get_all_governance_activities() {
|
|
Ok(activities) => activities,
|
|
Err(e) => {
|
|
eprintln!("Failed to load all activities: {}", e);
|
|
Vec::new()
|
|
}
|
|
};
|
|
ctx.insert("activities", &activities);
|
|
|
|
render_template(&tmpl, "governance/all_activities.html", &ctx)
|
|
}
|
|
|
|
/// Get recent governance activities from the database
|
|
fn get_recent_governance_activities() -> Result<Vec<Value>, String> {
|
|
// Get real activities from the database (no demo data)
|
|
let activities = get_recent_activities()?;
|
|
|
|
// Convert GovernanceActivity to the format expected by the template
|
|
let formatted_activities: Vec<Value> = activities
|
|
.into_iter()
|
|
.map(|activity| {
|
|
// Map activity type to appropriate icon
|
|
let (icon, action) = match activity.activity_type.as_str() {
|
|
"proposal_created" => ("bi-plus-circle-fill text-success", "created proposal"),
|
|
"vote_cast" => ("bi-check-circle-fill text-primary", "cast vote"),
|
|
"voting_started" => ("bi-play-circle-fill text-info", "started voting"),
|
|
"voting_ended" => ("bi-clock-fill text-warning", "ended voting"),
|
|
"proposal_status_changed" => ("bi-shield-check text-success", "changed status"),
|
|
"vote_option_added" => ("bi-list-ul text-secondary", "added vote option"),
|
|
_ => ("bi-circle-fill text-muted", "performed action"),
|
|
};
|
|
|
|
serde_json::json!({
|
|
"type": activity.activity_type,
|
|
"icon": icon,
|
|
"user": activity.creator_name,
|
|
"action": action,
|
|
"proposal_title": activity.proposal_title,
|
|
"created_at": activity.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
|
|
"proposal_id": activity.proposal_id
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(formatted_activities)
|
|
}
|
|
|
|
/// Get all governance activities from the database
|
|
fn get_all_governance_activities() -> Result<Vec<Value>, String> {
|
|
// Get all activities from the database
|
|
let activities = get_all_activities()?;
|
|
|
|
// Convert GovernanceActivity to the format expected by the template
|
|
let formatted_activities: Vec<Value> = activities
|
|
.into_iter()
|
|
.map(|activity| {
|
|
// Map activity type to appropriate icon
|
|
let (icon, action) = match activity.activity_type.as_str() {
|
|
"proposal_created" => ("bi-plus-circle-fill text-success", "created proposal"),
|
|
"vote_cast" => ("bi-check-circle-fill text-primary", "cast vote"),
|
|
"voting_started" => ("bi-play-circle-fill text-info", "started voting"),
|
|
"voting_ended" => ("bi-clock-fill text-warning", "ended voting"),
|
|
"proposal_status_changed" => ("bi-shield-check text-success", "changed status"),
|
|
"vote_option_added" => ("bi-list-ul text-secondary", "added vote option"),
|
|
_ => ("bi-circle-fill text-muted", "performed action"),
|
|
};
|
|
|
|
serde_json::json!({
|
|
"type": activity.activity_type,
|
|
"icon": icon,
|
|
"user": activity.creator_name,
|
|
"action": action,
|
|
"proposal_title": activity.proposal_title,
|
|
"created_at": activity.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
|
|
"proposal_id": activity.proposal_id
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(formatted_activities)
|
|
}
|
|
|
|
/// Generate mock votes for a specific proposal
|
|
fn get_mock_votes_for_proposal(proposal_id: &str) -> Vec<Vote> {
|
|
let now = Utc::now();
|
|
vec![
|
|
Vote {
|
|
id: "vote-001".to_string(),
|
|
proposal_id: proposal_id.to_string(),
|
|
voter_id: 1,
|
|
voter_name: "Robert Callingham".to_string(),
|
|
vote_type: VoteType::Yes,
|
|
comment: Some("I strongly support this initiative.".to_string()),
|
|
created_at: now - Duration::days(2),
|
|
updated_at: now - Duration::days(2),
|
|
},
|
|
Vote {
|
|
id: "vote-002".to_string(),
|
|
proposal_id: proposal_id.to_string(),
|
|
voter_id: 2,
|
|
voter_name: "Jane Smith".to_string(),
|
|
vote_type: VoteType::Yes,
|
|
comment: None,
|
|
created_at: now - Duration::days(2),
|
|
updated_at: now - Duration::days(2),
|
|
},
|
|
Vote {
|
|
id: "vote-003".to_string(),
|
|
proposal_id: proposal_id.to_string(),
|
|
voter_id: 3,
|
|
voter_name: "Bob Johnson".to_string(),
|
|
vote_type: VoteType::No,
|
|
comment: Some("I have concerns about the implementation cost.".to_string()),
|
|
created_at: now - Duration::days(1),
|
|
updated_at: now - Duration::days(1),
|
|
},
|
|
Vote {
|
|
id: "vote-004".to_string(),
|
|
proposal_id: proposal_id.to_string(),
|
|
voter_id: 4,
|
|
voter_name: "Alice Williams".to_string(),
|
|
vote_type: VoteType::Abstain,
|
|
comment: Some("I need more information before making a decision.".to_string()),
|
|
created_at: now - Duration::hours(12),
|
|
updated_at: now - Duration::hours(12),
|
|
},
|
|
]
|
|
}
|
|
|
|
/// Calculate voting results from a proposal
|
|
fn calculate_voting_results_from_proposal(proposal: &Proposal) -> VotingResults {
|
|
let mut results = VotingResults::new(proposal.base_data.id.to_string());
|
|
|
|
// Count votes for each option
|
|
for option in &proposal.options {
|
|
match option.id {
|
|
1 => results.yes_count = option.count as usize,
|
|
2 => results.no_count = option.count as usize,
|
|
3 => results.abstain_count = option.count as usize,
|
|
_ => {} // Ignore other options
|
|
}
|
|
}
|
|
|
|
// Calculate total votes
|
|
results.total_votes = results.yes_count + results.no_count + results.abstain_count;
|
|
|
|
results
|
|
}
|
|
|
|
/// Extract votes from a proposal's ballots
|
|
fn extract_votes_from_proposal(proposal: &Proposal) -> Vec<Vote> {
|
|
let mut votes = Vec::new();
|
|
|
|
// Debug: Print proposal ID and number of ballots
|
|
println!(
|
|
"Extracting votes from proposal ID: {}",
|
|
proposal.base_data.id
|
|
);
|
|
println!("Number of ballots in proposal: {}", proposal.ballots.len());
|
|
|
|
// If there are no ballots, create some mock votes for testing
|
|
if proposal.ballots.is_empty() {
|
|
println!("No ballots found in proposal, creating mock votes for testing");
|
|
|
|
// Create mock votes based on the option counts
|
|
for option in &proposal.options {
|
|
if option.count > 0 {
|
|
let vote_type = match option.id {
|
|
1 => VoteType::Yes,
|
|
2 => VoteType::No,
|
|
3 => VoteType::Abstain,
|
|
_ => continue,
|
|
};
|
|
|
|
// Create a mock vote for each count
|
|
for i in 0..option.count {
|
|
let vote = Vote::new(
|
|
proposal.base_data.id.to_string(),
|
|
i as i32 + 1,
|
|
format!("User {}", i + 1),
|
|
vote_type.clone(),
|
|
option.comment.clone(),
|
|
);
|
|
votes.push(vote);
|
|
}
|
|
}
|
|
}
|
|
|
|
println!("Created {} mock votes", votes.len());
|
|
return votes;
|
|
}
|
|
|
|
// Convert each ballot to a Vote
|
|
for (i, ballot) in proposal.ballots.iter().enumerate() {
|
|
println!(
|
|
"Processing ballot {}: user_id={}, option_id={}, shares={}",
|
|
i, ballot.user_id, ballot.vote_option_id, ballot.shares_count
|
|
);
|
|
|
|
// Map option_id to VoteType
|
|
let vote_type = match ballot.vote_option_id {
|
|
1 => VoteType::Yes,
|
|
2 => VoteType::No,
|
|
3 => VoteType::Abstain,
|
|
_ => {
|
|
println!(
|
|
"Unknown option_id: {}, defaulting to Abstain",
|
|
ballot.vote_option_id
|
|
);
|
|
VoteType::Abstain // Default to Abstain for unknown options
|
|
}
|
|
};
|
|
|
|
// Convert user_id from u32 to i32 safely
|
|
let voter_id = match i32::try_from(ballot.user_id) {
|
|
Ok(id) => id,
|
|
Err(e) => {
|
|
println!("Failed to convert user_id {} to i32: {}", ballot.user_id, e);
|
|
continue; // Skip this ballot if conversion fails
|
|
}
|
|
};
|
|
|
|
let ballot_timestamp =
|
|
match chrono::DateTime::from_timestamp(ballot.base_data.created_at, 0) {
|
|
Some(dt) => dt,
|
|
None => {
|
|
println!(
|
|
"Warning: Invalid timestamp {} for ballot, using current time",
|
|
ballot.base_data.created_at
|
|
);
|
|
Utc::now()
|
|
}
|
|
};
|
|
|
|
let vote = Vote {
|
|
id: uuid::Uuid::new_v4().to_string(),
|
|
proposal_id: proposal.base_data.id.to_string(),
|
|
voter_id,
|
|
voter_name: format!("User {}", voter_id),
|
|
vote_type,
|
|
comment: ballot.comment.clone(),
|
|
created_at: ballot_timestamp, // This is already local time
|
|
updated_at: ballot_timestamp, // Same as created_at for votes
|
|
};
|
|
|
|
votes.push(vote);
|
|
}
|
|
votes
|
|
}
|
|
|
|
// The calculate_statistics_from_database function is now defined at the top of the impl block
|
|
|
|
/// Calculate total vote counts across all proposals
|
|
/// Returns a tuple of (yes_count, no_count, abstain_count)
|
|
fn calculate_total_vote_counts(proposals: &[Proposal]) -> (usize, usize, usize) {
|
|
let mut yes_count = 0;
|
|
let mut no_count = 0;
|
|
let mut abstain_count = 0;
|
|
|
|
for proposal in proposals {
|
|
// Extract votes from this proposal
|
|
let votes = Self::extract_votes_from_proposal(proposal);
|
|
|
|
// Count votes by type
|
|
for vote in votes {
|
|
match vote.vote_type {
|
|
VoteType::Yes => yes_count += 1,
|
|
VoteType::No => no_count += 1,
|
|
VoteType::Abstain => abstain_count += 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
(yes_count, no_count, abstain_count)
|
|
}
|
|
}
|
|
|
|
/// Represents the data submitted in the proposal form
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ProposalForm {
|
|
/// Title of the proposal
|
|
pub title: String,
|
|
/// Description of the proposal
|
|
pub description: String,
|
|
/// Status of the proposal
|
|
pub draft: Option<bool>,
|
|
/// Start date for voting
|
|
pub voting_start_date: Option<String>,
|
|
/// End date for voting
|
|
pub voting_end_date: Option<String>,
|
|
}
|
|
|
|
/// Represents the data submitted in the vote form
|
|
#[derive(Debug, Deserialize)]
|
|
#[allow(dead_code)]
|
|
pub struct VoteForm {
|
|
/// Type of vote (yes, no, abstain)
|
|
pub vote_type: String,
|
|
/// Optional comment explaining the vote
|
|
pub comment: Option<String>,
|
|
}
|
|
|
|
/// Query parameters for filtering proposals
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ProposalQuery {
|
|
pub status: Option<String>,
|
|
pub search: Option<String>,
|
|
}
|
|
|
|
/// Represents statistics for the governance dashboard
|
|
#[derive(Debug, Serialize)]
|
|
pub struct GovernanceStats {
|
|
/// Total number of proposals
|
|
pub total_proposals: usize,
|
|
/// Number of active proposals
|
|
pub active_proposals: usize,
|
|
/// Number of approved proposals
|
|
pub approved_proposals: usize,
|
|
/// Number of rejected proposals
|
|
pub rejected_proposals: usize,
|
|
/// Number of draft proposals
|
|
pub draft_proposals: usize,
|
|
/// Total number of votes cast
|
|
pub total_votes: usize,
|
|
/// Participation rate (percentage)
|
|
pub participation_rate: f64,
|
|
}
|