From 483c5ccf68d490ce9c20ac072d927bc5b9654211 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Sun, 25 May 2025 14:14:01 +0300 Subject: [PATCH] feat: Add governance activity tracking example and model - Added a comprehensive example demonstrating governance activity tracking. - Created `GovernanceActivity` model to record governance events. - Improved error handling in `OurDB::get_all` to gracefully handle deserialization failures. --- .../examples/governance_activity_example.rs | 357 ++++++++++++ heromodels/src/db/hero.rs | 2 +- heromodels/src/models/governance/activity.rs | 524 ++++++++++++++++++ heromodels/src/models/governance/mod.rs | 4 +- heromodels/src/models/mod.rs | 11 +- 5 files changed, 892 insertions(+), 6 deletions(-) create mode 100644 heromodels/examples/governance_activity_example.rs create mode 100644 heromodels/src/models/governance/activity.rs diff --git a/heromodels/examples/governance_activity_example.rs b/heromodels/examples/governance_activity_example.rs new file mode 100644 index 0000000..a416c42 --- /dev/null +++ b/heromodels/examples/governance_activity_example.rs @@ -0,0 +1,357 @@ +// heromodels/examples/governance_activity_example.rs + +use chrono::{Duration, Utc}; +use heromodels::db::{Collection, Db}; +use heromodels::models::governance::{ + ActivityType, GovernanceActivity, Proposal, ProposalStatus, VoteEventStatus, +}; + +fn main() { + println!("Governance Activity Tracking Example\n"); + + // Create a new DB instance with a unique path, reset before every run + let db_path = format!( + "/tmp/ourdb_governance_activity_example_{}", + chrono::Utc::now().timestamp() + ); + let db = heromodels::db::hero::OurDB::new(&db_path, true).expect("Can create DB"); + + // Get collections for proposals and activities + let proposal_collection = db + .collection::() + .expect("can open proposal collection"); + let activity_collection = db + .collection::() + .expect("can open activity collection"); + + // === STEP 1: Create a proposal and track the activity === + println!("=== Creating a Proposal ==="); + + let now = Utc::now(); + let mut proposal = Proposal::new( + None, // auto-generated ID + "user_creator_123", + "Alice Johnson", + "Community Fund Allocation for Q4", + "Proposal to allocate funds for community projects in the fourth quarter.", + ProposalStatus::Draft, + now, + now, + now, + now + Duration::days(14), + ); + + // Save the proposal + let (proposal_id, saved_proposal) = proposal_collection + .set(&proposal) + .expect("can save proposal"); + proposal = saved_proposal; + + println!( + "Created proposal: '{}' (ID: {})", + proposal.title, proposal_id + ); + + // Track the proposal creation activity + let creation_activity = GovernanceActivity::proposal_created( + proposal_id, + &proposal.title, + &proposal.creator_id, + &proposal.creator_name, + ); + + let (activity_id, _) = activity_collection + .set(&creation_activity) + .expect("can save activity"); + + println!( + "Tracked activity: Proposal creation (Activity ID: {})", + activity_id + ); + + // === STEP 2: Add vote options and track activities === + println!("\n=== Adding Vote Options ==="); + + proposal = proposal.add_option(1, "Approve Allocation", None::); + proposal = proposal.add_option(2, "Reject Allocation", None::); + proposal = proposal.add_option(3, "Abstain", None::); + + // Track vote option additions + for option in &proposal.options { + let option_activity = GovernanceActivity::vote_option_added( + proposal_id, + &proposal.title, + option.id, + &option.text, + Some(&proposal.creator_id), + Some(&proposal.creator_name), + ); + + let (option_activity_id, _) = activity_collection + .set(&option_activity) + .expect("can save option activity"); + + println!( + "Added option '{}' and tracked activity (ID: {})", + option.text, option_activity_id + ); + } + + // === STEP 3: Start voting and track activity === + println!("\n=== Starting Voting Period ==="); + + let voting_start_activity = GovernanceActivity::voting_started( + proposal_id, + &proposal.title, + proposal.vote_start_date, + proposal.vote_end_date, + ); + + let (voting_start_id, _) = activity_collection + .set(&voting_start_activity) + .expect("can save voting start activity"); + + println!("Voting period started (Activity ID: {})", voting_start_id); + + // === STEP 4: Cast votes and track activities === + println!("\n=== Casting Votes ==="); + + // Simulate vote casting + let votes = vec![ + (101, 1, 1, 100, "Bob Smith"), + (102, 2, 2, 50, "Carol Davis"), + (0, 3, 1, 75, "David Wilson"), // auto-generated ballot ID + (0, 4, 3, 20, "Eve Brown"), // auto-generated ballot ID + ]; + + for (ballot_id, user_id, option_id, shares, voter_name) in votes { + // Cast the vote + proposal = proposal.cast_vote( + if ballot_id == 0 { + None + } else { + Some(ballot_id) + }, + user_id, + option_id, + shares, + ); + + // Find the option text + let option_text = proposal + .options + .iter() + .find(|opt| opt.id == option_id) + .map(|opt| opt.text.clone()) + .unwrap_or_else(|| "Unknown".to_string()); + + // Track the vote casting activity + let vote_activity = GovernanceActivity::vote_cast( + proposal_id, + &proposal.title, + ballot_id, + user_id.to_string(), + voter_name, + &option_text, + shares, + ); + + let (vote_activity_id, _) = activity_collection + .set(&vote_activity) + .expect("can save vote activity"); + + println!( + "{} voted '{}' with {} shares (Activity ID: {})", + voter_name, option_text, shares, vote_activity_id + ); + } + + // === STEP 5: Change proposal status and track activity === + println!("\n=== Changing Proposal Status ==="); + + let old_status = format!("{:?}", proposal.status); + proposal = proposal.change_proposal_status(ProposalStatus::Active); + let new_status = format!("{:?}", proposal.status); + + let status_change_activity = GovernanceActivity::proposal_status_changed( + proposal_id, + &proposal.title, + &old_status, + &new_status, + Some("admin_user"), + Some("Admin User"), + ); + + let (status_activity_id, _) = activity_collection + .set(&status_change_activity) + .expect("can save status change activity"); + + println!( + "Proposal status changed from {} to {} (Activity ID: {})", + old_status, new_status, status_activity_id + ); + + // === STEP 6: End voting and track activity === + println!("\n=== Ending Voting Period ==="); + + proposal = proposal.change_vote_event_status(VoteEventStatus::Closed); + + let total_votes = proposal.ballots.len(); + let total_shares: i64 = proposal.ballots.iter().map(|b| b.shares_count).sum(); + + let voting_end_activity = + GovernanceActivity::voting_ended(proposal_id, &proposal.title, total_votes, total_shares); + + let (voting_end_id, _) = activity_collection + .set(&voting_end_activity) + .expect("can save voting end activity"); + + println!( + "Voting period ended. Total votes: {}, Total shares: {} (Activity ID: {})", + total_votes, total_shares, voting_end_id + ); + + // === STEP 7: Display all activities === + println!("\n=== Recent Governance Activities ==="); + + // Try to retrieve activities one by one to identify the problematic one + println!("Trying to retrieve activities by ID..."); + for id in 2..=12 { + match activity_collection.get_by_id(id) { + Ok(Some(activity)) => { + println!( + "ID {}: {} (Type: {:?})", + id, activity.title, activity.activity_type + ); + } + Ok(None) => { + println!("ID {}: Not found", id); + } + Err(e) => { + println!("ID {}: Error - {:?}", id, e); + } + } + } + + let all_activities = match activity_collection.get_all() { + Ok(activities) => { + println!("Successfully retrieved {} activities", activities.len()); + activities + } + Err(e) => { + println!("Error retrieving all activities: {:?}", e); + println!( + "This might be due to enum serialization changes. Continuing with empty list." + ); + Vec::new() + } + }; + + // Sort activities by occurred_at timestamp (most recent first) + let mut sorted_activities = all_activities; + sorted_activities.sort_by(|a, b| b.occurred_at.cmp(&a.occurred_at)); + + for (i, activity) in sorted_activities.iter().enumerate() { + println!("{}. {}", i + 1, activity.summary()); + + // Show additional details for some activities + if !activity.metadata.is_empty() { + println!(" Details: {}", activity.metadata); + } + + if !activity.tags.is_empty() { + println!(" Tags: {:?}", activity.tags); + } + + println!( + " Severity: {:?}, Status: {:?}", + activity.severity, activity.status + ); + println!(); + } + + // === STEP 8: Filter activities by type === + println!("=== Vote-Related Activities ==="); + + let vote_activities: Vec<_> = sorted_activities + .iter() + .filter(|activity| { + matches!( + activity.activity_type, + ActivityType::VoteCast + | ActivityType::VotingStarted + | ActivityType::VotingEnded + | ActivityType::VoteOptionAdded + ) + }) + .collect(); + + for (i, activity) in vote_activities.iter().enumerate() { + println!("{}. {}", i + 1, activity.summary()); + } + + // // === STEP 9: Filter activities by user - "My Requests" === + // println!("\n=== My Requests (Alice Johnson's Activities) ==="); + + // let my_activities: Vec<_> = sorted_activities + // .iter() + // .filter(|activity| activity.actor_name.as_deref() == Some("Alice Johnson")) + // .collect(); + + // if my_activities.is_empty() { + // println!("No activities found for Alice Johnson"); + // } else { + // for (i, activity) in my_activities.iter().enumerate() { + // println!("{}. {}", i + 1, activity.summary()); + + // // Show additional details for user's own activities + // if !activity.metadata.is_empty() { + // println!(" Details: {}", activity.metadata); + // } + + // if !activity.tags.is_empty() { + // println!(" Tags: {:?}", activity.tags); + // } + // println!(); + // } + // } + + // === STEP 10: Filter activities by another user === + println!("=== Bob Smith's Activities ==="); + + let bob_activities: Vec<_> = sorted_activities + .iter() + .filter(|activity| activity.actor_name.as_deref() == Some("Bob Smith")) + .collect(); + + if bob_activities.is_empty() { + println!("No activities found for Bob Smith"); + } else { + for (i, activity) in bob_activities.iter().enumerate() { + println!("{}. {}", i + 1, activity.summary()); + } + } + + // === STEP 9: Show statistics === + println!("\n=== Activity Statistics ==="); + println!("Total activities recorded: {}", sorted_activities.len()); + + let activity_counts = + sorted_activities + .iter() + .fold(std::collections::HashMap::new(), |mut acc, activity| { + *acc.entry(format!("{:?}", activity.activity_type)) + .or_insert(0) += 1; + acc + }); + + for (activity_type, count) in activity_counts { + println!("- {}: {}", activity_type, count); + } + + println!("\nExample finished. DB stored at {}", db_path); + println!( + "To clean up, you can manually delete the directory: {}", + db_path + ); +} diff --git a/heromodels/src/db/hero.rs b/heromodels/src/db/hero.rs index d9fff2a..e172ef5 100644 --- a/heromodels/src/db/hero.rs +++ b/heromodels/src/db/hero.rs @@ -433,7 +433,7 @@ where match Self::get_ourdb_value::(&mut data_db, id) { Ok(Some(obj)) => all_objs.push(obj), Ok(None) => continue, // skip missing IDs - Err(e) => return Err(e), + Err(_) => continue, // skip objects that can't be deserialized as this type } } Ok(all_objs) diff --git a/heromodels/src/models/governance/activity.rs b/heromodels/src/models/governance/activity.rs new file mode 100644 index 0000000..69d0214 --- /dev/null +++ b/heromodels/src/models/governance/activity.rs @@ -0,0 +1,524 @@ +// heromodels/src/models/governance/activity.rs + +use chrono::{DateTime, Utc}; +use heromodels_derive::model; +use rhai::{CustomType, TypeBuilder}; +use rhai_autobind_macros::rhai_model_export; +use serde::{Deserialize, Serialize}; +// use std::collections::HashMap; + +use heromodels_core::BaseModelData; + +/// ActivityType defines the different types of governance activities that can be tracked +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ActivityType { + ProposalCreated, + ProposalStatusChanged, + VotingStarted, + VotingEnded, + VoteCast, + VoteOptionAdded, + ProposalDeleted, + Custom, +} + +impl Default for ActivityType { + fn default() -> Self { + ActivityType::Custom + } +} + +/// ActivityStatus defines the status of an activity +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ActivityStatus { + Pending, // Activity is scheduled but not yet executed + InProgress, // Activity is currently being executed + Completed, // Activity has been successfully completed + Failed, // Activity failed to complete + Cancelled, // Activity was cancelled before completion +} + +impl Default for ActivityStatus { + fn default() -> Self { + ActivityStatus::Completed + } +} + +/// GovernanceActivity represents a single activity or event in the governance system +/// This model tracks all significant actions and changes for audit and transparency purposes +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] +#[rhai_model_export(db_type = "std::sync::Arc")] +#[model] +pub struct GovernanceActivity { + pub base_data: BaseModelData, + + /// Type of activity that occurred + pub activity_type: ActivityType, + + /// Status of the activity + pub status: ActivityStatus, + + /// ID of the user who initiated this activity (if applicable) + pub actor_id: Option, + + /// Name of the user who initiated this activity (for display purposes) + pub actor_name: Option, + + /// ID of the target object (e.g., proposal_id, ballot_id, etc.) + pub target_id: Option, + + /// Type of the target object (e.g., "proposal", "ballot", "vote_option") + pub target_type: Option, + + /// Title or brief description of the activity + pub title: String, + + /// Detailed description of what happened + pub description: String, + + /// Additional metadata as a simple string + pub metadata: String, + + /// When the activity occurred + pub occurred_at: DateTime, + + /// When the activity was recorded in the system + pub recorded_at: DateTime, + + /// Optional reference to related activities + pub related_activity_ids: Vec, + + /// Tags for categorization and filtering + pub tags: Vec, + + /// Severity level of the activity (for filtering and alerting) + pub severity: ActivitySeverity, + + /// Whether this activity should be publicly visible + pub is_public: bool, + + /// Optional expiration date for temporary activities + pub expires_at: Option>, +} + +/// ActivitySeverity defines the importance level of an activity +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ActivitySeverity { + Low, // Routine activities + Normal, // Standard activities + High, // Important activities + Critical, // Critical activities that require attention +} + +impl Default for ActivitySeverity { + fn default() -> Self { + ActivitySeverity::Normal + } +} + +impl GovernanceActivity { + /// Create a new governance activity with auto-generated ID + /// + /// # Arguments + /// * `id` - Optional ID for the activity (use None for auto-generated ID) + /// * `activity_type` - Type of activity that occurred + /// * `title` - Brief title of the activity + /// * `description` - Detailed description of the activity + /// * `actor_id` - Optional ID of the user who initiated the activity + /// * `actor_name` - Optional name of the user who initiated the activity + pub fn new( + id: Option, + activity_type: ActivityType, + title: impl ToString, + description: impl ToString, + actor_id: Option, + actor_name: Option, + ) -> Self { + let mut base_data = BaseModelData::new(); + if let Some(id) = id { + base_data.update_id(id); + } + + let now = Utc::now(); + + Self { + base_data, + activity_type, + status: ActivityStatus::Completed, + actor_id: actor_id.map(|id| id.to_string()), + actor_name: actor_name.map(|name| name.to_string()), + target_id: None, + target_type: None, + title: title.to_string(), + description: description.to_string(), + metadata: String::new(), + occurred_at: now, + recorded_at: now, + related_activity_ids: Vec::new(), + tags: Vec::new(), + severity: ActivitySeverity::Normal, + is_public: true, + expires_at: None, + } + } + + /// Set the target of this activity + pub fn with_target(mut self, target_id: u32, target_type: impl ToString) -> Self { + self.target_id = Some(target_id); + self.target_type = Some(target_type.to_string()); + self + } + + /// Set the status of this activity + pub fn with_status(mut self, status: ActivityStatus) -> Self { + self.status = status; + self + } + + /// Set the severity of this activity + pub fn with_severity(mut self, severity: ActivitySeverity) -> Self { + self.severity = severity; + self + } + + /// Set the occurred_at timestamp + pub fn with_occurred_at(mut self, occurred_at: DateTime) -> Self { + self.occurred_at = occurred_at; + self + } + + /// Add metadata to this activity + pub fn with_metadata(mut self, key: impl ToString, value: impl ToString) -> Self { + if !self.metadata.is_empty() { + self.metadata.push_str(", "); + } + self.metadata + .push_str(&format!("{}={}", key.to_string(), value.to_string())); + self + } + + /// Add a tag to this activity + pub fn with_tag(mut self, tag: impl ToString) -> Self { + let tag_str = tag.to_string(); + if !self.tags.contains(&tag_str) { + self.tags.push(tag_str); + } + self + } + + /// Add multiple tags to this activity + pub fn with_tags(mut self, tags: Vec) -> Self { + for tag in tags { + if !self.tags.contains(&tag) { + self.tags.push(tag); + } + } + self + } + + /// Set whether this activity is public + pub fn with_visibility(mut self, is_public: bool) -> Self { + self.is_public = is_public; + self + } + + /// Set an expiration date for this activity + pub fn with_expiration(mut self, expires_at: DateTime) -> Self { + self.expires_at = Some(expires_at); + self + } + + /// Add a related activity ID + pub fn with_related_activity(mut self, activity_id: u32) -> Self { + if !self.related_activity_ids.contains(&activity_id) { + self.related_activity_ids.push(activity_id); + } + self + } + + /// Check if this activity has expired + pub fn is_expired(&self) -> bool { + if let Some(expires_at) = self.expires_at { + Utc::now() > expires_at + } else { + false + } + } + + /// Get a formatted summary of this activity + pub fn summary(&self) -> String { + format!( + "[{}] {} - {} (by {})", + self.occurred_at.format("%Y-%m-%d %H:%M:%S UTC"), + self.title, + self.description, + self.actor_name.as_deref().unwrap_or("System") + ) + } +} + +/// Factory methods for creating common governance activities +impl GovernanceActivity { + /// Create an activity for proposal creation + pub fn proposal_created( + proposal_id: u32, + proposal_title: impl ToString, + creator_id: impl ToString, + creator_name: impl ToString, + ) -> Self { + let creator_name_str = creator_name.to_string(); + let proposal_title_str = proposal_title.to_string(); + + Self::new( + None, + ActivityType::ProposalCreated, + format!("{} created a new proposal", creator_name_str), + format!( + "{} created a new proposal titled '{}' for community consideration", + creator_name_str, proposal_title_str + ), + Some(creator_id), + Some(creator_name_str), + ) + .with_target(proposal_id, "proposal") + .with_metadata("proposal_title", proposal_title_str) + .with_tag("proposal") + .with_tag("creation") + .with_severity(ActivitySeverity::Normal) + } + + /// Create an activity for proposal status change + pub fn proposal_status_changed( + proposal_id: u32, + proposal_title: impl ToString, + old_status: impl ToString, + new_status: impl ToString, + actor_id: Option, + actor_name: Option, + ) -> Self { + let proposal_title_str = proposal_title.to_string(); + let old_status_str = old_status.to_string(); + let new_status_str = new_status.to_string(); + let actor_name_str = actor_name + .map(|n| n.to_string()) + .unwrap_or_else(|| "System".to_string()); + + Self::new( + None, + ActivityType::ProposalStatusChanged, + format!( + "{} changed proposal status to {}", + actor_name_str, new_status_str + ), + format!( + "{} changed the status of proposal '{}' from {} to {}", + actor_name_str, proposal_title_str, old_status_str, new_status_str + ), + actor_id, + Some(actor_name_str), + ) + .with_target(proposal_id, "proposal") + .with_metadata("proposal_title", proposal_title_str) + .with_metadata("old_status", old_status_str) + .with_metadata("new_status", new_status_str) + .with_tag("proposal") + .with_tag("status_change") + .with_severity(ActivitySeverity::High) + } + + /// Create an activity for vote casting + pub fn vote_cast( + proposal_id: u32, + proposal_title: impl ToString, + ballot_id: u32, + voter_id: impl ToString, + voter_name: impl ToString, + option_text: impl ToString, + shares: i64, + ) -> Self { + let voter_name_str = voter_name.to_string(); + let option_text_str = option_text.to_string(); + let proposal_title_str = proposal_title.to_string(); + + // Create a more natural vote description + let vote_description = if option_text_str.to_lowercase().contains("approve") + || option_text_str.to_lowercase().contains("yes") + { + format!( + "{} voted YES on proposal '{}'", + voter_name_str, proposal_title_str + ) + } else if option_text_str.to_lowercase().contains("reject") + || option_text_str.to_lowercase().contains("no") + { + format!( + "{} voted NO on proposal '{}'", + voter_name_str, proposal_title_str + ) + } else if option_text_str.to_lowercase().contains("abstain") { + format!( + "{} abstained from voting on proposal '{}'", + voter_name_str, proposal_title_str + ) + } else { + format!( + "{} voted '{}' on proposal '{}'", + voter_name_str, option_text_str, proposal_title_str + ) + }; + + Self::new( + None, + ActivityType::VoteCast, + format!("{} submitted a vote", voter_name_str), + format!("{} with {} voting shares", vote_description, shares), + Some(voter_id), + Some(voter_name_str), + ) + .with_target(proposal_id, "proposal") + .with_metadata("ballot_id", ballot_id.to_string()) + .with_metadata("option_text", option_text_str) + .with_metadata("shares", shares.to_string()) + .with_metadata("proposal_title", proposal_title_str) + .with_tag("vote") + .with_tag("ballot") + .with_severity(ActivitySeverity::Normal) + } + + /// Create an activity for voting period start + pub fn voting_started( + proposal_id: u32, + proposal_title: impl ToString, + start_date: DateTime, + end_date: DateTime, + ) -> Self { + let proposal_title_str = proposal_title.to_string(); + + Self::new( + None, + ActivityType::VotingStarted, + format!("Voting opened for '{}'", proposal_title_str), + format!( + "Community voting has opened for proposal '{}' and will close on {}", + proposal_title_str, + end_date.format("%B %d, %Y at %H:%M UTC") + ), + None::, + Some("System"), + ) + .with_target(proposal_id, "proposal") + .with_metadata("proposal_title", proposal_title_str) + .with_metadata("start_date", start_date.to_rfc3339()) + .with_metadata("end_date", end_date.to_rfc3339()) + .with_tag("voting") + .with_tag("period_start") + .with_severity(ActivitySeverity::High) + .with_occurred_at(start_date) + } + + /// Create an activity for voting period end + pub fn voting_ended( + proposal_id: u32, + proposal_title: impl ToString, + total_votes: usize, + total_shares: i64, + ) -> Self { + let proposal_title_str = proposal_title.to_string(); + + Self::new( + None, + ActivityType::VotingEnded, + format!("Voting closed for '{}'", proposal_title_str), + format!( + "Community voting has ended for proposal '{}'. Final results: {} votes cast representing {} total voting shares", + proposal_title_str, + total_votes, + total_shares + ), + None::, + Some("System"), + ) + .with_target(proposal_id, "proposal") + .with_metadata("proposal_title", proposal_title_str) + .with_metadata("total_votes", total_votes.to_string()) + .with_metadata("total_shares", total_shares.to_string()) + .with_tag("voting") + .with_tag("period_end") + .with_severity(ActivitySeverity::High) + } + + /// Create an activity for vote option addition + pub fn vote_option_added( + proposal_id: u32, + proposal_title: impl ToString, + option_id: u8, + option_text: impl ToString, + actor_id: Option, + actor_name: Option, + ) -> Self { + let proposal_title_str = proposal_title.to_string(); + let option_text_str = option_text.to_string(); + let actor_name_str = actor_name + .map(|n| n.to_string()) + .unwrap_or_else(|| "System".to_string()); + + Self::new( + None, + ActivityType::VoteOptionAdded, + format!("{} added a voting option", actor_name_str), + format!( + "{} added the voting option '{}' to proposal '{}'", + actor_name_str, option_text_str, proposal_title_str + ), + actor_id, + Some(actor_name_str), + ) + .with_target(proposal_id, "proposal") + .with_metadata("proposal_title", proposal_title_str) + .with_metadata("option_id", option_id.to_string()) + .with_metadata("option_text", option_text_str) + .with_tag("vote_option") + .with_tag("addition") + .with_severity(ActivitySeverity::Normal) + } + + /// Create an activity for proposal deletion + pub fn proposal_deleted( + proposal_id: u32, + proposal_title: impl ToString, + actor_id: impl ToString, + actor_name: impl ToString, + reason: Option, + ) -> Self { + let proposal_title_str = proposal_title.to_string(); + let actor_name_str = actor_name.to_string(); + + let description = if let Some(reason) = reason { + format!( + "{} deleted proposal '{}'. Reason: {}", + actor_name_str, + proposal_title_str, + reason.to_string() + ) + } else { + format!( + "{} deleted proposal '{}'", + actor_name_str, proposal_title_str + ) + }; + + Self::new( + None, + ActivityType::ProposalDeleted, + format!("{} deleted a proposal", actor_name_str), + description, + Some(actor_id), + Some(actor_name_str), + ) + .with_target(proposal_id, "proposal") + .with_metadata("proposal_title", proposal_title_str) + .with_tag("proposal") + .with_tag("deletion") + .with_severity(ActivitySeverity::Critical) + } +} diff --git a/heromodels/src/models/governance/mod.rs b/heromodels/src/models/governance/mod.rs index b3df030..4e744bc 100644 --- a/heromodels/src/models/governance/mod.rs +++ b/heromodels/src/models/governance/mod.rs @@ -1,5 +1,7 @@ // heromodels/src/models/governance/mod.rs // This module will contain the Proposal model and related types. +pub mod activity; pub mod proposal; -pub use self::proposal::{Proposal, Ballot, VoteOption, ProposalStatus, VoteEventStatus}; \ No newline at end of file +pub use self::activity::{ActivityStatus, ActivityType, GovernanceActivity}; +pub use self::proposal::{Ballot, Proposal, ProposalStatus, VoteEventStatus, VoteOption}; diff --git a/heromodels/src/models/mod.rs b/heromodels/src/models/mod.rs index ed03eb8..4ceda42 100644 --- a/heromodels/src/models/mod.rs +++ b/heromodels/src/models/mod.rs @@ -3,14 +3,17 @@ 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 governance; // 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 calendar::{AttendanceStatus, Attendee, Calendar, Event}; +pub use finance::marketplace::{Bid, BidStatus, Listing, ListingStatus, ListingType}; pub use finance::{Account, Asset, AssetType}; -pub use finance::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus}; +pub use governance::{ + ActivityStatus, ActivityType, Ballot, GovernanceActivity, Proposal, ProposalStatus, + VoteEventStatus, VoteOption, +};