- Replaced custom `CalendarEvent` model with `heromodels`' `Event` model. - Updated database interactions and controller logic to use the new model. - Removed unnecessary `CalendarEvent` model and related code. - Updated views to reflect changes in event data structure.
324 lines
11 KiB
Rust
324 lines
11 KiB
Rust
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<String, Value>) -> tera::Result<Value> {
|
|
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<String, Value>) -> tera::Result<Value> {
|
|
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<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()))
|
|
}
|
|
}
|
|
|
|
/// Tera filter to format hour with zero padding
|
|
pub fn format_hour_filter(
|
|
value: &Value,
|
|
_args: &std::collections::HashMap<String, Value>,
|
|
) -> tera::Result<Value> {
|
|
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<String, Value>,
|
|
) -> tera::Result<Value> {
|
|
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<String, Value>,
|
|
) -> tera::Result<Value> {
|
|
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<Utc>, 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<HttpResponse, Error> {
|
|
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#"<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Template Error</title>
|
|
<style>
|
|
body {{ font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }}
|
|
.error-container {{ border: 1px solid #f5c6cb; background-color: #f8d7da; padding: 20px; border-radius: 5px; }}
|
|
.error-title {{ color: #721c24; }}
|
|
.error-details {{ background-color: #f8f9fa; padding: 15px; border-radius: 5px; margin-top: 20px; }}
|
|
pre {{ background-color: #f1f1f1; padding: 10px; overflow: auto; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="error-container">
|
|
<h1 class="error-title">Template Rendering Error</h1>
|
|
<p>There was an error rendering the template: <strong>{}</strong></p>
|
|
<div class="error-details">
|
|
<h3>Error Details:</h3>
|
|
<pre>{}</pre>
|
|
<h3>Error Chain:</h3>
|
|
<pre>{}</pre>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>"#,
|
|
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), "");
|
|
}
|
|
}
|