changes and release cut for 0.2.3 #31

Merged
ben merged 12 commits from ben/Jobboerse:main into main 2022-07-27 23:04:10 +02:00
9 changed files with 84 additions and 11 deletions
Showing only changes of commit 26f82691f7 - Show all commits

send a notification when a review causes a submission to be published

Bennet Bleßmann 2022-07-13 23:19:22 +02:00 committed by Bennet Bleßmann
Signed by: ben
GPG key ID: 3BE1A1A3CBC3CF99

View file

@ -36,7 +36,7 @@ fn user_agent() -> UserAgent {
} }
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
pub(crate) struct EmailData { pub(crate) struct ConfirmationEmailData {
pub(crate) confirmation_link: Url, pub(crate) confirmation_link: Url,
} }
@ -44,12 +44,12 @@ pub(crate) async fn send_confirmation_email(
hb: &Handlebars<'_>, hb: &Handlebars<'_>,
contact_address: Address, contact_address: Address,
email_config: &EmailConfig, email_config: &EmailConfig,
email_data: &EmailData, email_data: &ConfirmationEmailData,
) -> actix_web::Result<(), EmailError> { ) -> actix_web::Result<(), EmailError> {
// receiver of the confirmation e-mail // receiver of the confirmation e-mail
let to_mailbox = Mailbox::new(None, contact_address); let to_mailbox = Mailbox::new(None, contact_address);
let email_body = hb.render(template::EMAIL_PLAIN, &email_data)?; let email_body = hb.render(template::EMAIL_CONFIRMATION_PLAIN, email_data)?;
let message = lettre::Message::builder() let message = lettre::Message::builder()
.from(email_config.from.to_owned()) .from(email_config.from.to_owned())
@ -89,3 +89,32 @@ pub(crate) async fn send_reviewer_notice(email_config: &EmailConfig) {
} }
} }
} }
#[derive(serde::Serialize)]
pub(crate) struct PublishNoticeEmailData {
pub(crate) highlight_link: Url,
}
pub(crate) async fn send_publish_notice(
hb: &Handlebars<'_>,
contact_address: Address,
email_config: &EmailConfig,
email_data: &PublishNoticeEmailData,
) -> actix_web::Result<(), EmailError> {
// receiver of the publish notice e-mail
let to_mailbox = Mailbox::new(None, contact_address);
let email_body = hb.render(template::EMAIL_PUBLISH_NOTICE_PLAIN, &email_data)?;
let message = lettre::Message::builder()
.from(email_config.from.to_owned())
.to(to_mailbox)
.subject(&email_config.subject)
.header(user_agent())
.header(AutoGeneratedHeader::default())
.singlepart(SinglePart::plain(email_body))?;
lettre::AsyncSendmailTransport::new().send(message).await?;
Ok(())
}

View file

