features, cleanup and bug fixes #26
13 changed files with 301 additions and 111 deletions
more edit dialog allow addition/deletion of attachments
* form now also shows emty slots for links/attachments when the default form count is not already filled
commit
62c6b8170d
|
|
@ -114,7 +114,7 @@ pub(crate) async fn tmpfile_from_field(
|
|||
limit: usize,
|
||||
tmp_dir: &Path,
|
||||
) -> Result<(String, Option<NamedTempFile>), MultipartFieldError> {
|
||||
// while it is technically valid to not get a filename here, we expect it to be present
|
||||
// while it is technically valid to not get a filename, we expect it to be present
|
||||
let file_name = field
|
||||
.content_disposition()
|
||||
.get_filename()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::collections::BTreeMap;
|
|||
use std::hash::{Hash, Hasher};
|
||||
use std::io::ErrorKind;
|
||||
use std::ops::{Add, Deref, DerefMut};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use actix_web::error::UrlGenerationError;
|
||||
use actix_web::{HttpRequest, Result};
|
||||
|
|
@ -13,7 +13,7 @@ use error::DeleteError;
|
|||
use lettre::Address;
|
||||
use log::{debug, error, info, warn};
|
||||
use rand::distributions::DistString;
|
||||
use tempfile::NamedTempFile;
|
||||
use tempfile::{NamedTempFile, PersistError};
|
||||
use tokio::sync::{RwLock, RwLockMappedWriteGuard, RwLockReadGuard, RwLockWriteGuard};
|
||||
use url::Url;
|
||||
use view::{JobOfferActions, JobOfferViewData};
|
||||
|
|
@ -63,6 +63,26 @@ pub(crate) struct Attachment<Location> {
|
|||
pub(crate) attachment_location: Location,
|
||||
}
|
||||
|
||||
impl Attachment<NamedTempFile> {
|
||||
pub(crate) fn persist(
|
||||
self,
|
||||
idx: usize,
|
||||
folder_path: &Path,
|
||||
) -> Result<Attachment<PathBuf>, PersistError> {
|
||||
let attachment_location = Self::attachment_filename(idx);
|
||||
let file_path = folder_path.join(&attachment_location);
|
||||
self.attachment_location.persist(&file_path)?;
|
||||
Ok(Attachment {
|
||||
title: self.title,
|
||||
file_name: self.file_name,
|
||||
attachment_location,
|
||||
})
|
||||
}
|
||||
pub(crate) fn attachment_filename(idx: usize) -> PathBuf {
|
||||
PathBuf::from(format!("{idx}.attachment"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Attachment<PathBuf> {
|
||||
pub(crate) fn generate_link(
|
||||
&self,
|
||||
|
|
@ -208,7 +228,7 @@ impl<A> JobOffer<A> {
|
|||
self.status.is_published()
|
||||
}
|
||||
|
||||
fn folder_path(id: &JobOfferId, config: &ServerConfig) -> PathBuf {
|
||||
pub(crate) fn folder_path(id: &JobOfferId, config: &ServerConfig) -> PathBuf {
|
||||
config.config.data_storage_path.join(id)
|
||||
}
|
||||
|
||||
|
|
@ -232,16 +252,7 @@ impl JobOffer<NamedTempFile> {
|
|||
.attachments
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, entry)| {
|
||||
let attachment_location = PathBuf::from(format!("{idx}.attachment"));
|
||||
let file_path = folder_path.join(&attachment_location);
|
||||
entry.attachment_location.persist(&file_path)?;
|
||||
Ok(Attachment {
|
||||
title: entry.title,
|
||||
file_name: entry.file_name,
|
||||
attachment_location,
|
||||
})
|
||||
})
|
||||
.map(|(idx, entry)| entry.persist(idx, &folder_path))
|
||||
.collect::<Result<_, tempfile::PersistError>>()?;
|
||||
|
||||
// use https://github.com/rust-lang/rust/issues/86555 when stabilized
|
||||
|
|
@ -287,7 +298,6 @@ impl JobOffer<PathBuf> {
|
|||
let offer = match toml::ser::to_string(self) {
|
||||
Ok(offer) => offer,
|
||||
Err(err) => {
|
||||
dbg!(&err);
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
|
|
@ -346,8 +356,6 @@ impl JobOffer<PathBuf> {
|
|||
JobOffer::<PathBuf>::hash(self, &mut hasher);
|
||||
let hash = hasher.finish();
|
||||
|
||||
dbg!(hash);
|
||||
|
||||
let attachments = self
|
||||
.attachments
|
||||
.iter()
|
||||
|
|
@ -360,6 +368,12 @@ impl JobOffer<PathBuf> {
|
|||
})
|
||||
.collect::<Result<Vec<_>, UrlGenerationError>>()?;
|
||||
|
||||
let mut links: Vec<_> = self.links.iter().cloned().map(Some).collect();
|
||||
|
||||
if links.len() < crate::route::form_constants::LINK_FIELD_COUNT {
|
||||
links.resize(crate::route::form_constants::LINK_FIELD_COUNT, None)
|
||||
}
|
||||
|
||||
Ok(JobOfferEditData {
|
||||
id: id.to_owned(),
|
||||
hash,
|
||||
|
|
@ -371,7 +385,7 @@ impl JobOffer<PathBuf> {
|
|||
submission_date: self.date_of_submission.to_string(),
|
||||
title: self.title.clone(),
|
||||
attachments,
|
||||
links: self.links.clone(),
|
||||
links,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ pub(crate) struct JobOfferEditData {
|
|||
pub(crate) submission_date: String,
|
||||
pub(crate) title: String,
|
||||
pub(crate) attachments: Vec<Attachment<SerializableUrl>>,
|
||||
pub(crate) links: Vec<Link>,
|
||||
pub(crate) links: Vec<Option<Link>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use url::Url;
|
|||
|
||||
mod auth;
|
||||
pub(crate) mod error_handler;
|
||||
pub(crate) mod form_constants;
|
||||
mod job_offer;
|
||||
mod license;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ pub(crate) const PRE_APPROVED: &str = "pre_approved";
|
|||
pub(crate) const SKIP_CONFIRMATION: &str = "skip_confirmation";
|
||||
pub(crate) const PERMANENT_FIELD: &str = "permanent";
|
||||
pub(crate) const ATTACHMENT_TITLES: &str = "file_title[]";
|
||||
pub(crate) const ATTACHMENT_FILE_NAME: &str = "file_name[]";
|
||||
pub(crate) const ATTACHMENT_FILES: &str = "file[]";
|
||||
pub(crate) const LINK_TITLES: &str = "link_title[]";
|
||||
pub(crate) const LINK_URLS: &str = "link_url[]";
|
||||
|
|
@ -43,6 +42,9 @@ pub(crate) const MAX_LINK_URL_LEN: usize = 2048;
|
|||
pub(crate) const MAX_ATTACHMENT_TITLE_LEN: usize = 128;
|
||||
pub(crate) const MAX_LINK_COUNT: Option<u8> = Some(20);
|
||||
|
||||
pub(crate) const ATTACHMENT_FIELD_COUNT: usize = 4;
|
||||
pub(crate) const LINK_FIELD_COUNT: usize = 4;
|
||||
|
||||
pub(crate) struct UploadLimits {
|
||||
pub(crate) size: usize,
|
||||
pub(crate) count: Option<u8>,
|
||||
|
|
@ -63,3 +65,9 @@ pub(crate) const fn upload_limits(user: Option<&User>) -> UploadLimits {
|
|||
}
|
||||
|
||||
pub(crate) const HASH: &str = "hash";
|
||||
|
||||
pub(crate) const ATTACHMENT_FILENAME_EDIT_FIELD: &str = "file_name_edit[]";
|
||||
pub(crate) const ATTACHMENT_TITLE_EDIT_FIELD: &str = "file_title_edit[]";
|
||||
pub(crate) const ATTACHMENT_FILE_REPLACE_FIELD: &str = "file_replace[]";
|
||||
|
||||
pub(crate) const DELETE_ATTACHMENT_FIELD: &'static str = "delete_attachment[]";
|
||||
|
|
@ -14,7 +14,6 @@ pub(crate) mod create;
|
|||
pub(crate) mod delete;
|
||||
pub(crate) mod edit;
|
||||
pub(crate) mod error;
|
||||
pub(crate) mod form_constants;
|
||||
pub(crate) mod review;
|
||||
|
||||
use crate::auth::User;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use lettre::message::{Mailbox, SinglePart};
|
|||
use lettre::{Address, AsyncTransport};
|
||||
use log::{debug, error, warn};
|
||||
use rand::distributions::DistString;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use tempfile::NamedTempFile;
|
||||
use url::Url;
|
||||
|
|
@ -23,17 +24,23 @@ use crate::job_offers::{
|
|||
Attachment, ConfirmationStatus, JobOffer, JobOfferStatus, JobOffers, Link, MutBorrowedJobOffer,
|
||||
ReviewStatus,
|
||||
};
|
||||
use crate::route::form_constants::{self, UploadLimits};
|
||||
use crate::route::job_offer::confirmation::JOBOFFER_CONFIRM_ROUTE;
|
||||
use crate::route::job_offer::error::{FormProcessingError, SubmissionResponseError};
|
||||
use crate::route::job_offer::form_constants::{self, UploadLimits};
|
||||
use crate::route::HTML_CONTENT;
|
||||
use crate::server_config::{EmailConfig, ServerConfig};
|
||||
use crate::template;
|
||||
use crate::util::{parse_date, parse_datetime, process_links};
|
||||
use crate::util::{parse_date, parse_datetime, process_links, process_new_attachments};
|
||||
use multipart_helper::{multi_field, multi_file, once_field};
|
||||
|
||||
pub(crate) const JOBOFFER_CREATION_ROUTE: &str = "create_offer";
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SubmissionFormRenderData {
|
||||
attachments: Vec<()>,
|
||||
links: Vec<()>,
|
||||
}
|
||||
|
||||
#[get("/new", name = "create_offer")]
|
||||
pub(crate) async fn create_joboffer_get(
|
||||
req: HttpRequest,
|
||||
|
|
@ -43,9 +50,15 @@ pub(crate) async fn create_joboffer_get(
|
|||
) -> Result<HttpResponse, PresentationError> {
|
||||
let user = User::current(&session).ok();
|
||||
|
||||
let form_data = SubmissionFormRenderData {
|
||||
attachments: vec![(); form_constants::ATTACHMENT_FIELD_COUNT],
|
||||
links: vec![(); form_constants::LINK_FIELD_COUNT],
|
||||
};
|
||||
|
||||
let data = json!({
|
||||
"base": crate::route::base(&req, &config,"Create Joboffer")?,
|
||||
"user": user,
|
||||
"form": form_data,
|
||||
});
|
||||
|
||||
let rendered = hb.render(template::JOBOFFER_CREATE, &data)?;
|
||||
|
|
@ -469,26 +482,7 @@ impl JobOfferSubmitForm {
|
|||
|
||||
assert_eq!(attachment_titles.len(), attachment_datas.len());
|
||||
|
||||
let attachments = attachment_titles
|
||||
.into_iter()
|
||||
.zip(attachment_datas.into_iter())
|
||||
.filter_map(|(title, (file_name, attachment_location))| {
|
||||
attachment_location
|
||||
.map(|attachment_location| (title, file_name, attachment_location))
|
||||
})
|
||||
.enumerate()
|
||||
.map(
|
||||
|(idx, (title, file_name, attachment_location))| Attachment {
|
||||
title: if title.is_empty() {
|
||||
format!("Attachment {}: {}", idx + 1, file_name)
|
||||
} else {
|
||||
title
|
||||
},
|
||||
file_name,
|
||||
attachment_location,
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
let attachments = process_new_attachments(attachment_titles, attachment_datas);
|
||||
|
||||
assert_eq!(link_titles.len(), link_urls.len());
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@ use crate::auth::User;
|
|||
use crate::error::{LoginRequired, PresentationError};
|
||||
use crate::job_offers::error::SaveError;
|
||||
use crate::job_offers::{Attachment, JobOffer, Link};
|
||||
use crate::route::form_constants::{self, UploadLimits};
|
||||
use crate::route::job_offer::error::FormProcessingError;
|
||||
use crate::route::job_offer::form_constants;
|
||||
use crate::route::job_offer::form_constants::UploadLimits;
|
||||
use crate::route::{HTML_CONTENT, JOBOFFER_OVERVIEW_ROUTE};
|
||||
use crate::util::{parse_date, parse_datetime, process_links};
|
||||
use crate::util::{parse_date, parse_datetime, process_links, process_new_attachments};
|
||||
use crate::{template, JobOffers, ServerConfig};
|
||||
use actix_multipart::Multipart;
|
||||
use actix_session::Session;
|
||||
|
|
@ -16,11 +15,12 @@ use futures_util::StreamExt;
|
|||
use handlebars::Handlebars;
|
||||
use lettre::Address;
|
||||
use log::warn;
|
||||
use multipart_helper::{multi_field, once_field};
|
||||
use multipart_helper::{multi_field, multi_file, once_field};
|
||||
use serde_json::json;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::Hasher;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum EditResponseError {
|
||||
|
|
@ -63,9 +63,15 @@ pub(crate) async fn edit_joboffer_get(
|
|||
let base =
|
||||
crate::route::base(&req, &config, "Edit Job Offer").map_err(PresentationError::Url)?;
|
||||
|
||||
let additional_slots =
|
||||
form_constants::ATTACHMENT_FIELD_COUNT.saturating_sub(job_offer.attachments.len());
|
||||
|
||||
let data = json!({
|
||||
"base": base,
|
||||
"job_offer": job_offer
|
||||
"job_offer": job_offer,
|
||||
"form" : {
|
||||
"remaining_attachments": vec![(); additional_slots]
|
||||
}
|
||||
});
|
||||
|
||||
let body = hb
|
||||
|
|
@ -100,8 +106,6 @@ pub(crate) async fn edit_joboffer_post(
|
|||
JobOffer::<PathBuf>::hash(&offer, &mut orig_hasher);
|
||||
let orig_hash = orig_hasher.finish();
|
||||
|
||||
dbg!(orig_hash);
|
||||
|
||||
if orig_hash != form_data.hash {
|
||||
return Err(EditResponseError::ConflictingChange);
|
||||
}
|
||||
|
|
@ -111,6 +115,7 @@ pub(crate) async fn edit_joboffer_post(
|
|||
offer_mut_ref.title = form_data.title;
|
||||
offer_mut_ref.permanent = form_data.permanent;
|
||||
offer_mut_ref.offering_party = form_data.offering_party;
|
||||
offer_mut_ref.contact_info = form_data.contact_data;
|
||||
offer_mut_ref.public_contact_info = form_data.public_contact_data;
|
||||
|
||||
if let Some(back_date) = form_data.backdate {
|
||||
|
|
@ -121,16 +126,66 @@ pub(crate) async fn edit_joboffer_post(
|
|||
offer_mut_ref.links = form_data.links;
|
||||
|
||||
// currently adding/removing attachments is not supported
|
||||
assert_eq!(offer_mut_ref.attachments.len(), form_data.attachments.len());
|
||||
assert_eq!(
|
||||
offer_mut_ref.attachments.len(),
|
||||
form_data.attachment_edits.len()
|
||||
);
|
||||
|
||||
for (offer, form) in offer_mut_ref
|
||||
.attachments
|
||||
.iter_mut()
|
||||
.zip(form_data.attachments)
|
||||
{
|
||||
offer.title = form.title;
|
||||
offer.file_name = form.file_name;
|
||||
let job_offer_folder = JobOffer::<PathBuf>::folder_path(id, &config);
|
||||
|
||||
let tmp = std::mem::take(&mut offer_mut_ref.attachments)
|
||||
.into_iter()
|
||||
.zip(form_data.attachment_edits)
|
||||
.filter_map(|(mut offer, edit)| match edit {
|
||||
AttachmentEdit::Delete => {
|
||||
// TODO error handling
|
||||
let _todo = std::fs::remove_file(job_offer_folder.join(offer.attachment_location));
|
||||
None
|
||||
}
|
||||
AttachmentEdit::Edit {
|
||||
title,
|
||||
file_name,
|
||||
file,
|
||||
} => {
|
||||
offer.title = title;
|
||||
offer.file_name = file_name;
|
||||
|
||||
if let Some(tmp_file) = file {
|
||||
// TODO error handling
|
||||
let _todo =
|
||||
tmp_file.persist(&job_offer_folder.join(&offer.attachment_location));
|
||||
}
|
||||
|
||||
Some(offer)
|
||||
}
|
||||
});
|
||||
|
||||
offer_mut_ref.attachments.extend(tmp);
|
||||
|
||||
let existing_files: Vec<_> = offer_mut_ref
|
||||
.attachments
|
||||
.iter()
|
||||
.map(|attachment| attachment.attachment_location.clone())
|
||||
.collect();
|
||||
|
||||
let mut idx = 0;
|
||||
|
||||
let mut new_attachments = form_data
|
||||
.attachments_new
|
||||
.into_iter()
|
||||
.map(|attachment| {
|
||||
// deleting attachment might mean the attachment at index 0 uses the file 2.attachment instead of 0.attachment
|
||||
// so we cannot simply use the index this will get inserted at for the file name as that might already be in use
|
||||
while existing_files.contains(&Attachment::attachment_filename(idx)) {
|
||||
idx += 1;
|
||||
}
|
||||
attachment
|
||||
.persist(idx, &job_offer_folder)
|
||||
.map_err(SaveError::Persist)
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
offer_mut_ref.attachments.append(&mut new_attachments);
|
||||
|
||||
offer.try_clean().await?;
|
||||
|
||||
|
|
@ -143,7 +198,6 @@ pub(crate) async fn edit_joboffer_post(
|
|||
.finish())
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub(crate) struct JobOfferEditForm {
|
||||
pub(crate) hash: u64,
|
||||
pub(crate) offering_party: String,
|
||||
|
|
@ -153,17 +207,27 @@ pub(crate) struct JobOfferEditForm {
|
|||
pub(crate) expiry_date: Option<Date>,
|
||||
pub(crate) backdate: Option<Datetime>,
|
||||
pub(crate) title: String,
|
||||
pub(crate) attachments: Vec<Attachment<()>>,
|
||||
pub(crate) attachment_edits: Vec<AttachmentEdit>,
|
||||
pub(crate) attachments_new: Vec<Attachment<NamedTempFile>>,
|
||||
pub(crate) links: Vec<Link>,
|
||||
}
|
||||
|
||||
pub enum AttachmentEdit {
|
||||
Delete,
|
||||
Edit {
|
||||
title: String,
|
||||
file_name: String,
|
||||
file: Option<NamedTempFile>,
|
||||
},
|
||||
}
|
||||
|
||||
impl JobOfferEditForm {
|
||||
/// Convert the Multipart struct representing multipart form-data
|
||||
/// into structured form data
|
||||
async fn from_multipart_form(
|
||||
mut multipart: Multipart,
|
||||
user: Option<&User>,
|
||||
_config: &ServerConfig,
|
||||
config: &ServerConfig,
|
||||
) -> Result<Self, FormProcessingError> {
|
||||
let mut hash = None;
|
||||
let mut offering_party = None;
|
||||
|
|
@ -174,14 +238,20 @@ impl JobOfferEditForm {
|
|||
let mut back_date = None;
|
||||
let mut expiry_date = None;
|
||||
|
||||
let mut attachment_titles = Vec::new();
|
||||
let mut attachment_filenames = Vec::new();
|
||||
let mut attachment_title_edits = Vec::new();
|
||||
let mut attachment_filename_edits = Vec::new();
|
||||
let mut attachment_file_replace = Vec::new();
|
||||
|
||||
let mut delete_attachment = Vec::new();
|
||||
|
||||
let mut attachment_titles_new = Vec::new();
|
||||
let mut attachment_files_new = Vec::new();
|
||||
|
||||
let mut link_titles = Vec::new();
|
||||
let mut link_urls = Vec::new();
|
||||
|
||||
let UploadLimits {
|
||||
size: _upload_size_limit,
|
||||
size: upload_size_limit,
|
||||
count: upload_count_limit,
|
||||
} = form_constants::upload_limits(user);
|
||||
|
||||
|
|
@ -242,28 +312,70 @@ impl JobOfferEditForm {
|
|||
)
|
||||
.await?
|
||||
}
|
||||
form_constants::ATTACHMENT_TITLES => {
|
||||
form_constants::ATTACHMENT_TITLE_EDIT_FIELD => {
|
||||
multi_field(
|
||||
field,
|
||||
form_constants::ATTACHMENT_TITLES,
|
||||
&mut attachment_titles,
|
||||
form_constants::ATTACHMENT_TITLE_EDIT_FIELD,
|
||||
&mut attachment_title_edits,
|
||||
form_constants::MAX_ATTACHMENT_TITLE_LEN,
|
||||
upload_count_limit,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
form_constants::ATTACHMENT_FILE_NAME => {
|
||||
form_constants::ATTACHMENT_FILENAME_EDIT_FIELD => {
|
||||
multi_field(
|
||||
field,
|
||||
form_constants::ATTACHMENT_FILE_NAME,
|
||||
&mut attachment_filenames,
|
||||
form_constants::ATTACHMENT_FILENAME_EDIT_FIELD,
|
||||
&mut attachment_filename_edits,
|
||||
// the submission form does not enforce a limite here as there we get it via a header and
|
||||
// that is already in memory when we could check
|
||||
// that is already in memory when we could perform the check
|
||||
form_constants::MAX_ATTACHMENT_TITLE_LEN,
|
||||
upload_count_limit,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
form_constants::ATTACHMENT_FILE_REPLACE_FIELD => {
|
||||
multi_file(
|
||||
field,
|
||||
form_constants::ATTACHMENT_FILE_REPLACE_FIELD,
|
||||
&mut attachment_file_replace,
|
||||
upload_size_limit,
|
||||
upload_count_limit,
|
||||
&config.config.data_storage_path,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
form_constants::DELETE_ATTACHMENT_FIELD => {
|
||||
multi_field(
|
||||
field,
|
||||
form_constants::DELETE_ATTACHMENT_FIELD,
|
||||
&mut delete_attachment,
|
||||
2,
|
||||
upload_count_limit,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
form_constants::ATTACHMENT_TITLES => {
|
||||
multi_field(
|
||||
field,
|
||||
form_constants::ATTACHMENT_TITLES,
|
||||
&mut attachment_titles_new,
|
||||
form_constants::MAX_ATTACHMENT_TITLE_LEN,
|
||||
upload_count_limit,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
form_constants::ATTACHMENT_FILES => {
|
||||
multi_file(
|
||||
field,
|
||||
form_constants::ATTACHMENT_TITLES,
|
||||
&mut attachment_files_new,
|
||||
upload_size_limit,
|
||||
upload_count_limit,
|
||||
&config.config.data_storage_path,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
form_constants::LINK_TITLES => {
|
||||
multi_field(
|
||||
field,
|
||||
|
|
@ -315,20 +427,48 @@ impl JobOfferEditForm {
|
|||
let expiry_date = parse_date(expiry_date.as_deref())?;
|
||||
let backdate = parse_datetime(back_date.as_deref())?;
|
||||
|
||||
assert_eq!(attachment_filenames.len(), attachment_titles.len());
|
||||
assert_eq!(
|
||||
attachment_filename_edits.len(),
|
||||
attachment_title_edits.len()
|
||||
);
|
||||
// if not set we should still get a field with empty content
|
||||
assert_eq!(attachment_title_edits.len(), attachment_file_replace.len());
|
||||
// we only get a value if delete is checked
|
||||
assert!(delete_attachment.len() <= attachment_filename_edits.len());
|
||||
|
||||
let attachments = attachment_titles
|
||||
let attachment_edits = attachment_title_edits
|
||||
.into_iter()
|
||||
.zip(attachment_filenames.into_iter())
|
||||
.filter_map(|(title, file_name)| {
|
||||
Some(Attachment {
|
||||
.zip(attachment_filename_edits.into_iter())
|
||||
.zip(attachment_file_replace.into_iter())
|
||||
.enumerate()
|
||||
.map(
|
||||
|(idx, ((title, specified_file_name), (upload_file_name, file)))| {
|
||||
if delete_attachment.contains(&idx.to_string()) {
|
||||
// prefer deletion over everything else
|
||||
// maybe we should warn when a file has both been replaced and marked for deletion
|
||||
AttachmentEdit::Delete
|
||||
} else {
|
||||
// prefer the user specified file_name over the uploads file_name
|
||||
let file_name = if specified_file_name.is_empty() {
|
||||
// unless the user specified an empty string
|
||||
upload_file_name
|
||||
} else {
|
||||
specified_file_name
|
||||
};
|
||||
AttachmentEdit::Edit {
|
||||
title,
|
||||
file_name,
|
||||
attachment_location: (),
|
||||
})
|
||||
})
|
||||
file,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
|
||||
assert_eq!(attachment_titles_new.len(), attachment_files_new.len());
|
||||
|
||||
let attachments_new = process_new_attachments(attachment_titles_new, attachment_files_new);
|
||||
|
||||
assert_eq!(link_titles.len(), link_urls.len());
|
||||
|
||||
let links = process_links(link_titles, link_urls);
|
||||
|
|
@ -355,7 +495,8 @@ impl JobOfferEditForm {
|
|||
title: title.ok_or(FormProcessingError::MissingField {
|
||||
field: form_constants::TITLE,
|
||||
})?,
|
||||
attachments,
|
||||
attachment_edits,
|
||||
attachments_new,
|
||||
links,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
28
src/util.rs
28
src/util.rs
|
|
@ -1,8 +1,9 @@
|
|||
use crate::job_offers::Link;
|
||||
use crate::job_offers::{Attachment, Link};
|
||||
use better_toml_datetime::Offset;
|
||||
use chrono::{DateTime, FixedOffset, NaiveDate, Offset as _, TimeZone, Utc};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::str::FromStr;
|
||||
use tempfile::NamedTempFile;
|
||||
use url::Url;
|
||||
|
||||
// we basically don't do any proper error handling here,
|
||||
|
|
@ -200,3 +201,28 @@ pub(crate) fn process_links(titles: Vec<String>, urls: Vec<String>) -> Vec<Link>
|
|||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn process_new_attachments(
|
||||
titles: Vec<String>,
|
||||
files: Vec<(String, Option<NamedTempFile>)>,
|
||||
) -> Vec<Attachment<NamedTempFile>> {
|
||||
titles
|
||||
.into_iter()
|
||||
.zip(files.into_iter())
|
||||
.filter_map(|(title, (file_name, attachment_location))| {
|
||||
attachment_location.map(|attachment_location| (title, file_name, attachment_location))
|
||||
})
|
||||
.enumerate()
|
||||
.map(
|
||||
|(idx, (title, file_name, attachment_location))| Attachment {
|
||||
title: if title.is_empty() {
|
||||
format!("Attachment {}: {}", idx + 1, file_name)
|
||||
} else {
|
||||
title
|
||||
},
|
||||
file_name,
|
||||
attachment_location,
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ body main {
|
|||
margin: 10px;
|
||||
}
|
||||
|
||||
main {
|
||||
.header-main {
|
||||
flex-grow:1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
{{/each}}
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<div class="header-main">
|
||||
{{> header}}
|
||||
<main>
|
||||
{{> @partial-block }}
|
||||
|
|
|
|||
|
|
@ -18,36 +18,28 @@
|
|||
|
||||
<fieldset class="attachment-area">
|
||||
<legend>Anhänge</legend>
|
||||
{{#*inline "attachment" }}
|
||||
{{#each form.attachments }}
|
||||
{{#unless @first}}
|
||||
<hr />
|
||||
{{/unless}}
|
||||
<div>
|
||||
<input type="text" autocomplete="on" name="file_title[]" placeholder="Title"/>
|
||||
<input type="file" accept=".pdf" name="file[]"/>
|
||||
<div/>
|
||||
{{/inline}}
|
||||
{{> attachment }}
|
||||
<hr />
|
||||
{{> attachment }}
|
||||
<hr />
|
||||
{{> attachment }}
|
||||
<hr />
|
||||
{{> attachment }}
|
||||
{{/each}}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="link-area">
|
||||
<legend>Links<sup>2</sup></legend>
|
||||
{{#*inline "link" }}
|
||||
{{#each form.links }}
|
||||
{{#unless @first}}
|
||||
<hr />
|
||||
{{/unless}}
|
||||
<div>
|
||||
<input type="text" autocomplete="on" name="link_title[]" placeholder="Online-Stellenausschreibung" />
|
||||
<input type="url" autocomplete="url" name="link_url[]" pattern="https://.+" placeholder="{{base.routes.index}}" />
|
||||
<input type="url" autocomplete="url" name="link_url[]" pattern="https://.+" placeholder="{{@root.base.routes.index}}" />
|
||||
<div/>
|
||||
{{/inline}}
|
||||
{{> link }}
|
||||
<hr />
|
||||
{{> link }}
|
||||
<hr />
|
||||
{{> link }}
|
||||
<hr />
|
||||
{{> link }}
|
||||
{{/each}}
|
||||
</fieldset>
|
||||
|
||||
<div class="notes">
|
||||
|
|
|
|||
|
|
@ -25,9 +25,24 @@
|
|||
{{#unless @first}}
|
||||
<hr />
|
||||
{{/unless}}
|
||||
<input type="text" autocomplete="on" name="file_title[]" value="{{attachment.title}}" />
|
||||
<input type="text" name="file_name[]" value="{{attachment.file_name}}" />
|
||||
<a href="{{attachment.attachment_location}}" target="_blank">View Attachment</a>
|
||||
<input type="text" autocomplete="on" name="file_title_edit[]" value="{{attachment.title}}" />
|
||||
<input type="text" name="file_name_edit[]" value="{{attachment.file_name}}" />
|
||||
| <a href="{{attachment.attachment_location}}" target="_blank">View Attachment</a>
|
||||
| <label for="delete-{{@index}}">Delete Attachment</label> <input id="delete-{{@index}}" type="checkbox" name="delete_attachment[]" value="{{@index}}" />
|
||||
| <label for="replace-{{@index}}">Replace Attachment</label> <input id="replace-{{@index}}" type="file" accept=".pdf" name="file_replace[]" title="Replace Attachment" />
|
||||
<div/>
|
||||
{{/each}}
|
||||
{{#each form.remaining_attachments }}
|
||||
{{#unless @first}}
|
||||
<hr />
|
||||
{{else}}
|
||||
{{#if ../job_offer.attachments }}
|
||||
<hr />
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
<div>
|
||||
<input type="text" autocomplete="on" name="file_title[]" placeholder="Title"/>
|
||||
<input type="file" accept=".pdf" name="file[]"/>
|
||||
<div/>
|
||||
{{/each}}
|
||||
</fieldset>
|
||||
|
|
@ -39,8 +54,8 @@
|
|||
<hr />
|
||||
{{/unless}}
|
||||
<div>
|
||||
<input type="text" autocomplete="on" name="link_title[]" value="{{link.title}}" />
|
||||
<input type="url" autocomplete="url" name="link_url[]" pattern="https://.+" value="{{link.destination}}" />
|
||||
<input type="text" autocomplete="on" name="link_title[]" {{#if link}}value="{{link.title}}"{{/if}} />
|
||||
<input type="url" autocomplete="url" name="link_url[]" pattern="https://.+" {{#if link}}value="{{link.destination}}"{{/if}} />
|
||||
<div/>
|
||||
{{/each}}
|
||||
</fieldset>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue