From 53be6cb72dd1c8cf7a3250b1cfb41cf5f5a6f1d8 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Fri, 16 Dec 2022 11:44:32 +0100 Subject: [PATCH 1/4] feat: multiple parent csv export --- core/src/database/query/candidate.rs | 17 +++++------------ core/src/database/query/parent.rs | 2 +- core/src/models/candidate.rs | 7 ++++++- core/src/models/candidate_details.rs | 6 +++--- core/src/services/application_service.rs | 2 +- core/src/services/candidate_service.rs | 6 +++--- core/src/services/parent_service.rs | 6 +++--- core/src/utils/csv.rs | 24 +++++++++++++----------- 8 files changed, 35 insertions(+), 35 deletions(-) diff --git a/core/src/database/query/candidate.rs b/core/src/database/query/candidate.rs index f41cd0b..3077dfb 100644 --- a/core/src/database/query/candidate.rs +++ b/core/src/database/query/candidate.rs @@ -2,7 +2,7 @@ use sea_orm::*; use ::entity::{candidate, candidate::Entity as Candidate, parent}; -use crate::{Query, models::candidate::CandidateWithParent}; +use crate::Query; pub const PAGE_SIZE: u64 = 20; @@ -39,7 +39,7 @@ impl Query { .await } - pub async fn list_candidates( + pub async fn list_candidates_preview( db: &DbConn, field_of_study_opt: Option, page: Option, @@ -64,18 +64,11 @@ impl Query { } } - - pub async fn list_all_candidates_with_parents( - db: &DbConn, - ) -> Result, DbErr> { + pub async fn list_candidates_full( + db: &DbConn + ) -> Result, DbErr> { Candidate::find() .order_by(candidate::Column::Application, Order::Asc) - .join(JoinType::InnerJoin, candidate::Relation::Parent.def()) - .column_as(parent::Column::Name, "parent_name") - .column_as(parent::Column::Surname, "parent_surname") - .column_as(parent::Column::Telephone, "parent_telephone") - .column_as(parent::Column::Email, "parent_email") - .into_model::() .all(db) .await } diff --git a/core/src/database/query/parent.rs b/core/src/database/query/parent.rs index 52fd6e7..a325458 100644 --- a/core/src/database/query/parent.rs +++ b/core/src/database/query/parent.rs @@ -21,7 +21,7 @@ impl Query { pub async fn find_candidate_parents( db: &DbConn, - candidate: candidate::Model, + candidate: &candidate::Model, ) -> Result, DbErr> { candidate.find_related(parent::Entity) diff --git a/core/src/models/candidate.rs b/core/src/models/candidate.rs index a93e44a..3df8967 100644 --- a/core/src/models/candidate.rs +++ b/core/src/models/candidate.rs @@ -63,7 +63,7 @@ pub struct ApplicationDetails { /// CSV export (admin endpoint) #[derive(FromQueryResult, Serialize, Default)] #[serde(rename_all = "camelCase")] -pub struct CandidateWithParent { +pub struct Row { pub application: i32, pub name: Option, pub surname: Option, @@ -81,6 +81,11 @@ pub struct CandidateWithParent { pub parent_surname: Option, pub parent_telephone: Option, pub parent_email: Option, + + pub second_parent_name: Option, + pub second_parent_surname: Option, + pub second_parent_telephone: Option, + pub second_parent_email: Option, } impl BaseCandidateResponse { diff --git a/core/src/models/candidate_details.rs b/core/src/models/candidate_details.rs index 912e6c9..96fe25f 100644 --- a/core/src/models/candidate_details.rs +++ b/core/src/models/candidate_details.rs @@ -3,7 +3,7 @@ use chrono::NaiveDate; use entity::{candidate, parent}; use futures::future; -use crate::{crypto, models::candidate::{CandidateWithParent, ApplicationDetails}, error::ServiceError}; +use crate::{crypto, models::candidate::{Row, ApplicationDetails}, error::ServiceError}; use super::candidate::{CandidateDetails, ParentDetails}; @@ -290,11 +290,11 @@ impl TryFrom<(candidate::Model, Vec)> for EncryptedApplicationDet } } -impl TryFrom for EncryptedApplicationDetails { +impl TryFrom for EncryptedApplicationDetails { type Error = ServiceError; fn try_from( - cp: CandidateWithParent, + cp: Row, ) -> Result { Ok(EncryptedApplicationDetails { candidate: EncryptedCandidateDetails { diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index 6c225e3..933d223 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -43,7 +43,7 @@ impl ApplicationService { candidate: candidate::Model, // parents: Vec, ) -> Result { - let parents = Query::find_candidate_parents(db, candidate.clone()).await?; + let parents = Query::find_candidate_parents(db, &candidate).await?; let enc_details = EncryptedApplicationDetails::try_from((candidate, parents))?; enc_details.decrypt(private_key).await diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index a22ea53..c7f52cd 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -102,7 +102,7 @@ impl CandidateService { ) -> Result { let candidate = Query::find_candidate_by_id(db, id).await? .ok_or(ServiceError::CandidateNotFound)?; - let parents = Query::find_candidate_parents(db, candidate.clone()).await?; + let parents = Query::find_candidate_parents(db, &candidate).await?; let new_password_plain = crypto::random_8_char_string(); @@ -163,7 +163,7 @@ impl CandidateService { page: Option, ) -> Result, ServiceError> { - let candidates = Query::list_candidates( + let candidates = Query::list_candidates_preview( db, field_of_study, page @@ -374,7 +374,7 @@ pub mod tests { #[cfg(test)] pub async fn put_user_data(db: &DbConn) -> (candidate::Model, Vec) { - use crate::{models::candidate_details::tests::APPLICATION_DETAILS, Query}; + use crate::{models::candidate_details::tests::APPLICATION_DETAILS}; let plain_text_password = "test".to_string(); let (candidate, _parent) = ApplicationService::create_candidate_with_parent( diff --git a/core/src/services/parent_service.rs b/core/src/services/parent_service.rs index c3aee6c..0ba5779 100644 --- a/core/src/services/parent_service.rs +++ b/core/src/services/parent_service.rs @@ -1,7 +1,7 @@ use entity::{parent, candidate}; use sea_orm::DbConn; -use crate::{error::ServiceError, Mutation, models::{candidate_details::{EncryptedParentDetails}, candidate::ParentDetails}, Query, utils::db::get_recipients}; +use crate::{error::ServiceError, Mutation, models::{candidate_details::{EncryptedParentDetails}, candidate::ParentDetails}, Query}; pub struct ParentService; @@ -22,7 +22,7 @@ impl ParentService { parents_details: &Vec, recipients: &Vec, ) -> Result, ServiceError> { - let found_parents = Query::find_candidate_parents(db, ref_candidate.clone()).await?; + let found_parents = Query::find_candidate_parents(db, &ref_candidate).await?; if found_parents.len() > 2 { return Err(ServiceError::ParentOverflow); } @@ -48,7 +48,7 @@ mod tests { use once_cell::sync::Lazy; - use crate::{utils::db::get_memory_sqlite_connection, models::{candidate::{ParentDetails, ApplicationDetails, CandidateDetails}, candidate_details::{tests::APPLICATION_DETAILS, EncryptedParentDetails, EncryptedApplicationDetails}}, services::{candidate_service::CandidateService, application_service::ApplicationService}, crypto}; + use crate::{utils::db::get_memory_sqlite_connection, models::{candidate::{ParentDetails, ApplicationDetails, CandidateDetails}, candidate_details::EncryptedApplicationDetails}, services::{candidate_service::CandidateService, application_service::ApplicationService}, crypto}; use super::ParentService; diff --git a/core/src/utils/csv.rs b/core/src/utils/csv.rs index c741628..be3467f 100644 --- a/core/src/utils/csv.rs +++ b/core/src/utils/csv.rs @@ -1,13 +1,9 @@ use sea_orm::{DbConn}; -use crate::{error::ServiceError, models::candidate_details::{EncryptedApplicationDetails}, Query, models::candidate::{CandidateWithParent, ApplicationDetails}}; - - -type Row = CandidateWithParent; +use crate::{error::ServiceError, models::candidate_details::{EncryptedApplicationDetails}, Query, models::candidate::{Row, ApplicationDetails}}; impl From<(i32, ApplicationDetails)> for Row { fn from((application, d): (i32, ApplicationDetails)) -> Self { let c = d.candidate; - let p = d.parents[0].clone(); Self { application, name: Some(c.name), @@ -22,10 +18,15 @@ impl From<(i32, ApplicationDetails)> for Row { study: Some(c.study), personal_identification_number: Some(c.personal_id_number), - parent_name: Some(p.name), - parent_surname: Some(p.surname), - parent_telephone: Some(p.telephone), - parent_email: Some(p.email), + parent_name: d.parents.get(0).map(|p| p.name.clone()), + parent_surname: d.parents.get(0).map(|p| p.surname.clone()), + parent_telephone: d.parents.get(0).map(|p| p.telephone.clone()), + parent_email: d.parents.get(0).map(|p| p.email.clone()), + + second_parent_name: d.parents.get(1).map(|p| p.name.clone()), + second_parent_surname: d.parents.get(1).map(|p| p.surname.clone()), + second_parent_telephone: d.parents.get(1).map(|p| p.telephone.clone()), + second_parent_email: d.parents.get(1).map(|p| p.email.clone()), } } } @@ -36,11 +37,12 @@ pub async fn export( ) -> Result, ServiceError> { let mut wtr = csv::Writer::from_writer(vec![]); - let candidates_with_parents = Query::list_all_candidates_with_parents(&db).await?; + let candidates_with_parents = Query::list_candidates_full(&db).await?; for candidate in candidates_with_parents { let application = candidate.application; + let parents = Query::find_candidate_parents(db, &candidate).await?; - let row: Row = match EncryptedApplicationDetails::try_from(candidate) { + let row: Row = match EncryptedApplicationDetails::try_from((candidate, parents)) { Ok(d) => Row::from( d .decrypt(private_key.to_string()) From 91e411a390511e47d4f3ed167449d2efa5cf7f64 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Fri, 16 Dec 2022 11:59:25 +0100 Subject: [PATCH 2/4] feat: admin csv endpoint --- api/src/lib.rs | 8 +++++++- api/src/routes/admin.rs | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/api/src/lib.rs b/api/src/lib.rs index 29499c0..2bc516d 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -152,7 +152,13 @@ pub fn rocket() -> Rocket { routes::admin::get_candidate_portfolio, ], ) - .mount("/admin/list", routes![routes::admin::list_candidates,]) + .mount( + "/admin/list", + routes![ + routes::admin::list_candidates, + routes::admin::list_candidates_csv, + ] + ) .register("/", catchers![]) } diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index aac339f..25d9a57 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -2,7 +2,7 @@ use std::net::{SocketAddr, IpAddr, Ipv4Addr}; use portfolio_core::{ crypto::random_8_char_string, - services::{admin_service::AdminService, candidate_service::CandidateService, application_service::ApplicationService, portfolio_service::PortfolioService}, models::candidate::{BaseCandidateResponse, CreateCandidateResponse, ApplicationDetails}, sea_orm::prelude::Uuid, Query, error::ServiceError, + services::{admin_service::AdminService, candidate_service::CandidateService, application_service::ApplicationService, portfolio_service::PortfolioService}, models::candidate::{BaseCandidateResponse, CreateCandidateResponse, ApplicationDetails}, sea_orm::prelude::Uuid, Query, error::ServiceError, utils::csv, }; use requests::{AdminLoginRequest, RegisterRequest}; use rocket::http::{Cookie, Status, CookieJar}; @@ -137,6 +137,23 @@ pub async fn list_candidates( ) } +#[get("/candidates_csv")] +pub async fn list_candidates_csv( + conn: Connection<'_, Db>, + session: AdminAuth, +) -> Result, Custom> { + let db = conn.into_inner(); + let private_key = session.get_private_key(); + + let candidates = csv::export(db, private_key) + .await + .map_err(to_custom_error)?; + + Ok( + candidates + ) +} + #[get("/candidate/")] pub async fn get_candidate( conn: Connection<'_, Db>, From 9367013132482b8ff49670af67e6dbe703534f7a Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Fri, 16 Dec 2022 11:59:54 +0100 Subject: [PATCH 3/4] fix: warning --- api/src/routes/admin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 25d9a57..5bea5e5 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -59,7 +59,7 @@ pub async fn logout(conn: Connection<'_, Db>, _session: AdminAuth, cookies: &Coo let session_id = Uuid::try_parse(cookie.value()) // unwrap would be safe here because of the auth guard .map_err(|e| Custom(Status::BadRequest, e.to_string()))?; - let res = AdminService::logout(db, session_id) + let _res = AdminService::logout(db, session_id) .await .map_err(to_custom_error)?; From cdd7f0b5cf5da6de86727865a0d78c14dd1e8a5d Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Fri, 16 Dec 2022 12:14:16 +0100 Subject: [PATCH 4/4] feat: candidate delete endpoint --- api/src/lib.rs | 1 + api/src/routes/admin.rs | 18 ++++++++++++++++++ core/src/database/mutation/candidate.rs | 11 +++++++++++ core/src/services/candidate_service.rs | 9 ++++++++- core/src/services/portfolio_service.rs | 14 +++++++++++++- 5 files changed, 51 insertions(+), 2 deletions(-) diff --git a/api/src/lib.rs b/api/src/lib.rs index 2bc516d..22e6166 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -150,6 +150,7 @@ pub fn rocket() -> Rocket { routes::admin::get_candidate, routes::admin::reset_candidate_password, routes::admin::get_candidate_portfolio, + routes::admin::delete_candidate, ], ) .mount( diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 5bea5e5..c8e807e 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -181,6 +181,24 @@ pub async fn get_candidate( ) } +#[delete("/candidate/")] +pub async fn delete_candidate( + conn: Connection<'_, Db>, + _session: AdminAuth, + id: i32, +) -> Result<(), Custom> { + let db = conn.into_inner(); + + let candidate = Query::find_candidate_by_id(db, id) + .await + .map_err(|e| to_custom_error(ServiceError::DbError(e)))? + .ok_or(to_custom_error(ServiceError::CandidateNotFound))?; + + CandidateService::delete_candidate(db, candidate) + .await + .map_err(to_custom_error) +} + #[post("/candidate//reset_password")] pub async fn reset_candidate_password( conn: Connection<'_, Db>, diff --git a/core/src/database/mutation/candidate.rs b/core/src/database/mutation/candidate.rs index 411aa05..526a1de 100644 --- a/core/src/database/mutation/candidate.rs +++ b/core/src/database/mutation/candidate.rs @@ -30,6 +30,17 @@ impl Mutation { Ok(insert) } + pub async fn delete_candidate( + db: &DbConn, + candidate: candidate::Model, + ) -> Result { + let application = candidate.application; + let delete = candidate.delete(db).await?; + + warn!("CANDIDATE {} DELETED", application); + Ok(delete) + } + pub async fn update_candidate_password_and_keys( db: &DbConn, candidate: candidate::Model, diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index c7f52cd..d38d365 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -145,6 +145,13 @@ impl CandidateService { Ok(()) } + pub async fn delete_candidate(db: &DbConn, candidate: candidate::Model) -> Result<(), ServiceError> { + PortfolioService::delete_candidate_root(candidate.application).await?; + + Mutation::delete_candidate(db, candidate).await?; + Ok(()) + } + pub(in crate::services) async fn add_candidate_details( db: &DbConn, candidate: candidate::Model, @@ -374,7 +381,7 @@ pub mod tests { #[cfg(test)] pub async fn put_user_data(db: &DbConn) -> (candidate::Model, Vec) { - use crate::{models::candidate_details::tests::APPLICATION_DETAILS}; + use crate::models::candidate_details::tests::APPLICATION_DETAILS; let plain_text_password = "test".to_string(); let (candidate, _parent) = ApplicationService::create_candidate_with_parent( diff --git a/core/src/services/portfolio_service.rs b/core/src/services/portfolio_service.rs index e5b9618..58ce4f2 100644 --- a/core/src/services/portfolio_service.rs +++ b/core/src/services/portfolio_service.rs @@ -1,7 +1,7 @@ use std::{path::{PathBuf, Path}}; use entity::candidate; -use log::info; +use log::{info, warn}; use sea_orm::{DbConn}; use serde::{Serialize, ser::{SerializeStruct}}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -351,6 +351,18 @@ impl PortfolioService { Ok(()) } + /// Deletes all candidate folder. Used ONLY when candidate is deleted! + pub async fn delete_candidate_root(candidate_id: i32) -> Result<(), ServiceError> { + warn!("CANDIDATE {} ROOT DIRECTORY DELETE STARTED", candidate_id); + + let path = Self::get_file_store_path().join(&candidate_id.to_string()).to_path_buf(); + tokio::fs::remove_dir_all(path).await?; + + warn!("CANDIDATE {} ROOT DIRECTORY DELETE FINISHED", candidate_id); + + Ok(()) + } + /// Returns true if portfolio is submitted pub async fn is_portfolio_submitted(candidate_id: i32) -> bool { let path = Self::get_file_store_path().join(&candidate_id.to_string()).to_path_buf();