Skip to content

Commit

Permalink
email templating and offer CRUD
Browse files Browse the repository at this point in the history
  • Loading branch information
KavikaPalletenne committed Nov 26, 2024
1 parent 2ec7f6b commit b38f2fa
Show file tree
Hide file tree
Showing 15 changed files with 413 additions and 18 deletions.
12 changes: 12 additions & 0 deletions backend/migrations/20241124054711_email_templates.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE email_templates (
id BIGINT PRIMARY KEY,
organisation_id BIGINT NOT NULL,
name TEXT NOT NULL,
template TEXT NOT NULL,
CONSTRAINT FK_email_templates_organisations
FOREIGN KEY(organisation_id)
REFERENCES organisations(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
UNIQUE (organisation_id, name)
);
32 changes: 32 additions & 0 deletions backend/migrations/20241126113027_offers.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
CREATE TYPE offer_status AS ENUM ('Draft', 'Sent', 'Accepted', 'Declined');

CREATE TABLE offers (
id BIGINT PRIMARY KEY,
campaign_id BIGINT NOT NULL,
application_id BIGINT NOT NULL,
email_template_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
expiry TIMESTAMPTZ NOT NULL,
status offer_status NOT NULL DEFAULT 'Draft',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT FK_offers_campaigns
FOREIGN KEY(campaign_id)
REFERENCES campaigns(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT FK_offers_applications
FOREIGN KEY(application_id)
REFERENCES applications(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT FK_offers_email_templates
FOREIGN KEY(email_template_id)
REFERENCES email_templates(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT FK_offers_roles
FOREIGN KEY(role_id)
REFERENCES campaign_roles(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
2 changes: 1 addition & 1 deletion backend/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ rust-s3 = "0.34.0"
rs-snowflake = "0.6"
jsonwebtoken = "9.1"
dotenvy = "0.15"

handlebars = "6.2"
2 changes: 1 addition & 1 deletion backend/server/src/handler/campaign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::models::application::Application;
use crate::models::application::NewApplication;
use crate::models::auth::{AuthUser, SuperUser};
use crate::models::auth::CampaignAdmin;
use crate::models::campaign::{Campaign, CampaignSlugCheck};
use crate::models::campaign::{Campaign};
use crate::models::error::ChaosError;
use crate::models::role::{Role, RoleUpdate};
use crate::models::transaction::DBTransaction;
Expand Down
42 changes: 42 additions & 0 deletions backend/server/src/handler/email_template.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use axum::extract::{Path, State, Json};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use crate::models::app::AppState;
use crate::models::auth::EmailTemplateAdmin;
use crate::models::email_template::EmailTemplate;
use crate::models::error::ChaosError;
use crate::models::transaction::DBTransaction;

pub struct EmailTemplateHandler;
impl EmailTemplateHandler {
pub async fn get(
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_user: EmailTemplateAdmin,
) -> Result<impl IntoResponse, ChaosError> {
let email_template = EmailTemplate::get(id, &mut transaction.tx).await?;

Ok((StatusCode::OK, Json(email_template)))
}

pub async fn update(
_user: EmailTemplateAdmin,
Path(id): Path<i64>,
State(state): State<AppState>,
Json(request_body): Json<EmailTemplate>
) -> Result<impl IntoResponse, ChaosError> {
EmailTemplate::update(id, request_body.name, request_body.template, &state.db).await?;

Ok((StatusCode::OK, "Successfully updated email template"))
}

pub async fn delete(
_user: EmailTemplateAdmin,
Path(id): Path<i64>,
State(state): State<AppState>,
) -> Result<impl IntoResponse, ChaosError> {
EmailTemplate::delete(id, &state.db).await?;

Ok((StatusCode::OK, "Successfully delete email template"))
}
}
1 change: 1 addition & 0 deletions backend/server/src/handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ pub mod question;
pub mod rating;
pub mod role;
pub mod user;
pub mod email_template;
3 changes: 2 additions & 1 deletion backend/server/src/handler/organisation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::models::app::AppState;
use crate::models::auth::SuperUser;
use crate::models::auth::{AuthUser, OrganisationAdmin};
use crate::models::error::ChaosError;
use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation, Organisation, SlugCheck, CampaignSlugCheck};
use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation, Organisation, SlugCheck};
use crate::models::transaction::DBTransaction;
use axum::extract::{Json, Path, State};
use axum::http::StatusCode;
Expand All @@ -22,6 +22,7 @@ impl OrganisationHandler {
) -> Result<impl IntoResponse, ChaosError> {
Organisation::create(
data.admin,
data.slug,
data.name,
state.snowflake_generator,
&mut transaction.tx,
Expand Down
29 changes: 15 additions & 14 deletions backend/server/src/models/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ where
type Rejection = ChaosError;

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let user_id = extract_user_id_from_request(parts, app_state).await?;
let app_state = AppState::from_ref(state);
let user_id = extract_user_id_from_request(parts, &app_state).await?;

Ok(AuthUser {
user_id,
Expand All @@ -81,7 +82,7 @@ where

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let user_id = extract_user_id_from_request(parts, app_state).await?;
let user_id = extract_user_id_from_request(parts, &app_state).await?;

assert_is_super_user(user_id, &app_state.db).await?;

Expand All @@ -105,7 +106,7 @@ where

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let user_id = extract_user_id_from_request(parts, app_state).await?;
let user_id = extract_user_id_from_request(parts, &app_state).await?;

let organisation_id = *parts
.extract::<Path<HashMap<String, i64>>>()
Expand Down Expand Up @@ -134,7 +135,7 @@ where

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let user_id = extract_user_id_from_request(parts, app_state).await?;
let user_id = extract_user_id_from_request(parts, &app_state).await?;

let campaign_id = *parts
.extract::<Path<HashMap<String, i64>>>()
Expand Down Expand Up @@ -163,7 +164,7 @@ where

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let user_id = extract_user_id_from_request(parts, app_state).await?;
let user_id = extract_user_id_from_request(parts, &app_state).await?;

let role_id = *parts
.extract::<Path<HashMap<String, i64>>>()
Expand Down Expand Up @@ -192,7 +193,7 @@ where

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let user_id = extract_user_id_from_request(parts, app_state).await?;
let user_id = extract_user_id_from_request(parts, &app_state).await?;

let Path(application_id) = parts
.extract::<Path<i64>>()
Expand Down Expand Up @@ -227,7 +228,7 @@ where

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let user_id = extract_user_id_from_request(parts, app_state).await?;
let user_id = extract_user_id_from_request(parts, &app_state).await?;

let Path(application_id) = parts
.extract::<Path<i64>>()
Expand Down Expand Up @@ -255,7 +256,7 @@ where

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let user_id = extract_user_id_from_request(parts, app_state).await?;
let user_id = extract_user_id_from_request(parts, &app_state).await?;

let Path(application_id) = parts
.extract::<Path<i64>>()
Expand Down Expand Up @@ -283,7 +284,7 @@ where

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let user_id = extract_user_id_from_request(parts, app_state).await?;
let user_id = extract_user_id_from_request(parts, &app_state).await?;

let Path(rating_id) = parts
.extract::<Path<i64>>()
Expand All @@ -310,7 +311,7 @@ where

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let user_id = extract_user_id_from_request(parts, app_state).await?;
let user_id = extract_user_id_from_request(parts, &app_state).await?;

let Path(rating_id) = parts
.extract::<Path<i64>>()
Expand All @@ -337,7 +338,7 @@ where

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let user_id= extract_user_id_from_request(parts, app_state).await?;
let user_id= extract_user_id_from_request(parts, &app_state).await?;

let question_id = *parts
.extract::<Path<HashMap<String, i64>>>()
Expand Down Expand Up @@ -366,7 +367,7 @@ where

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let user_id = extract_user_id_from_request(parts, app_state).await?;
let user_id = extract_user_id_from_request(parts, &app_state).await?;

let application_id = *parts
.extract::<Path<HashMap<String, i64>>>()
Expand Down Expand Up @@ -395,7 +396,7 @@ where

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let user_id = extract_user_id_from_request(parts, app_state).await?;
let user_id = extract_user_id_from_request(parts, &app_state).await?;

let application_id = *parts
.extract::<Path<HashMap<String, i64>>>()
Expand Down Expand Up @@ -424,7 +425,7 @@ where

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let user_id= extract_user_id_from_request(parts, app_state).await?;
let user_id= extract_user_id_from_request(parts, &app_state).await?;

let template_id = *parts
.extract::<Path<HashMap<String, i64>>>()
Expand Down
96 changes: 96 additions & 0 deletions backend/server/src/models/email_template.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use std::collections::HashMap;
use std::ops::DerefMut;
use chrono::{DateTime, Local, Utc};
use handlebars::Handlebars;
use serde::{Deserialize, Serialize};
use snowflake::SnowflakeIdGenerator;
use sqlx::{Pool, Postgres, Transaction};
use crate::models::application::Application;
use crate::models::campaign::Campaign;
use crate::models::error::ChaosError;

/// Email templates to update applicants
/// Supported tags:
/// - `name`
/// - `role`
/// - `organisation_name`
/// - `expiry_date`
/// - `campaign_name`
#[derive(Deserialize, Serialize)]
pub struct EmailTemplate {
pub id: i64,
pub organisation_id: i64,
pub name: String,
pub template: String,
}

impl EmailTemplate {
pub async fn get(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result<EmailTemplate, ChaosError> {
let template = sqlx::query_as!(
EmailTemplate,
"SELECT * FROM email_templates WHERE id = $1",
id
)
.fetch_one(transaction.deref_mut()).await?;

Ok(template)
}

pub async fn get_all_by_organisation(organisation_id: i64, pool: &Pool<Postgres>) -> Result<Vec<EmailTemplate>, ChaosError> {
let templates = sqlx::query_as!(
EmailTemplate,
"SELECT * FROM email_templates WHERE organisation_id = $1",
organisation_id
)
.fetch_all(pool).await?;

Ok(templates)
}

pub async fn update(id: i64, name: String, template: String, pool: &Pool<Postgres>) -> Result<(), ChaosError> {
let _ = sqlx::query!(
"
UPDATE email_templates SET name = $2, template = $3 WHERE id = $1 RETURNING id
",
id, name, template
)
.fetch_one(pool).await?;

Ok(())
}

pub async fn delete(id: i64, pool: &Pool<Postgres>) -> Result<(), ChaosError> {
let _ = sqlx::query!(
"DELETE FROM email_templates WHERE id = $1 RETURNING id",
id
).fetch_one(pool).await?;

Ok(())
}

pub async fn generate_email(
name: String,
role: String,
organisation_name: String,
campaign_name: String,
expiry_date: DateTime<Utc>,
email_template_id: i64,
transaction: &mut Transaction<'_, Postgres>,
) -> Result<String, ChaosError> {
let template = EmailTemplate::get(email_template_id, transaction).await?;

let mut handlebars = Handlebars::new();
handlebars.register_template_string("template", template.template)?;

let mut data = HashMap::new();
data.insert("name", name);
data.insert("role", role);
data.insert("organisation_name", organisation_name);
data.insert("campaign_name", campaign_name);
data.insert("expiry_date", expiry_date.with_timezone(&Local).format("%d/%m/%Y %H:%M").to_string());

let final_string = handlebars.render("template", &data)?;

Ok(final_string)
}
}
6 changes: 6 additions & 0 deletions backend/server/src/models/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ pub enum ChaosError {

#[error("DotEnvy error")]
DotEnvyError(#[from] dotenvy::Error),

#[error("Templating error")]
TemplateError(#[from] handlebars::TemplateError),

#[error("Template rendering error")]
TemplateRendorError(#[from] handlebars::RenderError),
}

/// Implementation for converting errors into responses. Manages error code and message returned.
Expand Down
2 changes: 2 additions & 0 deletions backend/server/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ pub mod role;
pub mod storage;
pub mod transaction;
pub mod user;
pub mod email_template;
pub mod offer;
Loading

0 comments on commit b38f2fa

Please sign in to comment.