hostbasket/actix_mvc_app/src/controllers/governance.rs
2025-05-21 13:49:20 +03:00

699 lines
27 KiB
Rust

use crate::db::proposals::{self, get_proposal_by_id};
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::governance::{Proposal, ProposalStatus};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tera::Tera;
use chrono::prelude::*;
/// Controller for handling governance-related routes
pub struct GovernanceController;
#[allow(dead_code)]
impl GovernanceController {
/// Helper function to get user from session
/// For testing purposes, this will always return a mock user
fn get_user_from_session(session: &Session) -> Option<Value> {
// Try to get user from session first
let session_user = session
.get::<String>("user")
.ok()
.flatten()
.and_then(|user_json| serde_json::from_str(&user_json).ok());
// If user is not in session, return a mock user for testing
session_user.or_else(|| {
// Create a mock user
let mock_user = serde_json::json!({
"id": 1,
"username": "test_user",
"email": "test@example.com",
"name": "Test User",
"role": "member"
});
Some(mock_user)
})
}
/// Handles the governance dashboard page route
pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
// Get proposals from the database
let proposals = match crate::db::proposals::get_proposals() {
Ok(props) => props,
Err(e) => {
ctx.insert("error", &format!("Failed to load proposals: {}", e));
vec![]
}
};
// Filter for active proposals only
let active_proposals: Vec<heromodels::models::Proposal> = proposals
.into_iter()
.filter(|p| p.status == heromodels::models::ProposalStatus::Active)
.collect();
// Sort active proposals by voting end date (ascending)
let mut sorted_active_proposals = active_proposals.clone();
sorted_active_proposals.sort_by(|a, b| a.vote_start_date.cmp(&b.vote_end_date));
ctx.insert("proposals", &sorted_active_proposals);
// Get the nearest deadline proposal for the voting pane
if let Some(nearest_proposal) = sorted_active_proposals.first() {
ctx.insert("nearest_proposal", nearest_proposal);
}
// Get recent activity for the timeline
let recent_activity = Self::get_mock_recent_activity();
ctx.insert("recent_activity", &recent_activity);
// Get some statistics
let stats = Self::get_mock_statistics();
ctx.insert("stats", &stats);
render_template(&tmpl, "governance/index.html", &ctx)
}
/// Handles the proposal list page route
pub async fn proposals(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "proposals");
// Add user to context if available
if let Some(user) = Self::get_user_from_session(&session) {
ctx.insert("user", &user);
}
// Get proposals from the database
let proposals = match crate::db::proposals::get_proposals() {
Ok(props) => props,
Err(e) => {
ctx.insert("error", &format!("Failed to load proposals: {}", e));
vec![]
}
};
ctx.insert("proposals", &proposals);
render_template(&tmpl, "governance/proposals.html", &ctx)
}
/// Handles the proposal detail page route
pub async fn proposal_detail(
path: web::Path<String>,
tmpl: web::Data<Tera>,
session: Session,
) -> Result<impl Responder> {
let proposal_id = path.into_inner();
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
// Add user to context if available
if let Some(user) = Self::get_user_from_session(&session) {
ctx.insert("user", &user);
}
// Get mock proposal detail
let proposal = get_proposal_by_id(proposal_id.parse().unwrap());
if let Ok(Some(proposal)) = proposal {
ctx.insert("proposal", &proposal);
// Get mock votes for this proposal
let votes = Self::get_mock_votes_for_proposal(&proposal_id);
ctx.insert("votes", &votes);
// Calculate voting results directly from the proposal
let results = Self::calculate_voting_results_from_proposal(&proposal);
ctx.insert("results", &results);
render_template(&tmpl, "governance/proposal_detail.html", &ctx)
} else {
// Proposal not found
ctx.insert("error", "Proposal not found");
// For the error page, we'll use a special case to set the status code to 404
match tmpl.render("error.html", &ctx) {
Ok(content) => Ok(HttpResponse::NotFound()
.content_type("text/html")
.body(content)),
Err(e) => {
eprintln!("Error rendering error template: {}", e);
Err(actix_web::error::ErrorInternalServerError(format!(
"Error: {}",
e
)))
}
}
}
}
/// Handles the create proposal page route
pub async fn create_proposal_form(
tmpl: web::Data<Tera>,
session: Session,
) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "create");
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
render_template(&tmpl, "governance/create_proposal.html", &ctx)
}
/// Handles the submission of a new proposal
pub async fn submit_proposal(
_form: web::Form<ProposalForm>,
tmpl: web::Data<Tera>,
session: Session,
) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
let proposal_title = &_form.title;
let proposal_description = &_form.description;
// Use the DB-backed proposal creation
// Parse voting_start_date and voting_end_date from the form (YYYY-MM-DD expected)
let voting_start_date = _form.voting_start_date.as_ref().and_then(|s| {
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
.ok()
.and_then(|d| d.and_hms_opt(0, 0, 0))
.map(|naive| chrono::Utc.from_utc_datetime(&naive))
});
let voting_end_date = _form.voting_end_date.as_ref().and_then(|s| {
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
.ok()
.and_then(|d| d.and_hms_opt(23, 59, 59))
.map(|naive| chrono::Utc.from_utc_datetime(&naive))
});
// Extract user id and name from serde_json::Value
let user_id = user
.get("id")
.and_then(|v| v.as_i64())
.unwrap_or(1)
.to_string();
let user_name = user
.get("username")
.and_then(|v| v.as_str())
.unwrap_or("Test User")
.to_string();
let is_draft = _form.draft.is_some();
let status = if is_draft {
ProposalStatus::Draft
} else {
ProposalStatus::Active
};
match proposals::create_new_proposal(
&user_id,
&user_name,
proposal_title,
proposal_description,
status,
voting_start_date,
voting_end_date,
) {
Ok((proposal_id, saved_proposal)) => {
println!(
"Proposal saved to DB: ID={}, title={:?}",
proposal_id, saved_proposal.title
);
ctx.insert("success", "Proposal created successfully!");
}
Err(err) => {
println!("Failed to save proposal: {err}");
ctx.insert("error", &format!("Failed to save proposal: {err}"));
}
}
// For now, we'll just redirect to the proposals page with a success message
// Get proposals from the database
let proposals = match crate::db::proposals::get_proposals() {
Ok(props) => props,
Err(e) => {
ctx.insert("error", &format!("Failed to load proposals: {}", e));
vec![]
}
};
ctx.insert("proposals", &proposals);
render_template(&tmpl, "governance/proposals.html", &ctx)
}
/// Handles the submission of a vote on a proposal
pub async fn submit_vote(
path: web::Path<String>,
form: web::Form<VoteForm>,
tmpl: web::Data<Tera>,
session: Session,
) -> Result<impl Responder> {
let proposal_id = path.into_inner();
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
// Check if user is logged in
let user = match Self::get_user_from_session(&session) {
Some(user) => user,
None => {
return Ok(HttpResponse::Found()
.append_header(("Location", "/login"))
.finish());
}
};
ctx.insert("user", &user);
// Extract user ID
let user_id = user.get("id").and_then(|v| v.as_i64()).unwrap_or(1) as i32;
// Parse proposal ID
let proposal_id_u32 = match proposal_id.parse::<u32>() {
Ok(id) => id,
Err(_) => {
ctx.insert("error", "Invalid proposal ID");
return render_template(&tmpl, "error.html", &ctx);
}
};
// Submit the vote
match crate::db::proposals::submit_vote_on_proposal(
proposal_id_u32,
user_id,
&form.vote_type,
1, // Default to 1 share
) {
Ok(updated_proposal) => {
ctx.insert("proposal", &updated_proposal);
ctx.insert("success", "Your vote has been recorded!");
// Get votes for this proposal
// For now, we'll still use mock votes until we implement a function to extract votes from the proposal
let votes = Self::get_mock_votes_for_proposal(&proposal_id);
ctx.insert("votes", &votes);
// Calculate voting results directly from the updated proposal
let results = Self::calculate_voting_results_from_proposal(&updated_proposal);
ctx.insert("results", &results);
render_template(&tmpl, "governance/proposal_detail.html", &ctx)
}
Err(e) => {
ctx.insert("error", &format!("Failed to submit vote: {}", e));
render_template(&tmpl, "error.html", &ctx)
}
}
}
/// Handles the my votes page route
pub async fn my_votes(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "my_votes");
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
// Get mock votes for this user
let votes = Self::get_mock_votes_for_user(1); // Assuming user ID 1 for mock data
ctx.insert("votes", &votes);
render_template(&tmpl, "governance/my_votes.html", &ctx)
}
/// Generate mock recent activity data for the dashboard
fn get_mock_recent_activity() -> Vec<serde_json::Value> {
vec![
serde_json::json!({
"type": "vote",
"user": "Sarah Johnson",
"proposal_id": "prop-001",
"proposal_title": "Community Garden Initiative",
"action": "voted Yes",
"timestamp": (Utc::now() - Duration::hours(2)).to_rfc3339(),
"icon": "bi-check-circle-fill text-success"
}),
serde_json::json!({
"type": "comment",
"user": "Michael Chen",
"proposal_id": "prop-003",
"proposal_title": "Weekly Community Calls",
"action": "commented",
"comment": "I think this would greatly improve communication.",
"timestamp": (Utc::now() - Duration::hours(5)).to_rfc3339(),
"icon": "bi-chat-left-text-fill text-primary"
}),
serde_json::json!({
"type": "vote",
"user": "Robert Callingham",
"proposal_id": "prop-005",
"proposal_title": "Security Audit Implementation",
"action": "voted Yes",
"timestamp": (Utc::now() - Duration::hours(8)).to_rfc3339(),
"icon": "bi-check-circle-fill text-success"
}),
serde_json::json!({
"type": "proposal",
"user": "Emma Rodriguez",
"proposal_id": "prop-004",
"proposal_title": "Sustainability Roadmap",
"action": "created proposal",
"timestamp": (Utc::now() - Duration::hours(12)).to_rfc3339(),
"icon": "bi-file-earmark-text-fill text-info"
}),
serde_json::json!({
"type": "vote",
"user": "David Kim",
"proposal_id": "prop-002",
"proposal_title": "Governance Framework Update",
"action": "voted No",
"timestamp": (Utc::now() - Duration::hours(16)).to_rfc3339(),
"icon": "bi-x-circle-fill text-danger"
}),
serde_json::json!({
"type": "comment",
"user": "Lisa Wang",
"proposal_id": "prop-001",
"proposal_title": "Community Garden Initiative",
"action": "commented",
"comment": "I'd like to volunteer to help coordinate this effort.",
"timestamp": (Utc::now() - Duration::hours(24)).to_rfc3339(),
"icon": "bi-chat-left-text-fill text-primary"
}),
]
}
// Mock data generation methods
/// Generate mock proposals for testing
fn get_mock_proposals() -> Vec<Proposal> {
let now = Utc::now();
vec![
Proposal::new(
Some(1),
"1",
"Ibrahim Faraji",
"Establish Zanzibar Digital Trade Hub",
"This proposal aims to create a dedicated digital trade hub within the Zanzibar Digital Freezone to facilitate international e-commerce for local businesses. The hub will provide logistics support, digital marketing services, and regulatory compliance assistance to help Zanzibar businesses reach global markets.",
ProposalStatus::Active,
now - Duration::days(5),
now - Duration::days(5),
now - Duration::days(3),
now + Duration::days(4),
),
Proposal::new(
Some(2),
"2",
"Amina Salim",
"ZDFZ Sustainable Tourism Framework",
"A comprehensive framework for sustainable tourism development within the Zanzibar Digital Freezone. This proposal outlines environmental standards, community benefit-sharing mechanisms, and digital infrastructure for eco-tourism businesses. It includes tokenization standards for tourism assets and a certification system for sustainable operators.",
ProposalStatus::Approved,
now - Duration::days(15),
now - Duration::days(2),
now - Duration::days(14),
now - Duration::days(2),
),
Proposal::new(
Some(3),
"3",
"Hassan Mwinyi",
"Spice Industry Modernization Initiative",
"This proposal seeks to modernize Zanzibar's traditional spice industry through blockchain-based supply chain tracking, international quality certification, and digital marketplace integration. The initiative will help local spice farmers and processors access premium international markets while preserving traditional cultivation methods.",
ProposalStatus::Draft,
now - Duration::days(1),
now - Duration::days(1),
now - Duration::days(1),
now + Duration::days(1),
),
Proposal::new(
Some(4),
"4",
"Ibrahim Faraji",
"ZDFZ Regulatory Framework for Digital Financial Services",
"Establish a comprehensive regulatory framework for digital financial services within the Zanzibar Digital Freezone. This includes licensing requirements for crypto exchanges, digital payment providers, and tokenized asset platforms operating within the zone, while ensuring compliance with international AML/KYC standards.",
ProposalStatus::Rejected,
now - Duration::days(20),
now - Duration::days(5),
now - Duration::days(19),
now - Duration::days(5),
),
Proposal::new(
Some(5),
"5",
"Fatma Busaidy",
"Digital Arts Incubator and Artwork Marketplace",
"Create a dedicated digital arts incubator and Artwork marketplace to support Zanzibar's creative economy. The initiative will provide technical training, equipment, and a curated marketplace for local artists to create and sell digital art that celebrates Zanzibar's rich cultural heritage while accessing global markets.",
ProposalStatus::Active,
now - Duration::days(7),
now - Duration::days(7),
now - Duration::days(6),
now + Duration::days(1),
),
Proposal::new(
Some(6),
"6",
"Omar Makame",
"Zanzibar Renewable Energy Microgrid Network",
"Develop a network of renewable energy microgrids across the Zanzibar Digital Freezone using tokenized investment and community ownership models. This proposal outlines the technical specifications, governance structure, and token economics for deploying solar and tidal energy systems that will ensure energy independence for the zone.",
ProposalStatus::Active,
now - Duration::days(10),
now - Duration::days(9),
now - Duration::days(8),
now + Duration::days(6),
),
Proposal::new(
Some(7),
"7",
"Saida Juma",
"ZDFZ Educational Technology Initiative",
"Establish a comprehensive educational technology program within the Zanzibar Digital Freezone to develop local tech talent. This initiative includes coding academies, blockchain development courses, and digital entrepreneurship training, with a focus on preparing Zanzibar's youth for careers in the zone's growing digital economy.",
ProposalStatus::Draft,
now - Duration::days(3),
now - Duration::days(2),
now - Duration::days(1),
now + Duration::days(1),
),
]
}
/// Get a mock proposal by ID
fn get_mock_proposal_by_id(id: &str) -> Option<Proposal> {
Self::get_mock_proposals()
.into_iter()
.find(|p| p.base_data.id.to_string() == id)
}
/// Generate mock votes for a specific proposal
fn get_mock_votes_for_proposal(proposal_id: &str) -> Vec<Vote> {
let now = Utc::now();
vec![
Vote {
id: "vote-001".to_string(),
proposal_id: proposal_id.to_string(),
voter_id: 1,
voter_name: "Robert Callingham".to_string(),
vote_type: VoteType::Yes,
comment: Some("I strongly support this initiative.".to_string()),
created_at: now - Duration::days(2),
updated_at: now - Duration::days(2),
},
Vote {
id: "vote-002".to_string(),
proposal_id: proposal_id.to_string(),
voter_id: 2,
voter_name: "Jane Smith".to_string(),
vote_type: VoteType::Yes,
comment: None,
created_at: now - Duration::days(2),
updated_at: now - Duration::days(2),
},
Vote {
id: "vote-003".to_string(),
proposal_id: proposal_id.to_string(),
voter_id: 3,
voter_name: "Bob Johnson".to_string(),
vote_type: VoteType::No,
comment: Some("I have concerns about the implementation cost.".to_string()),
created_at: now - Duration::days(1),
updated_at: now - Duration::days(1),
},
Vote {
id: "vote-004".to_string(),
proposal_id: proposal_id.to_string(),
voter_id: 4,
voter_name: "Alice Williams".to_string(),
vote_type: VoteType::Abstain,
comment: Some("I need more information before making a decision.".to_string()),
created_at: now - Duration::hours(12),
updated_at: now - Duration::hours(12),
},
]
}
/// Generate mock votes for a specific user
fn get_mock_votes_for_user(user_id: i32) -> Vec<(Vote, Proposal)> {
let votes = vec![
Vote {
id: "vote-001".to_string(),
proposal_id: "prop-001".to_string(),
voter_id: user_id,
voter_name: "Robert Callingham".to_string(),
vote_type: VoteType::Yes,
comment: Some("I strongly support this initiative.".to_string()),
created_at: Utc::now() - Duration::days(2),
updated_at: Utc::now() - Duration::days(2),
},
Vote {
id: "vote-005".to_string(),
proposal_id: "prop-002".to_string(),
voter_id: user_id,
voter_name: "Robert Callingham".to_string(),
vote_type: VoteType::No,
comment: Some("I don't think this is a priority right now.".to_string()),
created_at: Utc::now() - Duration::days(10),
updated_at: Utc::now() - Duration::days(10),
},
Vote {
id: "vote-008".to_string(),
proposal_id: "prop-004".to_string(),
voter_id: user_id,
voter_name: "Robert Callingham".to_string(),
vote_type: VoteType::Yes,
comment: None,
created_at: Utc::now() - Duration::days(18),
updated_at: Utc::now() - Duration::days(18),
},
Vote {
id: "vote-010".to_string(),
proposal_id: "prop-005".to_string(),
voter_id: user_id,
voter_name: "Robert Callingham".to_string(),
vote_type: VoteType::Yes,
comment: Some("Security is always a top priority.".to_string()),
created_at: Utc::now() - Duration::days(5),
updated_at: Utc::now() - Duration::days(5),
},
];
let proposals = Self::get_mock_proposals();
votes
.into_iter()
.filter_map(|vote| {
proposals
.iter()
.find(|p| p.base_data.id.to_string() == vote.proposal_id)
.map(|p| (vote.clone(), p.clone()))
})
.collect()
}
/// Generate mock voting results for a proposal
fn get_mock_voting_results(proposal_id: &str) -> VotingResults {
let votes = Self::get_mock_votes_for_proposal(proposal_id);
let mut results = VotingResults::new(proposal_id.to_string());
for vote in votes {
results.add_vote(&vote.vote_type);
}
results
}
/// Calculate voting results from a proposal
fn calculate_voting_results_from_proposal(proposal: &Proposal) -> VotingResults {
let mut results = VotingResults::new(proposal.base_data.id.to_string());
// Count votes for each option
for option in &proposal.options {
match option.id {
1 => results.yes_count = option.count as usize,
2 => results.no_count = option.count as usize,
3 => results.abstain_count = option.count as usize,
_ => {} // Ignore other options
}
}
// Calculate total votes
results.total_votes = results.yes_count + results.no_count + results.abstain_count;
results
}
/// Generate mock statistics for the governance dashboard
fn get_mock_statistics() -> GovernanceStats {
GovernanceStats {
total_proposals: 5,
active_proposals: 2,
approved_proposals: 1,
rejected_proposals: 1,
draft_proposals: 1,
total_votes: 15,
participation_rate: 75.0,
}
}
}
/// Represents the data submitted in the proposal form
#[derive(Debug, Deserialize)]
pub struct ProposalForm {
/// Title of the proposal
pub title: String,
/// Description of the proposal
pub description: String,
/// Status of the proposal
pub draft: Option<bool>,
/// Start date for voting
pub voting_start_date: Option<String>,
/// End date for voting
pub voting_end_date: Option<String>,
}
/// Represents the data submitted in the vote form
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct VoteForm {
/// Type of vote (yes, no, abstain)
pub vote_type: String,
/// Optional comment explaining the vote
pub comment: Option<String>,
}
/// Represents statistics for the governance dashboard
#[derive(Debug, Serialize)]
pub struct GovernanceStats {
/// Total number of proposals
pub total_proposals: usize,
/// Number of active proposals
pub active_proposals: usize,
/// Number of approved proposals
pub approved_proposals: usize,
/// Number of rejected proposals
pub rejected_proposals: usize,
/// Number of draft proposals
pub draft_proposals: usize,
/// Total number of votes cast
pub total_votes: usize,
/// Participation rate (percentage)
pub participation_rate: f64,
}