features, cleanup and bug fixes #26
12 changed files with 214 additions and 82 deletions
show the user which entries will be deleted with delete expired
commit
8d0eacb79d
12
src/error.rs
12
src/error.rs
|
|
@ -103,24 +103,24 @@ impl ResponseError for MultipartFieldError {
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub(crate) enum PresentationError {
|
pub(crate) enum PresentationError {
|
||||||
#[error("Failed to render page template")]
|
#[error("Failed to render page template: {0}")]
|
||||||
Render(#[from] RenderError),
|
Render(#[from] RenderError),
|
||||||
#[error("Failed to generate URL for route")]
|
#[error("Failed to generate URL for route: {0}")]
|
||||||
Url(#[from] UrlGenerationError),
|
Url(#[from] UrlGenerationError),
|
||||||
|
#[error("Couldn't generate response as login is required")]
|
||||||
|
LoginRequired(#[from] LoginRequired),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for PresentationError {
|
impl ResponseError for PresentationError {
|
||||||
fn status_code(&self) -> StatusCode {
|
fn status_code(&self) -> StatusCode {
|
||||||
match self {
|
match self {
|
||||||
Self::Render(_) | Self::Url(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
Self::Render(_) | Self::Url(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Self::LoginRequired(_) => StatusCode::UNAUTHORIZED,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn error_response(&self) -> HttpResponse<BoxBody> {
|
fn error_response(&self) -> HttpResponse<BoxBody> {
|
||||||
let status_code = self.status_code();
|
let status_code = self.status_code();
|
||||||
match self {
|
default_error_response(self, status_code)
|
||||||
PresentationError::Render(inner) => default_error_response(inner, status_code),
|
|
||||||
PresentationError::Url(inner) => default_error_response(inner, status_code),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -166,13 +166,12 @@ impl JobOfferActions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn job_data(
|
pub(crate) fn job_data<'i, I: Iterator<Item = (&'i str, &'i JobOffer<PathBuf>)>>(
|
||||||
req: &HttpRequest,
|
req: &HttpRequest,
|
||||||
offers: BorrowedJobOffers<'_>,
|
offers: I,
|
||||||
user: Option<&User>,
|
user: Option<&User>,
|
||||||
) -> Vec<JobOfferData> {
|
) -> Vec<JobOfferData> {
|
||||||
let mut data: Vec<_> = offers
|
let mut data: Vec<_> = offers
|
||||||
.iter()
|
|
||||||
.filter_map(|(id, offer)| match offer.to_real_data(id, req, user) {
|
.filter_map(|(id, offer)| match offer.to_real_data(id, req, user) {
|
||||||
Ok(offer) => offer,
|
Ok(offer) => offer,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
@ -766,28 +765,6 @@ impl JobOffers {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn delete_expired(&self, config: &ServerConfig) -> Result<usize, DeleteError> {
|
|
||||||
let read_guard = self.data.read().await;
|
|
||||||
|
|
||||||
let expired_keys = self
|
|
||||||
.data
|
|
||||||
.read()
|
|
||||||
.await
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(key, value)| value.is_expired().then(|| key.clone()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// we need to drop the read guard before we try to acquire a write guard in delete_offer below otherwise we cause a dead-lock
|
|
||||||
drop(read_guard);
|
|
||||||
|
|
||||||
for key in &expired_keys {
|
|
||||||
// to not block readers for too long we perform individual deletes instead of a batch delete
|
|
||||||
self.delete_offer(key, true, config).await?
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(expired_keys.len())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct BorrowedJobOffers<'a>(
|
pub(crate) struct BorrowedJobOffers<'a>(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::error::default_error_response;
|
use crate::error::{default_error_response, LoginRequired};
|
||||||
use actix_web::body::BoxBody;
|
use actix_web::body::BoxBody;
|
||||||
use actix_web::error::UrlGenerationError;
|
use actix_web::error::UrlGenerationError;
|
||||||
use actix_web::{HttpResponse, ResponseError};
|
use actix_web::{HttpResponse, ResponseError};
|
||||||
|
|
@ -20,6 +20,8 @@ pub(crate) enum SaveError {
|
||||||
Email(#[from] EmailError),
|
Email(#[from] EmailError),
|
||||||
#[error("The Runtime encountered an error!")]
|
#[error("The Runtime encountered an error!")]
|
||||||
Runtime(#[from] tokio::task::JoinError),
|
Runtime(#[from] tokio::task::JoinError),
|
||||||
|
#[error("A reviewer-only setting was specified, but no valid session was found.")]
|
||||||
|
LoginRequired(#[from] LoginRequired),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for SaveError {
|
impl ResponseError for SaveError {
|
||||||
|
|
@ -47,6 +49,7 @@ impl ResponseError for SaveError {
|
||||||
| SaveError::Runtime(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
| SaveError::Runtime(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
SaveError::AlreadyExists => StatusCode::CONFLICT,
|
SaveError::AlreadyExists => StatusCode::CONFLICT,
|
||||||
SaveError::Email(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
SaveError::Email(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
SaveError::LoginRequired(_) => StatusCode::UNAUTHORIZED,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn error_response(&self) -> HttpResponse<BoxBody> {
|
fn error_response(&self) -> HttpResponse<BoxBody> {
|
||||||
|
|
|
||||||
53
src/route.rs
53
src/route.rs
|
|
@ -24,7 +24,7 @@ pub(crate) use job_offer::{
|
||||||
pub(crate) use license::{LICENSES_ROUTE, LICENSE_BUNDLE};
|
pub(crate) use license::{LICENSES_ROUTE, LICENSE_BUNDLE};
|
||||||
|
|
||||||
use crate::error::PresentationError;
|
use crate::error::PresentationError;
|
||||||
use crate::route::job_offer::action::JOBOFFER_DELETE_EXPIRED_ROUTE;
|
use crate::route::job_offer::action::{JOBOFFER_BULK_DELETE_ROUTE, JOBOFFER_DELETE_EXPIRED_ROUTE};
|
||||||
use crate::server_config::OperationMode;
|
use crate::server_config::OperationMode;
|
||||||
use crate::server_config::ServerConfig;
|
use crate::server_config::ServerConfig;
|
||||||
|
|
||||||
|
|
@ -86,6 +86,36 @@ struct StaticRoutes {
|
||||||
joboffer_summary: Url,
|
joboffer_summary: Url,
|
||||||
#[serde(serialize_with = "crate::route::url_as_string")]
|
#[serde(serialize_with = "crate::route::url_as_string")]
|
||||||
joboffers_delete_expired: Url,
|
joboffers_delete_expired: Url,
|
||||||
|
#[serde(serialize_with = "crate::route::url_as_string")]
|
||||||
|
joboffers_bulk_delete: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StaticRoutes {
|
||||||
|
fn new(req: &HttpRequest) -> Result<Self, UrlGenerationError> {
|
||||||
|
let licenses_route = req.url_for_static(LICENSES_ROUTE)?;
|
||||||
|
let login_route = req.url_for_static(LOGIN_ROUTE)?;
|
||||||
|
let logout_route = req.url_for_static(LOGOUT_ROUTE)?;
|
||||||
|
let sync_route = req.url_for_static(JOBOFFER_SYNC_ROUTE)?;
|
||||||
|
let index_route = req.url_for_static(INDEX_ROUTE)?;
|
||||||
|
let joboffer_overview_route = req.url_for_static(JOBOFFER_OVERVIEW_ROUTE)?;
|
||||||
|
let joboffer_creation_route = req.url_for_static(JOBOFFER_CREATION_ROUTE)?;
|
||||||
|
let joboffer_summary_route = req.url_for_static(JOBOFFER_SUMMARY_ROUTE)?;
|
||||||
|
let joboffers_delete_expired_route = req.url_for_static(JOBOFFER_DELETE_EXPIRED_ROUTE)?;
|
||||||
|
let joboffers_bulk_delete_route = req.url_for_static(JOBOFFER_BULK_DELETE_ROUTE)?;
|
||||||
|
|
||||||
|
Ok(StaticRoutes {
|
||||||
|
licenses: licenses_route,
|
||||||
|
login: login_route,
|
||||||
|
logout: logout_route,
|
||||||
|
sync: sync_route,
|
||||||
|
index: index_route,
|
||||||
|
joboffer_overview: joboffer_overview_route,
|
||||||
|
joboffer_create: joboffer_creation_route,
|
||||||
|
joboffer_summary: joboffer_summary_route,
|
||||||
|
joboffers_delete_expired: joboffers_delete_expired_route,
|
||||||
|
joboffers_bulk_delete: joboffers_bulk_delete_route,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn url_as_string<S>(url: &Url, serializer: S) -> Result<S::Ok, S::Error>
|
fn url_as_string<S>(url: &Url, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
|
@ -116,15 +146,6 @@ fn base<'a>(
|
||||||
config: &'a ServerConfig,
|
config: &'a ServerConfig,
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
) -> Result<BaseData<'a>, UrlGenerationError> {
|
) -> Result<BaseData<'a>, UrlGenerationError> {
|
||||||
let licenses_route = req.url_for_static(LICENSES_ROUTE)?;
|
|
||||||
let login_route = req.url_for_static(LOGIN_ROUTE)?;
|
|
||||||
let logout_route = req.url_for_static(LOGOUT_ROUTE)?;
|
|
||||||
let sync_route = req.url_for_static(JOBOFFER_SYNC_ROUTE)?;
|
|
||||||
let index_route = req.url_for_static(INDEX_ROUTE)?;
|
|
||||||
let joboffer_overview_route = req.url_for_static(JOBOFFER_OVERVIEW_ROUTE)?;
|
|
||||||
let joboffer_creation_route = req.url_for_static(JOBOFFER_CREATION_ROUTE)?;
|
|
||||||
let joboffer_summary_route = req.url_for_static(JOBOFFER_SUMMARY_ROUTE)?;
|
|
||||||
let joboffers_delete_expired_route = req.url_for_static(JOBOFFER_DELETE_EXPIRED_ROUTE)?;
|
|
||||||
let index_css = req.url_for_static(INDEX_CSS_ROUTE)?;
|
let index_css = req.url_for_static(INDEX_CSS_ROUTE)?;
|
||||||
|
|
||||||
let mut default_links = vec![(
|
let mut default_links = vec![(
|
||||||
|
|
@ -165,17 +186,7 @@ fn base<'a>(
|
||||||
short_lang: "de".into(),
|
short_lang: "de".into(),
|
||||||
styles: vec![index_css],
|
styles: vec![index_css],
|
||||||
links,
|
links,
|
||||||
routes: StaticRoutes {
|
routes: StaticRoutes::new(req)?,
|
||||||
licenses: licenses_route,
|
|
||||||
login: login_route,
|
|
||||||
logout: logout_route,
|
|
||||||
sync: sync_route,
|
|
||||||
index: index_route,
|
|
||||||
joboffer_overview: joboffer_overview_route,
|
|
||||||
joboffer_create: joboffer_creation_route,
|
|
||||||
joboffer_summary: joboffer_summary_route,
|
|
||||||
joboffers_delete_expired: joboffers_delete_expired_route,
|
|
||||||
},
|
|
||||||
banner: config.config.banner.clone(),
|
banner: config.config.banner.clone(),
|
||||||
operation_mode: config.args.mode.clone(),
|
operation_mode: config.args.mode.clone(),
|
||||||
dev_build: dev_available,
|
dev_build: dev_available,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use actix_web::web::ServiceConfig;
|
||||||
use actix_web::{get, http, post, web, HttpRequest, HttpResponse, Responder};
|
use actix_web::{get, http, post, web, HttpRequest, HttpResponse, Responder};
|
||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
use log::{error, trace};
|
use log::{error, trace};
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::error::{AuthenticationError, PresentationError};
|
use crate::error::{AuthenticationError, PresentationError};
|
||||||
|
|
@ -28,17 +28,10 @@ pub(crate) const LOGIN_ROUTE: &str = "login";
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub(crate) struct LoginQuery {
|
pub(crate) struct LoginQuery {
|
||||||
return_to: Option<String>,
|
return_to: Option<String>,
|
||||||
#[serde(deserialize_with = "boolean_from_flag", default)]
|
#[serde(deserialize_with = "crate::util::boolean_from_flag", default)]
|
||||||
retry: bool,
|
retry: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn boolean_from_flag<'de, D>(des: D) -> Result<bool, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
<Option<String> as Deserialize>::deserialize(des).map(|option| option.is_some())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/login", name = "login")]
|
#[get("/login", name = "login")]
|
||||||
pub(crate) async fn login_get(
|
pub(crate) async fn login_get(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use crate::job_offers::error::{DeleteError, SaveError};
|
||||||
use crate::route::LOGIN_ROUTE;
|
use crate::route::LOGIN_ROUTE;
|
||||||
use crate::{route, ServerConfig};
|
use crate::{route, ServerConfig};
|
||||||
|
|
||||||
use crate::route::job_offer::error::{ConfirmationError, SubmissionError};
|
use crate::route::job_offer::error::{ConfirmationError, DeletionError, SubmissionError};
|
||||||
use actix_session::SessionExt;
|
use actix_session::SessionExt;
|
||||||
use actix_web::dev::ServiceResponse;
|
use actix_web::dev::ServiceResponse;
|
||||||
use actix_web::error::UrlGenerationError;
|
use actix_web::error::UrlGenerationError;
|
||||||
|
|
@ -184,8 +184,22 @@ pub(crate) fn bad_request<B>(
|
||||||
warn!("Unexpected MultipartFieldError as top level error!");
|
warn!("Unexpected MultipartFieldError as top level error!");
|
||||||
msg = err.to_string();
|
msg = err.to_string();
|
||||||
Some(msg.as_str())
|
Some(msg.as_str())
|
||||||
|
} else if let Some(err) = err.as_error::<DeletionError>() {
|
||||||
|
match err {
|
||||||
|
DeletionError::Delete(err) => {
|
||||||
|
warn!("Couldn't delete Job Offer: {}", err);
|
||||||
|
Some("Could not delete a Job Offer")
|
||||||
|
}
|
||||||
|
DeletionError::Login(_) => {
|
||||||
|
error!(
|
||||||
|
"Response Status Code (Bad Request) and Error appear to disagree : {}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Bad Request Error of unknown type!");
|
warn!("Bad Request Error of unknown type!: {}", err);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -211,7 +225,28 @@ pub(crate) fn unauthorized_error_handler<B>(
|
||||||
if let Some(return_url) = res
|
if let Some(return_url) = res
|
||||||
.response()
|
.response()
|
||||||
.error()
|
.error()
|
||||||
.and_then(|err| err.as_error::<LoginRequired>())
|
.and_then(|err| {
|
||||||
|
err.as_error::<LoginRequired>()
|
||||||
|
.or_else(|| {
|
||||||
|
err.as_error::<SaveError>().and_then(|elem| match elem {
|
||||||
|
SaveError::LoginRequired(login) => Some(login),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
err.as_error::<DeletionError>().and_then(|elem| match elem {
|
||||||
|
DeletionError::Login(login) => Some(login),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
err.as_error::<PresentationError>()
|
||||||
|
.and_then(|elem| match elem {
|
||||||
|
PresentationError::LoginRequired(login) => Some(login),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
.and_then(|err| err.get_return())
|
.and_then(|err| err.get_return())
|
||||||
{
|
{
|
||||||
// we have a LoginRequired error type with a return_to URL set, redirect the user to the login page with the
|
// we have a LoginRequired error type with a return_to URL set, redirect the user to the login page with the
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ pub fn configure(service: &mut ServiceConfig) {
|
||||||
.service(confirmation::confirm_joboffer_post)
|
.service(confirmation::confirm_joboffer_post)
|
||||||
.service(confirmation::reject_joboffer_post)
|
.service(confirmation::reject_joboffer_post)
|
||||||
.service(action::delete_joboffer)
|
.service(action::delete_joboffer)
|
||||||
|
.service(action::bulk_delete)
|
||||||
.service(action::delete_expired_joboffers)
|
.service(action::delete_expired_joboffers)
|
||||||
.service(action::review_joboffer)
|
.service(action::review_joboffer)
|
||||||
.service(action::unpublish_joboffer)
|
.service(action::unpublish_joboffer)
|
||||||
|
|
@ -54,7 +55,7 @@ pub(crate) async fn index(
|
||||||
|
|
||||||
let job_offers = {
|
let job_offers = {
|
||||||
let guard = offers.get_offers().await;
|
let guard = offers.get_offers().await;
|
||||||
crate::job_offers::job_data(&req, guard, user.as_ref())
|
crate::job_offers::job_data(&req, guard.iter(), user.as_ref())
|
||||||
};
|
};
|
||||||
|
|
||||||
let base_data = super::base(&req, &config, "Joboffers")?;
|
let base_data = super::base(&req, &config, "Joboffers")?;
|
||||||
|
|
@ -157,7 +158,7 @@ pub(crate) async fn summary(
|
||||||
let user = User::current(&session).ok();
|
let user = User::current(&session).ok();
|
||||||
let previews = {
|
let previews = {
|
||||||
let guard = offers.get_offers().await;
|
let guard = offers.get_offers().await;
|
||||||
crate::job_offers::job_data(&req, guard, user.as_ref())
|
crate::job_offers::job_data(&req, guard.iter(), user.as_ref())
|
||||||
};
|
};
|
||||||
let data = json!(previews);
|
let data = json!(previews);
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
use actix_session::Session;
|
use actix_session::Session;
|
||||||
use actix_web::{http, post, web, HttpRequest, HttpResponse};
|
use actix_web::{get, http, post, web, HttpRequest, HttpResponse};
|
||||||
|
use handlebars::Handlebars;
|
||||||
|
use log::debug;
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::auth::User;
|
||||||
|
use crate::error::PresentationError;
|
||||||
use crate::route::job_offer::error::{DeletionError, StateChangeError};
|
use crate::route::job_offer::error::{DeletionError, StateChangeError};
|
||||||
use crate::route::JOBOFFER_OVERVIEW_ROUTE;
|
use crate::route::{HTML_CONTENT, JOBOFFER_OVERVIEW_ROUTE};
|
||||||
use crate::{auth, JobOffers, ServerConfig};
|
use crate::{auth, template, JobOffers, ServerConfig};
|
||||||
|
|
||||||
pub(crate) const JOBOFFER_DELETION_ROUTE: &str = "joboffer_delete";
|
pub(crate) const JOBOFFER_DELETION_ROUTE: &str = "joboffer_delete";
|
||||||
|
|
||||||
|
|
@ -34,18 +40,101 @@ pub(crate) async fn delete_joboffer(
|
||||||
|
|
||||||
pub(crate) const JOBOFFER_DELETE_EXPIRED_ROUTE: &str = "joboffers_delete_expired";
|
pub(crate) const JOBOFFER_DELETE_EXPIRED_ROUTE: &str = "joboffers_delete_expired";
|
||||||
|
|
||||||
#[post("/delete_expired", name = "joboffers_delete_expired")]
|
#[get("/delete_expired", name = "joboffers_delete_expired")]
|
||||||
pub(crate) async fn delete_expired_joboffers(
|
pub(crate) async fn delete_expired_joboffers(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
|
hb: web::Data<Handlebars<'_>>,
|
||||||
offers: web::Data<JobOffers>,
|
offers: web::Data<JobOffers>,
|
||||||
config: web::Data<ServerConfig>,
|
config: web::Data<ServerConfig>,
|
||||||
session: Session,
|
session: Session,
|
||||||
) -> actix_web::Result<HttpResponse, DeletionError> {
|
) -> actix_web::Result<HttpResponse, PresentationError> {
|
||||||
// TODO return the user to a page where they are asked to confirm deletion
|
let user = User::current(&session)?;
|
||||||
// aka. the get variant of this route
|
let offers_guard = offers.get_offers().await;
|
||||||
let _user = auth::User::current(&session)?;
|
|
||||||
|
|
||||||
offers.delete_expired(&config).await?;
|
let job_offers = crate::job_offers::job_data(
|
||||||
|
&req,
|
||||||
|
offers_guard.iter().filter(|(_, offer)| offer.is_expired()),
|
||||||
|
Some(&user),
|
||||||
|
);
|
||||||
|
|
||||||
|
let base = crate::route::base(&req, &config, "Delete Expired")?;
|
||||||
|
|
||||||
|
let data = json! {{
|
||||||
|
"base": base,
|
||||||
|
"expired_job_offers": job_offers,
|
||||||
|
}};
|
||||||
|
|
||||||
|
let rendered = hb.render(template::JOBOFFER_DELETE_EXPIRED, &data)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.insert_header((http::header::CONTENT_TYPE, HTML_CONTENT.clone()))
|
||||||
|
.body(rendered))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct BulkDeleteData {
|
||||||
|
only_expired: bool,
|
||||||
|
ids: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for BulkDeleteData {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::de::Error;
|
||||||
|
|
||||||
|
let key_value_pairs = <Vec<(String, String)>>::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
let mut only_expired = None;
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
|
||||||
|
for (key, value) in key_value_pairs {
|
||||||
|
match key.as_str() {
|
||||||
|
"id[]" => ids.push(value),
|
||||||
|
"only_expired" => {
|
||||||
|
if only_expired.is_some() {
|
||||||
|
return Err(D::Error::duplicate_field("only_expired"));
|
||||||
|
}
|
||||||
|
match value.as_str() {
|
||||||
|
"true" => only_expired = Some(true),
|
||||||
|
"false" => only_expired = Some(false),
|
||||||
|
value => {
|
||||||
|
return Err(D::Error::invalid_value(
|
||||||
|
serde::de::Unexpected::Other(value),
|
||||||
|
&"either 'true' or 'false'",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
field => return Err(D::Error::unknown_field(field, &["id[]", "only_expired"])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(only_expired) = only_expired {
|
||||||
|
Ok(BulkDeleteData { only_expired, ids })
|
||||||
|
} else {
|
||||||
|
Err(D::Error::missing_field("only_expired"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const JOBOFFER_BULK_DELETE_ROUTE: &str = "joboffers_bulk_delete";
|
||||||
|
|
||||||
|
#[post("/bulk_delete", name = "joboffers_bulk_delete")]
|
||||||
|
pub(crate) async fn bulk_delete(
|
||||||
|
req: HttpRequest,
|
||||||
|
offers: web::Data<JobOffers>,
|
||||||
|
config: web::Data<ServerConfig>,
|
||||||
|
form: web::Form<BulkDeleteData>,
|
||||||
|
session: Session,
|
||||||
|
) -> actix_web::Result<HttpResponse, DeletionError> {
|
||||||
|
debug!("Received bulk deletion request!");
|
||||||
|
|
||||||
|
let _user = User::current(&session)?;
|
||||||
|
|
||||||
|
for id in &form.ids {
|
||||||
|
offers.delete_offer(id, form.only_expired, &config).await?;
|
||||||
|
}
|
||||||
|
|
||||||
let dest = req
|
let dest = req
|
||||||
.url_for_static(JOBOFFER_OVERVIEW_ROUTE)
|
.url_for_static(JOBOFFER_OVERVIEW_ROUTE)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ pub(crate) const JOBOFFER_ATTACHMENT_PREVIEW: &str = "job_offer/attachement-prev
|
||||||
pub(crate) const JOBOFFER_CONFIRM_SUBMISSION: &str = "job_offer/submission-confirm";
|
pub(crate) const JOBOFFER_CONFIRM_SUBMISSION: &str = "job_offer/submission-confirm";
|
||||||
pub(crate) const JOBOFFER_CONFIRM_SUBMISSION_SUCCESS: &str = "job_offer/submission-confirm-success";
|
pub(crate) const JOBOFFER_CONFIRM_SUBMISSION_SUCCESS: &str = "job_offer/submission-confirm-success";
|
||||||
pub(crate) const JOBOFFER_REJECT_SUBMISSION_SUCCESS: &str = "job_offer/submission-rejected-success";
|
pub(crate) const JOBOFFER_REJECT_SUBMISSION_SUCCESS: &str = "job_offer/submission-rejected-success";
|
||||||
|
pub(crate) const JOBOFFER_DELETE_EXPIRED: &str = "job_offer/delete-expired";
|
||||||
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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use chrono::{DateTime, FixedOffset, NaiveDate, Offset as _, TimeZone, Utc};
|
use chrono::{DateTime, FixedOffset, NaiveDate, Offset as _, TimeZone, Utc};
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
use toml::value::Offset;
|
use toml::value::Offset;
|
||||||
|
|
||||||
// we basically don't do any proper error handling here,
|
// we basically don't do any proper error handling here,
|
||||||
|
|
@ -123,3 +124,10 @@ pub fn toml_datetime_to_chrono_datetime(
|
||||||
|
|
||||||
offset.from_local_datetime(&local_datetime).unwrap()
|
offset.from_local_datetime(&local_datetime).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn boolean_from_flag<'de, D>(des: D) -> Result<bool, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
<Option<String> as Deserialize>::deserialize(des).map(|option| option.is_some())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,7 @@
|
||||||
<span>|</span><a class="footer-element" href="{{base.routes.licenses}}"><h3 class="inline">Third-party Licenses</h3></a>
|
<span>|</span><a class="footer-element" href="{{base.routes.licenses}}"><h3 class="inline">Third-party Licenses</h3></a>
|
||||||
<span>|</span><a class="footer-element" href="{{base.routes.joboffer_create}}"><h3 class="inline">Stellenanzeige Einreichen</h3></a>
|
<span>|</span><a class="footer-element" href="{{base.routes.joboffer_create}}"><h3 class="inline">Stellenanzeige Einreichen</h3></a>
|
||||||
{{#if user}}
|
{{#if user}}
|
||||||
<span>|</span><label class="footer-element" for="submit-delete-expired" tabindex="0">
|
<span>|</span><a class="footer-element" href="{{base.routes.joboffers_delete_expired}}" title="Delete all expired Job Offers"><h3 class="inline">Delete Expired</h3></a>
|
||||||
<h3 class="inline" title="Delete all expired Job Offers">Delete Expired</h3>
|
|
||||||
<form class="hidden" method="post" action="{{base.routes.joboffers_delete_expired}}">
|
|
||||||
<input type="submit" id="submit-delete-expired" class="hidden">
|
|
||||||
</form>
|
|
||||||
</label>
|
|
||||||
<span>|</span><label class="footer-element" for="submit-sync" tabindex="0">
|
<span>|</span><label class="footer-element" for="submit-sync" tabindex="0">
|
||||||
<h3 class="inline" title="Reload Joboffer Metadata from Disk">Re-Sync</h3>
|
<h3 class="inline" title="Reload Joboffer Metadata from Disk">Re-Sync</h3>
|
||||||
<form class="hidden" method="post" action="{{base.routes.sync}}">
|
<form class="hidden" method="post" action="{{base.routes.sync}}">
|
||||||
|
|
|
||||||
19
templates/job_offer/delete-expired.hb
Normal file
19
templates/job_offer/delete-expired.hb
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{{#> base}}
|
||||||
|
<form method="post" action="{{base.routes.joboffers_bulk_delete}}">
|
||||||
|
<div class="joboffer-index">
|
||||||
|
{{#each expired_job_offers as |job_offer|}}
|
||||||
|
<label for="check-{{job_offer.id}}">
|
||||||
|
<input id="check-{{job_offer.id}}" type="checkbox" name="id[]" value="{{job_offer.id}}" checked="checked">
|
||||||
|
{{> job_offer/overview-entry job_offer=job_offer base=../base user=../user}}
|
||||||
|
</label>
|
||||||
|
{{else}}
|
||||||
|
There are no expired job offers to delete!
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{#if expired_job_offers }}
|
||||||
|
<input type="hidden" name="only_expired" value="true">
|
||||||
|
<hr />
|
||||||
|
<button type="submit">Delete selected Job Offers</button>
|
||||||
|
{{/if}}
|
||||||
|
</form>
|
||||||
|
{{/base}}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue