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:
parent
70ca9f1605
commit
11d7ae37b6
29
actix_mvc_app/Cargo.lock
generated
29
actix_mvc_app/Cargo.lock
generated
@ -1329,6 +1329,12 @@ version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
@ -1351,6 +1357,8 @@ dependencies = [
|
||||
"rhai_wrapper",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tst",
|
||||
]
|
||||
|
||||
@ -2344,7 +2352,7 @@ dependencies = [
|
||||
name = "rhai_autobind_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
@ -2701,6 +2709,25 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
@ -1,10 +1,13 @@
|
||||
use crate::db::governance_tracker;
|
||||
use crate::db::proposals::{self, get_proposal_by_id};
|
||||
use crate::db::proposals::{
|
||||
self, create_activity, get_all_activities, get_proposal_by_id, get_proposals,
|
||||
get_recent_activities,
|
||||
};
|
||||
use crate::models::governance::{Vote, VoteType, VotingResults};
|
||||
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;
|
||||
@ -80,6 +83,7 @@ impl GovernanceController {
|
||||
|
||||
/// Handles the governance dashboard page route
|
||||
pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
|
||||
println!("==============================================");
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "governance");
|
||||
ctx.insert("active_tab", "dashboard");
|
||||
@ -96,14 +100,32 @@ impl GovernanceController {
|
||||
let user = Self::get_user_from_session(&session).unwrap();
|
||||
ctx.insert("user", &user);
|
||||
|
||||
println!("==============================================");
|
||||
// Get proposals from the database
|
||||
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) => {
|
||||
println!("❌ Proposals list page: Failed to load proposals: {}", e);
|
||||
ctx.insert("error", &format!("Failed to load proposals: {}", e));
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
println!("==============================================");
|
||||
|
||||
// Make a copy of proposals for statistics
|
||||
let proposals_for_stats = proposals.clone();
|
||||
@ -170,8 +192,9 @@ impl GovernanceController {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
println!("============== Loading proposals =================");
|
||||
// Get proposals from the database
|
||||
let mut proposals = match crate::db::proposals::get_proposals() {
|
||||
let mut proposals = match get_proposals() {
|
||||
Ok(props) => props,
|
||||
Err(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
|
||||
if let Some(status_filter) = &query.status {
|
||||
if !status_filter.is_empty() {
|
||||
@ -372,16 +397,13 @@ impl GovernanceController {
|
||||
);
|
||||
|
||||
// Track the proposal creation activity
|
||||
let creation_activity =
|
||||
crate::models::governance::GovernanceActivity::proposal_created(
|
||||
let _ = create_activity(
|
||||
proposal_id,
|
||||
&saved_proposal.title,
|
||||
&user_id,
|
||||
&user_name,
|
||||
&ActivityType::ProposalCreated,
|
||||
);
|
||||
|
||||
let _ = governance_tracker::create_activity(creation_activity);
|
||||
|
||||
ctx.insert("success", "Proposal created successfully!");
|
||||
}
|
||||
Err(err) => {
|
||||
@ -394,8 +416,24 @@ impl GovernanceController {
|
||||
|
||||
// Get proposals from the database
|
||||
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) => {
|
||||
println!("❌ Failed to load proposals: {}", e);
|
||||
ctx.insert("error", &format!("Failed to load proposals: {}", e));
|
||||
vec![]
|
||||
}
|
||||
@ -407,6 +445,14 @@ impl GovernanceController {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -461,15 +507,12 @@ impl GovernanceController {
|
||||
|
||||
// Track the vote cast activity
|
||||
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.title,
|
||||
user_name,
|
||||
&form.vote_type,
|
||||
1, // shares
|
||||
&ActivityType::VoteCast,
|
||||
);
|
||||
|
||||
let _ = governance_tracker::create_activity(vote_activity);
|
||||
}
|
||||
|
||||
// Redirect to the proposal detail page with a success message
|
||||
@ -576,7 +619,7 @@ impl GovernanceController {
|
||||
/// 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 = governance_tracker::get_recent_activities()?;
|
||||
let activities = get_recent_activities()?;
|
||||
|
||||
// Convert GovernanceActivity to the format expected by the template
|
||||
let formatted_activities: Vec<Value> = activities
|
||||
@ -596,10 +639,10 @@ impl GovernanceController {
|
||||
serde_json::json!({
|
||||
"type": activity.activity_type,
|
||||
"icon": icon,
|
||||
"user": activity.actor_name,
|
||||
"user": activity.creator_name,
|
||||
"action": action,
|
||||
"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
|
||||
})
|
||||
})
|
||||
@ -611,7 +654,7 @@ impl GovernanceController {
|
||||
/// Get all governance activities from the database
|
||||
fn get_all_governance_activities() -> Result<Vec<Value>, String> {
|
||||
// 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
|
||||
let formatted_activities: Vec<Value> = activities
|
||||
@ -631,10 +674,10 @@ impl GovernanceController {
|
||||
serde_json::json!({
|
||||
"type": activity.activity_type,
|
||||
"icon": icon,
|
||||
"user": activity.actor_name,
|
||||
"user": activity.creator_name,
|
||||
"action": action,
|
||||
"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
|
||||
})
|
||||
})
|
||||
|
17
actix_mvc_app/src/db/db.rs
Normal file
17
actix_mvc_app/src/db/db.rs
Normal 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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -1,2 +1,2 @@
|
||||
pub mod governance_tracker;
|
||||
pub mod db;
|
||||
pub mod proposals;
|
||||
|
@ -1,25 +1,10 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use chrono::{Duration, Utc};
|
||||
use heromodels::db::hero::OurDB;
|
||||
use heromodels::{
|
||||
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.
|
||||
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)
|
||||
}
|
||||
use super::db::get_db;
|
||||
|
||||
/// Creates a new proposal and saves it to the database. Returns the saved proposal and its ID.
|
||||
pub fn create_new_proposal(
|
||||
@ -31,7 +16,7 @@ pub fn create_new_proposal(
|
||||
voting_start_date: Option<chrono::DateTime<Utc>>,
|
||||
voting_end_date: Option<chrono::DateTime<Utc>>,
|
||||
) -> 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 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>.
|
||||
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
|
||||
.collection::<Proposal>()
|
||||
.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.
|
||||
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
|
||||
.collection::<Proposal>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
@ -100,7 +85,7 @@ pub fn submit_vote_on_proposal(
|
||||
comment: Option<String>,
|
||||
) -> Result<Proposal, String> {
|
||||
// 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
|
||||
.collection::<Proposal>()
|
||||
.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
|
||||
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
|
||||
use heromodels::models::governance::Ballot;
|
||||
|
||||
@ -210,3 +188,69 @@ pub fn submit_vote_on_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)
|
||||
}
|
||||
|
@ -208,108 +208,8 @@ pub struct VotingResults {
|
||||
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 {
|
||||
/// Creates a new empty voting results object
|
||||
/// Creates a new VotingResults instance
|
||||
pub fn new(proposal_id: String) -> Self {
|
||||
Self {
|
||||
proposal_id,
|
||||
@ -319,38 +219,4 @@ impl VotingResults {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -92,17 +92,17 @@
|
||||
<div class="mt-auto">
|
||||
<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">
|
||||
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
|
||||
{% if proposal.vote_start_date and proposal.vote_end_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 class="text-center">
|
||||
<i class="bi bi-arrow-right fs-4 text-muted"></i>
|
||||
</div>
|
||||
<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>
|
||||
{% else %}
|
||||
<div class="text-center w-100">Not set</div>
|
||||
|
@ -106,9 +106,9 @@
|
||||
</td>
|
||||
<td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td>
|
||||
<td>
|
||||
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
|
||||
{{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} to {{
|
||||
proposal.voting_ends_at | date(format="%Y-%m-%d") }}
|
||||
{% if proposal.vote_start_date and proposal.vote_end_date %}
|
||||
{{ proposal.vote_start_date | date(format="%Y-%m-%d") }} to {{
|
||||
proposal.vote_end_date | date(format="%Y-%m-%d") }}
|
||||
{% else %}
|
||||
Not set
|
||||
{% endif %}
|
||||
|
Loading…
Reference in New Issue
Block a user