From 5d9eaac1f84f64efaf093a360ae012394a01ac4e Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 21 May 2025 13:49:20 +0300 Subject: [PATCH] feat: Implemented submit vote --- actix_mvc_app/src/controllers/governance.rs | 109 +++++++++++------- actix_mvc_app/src/db/proposals.rs | 97 +++++++++++++++- .../src/views/governance/proposal_detail.html | 10 ++ 3 files changed, 172 insertions(+), 44 deletions(-) diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index e037aff..14c1c6c 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -134,8 +134,8 @@ impl GovernanceController { let votes = Self::get_mock_votes_for_proposal(&proposal_id); ctx.insert("votes", &votes); - // Get voting results - let results = Self::get_mock_voting_results(&proposal_id); + // Calculate voting results directly from the proposal + let results = Self::calculate_voting_results_from_proposal(&proposal); ctx.insert("results", &results); render_template(&tmpl, "governance/proposal_detail.html", &ctx) @@ -264,57 +264,62 @@ impl GovernanceController { /// Handles the submission of a vote on a proposal pub async fn submit_vote( path: web::Path, - _form: web::Form, + form: web::Form, tmpl: web::Data, session: Session, ) -> Result { let proposal_id = path.into_inner(); - - // Check if user is logged in - if Self::get_user_from_session(&session).is_none() { - return Ok(HttpResponse::Found() - .append_header(("Location", "/login")) - .finish()); - } - let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); - // Add user to context if available - if let Some(user) = Self::get_user_from_session(&session) { - ctx.insert("user", &user); - } + // 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); - // Get mock proposal detail - let proposal = Self::get_mock_proposal_by_id(&proposal_id); - if let Some(proposal) = proposal { - ctx.insert("proposal", &proposal); - ctx.insert("success", "Your vote has been recorded!"); + // Extract user ID + let user_id = user.get("id").and_then(|v| v.as_i64()).unwrap_or(1) as i32; - // Get mock votes for this proposal - let votes = Self::get_mock_votes_for_proposal(&proposal_id); - ctx.insert("votes", &votes); + // Parse proposal ID + let proposal_id_u32 = match proposal_id.parse::() { + Ok(id) => id, + Err(_) => { + ctx.insert("error", "Invalid proposal ID"); + return render_template(&tmpl, "error.html", &ctx); + } + }; - // Get voting results - let results = Self::get_mock_voting_results(&proposal_id); - ctx.insert("results", &results); + // Submit the vote + match crate::db::proposals::submit_vote_on_proposal( + proposal_id_u32, + user_id, + &form.vote_type, + 1, // Default to 1 share + ) { + Ok(updated_proposal) => { + ctx.insert("proposal", &updated_proposal); + ctx.insert("success", "Your vote has been 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 - ))) - } + // Get votes for this proposal + // For now, we'll still use mock votes until we implement a function to extract votes from the proposal + let votes = Self::get_mock_votes_for_proposal(&proposal_id); + ctx.insert("votes", &votes); + + // Calculate voting results directly from the updated proposal + let results = Self::calculate_voting_results_from_proposal(&updated_proposal); + ctx.insert("results", &results); + + render_template(&tmpl, "governance/proposal_detail.html", &ctx) + } + Err(e) => { + ctx.insert("error", &format!("Failed to submit vote: {}", e)); + render_template(&tmpl, "error.html", &ctx) } } } @@ -614,6 +619,26 @@ impl GovernanceController { results } + /// 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 + } + /// Generate mock statistics for the governance dashboard fn get_mock_statistics() -> GovernanceStats { GovernanceStats { diff --git a/actix_mvc_app/src/db/proposals.rs b/actix_mvc_app/src/db/proposals.rs index 425d530..edc490f 100644 --- a/actix_mvc_app/src/db/proposals.rs +++ b/actix_mvc_app/src/db/proposals.rs @@ -4,11 +4,11 @@ use chrono::{Duration, Utc}; use heromodels::db::hero::OurDB; use heromodels::{ db::{Collection, Db}, - models::governance::{Proposal, ProposalStatus}, + models::governance::{Proposal, ProposalStatus, VoteEventStatus}, }; /// The path to the database file. Change this as needed for your environment. -pub const DB_PATH: &str = "/tmp/ourdb_governance2"; +pub const DB_PATH: &str = "/tmp/ourdb_governance3"; /// Returns a shared OurDB instance for the given path. You can wrap this in Arc/Mutex for concurrent access if needed. pub fn get_db(db_path: &str) -> Result { @@ -90,3 +90,96 @@ pub fn get_proposal_by_id(proposal_id: u32) -> Result, String> } } } + +/// Submits a vote on a proposal and returns the updated proposal +pub fn submit_vote_on_proposal( + proposal_id: u32, + user_id: i32, + vote_type: &str, + shares_count: u32, // Default to 1 if not specified +) -> Result { + // Get the proposal from the database + let db = get_db(DB_PATH).map_err(|e| format!("DB error: {}", e))?; + let collection = db + .collection::() + .map_err(|e| format!("Collection error: {:?}", e))?; + + // Get the proposal + let mut proposal = collection + .get_by_id(proposal_id) + .map_err(|e| format!("Failed to fetch proposal: {:?}", e))? + .ok_or_else(|| format!("Proposal not found with ID: {}", proposal_id))?; + + // Ensure the proposal has vote options + // Check if the proposal already has options + if proposal.options.is_empty() { + // Add standard vote options if they don't exist + proposal = proposal.add_option(1, "Approve", Some("Approve the proposal")); + proposal = proposal.add_option(2, "Reject", Some("Reject the proposal")); + proposal = proposal.add_option(3, "Abstain", Some("Abstain from voting")); + } + + // Map vote_type to option_id + let option_id = match vote_type { + "Yes" => 1, // Approve + "No" => 2, // Reject + "Abstain" => 3, // Abstain + _ => return Err(format!("Invalid vote type: {}", vote_type)), + }; + + // Since we're having issues with the cast_vote method, let's implement a workaround + // that directly updates the vote count for the selected option + + // Check if the proposal is active + if proposal.status != ProposalStatus::Active { + return Err(format!( + "Cannot vote on a proposal with status: {:?}", + proposal.status + )); + } + + // Check if voting period is valid + let now = Utc::now(); + if now > proposal.vote_end_date { + return Err("Voting period has ended".to_string()); + } + + if now < proposal.vote_start_date { + return Err("Voting period has not started yet".to_string()); + } + + // Find the option and increment its count + let mut option_found = false; + for option in &mut proposal.options { + if option.id == option_id { + option.count += shares_count as i64; + option_found = true; + break; + } + } + + if !option_found { + return Err(format!("Option with ID {} not found", option_id)); + } + + // Record the vote in the proposal's ballots + // We'll create a simple ballot with an auto-generated ID + let ballot_id = proposal.ballots.len() as u32 + 1; + + // We need to manually create a ballot since we can't use cast_vote + // This is a simplified version that just records the vote + println!( + "Recording vote: ballot_id={}, user_id={}, option_id={}, shares={}", + ballot_id, user_id, option_id, shares_count + ); + + // Update the proposal's updated_at timestamp + proposal.updated_at = Utc::now(); + + // Save the updated proposal + let (_, updated_proposal) = collection + .set(&proposal) + .map_err(|e| format!("Failed to save vote: {:?}", e))?; + + Ok(updated_proposal) +} diff --git a/actix_mvc_app/src/views/governance/proposal_detail.html b/actix_mvc_app/src/views/governance/proposal_detail.html index 98cd542..a4fba74 100644 --- a/actix_mvc_app/src/views/governance/proposal_detail.html +++ b/actix_mvc_app/src/views/governance/proposal_detail.html @@ -147,6 +147,16 @@ Login to Vote + {% elif proposal.status != "Active" %} +
+
+
+ + Note: Voting is only available for proposals with an Active status. + This proposal's current status is {{ proposal.status }}. +
+
+
{% endif %}