@ -21,6 +21,7 @@ use view::{JobOfferActions, JobOfferViewData};
use better_toml_datetime::Date; use better_toml_datetime::Date;
use crate::job_offers::view::JobOfferEditData; use crate::job_offers::view::JobOfferEditData;
use crate::route::JOBOFFER_OVERVIEW_ROUTE;
use crate::{ use crate::{
auth::User, auth::User,
error::PresentationError, error::PresentationError,
@ -486,6 +487,16 @@ impl JobOffer<PathBuf> {
links: self.links.clone(), links: self.links.clone(),
}) })
} }
pub(crate) fn highlight_link(
&self,
id: &JobOfferId,
req: &HttpRequest,
) -> Result<Url, UrlGenerationError> {
let mut url = req.url_for_static(JOBOFFER_OVERVIEW_ROUTE)?;
url.set_fragment(Some(&format!("joboffer-{}", id)));
Ok(url)
}
} }
pub struct JobOffers { pub struct JobOffers {

View file

@ -74,11 +74,11 @@ impl ResponseError for SaveResponseError {
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub(crate) enum EmailError { pub(crate) enum EmailError {
#[error("Construction of confirmation E-mail failed!")] #[error("Construction of E-mail failed!")]
Email(#[from] lettre::error::Error), Email(#[from] lettre::error::Error),
#[error("Sending of confirmation E-mail failed!")] #[error("Sending of E-mail failed!")]
SendMail(#[from] lettre::transport::sendmail::Error), SendMail(#[from] lettre::transport::sendmail::Error),
#[error("Error in Confirmation E-Mail template!")] #[error("Error in E-Mail template!")]
Template(#[from] handlebars::RenderError), Template(#[from] handlebars::RenderError),
} }

View file

@ -15,7 +15,7 @@ use serde_json::json;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use crate::auth::User; use crate::auth::User;
use crate::email::EmailData; use crate::email::ConfirmationEmailData;
use crate::error::{LoginRequired, PresentationError}; use crate::error::{LoginRequired, PresentationError};
use crate::job_offers::error::SaveResponseError; use crate::job_offers::error::SaveResponseError;
use crate::job_offers::lease::SubmissionLimiter; use crate::job_offers::lease::SubmissionLimiter;
@ -260,7 +260,7 @@ pub(crate) async fn create_job_offer<'data, 'config>(
hb, hb,
job_offer_form.contact, job_offer_form.contact,
email_config, email_config,
&EmailData { &ConfirmationEmailData {
confirmation_link: confirm_url.into(), confirmation_link: confirm_url.into(),
}, },
) )

View file

@ -12,7 +12,7 @@ use multipart_helper::MultipartFieldError;
use thiserror::Error; use thiserror::Error;
use crate::error::{default_error_response, LoginRequired, PresentationError}; use crate::error::{default_error_response, LoginRequired, PresentationError};
use crate::job_offers::error::{DeleteError, SaveError, SaveResponseError}; use crate::job_offers::error::{DeleteError, EmailError, SaveError, SaveResponseError};
use crate::job_offers::JobofferLoadError; use crate::job_offers::JobofferLoadError;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -104,6 +104,8 @@ pub(crate) enum StateChangeResponseError {
Login(#[from] LoginRequired), Login(#[from] LoginRequired),
#[error("Rendering the requests response failed: {0}")] #[error("Rendering the requests response failed: {0}")]
Presentation(#[from] PresentationError), Presentation(#[from] PresentationError),
#[error("Failed to send email: {0}")]
Email(#[from] EmailError),
} }
impl From<RenderError> for StateChangeResponseError { impl From<RenderError> for StateChangeResponseError {
@ -124,11 +126,14 @@ impl ResponseError for StateChangeResponseError {
StateChangeResponseError::Save(inner) => inner.as_status_code(), StateChangeResponseError::Save(inner) => inner.as_status_code(),
StateChangeResponseError::Login(_inner) => StatusCode::UNAUTHORIZED, StateChangeResponseError::Login(_inner) => StatusCode::UNAUTHORIZED,
StateChangeResponseError::Presentation(inner) => inner.status_code(), StateChangeResponseError::Presentation(inner) => inner.status_code(),
StateChangeResponseError::Email(_inner) => StatusCode::INTERNAL_SERVER_ERROR,
} }
} }
fn error_response(&self) -> HttpResponse<BoxBody> { fn error_response(&self) -> HttpResponse<BoxBody> {
match self { match self {
StateChangeResponseError::Login(_) | StateChangeResponseError::Save(_) => { StateChangeResponseError::Login(_)
| StateChangeResponseError::Save(_)
| StateChangeResponseError::Email(_) => {
default_error_response(self, self.status_code()) default_error_response(self, self.status_code())
} }
StateChangeResponseError::Presentation(inner) => inner.error_response(), StateChangeResponseError::Presentation(inner) => inner.error_response(),

View file

@ -1,6 +1,8 @@
use actix_session::Session; use actix_session::Session;
use actix_web::{http, post, web, HttpRequest, HttpResponse}; use actix_web::{http, post, web, HttpRequest, HttpResponse};
use handlebars::Handlebars;
use crate::email::PublishNoticeEmailData;
use crate::route::job_offer::error::StateChangeResponseError; use crate::route::job_offer::error::StateChangeResponseError;
use crate::route::JOBOFFER_OVERVIEW_ROUTE; use crate::route::JOBOFFER_OVERVIEW_ROUTE;
use crate::{auth, JobOffers, ServerConfig}; use crate::{auth, JobOffers, ServerConfig};
@ -11,6 +13,7 @@ pub(crate) const JOBOFFER_PUBLISH_ROUTE: &str = "review_offer";
pub(crate) async fn review_joboffer( pub(crate) async fn review_joboffer(
req: HttpRequest, req: HttpRequest,
path: web::Path<String>, path: web::Path<String>,
hb: web::Data<Handlebars<'_>>,
session: Session, session: Session,
config: web::Data<ServerConfig>, config: web::Data<ServerConfig>,
offers: web::Data<JobOffers>, offers: web::Data<JobOffers>,
@ -22,8 +25,23 @@ pub(crate) async fn review_joboffer(
let id = path.into_inner(); let id = path.into_inner();
if let Some(mut entry) = offers.get_offer_mut(&id, &config).await { if let Some(mut entry) = offers.get_offer_mut(&id, &config).await {
let requires_review = entry.requires_review();
entry.mark_as_reviewed(); entry.mark_as_reviewed();
entry.try_clean().await?; entry.try_clean().await?;
if requires_review && entry.is_published() {
if let Some(email_config) = &config.config.email {
let data = PublishNoticeEmailData {
highlight_link: entry.highlight_link(entry.id(), &req)?,
};
crate::email::send_publish_notice(
&hb,
entry.contact_info.clone(),
email_config,
&data,
)
.await?;
}
}
} }
let dest = req.url_for_static(JOBOFFER_OVERVIEW_ROUTE)?; let dest = req.url_for_static(JOBOFFER_OVERVIEW_ROUTE)?;

View file

@ -11,4 +11,5 @@ pub(crate) const JOBOFFER_FAQ: &str = "job_offer/faq";
pub(crate) const AUTH_LOGIN: &str = "auth/login"; pub(crate) const AUTH_LOGIN: &str = "auth/login";
pub(crate) const LICENCES: &str = "licenses"; pub(crate) const LICENCES: &str = "licenses";
pub const EMAIL_PLAIN: &str = "email/plaintext"; pub const EMAIL_CONFIRMATION_PLAIN: &str = "email/confirmation_plaintext";
pub const EMAIL_PUBLISH_NOTICE_PLAIN: &str = "email/publish_notice_plaintext";

View file

@ -0,0 +1,9 @@
Moin,
Es wurde eine Stellenausschreibung mit dieser E-Mail Address als Kontakt-Address in unserer Jobbörse veröffentlicht.
{{{highlight_link}}}
Freundliche Grüße
Das Team der FS-InfMath Jobbörse