feat: Integerated the DB:

- Added an initialization with the db
- Implemented 'add_new_proposal' function to be used in the form
This commit is contained in:
Mahmoud Emad 2025-05-18 09:07:59 +03:00
parent 2fd74defab
commit e4e403e231
7 changed files with 441 additions and 67 deletions

View File

@ -0,0 +1,2 @@
[net]
git-fetch-with-cli = true

259
actix_mvc_app/Cargo.lock generated
View File

@ -296,6 +296,8 @@ dependencies = [
"env_logger",
"futures",
"futures-util",
"heromodels",
"heromodels_core",
"jsonwebtoken",
"lazy_static",
"log",
@ -309,6 +311,14 @@ dependencies = [
"uuid",
]
[[package]]
name = "adapter_macros"
version = "0.1.0"
dependencies = [
"chrono",
"rhai",
]
[[package]]
name = "addr2line"
version = "0.24.2"
@ -366,6 +376,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"const-random",
"getrandom 0.2.15",
"once_cell",
"version_check",
"zerocopy 0.7.35",
@ -478,6 +490,12 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-trait"
version = "0.1.88"
@ -547,6 +565,26 @@ dependencies = [
"zeroize",
]
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"bincode_derive",
"serde",
"unty",
]
[[package]]
name = "bincode_derive"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
dependencies = [
"virtue",
]
[[package]]
name = "bitflags"
version = "2.9.0"
@ -1285,12 +1323,54 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "heromodels"
version = "0.1.0"
dependencies = [
"adapter_macros",
"bincode",
"chrono",
"heromodels-derive",
"heromodels_core",
"ourdb",
"rhai",
"rhai_autobind_macros",
"rhai_client_macros",
"rhai_wrapper",
"serde",
"serde_json",
"tst",
]
[[package]]
name = "heromodels-derive"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "heromodels_core"
version = "0.1.0"
dependencies = [
"chrono",
"serde",
]
[[package]]
name = "hkdf"
version = "0.12.4"
@ -1557,6 +1637,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -1756,6 +1845,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "no-std-compat"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
dependencies = [
"spin",
]
[[package]]
name = "nom"
version = "7.1.3"
@ -1824,6 +1922,9 @@ name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
dependencies = [
"portable-atomic",
]
[[package]]
name = "opaque-debug"
@ -1841,6 +1942,16 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "ourdb"
version = "0.1.0"
dependencies = [
"crc32fast",
"log",
"rand 0.8.5",
"thiserror 1.0.69",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
@ -1907,7 +2018,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
dependencies = [
"memchr",
"thiserror",
"thiserror 2.0.12",
"ucd-trie",
]
@ -2210,6 +2321,75 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rhai"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
dependencies = [
"ahash",
"bitflags",
"instant",
"no-std-compat",
"num-traits",
"once_cell",
"rhai_codegen",
"rust_decimal",
"smallvec",
"smartstring",
"thin-vec",
]
[[package]]
name = "rhai_autobind_macros"
version = "0.1.0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "rhai_client_macros"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"rhai",
"syn",
]
[[package]]
name = "rhai_codegen"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "rhai_macros_derive"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "rhai_wrapper"
version = "0.1.0"
dependencies = [
"chrono",
"rhai",
"rhai_macros_derive",
"serde",
]
[[package]]
name = "ring"
version = "0.16.20"
@ -2247,6 +2427,16 @@ dependencies = [
"ordered-multimap",
]
[[package]]
name = "rust_decimal"
version = "1.37.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50"
dependencies = [
"arrayvec",
"num-traits",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@ -2421,7 +2611,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [
"num-bigint",
"num-traits",
"thiserror",
"thiserror 2.0.12",
"time",
]
@ -2456,6 +2646,17 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "smartstring"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [
"autocfg",
"static_assertions",
"version_check",
]
[[package]]
name = "socket2"
version = "0.4.10"
@ -2488,6 +2689,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.11.1"
@ -2557,13 +2764,39 @@ dependencies = [
"unic-segment",
]
[[package]]
name = "thin-vec"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
"thiserror-impl 2.0.12",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -2723,6 +2956,14 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tst"
version = "0.1.0"
dependencies = [
"ourdb",
"thiserror 1.0.69",
]
[[package]]
name = "typenum"
version = "1.18.0"
@ -2831,6 +3072,12 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "url"
version = "2.5.4"
@ -2888,6 +3135,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "virtue"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
[[package]]
name = "walkdir"
version = "2.5.0"

View File

@ -15,6 +15,8 @@ env_logger = "0.11.2"
log = "0.4.21"
dotenv = "0.15.0"
chrono = { version = "0.4.35", features = ["serde"] }
heromodels = { path = "../../db/heromodels" }
heromodels_core = { path = "../../db/heromodels_core" }
config = "0.14.0"
num_cpus = "1.16.0"
futures = "0.3.30"
@ -27,3 +29,8 @@ redis = { version = "0.23.0", features = ["tokio-comp"] }
jsonwebtoken = "8.3.0"
pulldown-cmark = "0.13.0"
urlencoding = "2.1.3"
[patch."https://git.ourworld.tf/herocode/db.git"]
rhai_autobind_macros = { path = "../../rhaj/rhai_autobind_macros" }
rhai_wrapper = { path = "../../rhaj/rhai_wrapper" }

View File

@ -1,11 +1,14 @@
use actix_web::{web, HttpResponse, Responder, Result};
use actix_session::Session;
use tera::Tera;
use serde_json::Value;
use serde::{Deserialize, Serialize};
use chrono::{Utc, Duration};
use crate::models::governance::{Proposal, Vote, ProposalStatus, VoteType, VotingResults};
use crate::db::proposals;
use crate::models::governance::{Proposal, ProposalStatus, Vote, VoteType, VotingResults};
use crate::utils::render_template;
use actix_session::Session;
use actix_web::{HttpResponse, Responder, Result, web};
use chrono::{Duration, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tera::Tera;
use chrono::prelude::*;
/// Controller for handling governance-related routes
pub struct GovernanceController;
@ -15,9 +18,11 @@ impl GovernanceController {
/// 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()
});
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(|| {
@ -43,10 +48,11 @@ impl GovernanceController {
ctx.insert("user", &user);
// Get mock proposals for the dashboard
let mut proposals = Self::get_mock_proposals();
let proposals = Self::get_mock_proposals();
// Filter for active proposals only
let active_proposals: Vec<Proposal> = proposals.into_iter()
let active_proposals: Vec<Proposal> = proposals
.into_iter()
.filter(|p| p.status == ProposalStatus::Active)
.collect();
@ -94,7 +100,7 @@ impl GovernanceController {
pub async fn proposal_detail(
path: web::Path<String>,
tmpl: web::Data<Tera>,
session: Session
session: Session,
) -> Result<impl Responder> {
let proposal_id = path.into_inner();
let mut ctx = tera::Context::new();
@ -124,17 +130,25 @@ impl GovernanceController {
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)),
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)))
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> {
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");
@ -150,7 +164,7 @@ impl GovernanceController {
pub async fn submit_proposal(
_form: web::Form<ProposalForm>,
tmpl: web::Data<Tera>,
session: Session
session: Session,
) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
@ -159,9 +173,52 @@ impl GovernanceController {
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
// In a real application, we would save the proposal to a database
// For now, we'll just redirect to the proposals page with a success message
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();
match proposals::create_new_proposal(
&user_id,
proposal_title,
proposal_description,
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 mock proposals
let proposals = Self::get_mock_proposals();
@ -175,13 +232,15 @@ impl GovernanceController {
path: web::Path<String>,
_form: web::Form<VoteForm>,
tmpl: web::Data<Tera>,
session: Session
session: Session,
) -> Result<impl Responder> {
let proposal_id = path.into_inner();
// Check if user is logged in
if Self::get_user_from_session(&session).is_none() {
return Ok(HttpResponse::Found().append_header(("Location", "/login")).finish());
return Ok(HttpResponse::Found()
.append_header(("Location", "/login"))
.finish());
}
let mut ctx = tera::Context::new();
@ -212,10 +271,15 @@ impl GovernanceController {
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)),
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)))
Err(actix_web::error::ErrorInternalServerError(format!(
"Error: {}",
e
)))
}
}
}
@ -491,9 +555,11 @@ impl GovernanceController {
];
let proposals = Self::get_mock_proposals();
votes.into_iter()
votes
.into_iter()
.filter_map(|vote| {
proposals.iter()
proposals
.iter()
.find(|p| p.id == vote.proposal_id)
.map(|p| (vote.clone(), p.clone()))
})

View File

@ -0,0 +1 @@
pub mod proposals;

View File

@ -0,0 +1,44 @@
use chrono::{Duration, Utc};
use heromodels::db::hero::OurDB;
use heromodels::{
db::{Collection, Db},
models::governance::{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 = heromodels::db::hero::OurDB::new(db_path, true).expect("Can create DB");
Ok(db)
}
/// Creates a new proposal and saves it to the database. Returns the saved proposal and its ID.
pub fn create_new_proposal(
creator_id: &str,
title: &str,
description: &str,
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");
// Create a new proposal (with auto-generated ID)
let mut proposal = Proposal::new(
None,
creator_id,
title,
description,
voting_start_date.unwrap_or_else(Utc::now),
voting_end_date.unwrap_or_else(|| Utc::now() + Duration::days(7)),
);
proposal.status = ProposalStatus::Draft;
// Save the proposal to the database
let collection = db
.collection::<Proposal>()
.expect("can open proposal collection");
let (proposal_id, saved_proposal) = collection.set(&proposal).expect("can save proposal");
Ok((proposal_id, saved_proposal))
}

View File

@ -8,6 +8,7 @@ use lazy_static::lazy_static;
mod config;
mod controllers;
mod db;
mod middleware;
mod models;
mod routes;