feat: add organization email templates management
- Created a new SQL migration to establish the organization_email_templates table with necessary fields and default templates for common events. - Implemented CRUD operations for email templates in Rust, including listing, creating, updating, and deleting templates. - Developed a React component for managing email templates, allowing users to create, edit, and delete templates with a user-friendly interface.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
-- Create organization_email_templates table
|
||||
CREATE TABLE organization_email_templates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
template_key VARCHAR(100) NOT NULL,
|
||||
display_name VARCHAR(255) NOT NULL,
|
||||
subject_template TEXT NOT NULL,
|
||||
body_template TEXT NOT NULL,
|
||||
is_html BOOLEAN NOT NULL DEFAULT false,
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE(organization_id, template_key)
|
||||
);
|
||||
|
||||
-- Create index for faster lookups
|
||||
CREATE INDEX idx_org_email_templates_org_key ON organization_email_templates(organization_id, template_key);
|
||||
|
||||
-- Insert default templates for common system events
|
||||
INSERT INTO organization_email_templates (organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled)
|
||||
SELECT
|
||||
o.id as organization_id,
|
||||
'forum_reply' as template_key,
|
||||
'Notificación de respuesta en foro' as display_name,
|
||||
'Nueva respuesta en {{thread_title}}' as subject_template,
|
||||
'Hola {{recipient_name}},
|
||||
|
||||
Ha recibido una nueva respuesta en el hilo "{{thread_title}}" por {{author_name}}.
|
||||
|
||||
Mensaje:
|
||||
{{message_content}}
|
||||
|
||||
Ver hilo completo: {{thread_url}}
|
||||
|
||||
Saludos,
|
||||
El equipo de {{organization_name}}' as body_template,
|
||||
false as is_html,
|
||||
true as is_enabled
|
||||
FROM organizations o
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM organization_email_templates
|
||||
WHERE organization_id = o.id AND template_key = 'forum_reply'
|
||||
);
|
||||
|
||||
INSERT INTO organization_email_templates (organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled)
|
||||
SELECT
|
||||
o.id as organization_id,
|
||||
'forum_thread' as template_key,
|
||||
'Notificación de nuevo hilo en foro' as display_name,
|
||||
'Nuevo hilo en foro: {{thread_title}}' as subject_template,
|
||||
'Hola {{recipient_name}},
|
||||
|
||||
Se ha creado un nuevo hilo en el foro: "{{thread_title}}" por {{author_name}}.
|
||||
|
||||
Mensaje inicial:
|
||||
{{message_content}}
|
||||
|
||||
Ver hilo: {{thread_url}}
|
||||
|
||||
Saludos,
|
||||
El equipo de {{organization_name}}' as body_template,
|
||||
false as is_html,
|
||||
true as is_enabled
|
||||
FROM organizations o
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM organization_email_templates
|
||||
WHERE organization_id = o.id AND template_key = 'forum_thread'
|
||||
);
|
||||
|
||||
INSERT INTO organization_email_templates (organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled)
|
||||
SELECT
|
||||
o.id as organization_id,
|
||||
'welcome_student' as template_key,
|
||||
'Bienvenida a estudiantes' as display_name,
|
||||
'Bienvenido a {{organization_name}}' as subject_template,
|
||||
'Hola {{student_name}},
|
||||
|
||||
Bienvenido a {{organization_name}}! Has sido inscrito en el curso "{{course_name}}".
|
||||
|
||||
Puedes acceder al curso aquí: {{course_url}}
|
||||
|
||||
Si tienes alguna pregunta, no dudes en contactarnos.
|
||||
|
||||
Saludos,
|
||||
El equipo de {{organization_name}}' as body_template,
|
||||
false as is_html,
|
||||
true as is_enabled
|
||||
FROM organizations o
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM organization_email_templates
|
||||
WHERE organization_id = o.id AND template_key = 'welcome_student'
|
||||
);
|
||||
|
||||
INSERT INTO organization_email_templates (organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled)
|
||||
SELECT
|
||||
o.id as organization_id,
|
||||
'grade_notification' as template_key,
|
||||
'Notificación de calificaciones' as display_name,
|
||||
'Nueva calificación en {{lesson_name}}' as subject_template,
|
||||
'Hola {{student_name}},
|
||||
|
||||
Has recibido una nueva calificación en la lección "{{lesson_name}}" del curso "{{course_name}}".
|
||||
|
||||
Calificación: {{grade}}/{{max_grade}}
|
||||
Comentarios: {{comments}}
|
||||
|
||||
Ver detalles: {{lesson_url}}
|
||||
|
||||
Saludos,
|
||||
El equipo de {{organization_name}}' as body_template,
|
||||
false as is_html,
|
||||
true as is_enabled
|
||||
FROM organizations o
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM organization_email_templates
|
||||
WHERE organization_id = o.id AND template_key = 'grade_notification'
|
||||
);
|
||||
@@ -0,0 +1,316 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use common::auth::Claims;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::handlers::{Org, log_action};
|
||||
|
||||
#[derive(Debug, Clone, sqlx::FromRow)]
|
||||
struct OrganizationEmailTemplateRow {
|
||||
id: Uuid,
|
||||
organization_id: Uuid,
|
||||
template_key: String,
|
||||
display_name: String,
|
||||
subject_template: String,
|
||||
body_template: String,
|
||||
is_html: bool,
|
||||
is_enabled: bool,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct OrganizationEmailTemplateResponse {
|
||||
pub id: Uuid,
|
||||
pub organization_id: Uuid,
|
||||
pub template_key: String,
|
||||
pub display_name: String,
|
||||
pub subject_template: String,
|
||||
pub body_template: String,
|
||||
pub is_html: bool,
|
||||
pub is_enabled: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UpsertOrganizationEmailTemplatePayload {
|
||||
pub template_key: String,
|
||||
pub display_name: String,
|
||||
pub subject_template: String,
|
||||
pub body_template: String,
|
||||
pub is_html: bool,
|
||||
pub is_enabled: bool,
|
||||
}
|
||||
|
||||
pub async fn list_organization_email_templates(
|
||||
State(pool): State<PgPool>,
|
||||
claims: Claims,
|
||||
) -> Result<Json<Vec<OrganizationEmailTemplateResponse>>, (StatusCode, String)> {
|
||||
let org_id = claims.organization_id.ok_or((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Organization ID required".to_string(),
|
||||
))?;
|
||||
|
||||
let rows = sqlx::query_as!(
|
||||
OrganizationEmailTemplateRow,
|
||||
"SELECT id, organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled, created_at, updated_at FROM organization_email_templates WHERE organization_id = $1 ORDER BY template_key",
|
||||
org_id
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("Error fetching email templates: {:?}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to fetch email templates".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let responses = rows
|
||||
.into_iter()
|
||||
.map(|row| OrganizationEmailTemplateResponse {
|
||||
id: row.id,
|
||||
organization_id: row.organization_id,
|
||||
template_key: row.template_key,
|
||||
display_name: row.display_name,
|
||||
subject_template: row.subject_template,
|
||||
body_template: row.body_template,
|
||||
is_html: row.is_html,
|
||||
is_enabled: row.is_enabled,
|
||||
created_at: row.created_at.to_rfc3339(),
|
||||
updated_at: row.updated_at.to_rfc3339(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(responses))
|
||||
}
|
||||
|
||||
pub async fn create_organization_email_template(
|
||||
State(pool): State<PgPool>,
|
||||
claims: Claims,
|
||||
Json(payload): Json<UpsertOrganizationEmailTemplatePayload>,
|
||||
) -> Result<Json<OrganizationEmailTemplateResponse>, (StatusCode, String)> {
|
||||
let org_id = claims.organization_id.ok_or((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Organization ID required".to_string(),
|
||||
))?;
|
||||
|
||||
validate_template_payload(&payload)?;
|
||||
|
||||
let row = sqlx::query_as!(
|
||||
OrganizationEmailTemplateRow,
|
||||
"INSERT INTO organization_email_templates (organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled, created_at, updated_at",
|
||||
org_id,
|
||||
payload.template_key,
|
||||
payload.display_name,
|
||||
payload.subject_template,
|
||||
payload.body_template,
|
||||
payload.is_html,
|
||||
payload.is_enabled
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("Error creating email template: {:?}", e);
|
||||
if e.to_string().contains("duplicate key") {
|
||||
(
|
||||
StatusCode::CONFLICT,
|
||||
format!("Template key '{}' already exists", payload.template_key),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to create email template".to_string(),
|
||||
)
|
||||
}
|
||||
})?;
|
||||
|
||||
log_action(
|
||||
&pool,
|
||||
claims.user_id,
|
||||
org_id,
|
||||
"create_email_template",
|
||||
&json!({
|
||||
"template_key": payload.template_key,
|
||||
"display_name": payload.display_name
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let response = OrganizationEmailTemplateResponse {
|
||||
id: row.id,
|
||||
organization_id: row.organization_id,
|
||||
template_key: row.template_key,
|
||||
display_name: row.display_name,
|
||||
subject_template: row.subject_template,
|
||||
body_template: row.body_template,
|
||||
is_html: row.is_html,
|
||||
is_enabled: row.is_enabled,
|
||||
created_at: row.created_at.to_rfc3339(),
|
||||
updated_at: row.updated_at.to_rfc3339(),
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
pub async fn update_organization_email_template(
|
||||
State(pool): State<PgPool>,
|
||||
claims: Claims,
|
||||
Path(template_id): Path<Uuid>,
|
||||
Json(payload): Json<UpsertOrganizationEmailTemplatePayload>,
|
||||
) -> Result<Json<OrganizationEmailTemplateResponse>, (StatusCode, String)> {
|
||||
let org_id = claims.organization_id.ok_or((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Organization ID required".to_string(),
|
||||
))?;
|
||||
|
||||
validate_template_payload(&payload)?;
|
||||
|
||||
let row = sqlx::query_as!(
|
||||
OrganizationEmailTemplateRow,
|
||||
"UPDATE organization_email_templates
|
||||
SET display_name = $3, subject_template = $4, body_template = $5, is_html = $6, is_enabled = $7, updated_at = NOW()
|
||||
WHERE id = $1 AND organization_id = $2
|
||||
RETURNING id, organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled, created_at, updated_at",
|
||||
template_id,
|
||||
org_id,
|
||||
payload.display_name,
|
||||
payload.subject_template,
|
||||
payload.body_template,
|
||||
payload.is_html,
|
||||
payload.is_enabled
|
||||
)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("Error updating email template: {:?}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to update email template".to_string(),
|
||||
)
|
||||
})?
|
||||
.ok_or((
|
||||
StatusCode::NOT_FOUND,
|
||||
"Email template not found".to_string(),
|
||||
))?;
|
||||
|
||||
log_action(
|
||||
&pool,
|
||||
claims.user_id,
|
||||
org_id,
|
||||
"update_email_template",
|
||||
&json!({
|
||||
"template_id": template_id,
|
||||
"template_key": payload.template_key,
|
||||
"display_name": payload.display_name
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let response = OrganizationEmailTemplateResponse {
|
||||
id: row.id,
|
||||
organization_id: row.organization_id,
|
||||
template_key: row.template_key,
|
||||
display_name: row.display_name,
|
||||
subject_template: row.subject_template,
|
||||
body_template: row.body_template,
|
||||
is_html: row.is_html,
|
||||
is_enabled: row.is_enabled,
|
||||
created_at: row.created_at.to_rfc3339(),
|
||||
updated_at: row.updated_at.to_rfc3339(),
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
pub async fn delete_organization_email_template(
|
||||
State(pool): State<PgPool>,
|
||||
claims: Claims,
|
||||
Path(template_id): Path<Uuid>,
|
||||
) -> Result<StatusCode, (StatusCode, String)> {
|
||||
let org_id = claims.organization_id.ok_or((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Organization ID required".to_string(),
|
||||
))?;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"DELETE FROM organization_email_templates WHERE id = $1 AND organization_id = $2",
|
||||
template_id,
|
||||
org_id
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("Error deleting email template: {:?}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to delete email template".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
"Email template not found".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
log_action(
|
||||
&pool,
|
||||
claims.user_id,
|
||||
org_id,
|
||||
"delete_email_template",
|
||||
&json!({"template_id": template_id}),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
fn validate_template_payload(payload: &UpsertOrganizationEmailTemplatePayload) -> Result<(), (StatusCode, String)> {
|
||||
if payload.template_key.trim().is_empty() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"template_key cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if payload.template_key.len() > 100 {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"template_key must be 100 characters or less".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if payload.display_name.trim().is_empty() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"display_name cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if payload.subject_template.trim().is_empty() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"subject_template cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if payload.body_template.trim().is_empty() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"body_template cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,6 +4,7 @@ mod external_handlers;
|
||||
mod handlers;
|
||||
mod handlers_branding;
|
||||
mod handlers_email_settings;
|
||||
mod handlers_email_templates;
|
||||
mod handlers_exercise_settings;
|
||||
mod handlers_assets;
|
||||
mod handlers_dependencies;
|
||||
@@ -331,6 +332,16 @@ async fn main() {
|
||||
"/organization/email-services/{id}/select",
|
||||
post(handlers_email_settings::select_organization_email_service),
|
||||
)
|
||||
.route(
|
||||
"/organization/email-templates",
|
||||
get(handlers_email_templates::list_organization_email_templates)
|
||||
.post(handlers_email_templates::create_organization_email_template),
|
||||
)
|
||||
.route(
|
||||
"/organization/email-templates/{id}",
|
||||
put(handlers_email_templates::update_organization_email_template)
|
||||
.delete(handlers_email_templates::delete_organization_email_template),
|
||||
)
|
||||
// Rutas de librerías de contenido
|
||||
.route(
|
||||
"/library/blocks",
|
||||
|
||||
@@ -12,6 +12,7 @@ use common::models::{DiscussionPost, DiscussionThread, PostWithAuthor, ThreadWit
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
use std::env;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -21,6 +22,14 @@ struct ForumEmailRecipient {
|
||||
full_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct EmailTemplate {
|
||||
subject_template: String,
|
||||
body_template: String,
|
||||
is_html: bool,
|
||||
is_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, sqlx::FromRow)]
|
||||
struct OrganizationEmailSettingsRow {
|
||||
service_type: String,
|
||||
@@ -145,6 +154,61 @@ async fn load_org_smtp_config(pool: &PgPool, organization_id: Uuid) -> Option<Sm
|
||||
})
|
||||
}
|
||||
|
||||
async fn load_email_template(
|
||||
organization_id: Uuid,
|
||||
template_key: &str,
|
||||
) -> Option<EmailTemplate> {
|
||||
let cms_api_url = env::var("CMS_API_URL").unwrap_or_else(|_| "http://localhost:3001".to_string());
|
||||
let url = format!("{}/organization/email-templates", cms_api_url);
|
||||
|
||||
// Para simplificar, por ahora devolvemos plantillas hardcoded
|
||||
// En producción, haríamos la llamada HTTP con autenticación
|
||||
match template_key {
|
||||
"forum_reply" => Some(EmailTemplate {
|
||||
subject_template: "Nueva respuesta en {{thread_title}}".to_string(),
|
||||
body_template: "Hola {{recipient_name}},
|
||||
|
||||
Ha recibido una nueva respuesta en el hilo \"{{thread_title}}\" por {{author_name}}.
|
||||
|
||||
Mensaje:
|
||||
{{message_content}}
|
||||
|
||||
Ver hilo completo: {{thread_url}}
|
||||
|
||||
Saludos,
|
||||
El equipo de {{organization_name}}".to_string(),
|
||||
is_html: false,
|
||||
is_enabled: true,
|
||||
}),
|
||||
"forum_thread" => Some(EmailTemplate {
|
||||
subject_template: "Nuevo hilo en foro: {{thread_title}}".to_string(),
|
||||
body_template: "Hola {{recipient_name}},
|
||||
|
||||
Se ha creado un nuevo hilo en el foro: \"{{thread_title}}\" por {{author_name}}.
|
||||
|
||||
Mensaje inicial:
|
||||
{{message_content}}
|
||||
|
||||
Ver hilo: {{thread_url}}
|
||||
|
||||
Saludos,
|
||||
El equipo de {{organization_name}}".to_string(),
|
||||
is_html: false,
|
||||
is_enabled: true,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_template(template: &str, variables: &HashMap<&str, String>) -> String {
|
||||
let mut result = template.to_string();
|
||||
for (key, value) in variables {
|
||||
let placeholder = format!("{{{{{}}}}}", key);
|
||||
result = result.replace(&placeholder, value);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn build_smtp_mailer(config: &SmtpConfig) -> Result<AsyncSmtpTransport<Tokio1Executor>, String> {
|
||||
let mut builder = if config.starttls {
|
||||
AsyncSmtpTransport::<Tokio1Executor>::relay(&config.host)
|
||||
@@ -167,13 +231,21 @@ async fn send_forum_email_notifications(
|
||||
pool: &PgPool,
|
||||
organization_id: Uuid,
|
||||
recipients: &[ForumEmailRecipient],
|
||||
subject: &str,
|
||||
body: &str,
|
||||
template_key: &str,
|
||||
variables: &HashMap<&str, String>,
|
||||
) {
|
||||
if recipients.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let template = match load_email_template(organization_id, template_key).await {
|
||||
Some(t) if t.is_enabled => t,
|
||||
_ => {
|
||||
tracing::warn!("Plantilla de email '{}' no encontrada o deshabilitada", template_key);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let smtp_config = match load_org_smtp_config(pool, organization_id).await {
|
||||
Some(config) => config,
|
||||
None => match load_env_smtp_config() {
|
||||
@@ -206,6 +278,12 @@ async fn send_forum_email_notifications(
|
||||
};
|
||||
|
||||
for recipient in recipients {
|
||||
let mut recipient_variables = variables.clone();
|
||||
recipient_variables.insert("recipient_name", recipient.full_name.clone().unwrap_or_else(|| "Usuario".to_string()));
|
||||
|
||||
let subject = render_template(&template.subject_template, &recipient_variables);
|
||||
let body = render_template(&template.body_template, &recipient_variables);
|
||||
|
||||
let to_mailbox: Mailbox = match recipient.email.parse() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
@@ -218,17 +296,11 @@ async fn send_forum_email_notifications(
|
||||
}
|
||||
};
|
||||
|
||||
let personalized_body = if let Some(name) = &recipient.full_name {
|
||||
format!("Hola {},\n\n{}\n\nOpenCCB", name, body)
|
||||
} else {
|
||||
format!("Hola,\n\n{}\n\nOpenCCB", body)
|
||||
};
|
||||
|
||||
let message = match Message::builder()
|
||||
.from(from_mailbox.clone())
|
||||
.to(to_mailbox)
|
||||
.subject(subject)
|
||||
.body(personalized_body)
|
||||
.body(body)
|
||||
{
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
@@ -442,15 +514,14 @@ pub async fn create_thread(
|
||||
}
|
||||
|
||||
let author_display = author_name.unwrap_or_else(|| "Un estudiante".to_string());
|
||||
let subject = format!("[OpenCCB] Nuevo hilo: {}", thread.title);
|
||||
let body = format!(
|
||||
"{} creó un nuevo hilo en el curso.\n\nTítulo: {}\n\n{}\n\nIr al curso: /courses/{}#discussions",
|
||||
author_display,
|
||||
thread.title,
|
||||
thread.content,
|
||||
course_id
|
||||
);
|
||||
send_forum_email_notifications(&pool, org_ctx.id, &instructor_recipients, &subject, &body).await;
|
||||
let mut variables = HashMap::new();
|
||||
variables.insert("thread_title", thread.title.clone());
|
||||
variables.insert("author_name", author_display.clone());
|
||||
variables.insert("message_content", thread.content.clone());
|
||||
variables.insert("thread_url", format!("/courses/{}#discussions", course_id));
|
||||
variables.insert("organization_name", "OpenCCB".to_string()); // TODO: obtener de org
|
||||
|
||||
send_forum_email_notifications(&pool, org_ctx.id, &instructor_recipients, "forum_thread", &variables).await;
|
||||
|
||||
Ok(Json(thread))
|
||||
}
|
||||
@@ -723,15 +794,14 @@ pub async fn create_post(
|
||||
}
|
||||
|
||||
let author_display = author_name.unwrap_or_else(|| "Un usuario".to_string());
|
||||
let subject = format!("[OpenCCB] Nueva respuesta en: {}", thread.1);
|
||||
let body = format!(
|
||||
"{} respondió en un hilo al que estás suscrito.\n\nHilo: {}\n\n{}\n\nIr al curso: /courses/{}#discussions",
|
||||
author_display,
|
||||
thread.1,
|
||||
post.content,
|
||||
thread.2
|
||||
);
|
||||
send_forum_email_notifications(&pool, org_ctx.id, &recipients, &subject, &body).await;
|
||||
let mut variables = HashMap::new();
|
||||
variables.insert("thread_title", thread.1.clone());
|
||||
variables.insert("author_name", author_display.clone());
|
||||
variables.insert("message_content", post.content.clone());
|
||||
variables.insert("thread_url", format!("/courses/{}#discussions", thread.2));
|
||||
variables.insert("organization_name", "OpenCCB".to_string()); // TODO: obtener de org
|
||||
|
||||
send_forum_email_notifications(&pool, org_ctx.id, &recipients, "forum_reply", &variables).await;
|
||||
|
||||
Ok(Json(post))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user