use actix_web::{Error, HttpResponse}; use chrono::{DateTime, Utc}; use std::error::Error as StdError; use tera::{self, Context, Function, Tera, Value}; // Export modules pub mod redis_service; // Re-export for easier imports // pub use redis_service::RedisCalendarService; // Currently unused /// Error type for template rendering #[derive(Debug)] #[allow(dead_code)] pub struct TemplateError { pub message: String, pub details: String, pub location: String, } impl std::fmt::Display for TemplateError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Template error in {}: {}", self.location, self.message) } } impl std::error::Error for TemplateError {} /// Registers custom Tera functions and filters 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); // Register custom filters tera.register_filter("format_hour", format_hour_filter); tera.register_filter("extract_hour", extract_hour_filter); tera.register_filter("format_time", format_time_filter); } /// Tera function to get the current date/time #[derive(Clone)] pub struct NowFunction; impl Function for NowFunction { fn call(&self, args: &std::collections::HashMap) -> tera::Result { let format = match args.get("format") { Some(val) => match val.as_str() { Some(s) => s, None => "%Y-%m-%d %H:%M:%S", }, None => "%Y-%m-%d %H:%M:%S", }; let now = Utc::now(); // Special case for just getting the year if args.get("year").and_then(|v| v.as_bool()).unwrap_or(false) { return Ok(Value::String(now.format("%Y").to_string())); } Ok(Value::String(now.format(format).to_string())) } } /// Tera function to format a date #[derive(Clone)] pub struct FormatDateFunction; impl Function for FormatDateFunction { fn call(&self, args: &std::collections::HashMap) -> tera::Result { let timestamp = match args.get("timestamp") { Some(val) => match val.as_i64() { Some(ts) => ts, None => { return Err(tera::Error::msg( "The 'timestamp' argument must be a valid timestamp", )); } }, None => return Err(tera::Error::msg("The 'timestamp' argument is required")), }; let format = match args.get("format") { Some(val) => match val.as_str() { Some(s) => s, None => "%Y-%m-%d %H:%M:%S", }, None => "%Y-%m-%d %H:%M:%S", }; // Convert timestamp to DateTime using the non-deprecated method let datetime = match DateTime::from_timestamp(timestamp, 0) { Some(dt) => dt, None => return Err(tera::Error::msg("Failed to convert timestamp to datetime")), }; Ok(Value::String(datetime.format(format).to_string())) } } /// 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) -> tera::Result { 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 // 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())) } } /// Tera filter to format hour with zero padding pub fn format_hour_filter( value: &Value, _args: &std::collections::HashMap, ) -> tera::Result { match value.as_i64() { Some(hour) => Ok(Value::String(format!("{:02}", hour))), None => Err(tera::Error::msg("Value must be a number")), } } /// Tera filter to extract hour from datetime string pub fn extract_hour_filter( value: &Value, _args: &std::collections::HashMap, ) -> tera::Result { match value.as_str() { Some(datetime_str) => { // Try to parse as RFC3339 first if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_str) { Ok(Value::String(dt.format("%H").to_string())) } else { // Try to parse as our standard format match DateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S%.f UTC") { Ok(dt) => Ok(Value::String(dt.format("%H").to_string())), Err(_) => Err(tera::Error::msg("Invalid datetime string format")), } } } None => Err(tera::Error::msg("Value must be a string")), } } /// Tera filter to format time from datetime string pub fn format_time_filter( value: &Value, args: &std::collections::HashMap, ) -> tera::Result { let format = match args.get("format") { Some(val) => match val.as_str() { Some(s) => s, None => "%H:%M", }, None => "%H:%M", }; match value.as_str() { Some(datetime_str) => { // Try to parse as RFC3339 first if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_str) { Ok(Value::String(dt.format(format).to_string())) } else { // Try to parse as our standard format match DateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S%.f UTC") { Ok(dt) => Ok(Value::String(dt.format(format).to_string())), Err(_) => Err(tera::Error::msg("Invalid datetime string format")), } } } None => Err(tera::Error::msg("Value must be a string")), } } /// Formats a date for display #[allow(dead_code)] pub fn format_date(date: &DateTime, format: &str) -> String { date.format(format).to_string() } /// Truncates a string to a maximum length and adds an ellipsis if truncated #[allow(dead_code)] pub fn truncate_string(s: &str, max_length: usize) -> String { if s.len() <= max_length { s.to_string() } else { format!("{}...", &s[0..max_length]) } } /// Renders a template with error handling /// /// This function attempts to render a template and handles any errors by rendering /// the error template with detailed error information. pub fn render_template( tmpl: &Tera, template_name: &str, ctx: &Context, ) -> Result { println!("DEBUG: Attempting to render template: {}", template_name); // Print all context keys for debugging let mut keys = Vec::new(); for (key, _) in ctx.clone().into_json().as_object().unwrap().iter() { keys.push(key.clone()); } println!("DEBUG: Context keys: {:?}", keys); match tmpl.render(template_name, ctx) { Ok(content) => { println!("DEBUG: Successfully rendered template: {}", template_name); Ok(HttpResponse::Ok().content_type("text/html").body(content)) } Err(e) => { // Log the error with more details println!( "DEBUG: Template rendering error for {}: {}", template_name, e ); println!("DEBUG: Error details: {:?}", e); // Print the error cause chain for better debugging let mut current_error: Option<&dyn StdError> = Some(&e); let mut error_chain = Vec::new(); while let Some(error) = current_error { error_chain.push(format!("{}", error)); current_error = error.source(); } println!("DEBUG: Error chain: {:?}", error_chain); // Log the error log::error!("Template rendering error: {}", e); // Create a simple error response with more detailed information let error_html = format!( r#" Template Error

Template Rendering Error

There was an error rendering the template: {}

Error Details:

{}

Error Chain:

{}
"#, template_name, e, error_chain.join("\n") ); println!("DEBUG: Returning simple error page"); Ok(HttpResponse::InternalServerError() .content_type("text/html") .body(error_html)) } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_truncate_string() { assert_eq!(truncate_string("Hello", 10), "Hello"); assert_eq!(truncate_string("Hello, world!", 5), "Hello..."); assert_eq!(truncate_string("", 5), ""); } }