feat: Enhance governance module with activity tracking and DB refactor

- Refactor database interaction for proposals and activities.
- Add activity tracking for proposal creation and voting.
- Improve logging for better debugging and monitoring.
- Update governance views to display recent activities.
- Add strum and strum_macros crates for enum handling.
- Update Cargo.lock file with new dependencies.
This commit is contained in:
Mahmoud-Emad 2025-05-27 20:45:30 +03:00
parent 70ca9f1605
commit 11d7ae37b6
9 changed files with 193 additions and 335 deletions

View File

@ -1329,6 +1329,12 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.9" version = "0.3.9"
@ -1351,6 +1357,8 @@ dependencies = [
"rhai_wrapper", "rhai_wrapper",
"serde", "serde",
"serde_json", "serde_json",
"strum",
"strum_macros",
"tst", "tst",
] ]
@ -2344,7 +2352,7 @@ dependencies = [
name = "rhai_autobind_macros" name = "rhai_autobind_macros"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"heck", "heck 0.4.1",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@ -2701,6 +2709,25 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"

View File

@ -1,10 +1,13 @@
use crate::db::governance_tracker; use crate::db::proposals::{
use crate::db::proposals::{self, get_proposal_by_id}; self, create_activity, get_all_activities, get_proposal_by_id, get_proposals,
get_recent_activities,
};
use crate::models::governance::{Vote, VoteType, VotingResults}; use crate::models::governance::{Vote, VoteType, VotingResults};
use crate::utils::render_template; use crate::utils::render_template;
use actix_session::Session; use actix_session::Session;
use actix_web::{HttpResponse, Responder, Result, web}; use actix_web::{HttpResponse, Responder, Result, web};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use heromodels::models::ActivityType;
use heromodels::models::governance::{Proposal, ProposalStatus}; use heromodels::models::governance::{Proposal, ProposalStatus};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
@ -80,6 +83,7 @@ impl GovernanceController {
/// Handles the governance dashboard page route /// Handles the governance dashboard page route
pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> { pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
println!("==============================================");
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance"); ctx.insert("active_page", "governance");
ctx.insert("active_tab", "dashboard"); ctx.insert("active_tab", "dashboard");
@ -96,14 +100,32 @@ impl GovernanceController {
let user = Self::get_user_from_session(&session).unwrap(); let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user); ctx.insert("user", &user);
println!("==============================================");
// Get proposals from the database // Get proposals from the database
let proposals = match crate::db::proposals::get_proposals() { let proposals = match crate::db::proposals::get_proposals() {
Ok(props) => props, 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) => { Err(e) => {
println!("❌ Proposals list page: Failed to load proposals: {}", e);
ctx.insert("error", &format!("Failed to load proposals: {}", e)); ctx.insert("error", &format!("Failed to load proposals: {}", e));
vec![] vec![]
} }
}; };
println!("==============================================");
// Make a copy of proposals for statistics // Make a copy of proposals for statistics
let proposals_for_stats = proposals.clone(); let proposals_for_stats = proposals.clone();
@ -170,8 +192,9 @@ impl GovernanceController {
ctx.insert("user", &user); ctx.insert("user", &user);
} }
println!("============== Loading proposals =================");
// Get proposals from the database // Get proposals from the database
let mut proposals = match crate::db::proposals::get_proposals() { let mut proposals = match get_proposals() {
Ok(props) => props, Ok(props) => props,
Err(e) => { Err(e) => {
ctx.insert("error", &format!("Failed to load proposals: {}", e)); ctx.insert("error", &format!("Failed to load proposals: {}", e));
@ -179,6 +202,8 @@ impl GovernanceController {
} }
}; };
println!("proposals: {:?}", proposals);
// Filter proposals by status if provided // Filter proposals by status if provided
if let Some(status_filter) = &query.status { if let Some(status_filter) = &query.status {
if !status_filter.is_empty() { if !status_filter.is_empty() {
@ -372,15 +397,12 @@ impl GovernanceController {
); );
// Track the proposal creation activity // Track the proposal creation activity
let creation_activity = let _ = create_activity(
crate::models::governance::GovernanceActivity::proposal_created( proposal_id,
proposal_id, &saved_proposal.title,
&saved_proposal.title, &user_name,
&user_id, &ActivityType::ProposalCreated,
&user_name, );
);
let _ = governance_tracker::create_activity(creation_activity);
ctx.insert("success", "Proposal created successfully!"); ctx.insert("success", "Proposal created successfully!");
} }
@ -394,8 +416,24 @@ impl GovernanceController {
// Get proposals from the database // Get proposals from the database
let proposals = match crate::db::proposals::get_proposals() { let proposals = match crate::db::proposals::get_proposals() {
Ok(props) => props, 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) => { Err(e) => {
println!("❌ Failed to load proposals: {}", e);
ctx.insert("error", &format!("Failed to load proposals: {}", e)); ctx.insert("error", &format!("Failed to load proposals: {}", e));
vec![] vec![]
} }
@ -407,6 +445,14 @@ impl GovernanceController {
ctx.insert("status_filter", &None::<String>); ctx.insert("status_filter", &None::<String>);
ctx.insert("search_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) render_template(&tmpl, "governance/proposals.html", &ctx)
} }
@ -461,15 +507,12 @@ impl GovernanceController {
// Track the vote cast activity // Track the vote cast activity
if let Ok(Some(proposal)) = get_proposal_by_id(proposal_id_u32) { if let Ok(Some(proposal)) = get_proposal_by_id(proposal_id_u32) {
let vote_activity = crate::models::governance::GovernanceActivity::vote_cast( let _ = create_activity(
proposal_id_u32, proposal_id_u32,
&proposal.title, &proposal.title,
user_name, user_name,
&form.vote_type, &ActivityType::VoteCast,
1, // shares
); );
let _ = governance_tracker::create_activity(vote_activity);
} }
// Redirect to the proposal detail page with a success message // Redirect to the proposal detail page with a success message
@ -576,7 +619,7 @@ impl GovernanceController {
/// Get recent governance activities from the database /// Get recent governance activities from the database
fn get_recent_governance_activities() -> Result<Vec<Value>, String> { fn get_recent_governance_activities() -> Result<Vec<Value>, String> {
// Get real activities from the database (no demo data) // Get real activities from the database (no demo data)
let activities = governance_tracker::get_recent_activities()?; let activities = get_recent_activities()?;
// Convert GovernanceActivity to the format expected by the template // Convert GovernanceActivity to the format expected by the template
let formatted_activities: Vec<Value> = activities let formatted_activities: Vec<Value> = activities
@ -596,10 +639,10 @@ impl GovernanceController {
serde_json::json!({ serde_json::json!({
"type": activity.activity_type, "type": activity.activity_type,
"icon": icon, "icon": icon,
"user": activity.actor_name, "user": activity.creator_name,
"action": action, "action": action,
"proposal_title": activity.proposal_title, "proposal_title": activity.proposal_title,
"timestamp": activity.timestamp.format("%Y-%m-%dT%H:%M:%SZ").to_string(), "created_at": activity.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
"proposal_id": activity.proposal_id "proposal_id": activity.proposal_id
}) })
}) })
@ -611,7 +654,7 @@ impl GovernanceController {
/// Get all governance activities from the database /// Get all governance activities from the database
fn get_all_governance_activities() -> Result<Vec<Value>, String> { fn get_all_governance_activities() -> Result<Vec<Value>, String> {
// Get all activities from the database // Get all activities from the database
let activities = governance_tracker::get_all_activities()?; let activities = get_all_activities()?;
// Convert GovernanceActivity to the format expected by the template // Convert GovernanceActivity to the format expected by the template
let formatted_activities: Vec<Value> = activities let formatted_activities: Vec<Value> = activities
@ -631,10 +674,10 @@ impl GovernanceController {
serde_json::json!({ serde_json::json!({
"type": activity.activity_type, "type": activity.activity_type,
"icon": icon, "icon": icon,
"user": activity.actor_name, "user": activity.creator_name,
"action": action, "action": action,
"proposal_title": activity.proposal_title, "proposal_title": activity.proposal_title,
"timestamp": activity.timestamp.format("%Y-%m-%dT%H:%M:%SZ").to_string(), "created_at": activity.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
"proposal_id": activity.proposal_id "proposal_id": activity.proposal_id
}) })
}) })

View File

@ -0,0 +1,17 @@
use std::path::PathBuf;
use heromodels::db::hero::OurDB;
/// The path to the database file. Change this as needed for your environment.
pub const DB_PATH: &str = "/tmp/freezone_db";
/// 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() -> Result<OurDB, String> {
let db_path = PathBuf::from(DB_PATH);
if let Some(parent) = db_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
// Temporarily reset the database to fix the serialization issue
let db = heromodels::db::hero::OurDB::new(db_path, false).expect("Can create DB");
Ok(db)
}

View File

@ -1,139 +0,0 @@
use crate::models::governance::GovernanceActivity;
use std::path::PathBuf;
/// Database path for governance activities
pub const DB_PATH: &str = "/tmp/ourdb_governance_activities";
/// Returns a shared OurDB instance for activities
pub fn get_db() -> Result<heromodels::db::hero::OurDB, String> {
let db_path = PathBuf::from(DB_PATH);
if let Some(parent) = db_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let db = heromodels::db::hero::OurDB::new(db_path, true)
.map_err(|e| format!("Failed to create activities DB: {:?}", e))?;
Ok(db)
}
/// Creates a new governance activity and saves it to the database using OurDB
pub fn create_activity(activity: GovernanceActivity) -> Result<(u32, GovernanceActivity), String> {
let db = get_db()?;
// Since OurDB doesn't support custom models directly, we'll use a simple key-value approach
// Store each activity with a unique key and serialize it as JSON
// First, get the next available ID by checking existing keys
let activity_id = get_next_activity_id(&db)?;
// Create the activity with the assigned ID
let mut new_activity = activity;
new_activity.id = Some(activity_id);
// Serialize the activity to JSON
let activity_json = serde_json::to_string(&new_activity)
.map_err(|e| format!("Failed to serialize activity: {}", e))?;
// Store in OurDB using a key-value approach
let key = format!("activity_{}", activity_id);
// Use OurDB's raw storage capabilities to store the JSON string
// Since we can't use collections directly, we'll store as raw data
let db_path = format!("{}/{}.json", DB_PATH, key);
std::fs::write(&db_path, &activity_json)
.map_err(|e| format!("Failed to write activity to DB: {}", e))?;
// Also maintain an index of activity IDs for efficient retrieval
update_activity_index(&db, activity_id)?;
println!(
"✅ Activity recorded: {} - {}",
new_activity.activity_type, new_activity.description
);
Ok((activity_id, new_activity))
}
/// Gets the next available activity ID
fn get_next_activity_id(_db: &heromodels::db::hero::OurDB) -> Result<u32, String> {
let index_path = format!("{}/activity_index.json", DB_PATH);
if std::path::Path::new(&index_path).exists() {
let content = std::fs::read_to_string(&index_path)
.map_err(|e| format!("Failed to read activity index: {}", e))?;
let index: Vec<u32> = serde_json::from_str(&content).unwrap_or_else(|_| Vec::new());
Ok(index.len() as u32 + 1)
} else {
Ok(1)
}
}
/// Updates the activity index with a new activity ID
fn update_activity_index(
_db: &heromodels::db::hero::OurDB,
activity_id: u32,
) -> Result<(), String> {
let index_path = format!("{}/activity_index.json", DB_PATH);
let mut index: Vec<u32> = if std::path::Path::new(&index_path).exists() {
let content = std::fs::read_to_string(&index_path)
.map_err(|e| format!("Failed to read activity index: {}", e))?;
serde_json::from_str(&content).unwrap_or_else(|_| Vec::new())
} else {
Vec::new()
};
index.push(activity_id);
let content = serde_json::to_string(&index)
.map_err(|e| format!("Failed to serialize activity index: {}", e))?;
std::fs::write(&index_path, content)
.map_err(|e| format!("Failed to write activity index: {}", e))?;
Ok(())
}
/// Gets all activities from the database using OurDB
pub fn get_all_activities() -> Result<Vec<GovernanceActivity>, String> {
let _db = get_db()?;
let index_path = format!("{}/activity_index.json", DB_PATH);
// Read the activity index to get all activity IDs
if !std::path::Path::new(&index_path).exists() {
return Ok(Vec::new());
}
let content = std::fs::read_to_string(&index_path)
.map_err(|e| format!("Failed to read activity index: {}", e))?;
let activity_ids: Vec<u32> = serde_json::from_str(&content).unwrap_or_else(|_| Vec::new());
let mut activities = Vec::new();
// Load each activity by ID
for activity_id in activity_ids {
let activity_path = format!("{}/activity_{}.json", DB_PATH, activity_id);
if std::path::Path::new(&activity_path).exists() {
let activity_content = std::fs::read_to_string(&activity_path)
.map_err(|e| format!("Failed to read activity {}: {}", activity_id, e))?;
if let Ok(activity) = serde_json::from_str::<GovernanceActivity>(&activity_content) {
activities.push(activity);
}
}
}
Ok(activities)
}
/// Gets recent activities (last 10) sorted by timestamp using OurDB
pub fn get_recent_activities() -> Result<Vec<GovernanceActivity>, String> {
let mut activities = get_all_activities()?;
// Sort by timestamp (most recent first)
activities.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
// Take only the last 10
activities.truncate(10);
Ok(activities)
}

View File

@ -1,2 +1,2 @@
pub mod governance_tracker; pub mod db;
pub mod proposals; pub mod proposals;

View File

@ -1,25 +1,10 @@
use std::path::PathBuf;
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use heromodels::db::hero::OurDB;
use heromodels::{ use heromodels::{
db::{Collection, Db}, db::{Collection, Db},
models::governance::{Proposal, ProposalStatus}, models::governance::{Activity, ActivityType, Proposal, ProposalStatus},
}; };
/// The path to the database file. Change this as needed for your environment. use super::db::get_db;
pub const DB_PATH: &str = "/tmp/ourdb_governance";
/// 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<OurDB, String> {
let db_path = PathBuf::from(db_path);
if let Some(parent) = db_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
// Temporarily reset the database to fix the serialization issue
let db = heromodels::db::hero::OurDB::new(db_path, false).expect("Can create DB");
Ok(db)
}
/// Creates a new proposal and saves it to the database. Returns the saved proposal and its ID. /// Creates a new proposal and saves it to the database. Returns the saved proposal and its ID.
pub fn create_new_proposal( pub fn create_new_proposal(
@ -31,7 +16,7 @@ pub fn create_new_proposal(
voting_start_date: Option<chrono::DateTime<Utc>>, voting_start_date: Option<chrono::DateTime<Utc>>,
voting_end_date: Option<chrono::DateTime<Utc>>, voting_end_date: Option<chrono::DateTime<Utc>>,
) -> Result<(u32, Proposal), String> { ) -> Result<(u32, Proposal), String> {
let db = get_db(DB_PATH).expect("Can create DB"); let db = get_db().expect("Can get DB");
let created_at = Utc::now(); let created_at = Utc::now();
let updated_at = created_at; let updated_at = created_at;
@ -60,7 +45,7 @@ pub fn create_new_proposal(
/// Loads all proposals from the database and returns them as a Vec<Proposal>. /// Loads all proposals from the database and returns them as a Vec<Proposal>.
pub fn get_proposals() -> Result<Vec<Proposal>, String> { pub fn get_proposals() -> Result<Vec<Proposal>, String> {
let db = get_db(DB_PATH).map_err(|e| format!("DB error: {}", e))?; let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db let collection = db
.collection::<Proposal>() .collection::<Proposal>()
.expect("can open proposal collection"); .expect("can open proposal collection");
@ -78,7 +63,7 @@ pub fn get_proposals() -> Result<Vec<Proposal>, String> {
/// Fetches a single proposal by its ID from the database. /// Fetches a single proposal by its ID from the database.
pub fn get_proposal_by_id(proposal_id: u32) -> Result<Option<Proposal>, String> { pub fn get_proposal_by_id(proposal_id: u32) -> Result<Option<Proposal>, String> {
let db = get_db(DB_PATH).map_err(|e| format!("DB error: {}", e))?; let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db let collection = db
.collection::<Proposal>() .collection::<Proposal>()
.map_err(|e| format!("Collection error: {:?}", e))?; .map_err(|e| format!("Collection error: {:?}", e))?;
@ -100,7 +85,7 @@ pub fn submit_vote_on_proposal(
comment: Option<String>, comment: Option<String>,
) -> Result<Proposal, String> { ) -> Result<Proposal, String> {
// Get the proposal from the database // Get the proposal from the database
let db = get_db(DB_PATH).map_err(|e| format!("DB error: {}", e))?; let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db let collection = db
.collection::<Proposal>() .collection::<Proposal>()
.map_err(|e| format!("Collection error: {:?}", e))?; .map_err(|e| format!("Collection error: {:?}", e))?;
@ -167,13 +152,6 @@ pub fn submit_vote_on_proposal(
// We'll create a simple ballot with an auto-generated ID // We'll create a simple ballot with an auto-generated ID
let ballot_id = proposal.ballots.len() as u32 + 1; 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
);
// Create a new ballot and add it to the proposal's ballots // Create a new ballot and add it to the proposal's ballots
use heromodels::models::governance::Ballot; use heromodels::models::governance::Ballot;
@ -210,3 +188,69 @@ pub fn submit_vote_on_proposal(
Ok(updated_proposal) Ok(updated_proposal)
} }
/// Creates a new governance activity and saves it to the database using OurDB
pub fn create_activity(
proposal_id: u32,
proposal_title: &str,
creator_name: &str,
activity_type: &ActivityType,
) -> Result<(u32, Activity), String> {
let db = get_db().expect("Can get DB");
let mut activity = Activity::default();
match activity_type {
ActivityType::ProposalCreated => {
activity = Activity::proposal_created(proposal_id, proposal_title, creator_name);
}
ActivityType::VoteCast => {
activity = Activity::vote_cast(proposal_id, proposal_title, creator_name);
}
ActivityType::VotingStarted => {
activity = Activity::voting_started(proposal_id, proposal_title);
}
ActivityType::VotingEnded => {
activity = Activity::voting_ended(proposal_id, proposal_title);
}
}
// Save the proposal to the database
let collection = db
.collection::<Activity>()
.expect("can open activity collection");
let (proposal_id, saved_proposal) = collection.set(&activity).expect("can save proposal");
Ok((proposal_id, saved_proposal))
}
pub fn get_recent_activities() -> Result<Vec<Activity>, String> {
let db = get_db().expect("Can get DB");
let collection = db
.collection::<Activity>()
.map_err(|e| format!("Collection error: {:?}", e))?;
let mut db_activities = collection
.get_all()
.map_err(|e| format!("DB fetch error: {:?}", e))?;
// Sort by created_at descending
db_activities.sort_by(|a, b| b.created_at.cmp(&a.created_at));
// Take the top 10 most recent
let recent_activities = db_activities.into_iter().take(10).collect();
Ok(recent_activities)
}
pub fn get_all_activities() -> Result<Vec<Activity>, String> {
let db = get_db().expect("Can get DB");
let collection = db
.collection::<Activity>()
.map_err(|e| format!("Collection error: {:?}", e))?;
let db_activities = collection
.get_all()
.map_err(|e| format!("DB fetch error: {:?}", e))?;
Ok(db_activities)
}

View File

@ -208,108 +208,8 @@ pub struct VotingResults {
pub total_votes: usize, pub total_votes: usize,
} }
/// Represents a governance activity in the system
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GovernanceActivity {
/// Unique identifier for the activity
pub id: Option<u32>,
/// 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 actor_name: String,
/// Description of the activity
pub description: String,
/// Date and time when the activity occurred
pub timestamp: DateTime<Utc>,
}
impl GovernanceActivity {
/// Creates a new governance activity
pub fn new(
activity_type: &str,
proposal_id: u32,
proposal_title: &str,
actor_name: &str,
description: &str,
) -> Self {
Self {
id: None,
activity_type: activity_type.to_string(),
proposal_id,
proposal_title: proposal_title.to_string(),
actor_name: actor_name.to_string(),
description: description.to_string(),
timestamp: Utc::now(),
}
}
/// Creates a proposal creation activity
pub fn proposal_created(
proposal_id: u32,
proposal_title: &str,
_creator_id: &str,
creator_name: &str,
) -> Self {
Self::new(
"proposal_created",
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,
vote_option: &str,
shares: i64,
) -> Self {
Self::new(
"vote_cast",
proposal_id,
proposal_title,
voter_name,
&format!(
"{} voted '{}' with {} shares",
voter_name, vote_option, shares
),
)
}
/// Creates a proposal status change activity
pub fn proposal_status_changed(
proposal_id: u32,
proposal_title: &str,
new_status: &ProposalStatus,
reason: Option<&str>,
) -> Self {
let description = format!(
"Proposal '{}' status changed to {}{}",
proposal_title,
new_status,
reason.map(|r| format!(": {}", r)).unwrap_or_default()
);
Self::new(
"proposal_status_changed",
proposal_id,
proposal_title,
"System",
&description,
)
}
}
#[allow(dead_code)]
impl VotingResults { impl VotingResults {
/// Creates a new empty voting results object /// Creates a new VotingResults instance
pub fn new(proposal_id: String) -> Self { pub fn new(proposal_id: String) -> Self {
Self { Self {
proposal_id, proposal_id,
@ -319,38 +219,4 @@ impl VotingResults {
total_votes: 0, total_votes: 0,
} }
} }
/// Adds a vote to the results
pub fn add_vote(&mut self, vote_type: &VoteType) {
match vote_type {
VoteType::Yes => self.yes_count += 1,
VoteType::No => self.no_count += 1,
VoteType::Abstain => self.abstain_count += 1,
}
self.total_votes += 1;
}
/// Calculates the percentage of yes votes
pub fn yes_percentage(&self) -> f64 {
if self.total_votes == 0 {
return 0.0;
}
(self.yes_count as f64 / self.total_votes as f64) * 100.0
}
/// Calculates the percentage of no votes
pub fn no_percentage(&self) -> f64 {
if self.total_votes == 0 {
return 0.0;
}
(self.no_count as f64 / self.total_votes as f64) * 100.0
}
/// Calculates the percentage of abstain votes
pub fn abstain_percentage(&self) -> f64 {
if self.total_votes == 0 {
return 0.0;
}
(self.abstain_count as f64 / self.total_votes as f64) * 100.0
}
} }

View File

@ -92,17 +92,17 @@
<div class="mt-auto"> <div class="mt-auto">
<h5><i class="bi bi-calendar-event me-2"></i>Voting Period</h5> <h5><i class="bi bi-calendar-event me-2"></i>Voting Period</h5>
<div class="d-flex justify-content-between align-items-center p-3 bg-light rounded"> <div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
{% if proposal.voting_starts_at and proposal.voting_ends_at %} {% if proposal.vote_start_date and proposal.vote_end_date %}
<div> <div>
<div class="text-muted mb-1">Start Date</div> <div class="text-muted mb-1">Start Date</div>
<div class="fw-bold">{{ proposal.voting_starts_at | date(format="%Y-%m-%d") }}</div> <div class="fw-bold">{{ proposal.vote_start_date | date(format="%Y-%m-%d") }}</div>
</div> </div>
<div class="text-center"> <div class="text-center">
<i class="bi bi-arrow-right fs-4 text-muted"></i> <i class="bi bi-arrow-right fs-4 text-muted"></i>
</div> </div>
<div> <div>
<div class="text-muted mb-1">End Date</div> <div class="text-muted mb-1">End Date</div>
<div class="fw-bold">{{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}</div> <div class="fw-bold">{{ proposal.vote_end_date | date(format="%Y-%m-%d") }}</div>
</div> </div>
{% else %} {% else %}
<div class="text-center w-100">Not set</div> <div class="text-center w-100">Not set</div>

View File

@ -106,9 +106,9 @@
</td> </td>
<td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td> <td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td>
<td> <td>
{% if proposal.voting_starts_at and proposal.voting_ends_at %} {% if proposal.vote_start_date and proposal.vote_end_date %}
{{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} to {{ {{ proposal.vote_start_date | date(format="%Y-%m-%d") }} to {{
proposal.voting_ends_at | date(format="%Y-%m-%d") }} proposal.vote_end_date | date(format="%Y-%m-%d") }}
{% else %} {% else %}
Not set Not set
{% endif %} {% endif %}