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"
|
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"
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
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;
|
pub mod proposals;
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
Loading…
Reference in New Issue
Block a user