feat: Enhance Governance Controller and Proposal Handling
- Improve proposal search to include description field: This allows for more comprehensive search results. - Fix redirect after voting: The redirect now correctly handles the success message. - Handle potential invalid timestamps in ballots: The code now gracefully handles ballots with invalid timestamps, preventing crashes and using the current time as a fallback. - Add local time formatting function: This provides a way to display dates and times in the user's local timezone. - Update database path: This simplifies the database setup. - Improve proposal vote handling: Addresses issues with vote submission and timestamping. - Add client-side pagination and filtering to proposal details: Improves user experience for viewing large vote lists.
This commit is contained in:
parent
97e7a04827
commit
d12a082ca1
@ -136,7 +136,7 @@ impl GovernanceController {
|
||||
pub async fn proposals(
|
||||
query: web::Query<ProposalQuery>,
|
||||
tmpl: web::Data<Tera>,
|
||||
session: Session
|
||||
session: Session,
|
||||
) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "governance");
|
||||
@ -176,8 +176,8 @@ impl GovernanceController {
|
||||
proposals = proposals
|
||||
.into_iter()
|
||||
.filter(|p| {
|
||||
p.title.to_lowercase().contains(&search_term) ||
|
||||
p.description.to_lowercase().contains(&search_term)
|
||||
p.title.to_lowercase().contains(&search_term)
|
||||
|| p.description.to_lowercase().contains(&search_term)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
@ -400,10 +400,13 @@ impl GovernanceController {
|
||||
1, // Default to 1 share
|
||||
form.comment.as_ref().map(|s| s.to_string()), // Pass the comment from the form
|
||||
) {
|
||||
Ok(updated_proposal) => {
|
||||
Ok(_) => {
|
||||
// Redirect to the proposal detail page with a success message
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("/governance/proposals/{}?vote_success=true", proposal_id)))
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("/governance/proposals/{}?vote_success=true", proposal_id),
|
||||
))
|
||||
.finish());
|
||||
}
|
||||
Err(e) => {
|
||||
@ -602,19 +605,31 @@ impl GovernanceController {
|
||||
}
|
||||
};
|
||||
|
||||
// Create a Vote from the ballot
|
||||
let vote = Vote::new(
|
||||
proposal.base_data.id.to_string(),
|
||||
voter_id,
|
||||
format!("User {}", voter_id),
|
||||
vote_type,
|
||||
ballot.comment.clone(), // Use the comment from the ballot
|
||||
let ballot_timestamp =
|
||||
match chrono::DateTime::from_timestamp(ballot.base_data.created_at, 0) {
|
||||
Some(dt) => dt,
|
||||
None => {
|
||||
println!(
|
||||
"Warning: Invalid timestamp {} for ballot, using current time",
|
||||
ballot.base_data.created_at
|
||||
);
|
||||
Utc::now()
|
||||
}
|
||||
};
|
||||
|
||||
let vote = Vote {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
proposal_id: proposal.base_data.id.to_string(),
|
||||
voter_id,
|
||||
voter_name: format!("User {}", voter_id),
|
||||
vote_type,
|
||||
comment: ballot.comment.clone(),
|
||||
created_at: ballot_timestamp, // This is already local time
|
||||
updated_at: ballot_timestamp, // Same as created_at for votes
|
||||
};
|
||||
|
||||
votes.push(vote);
|
||||
}
|
||||
|
||||
println!("Extracted {} votes from proposal", votes.len());
|
||||
votes
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ use heromodels::{
|
||||
};
|
||||
|
||||
/// The path to the database file. Change this as needed for your environment.
|
||||
pub const DB_PATH: &str = "/tmp/ourdb_governance6";
|
||||
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> {
|
||||
@ -188,6 +188,15 @@ pub fn submit_vote_on_proposal(
|
||||
// Set the comment if provided
|
||||
ballot.comment = comment;
|
||||
|
||||
// Store the local time (EEST = UTC+3) as the vote timestamp
|
||||
// This ensures the displayed time matches the user's local time
|
||||
let utc_now = Utc::now();
|
||||
let local_offset = chrono::FixedOffset::east_opt(3 * 3600).unwrap(); // +3 hours for EEST
|
||||
let local_time = utc_now.with_timezone(&local_offset);
|
||||
|
||||
// Store the local time as a timestamp (this is what will be displayed)
|
||||
ballot.base_data.created_at = local_time.timestamp();
|
||||
|
||||
// Add the ballot to the proposal's ballots
|
||||
proposal.ballots.push(ballot);
|
||||
|
||||
|
@ -30,6 +30,7 @@ impl std::error::Error for TemplateError {}
|
||||
pub fn register_tera_functions(tera: &mut tera::Tera) {
|
||||
tera.register_function("now", NowFunction);
|
||||
tera.register_function("format_date", FormatDateFunction);
|
||||
tera.register_function("local_time", LocalTimeFunction);
|
||||
}
|
||||
|
||||
/// Tera function to get the current date/time
|
||||
@ -93,6 +94,52 @@ impl Function for FormatDateFunction {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tera function to convert UTC datetime to local time
|
||||
#[derive(Clone)]
|
||||
pub struct LocalTimeFunction;
|
||||
|
||||
impl Function for LocalTimeFunction {
|
||||
fn call(&self, args: &std::collections::HashMap<String, Value>) -> tera::Result<Value> {
|
||||
let datetime_value = match args.get("datetime") {
|
||||
Some(val) => val,
|
||||
None => return Err(tera::Error::msg("The 'datetime' argument is required")),
|
||||
};
|
||||
|
||||
let format = match args.get("format") {
|
||||
Some(val) => match val.as_str() {
|
||||
Some(s) => s,
|
||||
None => "%Y-%m-%d %H:%M",
|
||||
},
|
||||
None => "%Y-%m-%d %H:%M",
|
||||
};
|
||||
|
||||
// The datetime comes from Rust as a serialized DateTime<Utc>
|
||||
// We need to handle it properly
|
||||
let utc_datetime = if let Some(dt_str) = datetime_value.as_str() {
|
||||
// Try to parse as RFC3339 first
|
||||
match DateTime::parse_from_rfc3339(dt_str) {
|
||||
Ok(dt) => dt.with_timezone(&Utc),
|
||||
Err(_) => {
|
||||
// Try to parse as our standard format
|
||||
match DateTime::parse_from_str(dt_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
|
||||
Ok(dt) => dt.with_timezone(&Utc),
|
||||
Err(_) => return Err(tera::Error::msg("Invalid datetime string format")),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(tera::Error::msg("Datetime must be a string"));
|
||||
};
|
||||
|
||||
// Convert UTC to local time (EEST = UTC+3)
|
||||
// In a real application, you'd want to get the user's timezone from their profile
|
||||
let local_offset = chrono::FixedOffset::east_opt(3 * 3600).unwrap(); // +3 hours for EEST
|
||||
let local_datetime = utc_datetime.with_timezone(&local_offset);
|
||||
|
||||
Ok(Value::String(local_datetime.format(format).to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a date for display
|
||||
#[allow(dead_code)]
|
||||
pub fn format_date(date: &DateTime<Utc>, format: &str) -> String {
|
||||
|
@ -362,7 +362,8 @@
|
||||
</nav>
|
||||
</div>
|
||||
<div class="text-muted small" id="paginationInfo">
|
||||
Showing <span id="startRow">1</span>-<span id="endRow">10</span> of <span id="totalRows">{{ votes | length }}</span>
|
||||
Showing <span id="startRow">1</span>-<span id="endRow">10</span> of <span
|
||||
id="totalRows">{{ votes | length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -383,9 +384,9 @@
|
||||
// Auto-hide the success alert after 5 seconds
|
||||
const successAlert = document.querySelector('.alert-success');
|
||||
if (successAlert) {
|
||||
setTimeout(function() {
|
||||
setTimeout(function () {
|
||||
successAlert.classList.remove('show');
|
||||
setTimeout(function() {
|
||||
setTimeout(function () {
|
||||
successAlert.remove();
|
||||
}, 500);
|
||||
}, 5000);
|
||||
@ -399,7 +400,7 @@
|
||||
|
||||
// Filter votes by type
|
||||
filterButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
button.addEventListener('click', function () {
|
||||
// Update active button
|
||||
filterButtons.forEach(btn => btn.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
@ -412,7 +413,7 @@
|
||||
|
||||
// Search functionality
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
searchInput.addEventListener('input', function () {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
|
||||
voteRows.forEach(row => {
|
||||
@ -505,7 +506,7 @@
|
||||
a.className = 'page-link';
|
||||
a.href = '#';
|
||||
a.textContent = i;
|
||||
a.addEventListener('click', function(e) {
|
||||
a.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
currentPage = i;
|
||||
updatePagination();
|
||||
@ -576,7 +577,7 @@
|
||||
|
||||
// Event listeners for pagination
|
||||
if (prevPageBtn) {
|
||||
prevPageBtn.addEventListener('click', function(e) {
|
||||
prevPageBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
@ -586,7 +587,7 @@
|
||||
}
|
||||
|
||||
if (nextPageBtn) {
|
||||
nextPageBtn.addEventListener('click', function(e) {
|
||||
nextPageBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
// Get all rows that match the current filter
|
||||
const currentFilter = document.querySelector('[data-filter].active');
|
||||
@ -619,7 +620,7 @@
|
||||
}
|
||||
|
||||
if (rowsPerPageSelect) {
|
||||
rowsPerPageSelect.addEventListener('change', function() {
|
||||
rowsPerPageSelect.addEventListener('change', function () {
|
||||
rowsPerPage = parseInt(this.value);
|
||||
currentPage = 1; // Reset to first page
|
||||
updatePagination();
|
||||
|
Loading…
Reference in New Issue
Block a user