[Draft] Implement Early deletion by Submitter #37 #38

Merged
ben merged 15 commits from ben/Jobboerse:main into main 2023-01-28 18:00:00 +01:00
5 changed files with 119 additions and 25 deletions
Showing only changes of commit 70797cc298 - Show all commits

don't remove confirmation token after confirmation

this way it can be used for early deletion
rename it to submitter token in the process
Bennet Bleßmann 2022-10-08 20:42:39 +02:00 committed by Bennet Bleßmann
Signed by: ben
GPG key ID: 3BE1A1A3CBC3CF99

View file

@ -110,6 +110,13 @@ pub(crate) struct Link {
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Hash)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Hash)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum ConfirmationStatus { pub enum ConfirmationStatus {
AwaitingConfirmation,
Confirmed,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Hash)]
#[serde(tag = "type")]
pub enum V1ConfirmationStatus {
AwaitingConfirmation { token: String }, AwaitingConfirmation { token: String },
Confirmed, Confirmed,
} }
@ -128,16 +135,47 @@ pub enum ReviewStatus {
pub struct JobOfferStatus { pub struct JobOfferStatus {
review_status: ReviewStatus, review_status: ReviewStatus,
confirmation_status: ConfirmationStatus, confirmation_status: ConfirmationStatus,
#[serde(default)]
submitter_token: Option<String>,
}
impl From<V1JobOfferStatus> for JobOfferStatus {
fn from(
V1JobOfferStatus {
review_status,
confirmation_status,
}: V1JobOfferStatus,
) -> Self {
let (confirmation_status, token) = match confirmation_status {
V1ConfirmationStatus::AwaitingConfirmation { token } => {
(ConfirmationStatus::AwaitingConfirmation, Some(token))
}
V1ConfirmationStatus::Confirmed => (ConfirmationStatus::Confirmed, None),
};
JobOfferStatus {
review_status,
confirmation_status,
submitter_token: token,
}
}
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Hash)]
pub struct V1JobOfferStatus {
review_status: ReviewStatus,
confirmation_status: V1ConfirmationStatus,
} }
impl JobOfferStatus { impl JobOfferStatus {
pub(crate) fn new( pub(crate) fn new(
review_status: ReviewStatus, review_status: ReviewStatus,
confirmation_status: ConfirmationStatus, confirmation_status: ConfirmationStatus,
submitter_token: String,
) -> Self { ) -> Self {
Self { Self {
review_status, review_status,
confirmation_status, confirmation_status,
submitter_token: Some(submitter_token),
} }
} }
@ -155,15 +193,12 @@ impl JobOfferStatus {
} }
} }
pub fn check_confirmation_token(&self, presented_token: &str) -> bool { pub fn check_submitter_token(&self, presented_token: &str) -> bool {
match &self.confirmation_status { self.submitter_token.as_deref() == Some(presented_token)
ConfirmationStatus::AwaitingConfirmation { token } => token == presented_token,
ConfirmationStatus::Confirmed => false,
}
} }
pub fn mark_as_confirmed(&mut self, presented_token: &str) -> Result<(), ()> { pub fn mark_as_confirmed(&mut self, presented_token: &str) -> Result<(), ()> {
if self.check_confirmation_token(presented_token) { if self.check_submitter_token(presented_token) {
self.confirmation_status = ConfirmationStatus::Confirmed; self.confirmation_status = ConfirmationStatus::Confirmed;
Ok(()) Ok(())
} else { } else {
@ -177,6 +212,7 @@ impl JobOfferStatus {
JobOfferStatus { JobOfferStatus {
review_status: ReviewStatus::Reviewed, review_status: ReviewStatus::Reviewed,
confirmation_status: ConfirmationStatus::Confirmed, confirmation_status: ConfirmationStatus::Confirmed,
submitter_token: _
} }
) )
} }
@ -216,6 +252,43 @@ pub struct JobOffer<AttachmentLocation> {
pub(crate) links: Vec<Link>, pub(crate) links: Vec<Link>,
} }
impl From<V1JobOffer> for JobOffer<PathBuf> {
fn from(old: V1JobOffer) -> Self {
JobOffer {
title: old.title,
offering_party: old.offering_party,
public_contact_info: old.public_contact_info,
date_of_submission: old.date_of_submission,
date_of_expiry: old.date_of_expiry,
permanent: old.permanent,
contact_info: old.contact_info,
status: old.status.into(),
attachments: old.attachments,
links: old.links,
}
}
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Hash)]
pub struct V1JobOffer {
pub(crate) title: String,
pub(crate) offering_party: String,
#[serde(skip_serializing_if = "std::ops::Not::not", default)]
pub(crate) public_contact_info: bool,
pub(crate) date_of_submission: better_toml_datetime::Datetime,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub(crate) date_of_expiry: Option<Date>,
#[serde(skip_serializing_if = "std::ops::Not::not", default)]
pub(crate) permanent: bool,
// complex fields need to come after simple ones for derived serialization to work with toml format!
pub(crate) contact_info: Address,
pub(crate) status: V1JobOfferStatus,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub(crate) attachments: Vec<Attachment<PathBuf>>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub(crate) links: Vec<Link>,
}
impl Deref for JobOffer<PathBuf> { impl Deref for JobOffer<PathBuf> {
type Target = JobOfferStatus; type Target = JobOfferStatus;
@ -441,7 +514,7 @@ impl JobOffer<PathBuf> {
.map(|attachment| { .map(|attachment| {
let location = match (is_preview && !self.is_published(), confirmation_token) { let location = match (is_preview && !self.is_published(), confirmation_token) {
(false, _) => attachment.generate_link(id, req, None)?.to_string(), (false, _) => attachment.generate_link(id, req, None)?.to_string(),
(true, Some(token)) if self.check_confirmation_token(token) => { (true, Some(token)) if self.check_submitter_token(token) => {
attachment.generate_link(id, req, Some(token))?.to_string() attachment.generate_link(id, req, Some(token))?.to_string()
} }
(true, _) => preview_location.to_string(), (true, _) => preview_location.to_string(),
@ -560,12 +633,7 @@ impl JobOffers {
let file_content = tokio::fs::read_to_string(&index) let file_content = tokio::fs::read_to_string(&index)
.await .await
.map_err(|err| JobofferLoadError::IO(err, index))?; .map_err(|err| JobofferLoadError::IO(err, index))?;
let job = toml::de::from_str(&file_content).map_err(|err| { let job = Self::load_job_offer(file_content, &key).await?;
JobofferLoadError::TomlDeserializationError {
id: key.clone(),
err,
}
})?;
job_offers.insert(key, job); job_offers.insert(key, job);
} }
} }
@ -577,6 +645,32 @@ impl JobOffers {
Ok(()) Ok(())
} }
async fn load_job_offer(
file_content: String,
key: &String,
) -> Result<JobOffer<PathBuf>, JobofferLoadError> {
let job = toml::de::from_str(&file_content)
.map_err(|err| JobofferLoadError::TomlDeserializationError {
id: key.clone(),
err,
})
.or_else(|_prev_err| {
let v1: V1JobOffer = toml::de::from_str(&file_content).map_err(|err| {
JobofferLoadError::TomlDeserializationError {
id: key.clone(),
err,
}
})?;
// migration will only be persisted on the next change to the joboffer
info!(
"In-Memory migration applied while loading joboffer id: {}",
key
);
Ok(v1.into())
})?;
Ok(job)
}
pub(crate) async fn create_new_offer<'data, 'config, Tz>( pub(crate) async fn create_new_offer<'data, 'config, Tz>(
&'data self, &'data self,
submission_time: chrono::DateTime<Tz>, submission_time: chrono::DateTime<Tz>,

View file

@ -80,11 +80,13 @@ impl JobOfferActions {
.url_for(JOBOFFER_EDIT_ROUTE, &[id]) .url_for(JOBOFFER_EDIT_ROUTE, &[id])
.expect("generation of delete route urls should succeed"); .expect("generation of delete route urls should succeed");
let confirmation_url = match &offer.status.confirmation_status { let confirmation_url = match &offer.status {
ConfirmationStatus::AwaitingConfirmation { token } => { JobOfferStatus {
Some(req.url_for(JOBOFFER_CONFIRM_ROUTE, [id, token])?) confirmation_status: ConfirmationStatus::AwaitingConfirmation,
} submitter_token: Some(token),
ConfirmationStatus::Confirmed => None, review_status: _,
} => Some(req.url_for(JOBOFFER_CONFIRM_ROUTE, [id, token])?),
_ => None,
}; };
let highlight_url = offer.highlight_link(id, req)?; let highlight_url = offer.highlight_link(id, req)?;

View file

@ -145,7 +145,7 @@ pub(crate) async fn job_offer_attachment(
&& !query && !query
.token .token
.as_deref() .as_deref()
.map_or(false, |token| offer.check_confirmation_token(token)) .map_or(false, |token| offer.check_submitter_token(token))
{ {
let dest = req let dest = req
.url_for(JOBOFFER_ATTACHMENT_ROUTE, &[id, attachment_name]) .url_for(JOBOFFER_ATTACHMENT_ROUTE, &[id, attachment_name])

View file

@ -40,7 +40,7 @@ pub(crate) async fn confirm_joboffer_get(
let req_token = &path.1; let req_token = &path.1;
if let Some(job_offer) = offers.get_offer(id).await { if let Some(job_offer) = offers.get_offer(id).await {
if !job_offer.check_confirmation_token(req_token) { if !job_offer.check_submitter_token(req_token) {
Err(ConfirmationResponseError::InvalidRequest) Err(ConfirmationResponseError::InvalidRequest)
} else { } else {
let user = User::current(&session).ok(); let user = User::current(&session).ok();
@ -137,7 +137,7 @@ pub(crate) async fn reject_joboffer_post(
let user = User::current(&session).ok(); let user = User::current(&session).ok();
if let Some(job_offer) = offers.get_offer(id).await { if let Some(job_offer) = offers.get_offer(id).await {
if job_offer.check_confirmation_token(req_token) { if job_offer.check_submitter_token(req_token) {
drop(job_offer); drop(job_offer);
offers.delete_offer(id, false, &config).await?; offers.delete_offer(id, false, &config).await?;

View file

@ -228,9 +228,7 @@ pub(crate) async fn create_job_offer<'data, 'config>(
let confirmation_status = if skip_confirmation { let confirmation_status = if skip_confirmation {
ConfirmationStatus::Confirmed ConfirmationStatus::Confirmed
} else { } else {
ConfirmationStatus::AwaitingConfirmation { ConfirmationStatus::AwaitingConfirmation
token: token.clone(),
}
}; };
let job_offer = JobOffer { let job_offer = JobOffer {
@ -243,7 +241,7 @@ pub(crate) async fn create_job_offer<'data, 'config>(
permanent: is_permanent, permanent: is_permanent,
attachments: job_offer_form.attachments, attachments: job_offer_form.attachments,
links: job_offer_form.links, links: job_offer_form.links,
status: JobOfferStatus::new(review_status, confirmation_status), status: JobOfferStatus::new(review_status, confirmation_status, token.clone()),
}; };
let created_offer = offers.create_new_offer(now_date, job_offer, config).await?; let created_offer = offers.create_new_offer(now_date, job_offer, config).await?;