diff --git a/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql b/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql index 5492d1a..6273629 100644 --- a/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql +++ b/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql @@ -68,17 +68,20 @@ CREATE TABLE members ( ); CREATE TABLE couples ( - wife integer NOT NULL REFERENCES members, - husband integer NOT NULL REFERENCES members, + id SERIAL PRIMARY KEY, + family_id integer NOT NULL REFERENCES families, + wife integer NULL REFERENCES members, + husband integer NULL REFERENCES members, + state varchar(1) NULL, photo_id INTEGER NULL REFERENCES photos ON DELETE SET NULL, + time_create BIGINT NOT NULL, + time_update BIGINT NOT NULL, wedding_year smallint NULL, wedding_month smallint NULL, wedding_day smallint NULL, divorce_year smallint NULL, divorce_month smallint NULL, - divorce_day smallint NULL, - - PRIMARY KEY(wife, husband) + divorce_day smallint NULL ); -- Create views diff --git a/geneit_backend/src/controllers/couples_controller.rs b/geneit_backend/src/controllers/couples_controller.rs new file mode 100644 index 0000000..792a8d6 --- /dev/null +++ b/geneit_backend/src/controllers/couples_controller.rs @@ -0,0 +1,99 @@ +use crate::controllers::members_controller::RequestDate; +use crate::controllers::HttpResult; +use crate::extractors::family_extractor::FamilyInPath; +use crate::models::{Couple, CoupleState, MemberID}; +use crate::services::{couples_service, members_service}; +use actix_web::{web, HttpResponse}; + +serde_with::with_prefix!(prefix_wedding "wedding_"); +serde_with::with_prefix!(prefix_divorce "divorce_"); + +#[derive(thiserror::Error, Debug)] +enum CoupleControllerErr { + #[error("Wife and husband are identical!")] + IdenticalWifeHusband, + #[error("Wife does not exist!")] + WifeNotExisting, + #[error("Husband does not exist!")] + HusbandNotExisting, + #[error("Invalid date of wedding")] + MalformedDateOfWedding, + #[error("Invalid date of divorce")] + MalformedDateOfDivorce, +} + +#[derive(serde::Deserialize)] +pub struct CoupleRequest { + wife: Option, + husband: Option, + state: Option, + #[serde(flatten, with = "prefix_wedding")] + wedding: Option, + #[serde(flatten, with = "prefix_divorce")] + divorce: Option, +} + +impl CoupleRequest { + pub async fn to_couple(self, couple: &mut Couple) -> anyhow::Result<()> { + if let Some(wife) = self.wife { + if !members_service::exists(couple.family_id(), wife).await? { + return Err(CoupleControllerErr::WifeNotExisting.into()); + } + + if self.wife == self.husband { + return Err(CoupleControllerErr::IdenticalWifeHusband.into()); + } + } + + if let Some(husband) = self.husband { + if !members_service::exists(couple.family_id(), husband).await? { + return Err(CoupleControllerErr::HusbandNotExisting.into()); + } + } + + if let Some(d) = &self.wedding { + if !d.check() { + return Err(CoupleControllerErr::MalformedDateOfWedding.into()); + } + } + + if let Some(d) = &self.divorce { + if !d.check() { + return Err(CoupleControllerErr::MalformedDateOfDivorce.into()); + } + } + + couple.set_wife(self.wife); + couple.set_husband(self.husband); + couple.set_state(self.state); + + couple.wedding_year = self.wedding.as_ref().map(|m| m.year).unwrap_or_default(); + couple.wedding_month = self.wedding.as_ref().map(|m| m.month).unwrap_or_default(); + couple.wedding_day = self.wedding.as_ref().map(|m| m.day).unwrap_or_default(); + + couple.divorce_year = self.divorce.as_ref().map(|m| m.year).unwrap_or_default(); + couple.divorce_month = self.divorce.as_ref().map(|m| m.month).unwrap_or_default(); + couple.divorce_day = self.divorce.as_ref().map(|m| m.day).unwrap_or_default(); + + Ok(()) + } +} + +/// Create a new couple +pub async fn create(m: FamilyInPath, req: web::Json) -> HttpResult { + let mut couple = couples_service::create(m.family_id()).await?; + + if let Err(e) = req.0.to_couple(&mut couple).await { + log::error!("Failed to apply couple information! {e}"); + couples_service::delete(&mut couple).await?; + return Ok(HttpResponse::BadRequest().body(e.to_string())); + } + + if let Err(e) = couples_service::update(&mut couple).await { + log::error!("Failed to update couple information! {e}"); + couples_service::delete(&mut couple).await?; + return Ok(HttpResponse::InternalServerError().finish()); + } + + Ok(HttpResponse::Ok().json(couple)) +} diff --git a/geneit_backend/src/controllers/mod.rs b/geneit_backend/src/controllers/mod.rs index aff2be5..a5b5307 100644 --- a/geneit_backend/src/controllers/mod.rs +++ b/geneit_backend/src/controllers/mod.rs @@ -5,6 +5,7 @@ use actix_web::HttpResponse; use std::fmt::{Debug, Display, Formatter}; pub mod auth_controller; +pub mod couples_controller; pub mod families_controller; pub mod members_controller; pub mod photos_controller; diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 5957862..f9b1a2f 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -6,8 +6,8 @@ use actix_web::{web, App, HttpServer}; use geneit_backend::app_config::AppConfig; use geneit_backend::connections::s3_connection; use geneit_backend::controllers::{ - auth_controller, families_controller, members_controller, photos_controller, server_controller, - users_controller, + auth_controller, couples_controller, families_controller, members_controller, + photos_controller, server_controller, users_controller, }; #[actix_web::main] @@ -162,6 +162,12 @@ async fn main() -> std::io::Result<()> { "/family/{id}/member/{member_id}/photo", web::delete().to(members_controller::remove_photo), ) + // Couples controller + .route( + "/family/{id}/couple/create", + web::post().to(couples_controller::create), + ) + // Photos controller .route( "/photo/{id}", web::get().to(photos_controller::get_full_size), diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index 990cde7..3e05526 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -1,5 +1,5 @@ use crate::app_config::AppConfig; -use crate::schema::{families, members, memberships, photos, users}; +use crate::schema::{couples, families, members, memberships, photos, users}; use crate::utils::crypt_utils::sha256; use diesel::prelude::*; @@ -297,3 +297,105 @@ pub struct NewMember { pub time_create: i64, pub time_update: i64, } + +/// Member ID holder +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] +pub struct CoupleID(pub i32); + +#[derive(serde::Serialize, serde::Deserialize)] +pub enum CoupleState { + #[serde(rename = "N")] + None, + #[serde(rename = "E")] + Engaged, + #[serde(rename = "M")] + Married, + #[serde(rename = "D")] + Divorced, +} + +impl CoupleState { + pub fn as_str(&self) -> &str { + match self { + CoupleState::None => "N", + CoupleState::Engaged => "E", + CoupleState::Married => "M", + CoupleState::Divorced => "D", + } + } + + pub fn parse_str(s: &str) -> Option { + serde_json::from_str(s).ok() + } +} + +#[derive(Queryable, Debug, serde::Serialize)] +pub struct Couple { + id: i32, + family_id: i32, + wife: Option, + husband: Option, + state: Option, + photo_id: Option, + time_create: i64, + pub time_update: i64, + pub wedding_year: Option, + pub wedding_month: Option, + pub wedding_day: Option, + pub divorce_year: Option, + pub divorce_month: Option, + pub divorce_day: Option, +} + +impl Couple { + pub fn id(&self) -> CoupleID { + CoupleID(self.id) + } + + pub fn family_id(&self) -> FamilyID { + FamilyID(self.family_id) + } + + pub fn state(&self) -> Option { + self.state + .as_deref() + .map(CoupleState::parse_str) + .unwrap_or_default() + } + + pub fn set_state(&mut self, s: Option) { + self.state = s.map(|s| s.as_str().to_string()) + } + + pub fn set_wife(&mut self, s: Option) { + self.wife = s.map(|s| s.0) + } + + pub fn wife(&self) -> Option { + self.wife.map(MemberID) + } + + pub fn set_husband(&mut self, s: Option) { + self.husband = s.map(|s| s.0) + } + + pub fn husband(&self) -> Option { + self.husband.map(MemberID) + } + + pub fn set_photo_id(&mut self, p: Option) { + self.photo_id = p.map(|p| p.0); + } + + pub fn photo_id(&self) -> Option { + self.photo_id.map(PhotoID) + } +} + +#[derive(Insertable)] +#[diesel(table_name = couples)] +pub struct NewCouple { + pub family_id: i32, + pub time_create: i64, + pub time_update: i64, +} diff --git a/geneit_backend/src/schema.rs b/geneit_backend/src/schema.rs index 1044adf..3bb9698 100644 --- a/geneit_backend/src/schema.rs +++ b/geneit_backend/src/schema.rs @@ -1,10 +1,15 @@ // @generated automatically by Diesel CLI. diesel::table! { - couples (wife, husband) { - wife -> Int4, - husband -> Int4, + couples (id) { + id -> Int4, + family_id -> Int4, + wife -> Nullable, + husband -> Nullable, + state -> Nullable, photo_id -> Nullable, + time_create -> Int8, + time_update -> Int8, wedding_year -> Nullable, wedding_month -> Nullable, wedding_day -> Nullable, @@ -90,6 +95,7 @@ diesel::table! { } } +diesel::joinable!(couples -> families (family_id)); diesel::joinable!(couples -> photos (photo_id)); diesel::joinable!(members -> families (family_id)); diesel::joinable!(members -> photos (photo_id)); diff --git a/geneit_backend/src/services/couples_service.rs b/geneit_backend/src/services/couples_service.rs new file mode 100644 index 0000000..d040180 --- /dev/null +++ b/geneit_backend/src/services/couples_service.rs @@ -0,0 +1,124 @@ +use crate::connections::db_connection; +use crate::models::{Couple, CoupleID, FamilyID, MemberID, NewCouple}; +use crate::schema::couples; +use crate::services::photos_service; +use crate::utils::time_utils::time; +use diesel::prelude::*; + +/// Create a new couple +pub async fn create(family_id: FamilyID) -> anyhow::Result { + db_connection::execute(|conn| { + let res: Couple = diesel::insert_into(couples::table) + .values(&NewCouple { + family_id: family_id.0, + time_create: time() as i64, + time_update: time() as i64, + }) + .get_result(conn)?; + + Ok(res) + }) +} + +/// Get the information of a couple +pub async fn get_by_id(id: CoupleID) -> anyhow::Result { + db_connection::execute(|conn| couples::table.filter(couples::dsl::id.eq(id.0)).first(conn)) +} + +/// Get all the couples of a family +pub async fn get_all_of_family(id: FamilyID) -> anyhow::Result> { + db_connection::execute(|conn| { + couples::table + .filter(couples::dsl::family_id.eq(id.0)) + .get_results(conn) + }) +} + +/// Get all the couples associated to a member +pub async fn get_all_of_member(id: MemberID) -> anyhow::Result> { + db_connection::execute(|conn| { + couples::table + .filter( + couples::dsl::wife + .eq(id.0) + .or(couples::dsl::husband.eq(id.0)), + ) + .get_results(conn) + }) +} + +/// Check whether a couple with a given id exists or not +pub async fn exists(family_id: FamilyID, couple_id: CoupleID) -> anyhow::Result { + db_connection::execute(|conn| { + let count: i64 = couples::table + .filter( + couples::id + .eq(couple_id.0) + .and(couples::family_id.eq(family_id.0)), + ) + .count() + .get_result(conn)?; + + Ok(count != 0) + }) +} + +/// Update the information of a couple +pub async fn update(couple: &mut Couple) -> anyhow::Result<()> { + couple.time_update = time() as i64; + + db_connection::execute(|conn| { + diesel::update(couples::dsl::couples.filter(couples::dsl::id.eq(couple.id().0))) + .set(( + couples::dsl::state.eq(couple.state().map(|c| c.as_str().to_string())), + couples::dsl::wife.eq(couple.wife().map(|m| m.0)), + couples::dsl::husband.eq(couple.husband().map(|m| m.0)), + couples::dsl::photo_id.eq(couple.photo_id().map(|p| p.0)), + couples::dsl::time_update.eq(couple.time_update), + couples::dsl::wedding_year.eq(couple.wedding_year), + couples::dsl::wedding_month.eq(couple.wedding_month), + couples::dsl::wedding_day.eq(couple.wedding_day), + couples::dsl::divorce_year.eq(couple.divorce_year), + couples::dsl::divorce_month.eq(couple.divorce_month), + couples::dsl::divorce_day.eq(couple.divorce_day), + )) + .execute(conn) + })?; + + Ok(()) +} + +/// Delete a couple photo +pub async fn remove_photo(couple: &mut Couple) -> anyhow::Result<()> { + match couple.photo_id() { + None => {} + Some(photo) => { + photos_service::delete(photo).await?; + couple.set_photo_id(None); + update(couple).await?; + } + } + + Ok(()) +} + +/// Delete a couple +pub async fn delete(couple: &mut Couple) -> anyhow::Result<()> { + remove_photo(couple).await?; + + // Remove the couple + db_connection::execute(|conn| { + diesel::delete(couples::dsl::couples.filter(couples::dsl::id.eq(couple.id().0))) + .execute(conn) + })?; + + Ok(()) +} + +/// Delete all the couples of a family +pub async fn delete_all_family(family_id: FamilyID) -> anyhow::Result<()> { + for mut m in get_all_of_family(family_id).await? { + delete(&mut m).await?; + } + Ok(()) +} diff --git a/geneit_backend/src/services/families_service.rs b/geneit_backend/src/services/families_service.rs index 4212eaf..475b406 100644 --- a/geneit_backend/src/services/families_service.rs +++ b/geneit_backend/src/services/families_service.rs @@ -5,7 +5,7 @@ use crate::models::{ Family, FamilyID, FamilyMembership, Membership, NewFamily, NewMembership, UserID, }; use crate::schema::{families, memberships}; -use crate::services::{members_service, users_service}; +use crate::services::{couples_service, members_service, users_service}; use crate::utils::string_utils::rand_str; use crate::utils::time_utils::time; use diesel::prelude::*; @@ -183,7 +183,8 @@ pub async fn update_family(family: &Family) -> anyhow::Result<()> { /// Delete a family pub async fn delete_family(family_id: FamilyID) -> anyhow::Result<()> { - // TODO : delete couples + // Delete all family couples + couples_service::delete_all_family(family_id).await?; // Remove all family members members_service::delete_all_family(family_id).await?; diff --git a/geneit_backend/src/services/members_service.rs b/geneit_backend/src/services/members_service.rs index 96e6b4a..cb1f547 100644 --- a/geneit_backend/src/services/members_service.rs +++ b/geneit_backend/src/services/members_service.rs @@ -1,7 +1,7 @@ use crate::connections::db_connection; use crate::models::{FamilyID, Member, MemberID, NewMember}; use crate::schema::members; -use crate::services::photos_service; +use crate::services::{couples_service, photos_service}; use crate::utils::time_utils::time; use diesel::prelude::*; use diesel::RunQueryDsl; @@ -102,7 +102,16 @@ pub async fn remove_photo(member: &mut Member) -> anyhow::Result<()> { /// Delete a member pub async fn delete(member: &mut Member) -> anyhow::Result<()> { - // TODO : remove associated couple + // Remove associated couple + for mut c in couples_service::get_all_of_member(member.id()).await? { + // Check if only one person is attached to the couple + // i.e. if the current member is the last person attached to the couple + if c.wife().is_some() && c.husband().is_some() { + continue; + } + + couples_service::delete(&mut c).await?; + } remove_photo(member).await?; diff --git a/geneit_backend/src/services/mod.rs b/geneit_backend/src/services/mod.rs index 02c2085..935229e 100644 --- a/geneit_backend/src/services/mod.rs +++ b/geneit_backend/src/services/mod.rs @@ -1,5 +1,6 @@ //! # Backend services +pub mod couples_service; pub mod families_service; pub mod login_token_service; pub mod mail_service;