From 972d0982b08e43a71b0bb7a17251225ef3035616 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Tue, 27 May 2025 20:47:34 +0300 Subject: [PATCH] feat: Enhance governance proposal model with activity tracking - Add `Activity` model to track proposal creation and voting. - Include `ActivityType` enum for different activity types. - Improve `Proposal` model to record voting with comments. - Refactor `models/governance` module for better organization. --- .../governance_proposal_example/main.rs | 60 +++---- heromodels/src/models/governance/mod.rs | 4 +- heromodels/src/models/governance/proposal.rs | 157 +++++++++++++++++- heromodels/src/models/mod.rs | 30 ++-- 4 files changed, 197 insertions(+), 54 deletions(-) diff --git a/heromodels/examples/governance_proposal_example/main.rs b/heromodels/examples/governance_proposal_example/main.rs index bfbe78a..554b56e 100644 --- a/heromodels/examples/governance_proposal_example/main.rs +++ b/heromodels/examples/governance_proposal_example/main.rs @@ -2,7 +2,7 @@ use chrono::{Duration, Utc}; use heromodels::db::{Collection, Db}; -use heromodels::models::governance::{Ballot, Proposal, ProposalStatus, VoteEventStatus}; +use heromodels::models::governance::{Proposal, ProposalStatus, VoteEventStatus}; fn main() { println!("Governance Proposal Model Example\n"); @@ -101,23 +101,23 @@ fn main() { // Example of voting with comments using the cast_vote_with_comment method println!("Adding votes with comments..."); - + // User 7 votes for 'Approve Allocation' with a comment proposal = proposal.cast_vote_with_comment( Some(110), // ballot_id - 7, // user_id - 1, // chosen_option_id (Approve Allocation) - 80, // shares - "I strongly support this proposal because it aligns with our community values." + 7, // user_id + 1, // chosen_option_id (Approve Allocation) + 80, // shares + "I strongly support this proposal because it aligns with our community values.", ); - + // User 8 votes for 'Reject Allocation' with a comment proposal = proposal.cast_vote_with_comment( Some(111), // ballot_id - 8, // user_id - 2, // chosen_option_id (Reject Allocation) - 60, // shares - "I have concerns about the allocation priorities." + 8, // user_id + 2, // chosen_option_id (Reject Allocation) + 60, // shares + "I have concerns about the allocation priorities.", ); println!("\nBallots with Comments:"); @@ -218,34 +218,34 @@ fn main() { // Example of voting with comments on a private proposal println!("\nAdding votes with comments to private proposal..."); - + // User 20 (eligible) votes with a comment private_proposal = private_proposal.cast_vote_with_comment( - Some(202), // ballot_id - 20, // user_id (eligible) - 1, // chosen_option_id - 75, // shares - "I support this restructuring plan with some reservations." + Some(202), // ballot_id + 20, // user_id (eligible) + 1, // chosen_option_id + 75, // shares + "I support this restructuring plan with some reservations.", ); - + // User 30 (eligible) votes with a comment private_proposal = private_proposal.cast_vote_with_comment( - Some(203), // ballot_id - 30, // user_id (eligible) - 2, // chosen_option_id - 90, // shares - "I believe we should reconsider the timing of these changes." + Some(203), // ballot_id + 30, // user_id (eligible) + 2, // chosen_option_id + 90, // shares + "I believe we should reconsider the timing of these changes.", ); - + // User 40 (ineligible) tries to vote with a comment private_proposal = private_proposal.cast_vote_with_comment( - Some(204), // ballot_id - 40, // user_id (ineligible) - 1, // chosen_option_id - 50, // shares - "This restructuring seems unnecessary." + Some(204), // ballot_id + 40, // user_id (ineligible) + 1, // chosen_option_id + 50, // shares + "This restructuring seems unnecessary.", ); - + println!("Eligible users 20 and 30 added votes with comments."); println!("Ineligible user 40 attempted to vote with a comment (should be rejected)."); diff --git a/heromodels/src/models/governance/mod.rs b/heromodels/src/models/governance/mod.rs index b3df030..792f8b9 100644 --- a/heromodels/src/models/governance/mod.rs +++ b/heromodels/src/models/governance/mod.rs @@ -2,4 +2,6 @@ // This module will contain the Proposal model and related types. pub mod proposal; -pub use self::proposal::{Proposal, Ballot, VoteOption, ProposalStatus, VoteEventStatus}; \ No newline at end of file +pub use self::proposal::{ + Activity, ActivityType, Ballot, Proposal, ProposalStatus, VoteEventStatus, VoteOption, +}; diff --git a/heromodels/src/models/governance/proposal.rs b/heromodels/src/models/governance/proposal.rs index 0440f89..e051d47 100644 --- a/heromodels/src/models/governance/proposal.rs +++ b/heromodels/src/models/governance/proposal.rs @@ -70,9 +70,9 @@ impl VoteOption { #[model] // Has base.Base in V spec pub struct Ballot { pub base_data: BaseModelData, - pub user_id: u32, // The ID of the user who cast this ballot - pub vote_option_id: u8, // The 'id' of the VoteOption chosen - pub shares_count: i64, // Number of shares/tokens/voting power + pub user_id: u32, // The ID of the user who cast this ballot + pub vote_option_id: u8, // The 'id' of the VoteOption chosen + pub shares_count: i64, // Number of shares/tokens/voting power pub comment: Option, // Optional comment from the voter } @@ -253,7 +253,7 @@ impl Proposal { eprintln!("Voting is not open for proposal '{}'", self.title); return self; } - + // Check if the option exists if !self.options.iter().any(|opt| opt.id == chosen_option_id) { eprintln!( @@ -262,7 +262,7 @@ impl Proposal { ); return self; } - + // Check eligibility for private proposals if let Some(group) = &self.private_group { if !group.contains(&user_id) { @@ -273,14 +273,14 @@ impl Proposal { return self; } } - + // Create a new ballot with the comment let mut new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares); new_ballot.comment = Some(comment.to_string()); - + // Add the ballot to the proposal self.ballots.push(new_ballot); - + // Update the vote count for the chosen option if let Some(option) = self .options @@ -289,7 +289,146 @@ impl Proposal { { option.count += shares; } - + self } } + +pub enum ActivityType { + ProposalCreated, + VoteCast, + VotingStarted, + VotingEnded, +} + +impl ToString for ActivityType { + fn to_string(&self) -> String { + match self { + ActivityType::ProposalCreated => "proposal_created", + ActivityType::VoteCast => "vote_cast", + ActivityType::VotingStarted => "voting_started", + ActivityType::VotingEnded => "voting_ended", + } + .to_string() + } +} + +/// Represents a governance activity in the system +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] +#[rhai_model_export(db_type = "std::sync::Arc")] +#[model] // Has base.Base in V spec +pub struct Activity { + /// Base model data + pub base_data: BaseModelData, + /// Type of activity (proposal_created, vote_cast, etc.) + pub activity_type: String, + /// ID of the related proposal + pub proposal_id: u32, + /// Title of the related proposal + pub proposal_title: String, + /// Name of the user who performed the action + pub creator_name: String, + /// Description of the activity + pub description: String, + /// Date and time when the activity was last updated + pub updated_at: DateTime, + /// Date and time when the activity was created + pub created_at: DateTime, +} + +impl Activity { + /// Create a default instance + pub fn default() -> Self { + let base_data = BaseModelData::new(); + let created_at = Utc::now(); + let updated_at = created_at; + + Self { + base_data, + activity_type: String::new(), + proposal_id: 0, + proposal_title: String::new(), + creator_name: String::new(), + description: String::new(), + updated_at, + created_at, + } + } + + /// Creates a new governance activity + pub fn new( + id: Option, + activity_type: &str, + proposal_id: u32, + proposal_title: &str, + creator_name: &str, + description: &str, + ) -> Self { + let mut base_data = BaseModelData::new(); + if let Some(id) = id { + base_data.update_id(id); + } + + let created_at = Utc::now(); + let updated_at = created_at; + + Self { + base_data, + activity_type: activity_type.to_string(), + proposal_id, + proposal_title: proposal_title.to_string(), + creator_name: creator_name.to_string(), + description: description.to_string(), + updated_at, + created_at, + } + } + + /// Creates a proposal creation activity + pub fn proposal_created(proposal_id: u32, proposal_title: &str, creator_name: &str) -> Self { + Self::new( + None, + &ActivityType::ProposalCreated.to_string(), + proposal_id, + proposal_title, + creator_name, + &format!("Proposal '{}' created by {}", proposal_title, creator_name), + ) + } + + /// Creates a vote cast activity + pub fn vote_cast(proposal_id: u32, proposal_title: &str, voter_name: &str) -> Self { + Self::new( + None, + &ActivityType::VoteCast.to_string(), + proposal_id, + proposal_title, + voter_name, + &format!("{} voted on proposal '{}'", voter_name, proposal_title), + ) + } + + // Create voting start activity + pub fn voting_started(proposal_id: u32, proposal_title: &str) -> Self { + Self::new( + None, + &ActivityType::VotingStarted.to_string(), + proposal_id, + proposal_title, + "System", + &format!("Voting started for proposal '{}'", proposal_title), + ) + } + + // Create voting ended activity + pub fn voting_ended(proposal_id: u32, proposal_title: &str) -> Self { + Self::new( + None, + &ActivityType::VotingEnded.to_string(), + proposal_id, + proposal_title, + "System", + &format!("Voting ended for proposal '{}'", proposal_title), + ) + } +} diff --git a/heromodels/src/models/mod.rs b/heromodels/src/models/mod.rs index 5e37df7..5145503 100644 --- a/heromodels/src/models/mod.rs +++ b/heromodels/src/models/mod.rs @@ -2,30 +2,32 @@ pub mod core; pub mod userexample; // pub mod productexample; // Temporarily remove as files are missing -pub mod calendar; -pub mod governance; -pub mod finance; -pub mod legal; -pub mod flow; pub mod biz; +pub mod calendar; +pub mod finance; +pub mod flow; +pub mod governance; +pub mod legal; pub mod projects; // Re-export key types for convenience pub use core::Comment; pub use userexample::User; // pub use productexample::Product; // Temporarily remove -pub use calendar::{Calendar, Event, Attendee, AttendanceStatus}; -pub use governance::{Proposal, ProposalStatus, VoteEventStatus, Ballot, VoteOption}; -pub use finance::{Account, Asset, AssetType}; -pub use finance::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus}; -pub use legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus}; -pub use flow::{Flow, FlowStep, SignatureRequirement}; pub use biz::{Sale, SaleItem, SaleStatus}; +pub use calendar::{AttendanceStatus, Attendee, Calendar, Event}; +pub use finance::marketplace::{Bid, BidStatus, Listing, ListingStatus, ListingType}; +pub use finance::{Account, Asset, AssetType}; +pub use flow::{Flow, FlowStep, SignatureRequirement}; +pub use governance::{ + Activity, ActivityType, Ballot, Proposal, ProposalStatus, VoteEventStatus, VoteOption, +}; +pub use legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus}; -pub use flow::register_flow_rhai_module; -pub use calendar::register_calendar_rhai_module; -pub use legal::register_legal_rhai_module; #[cfg(feature = "rhai")] pub use biz::register_biz_rhai_module; +pub use calendar::register_calendar_rhai_module; +pub use flow::register_flow_rhai_module; +pub use legal::register_legal_rhai_module; #[cfg(feature = "rhai")] pub use projects::register_projects_rhai_module;