From 0287670542749f6ee760aa1c761f64edb09423fd Mon Sep 17 00:00:00 2001 From: Sam-Cordry Date: Fri, 6 Oct 2023 02:59:52 -0400 Subject: [PATCH 1/4] seminar attendance api routes --- .gitignore | 2 + Cargo.toml | 2 +- src/attendance/routes.rs | 87 ++++++++++++++++++++++++++++++++++++++++ src/schema/api.rs | 9 +++++ 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/attendance/routes.rs diff --git a/.gitignore b/.gitignore index fedaa2b..a4a3eaf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target .env +.idea +.vscode \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index c7e7be3..e16d072 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" actix-cors = "0.6.4" actix-web = "4.4.0" anyhow = "1.0.75" -chrono = { version = "0.4.31", features=["serde"]} +chrono = { version = "0.4.31", features = ["serde"] } serde = { version = "1.0.188", features=["derive"] } serde_json = "1.0.107" sqlx = { version = "0.7.2", features=["postgres", "chrono", "runtime-tokio-native-tls", "macros"] } diff --git a/src/attendance/routes.rs b/src/attendance/routes.rs new file mode 100644 index 0000000..77219a5 --- /dev/null +++ b/src/attendance/routes.rs @@ -0,0 +1,87 @@ +use actix_web::{ + get, post, + web::{Data, Json, Path}, + HttpResponse, Responder, +}; +use serde_json::json; +use sqlx::{query, query_as}; +use crate::schema::db; +mod schema; + +#[post("/attendance/seminar")] +pub async fn submit_attendance(state: Data, body: SeminarAttendance) -> impl Responder { + // TODO: eboard should auto approve + match query!("INSERT INTO technical_seminars(name, timestamp, active, approved) VALUES ($1::varchar(128), $2::timestamp, true, $3::bool", body.name, body.date, false) + .execute(&state.db) + .await + { + Ok(seminars) => HttpResponse::Ok().json(seminars), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[get("/attendance/seminar/{user}")] +pub async fn get_seminars_by_user(state: Data) -> impl Responder { + // TODO: authenticate with token + let (name,) = path.into_inner(); + if name.len() < 1 { + return HttpResponse::BadRequest().body("No name found".to_string()); + } + match query_as!(Seminar, format!("SELECT * FROM {} WHERE {} = $1 AND seminar_id IN (SELECT id FROM technical_seminars WHERE timestamp > ($2::timestamp))", if name.chars().next().is_numeric() { "freshman_seminar_attendance" } else { "member_seminar_attendance" }, if name.chars().next().is_numeric() { "fid" } else { "uid" }), body.name, &state.year_start) + .fetch_all(&state.db) + .await + { + Ok(seminars) => HttpResponse::Ok().json(seminars), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[get("/attendance/seminar")] +pub async fn get_seminars(state: Data) -> impl Responder { + // TODO: Joe: year_start should be the day the new year button was pressed by Evals, formatted for postgres + match query_as!(Seminar, "SELECT * FROM technical_seminars WHERE timestamp > ($1::timestamp)", &state.year_start) + .fetch_all(&state.db) + .await + { + Ok(seminars) => HttpResponse::Ok().json(seminars), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[put("/attendance/seminar/{id}")] +pub async fn put_seminar(state: Data, body: Json) -> impl Responder { + let (id,) = path.into_inner(); + let usernames: Vec<&String> = body.iter().map(|a| a.member_id); + let frosh: Vec = usernames.filter(|a| { + let c = a.chars().next(); + if c.is_some() { + c.unwrap().is_numeric() + } + }).map(|a| *a.parse()).collect(); + let members = usernames.filter(|a| { + let c = a.chars().next(); + if c.is_some() { + !c.unwrap().is_numeric() + } + }).collect::>(); + let seminar_id_vec = vec![id; usernames.len()]; + match query!("DELETE FROM freshman_seminar_attendance WHERE seminar_id = ($1::i32); DELETE FROM member_seminar_attendance WHERE seminar_id = ($2::i32); INSERT INTO freshman_seminar_attendance(fid, seminar_id) SELECT * FROM UNNEST($3::int4[], $4::int4[]); INSERT INTO member_seminar_attendance(uid, seminar_id) SELECT * FROM UNNEST($5::text[], $6::int4[]);", id, id, frosh, seminar_id_vec, members, seminar_id_vec) + .execute(&state.db) + .await + { + Ok(_) => HttpResponse::Ok(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[delete("/attendance/seminar/{id}")] +pub async fn delete_seminar(state: Data) -> impl Responder { + let (id,) = path.into_inner(); + match query!("DELETE FROM technical_seminars WHERE id = (1)", id) + .execute(&state.db) + .await + { + Ok(_) => HttpResponse::Ok(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} diff --git a/src/schema/api.rs b/src/schema/api.rs index 3bf3971..212cf91 100644 --- a/src/schema/api.rs +++ b/src/schema/api.rs @@ -3,3 +3,12 @@ // directorship attendance may be relayed to the fronted as a list of member // names / usernames, while directorship attendance is stored in the database // as relations in one of two tables + +use chrono::NaiveDateTime; +use sqlx::types::Json; + +struct SeminarAttendance { + name: String, + date: NaiveDateTime, + body: Json, +} \ No newline at end of file From 1adc332e2761a2aaad490901d5654b326301a56e Mon Sep 17 00:00:00 2001 From: Sam-Cordry Date: Fri, 6 Oct 2023 04:04:43 -0400 Subject: [PATCH 2/4] committee attendance api routes --- src/attendance/routes.rs | 141 ++++++++++++++++++++++++++++++++++++--- src/schema/db.rs | 8 +-- 2 files changed, 136 insertions(+), 13 deletions(-) diff --git a/src/attendance/routes.rs b/src/attendance/routes.rs index 77219a5..aa3a058 100644 --- a/src/attendance/routes.rs +++ b/src/attendance/routes.rs @@ -9,15 +9,37 @@ use crate::schema::db; mod schema; #[post("/attendance/seminar")] -pub async fn submit_attendance(state: Data, body: SeminarAttendance) -> impl Responder { +pub async fn submit_seminar_attendance(state: Data, body: SeminarAttendance) -> impl Responder { // TODO: eboard should auto approve - match query!("INSERT INTO technical_seminars(name, timestamp, active, approved) VALUES ($1::varchar(128), $2::timestamp, true, $3::bool", body.name, body.date, false) - .execute(&state.db) + let id = match query_as!(i32, "INSERT INTO technical_seminars(name, timestamp, active, approved) OUTPUT INSERTED.id VALUES ($1::varchar(128), $2::timestamp, true, $3::bool", body.name, body.date, false) + .fetch_one(&state.db) .await { - Ok(seminars) => HttpResponse::Ok().json(seminars), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), - } + Ok(id) => id, + Err(e) => return HttpResponse::InternalServerError().body(e.to_string()), + }; + + let usernames: Vec<&String> = body.body.iter(); + let frosh: Vec = usernames.filter(|a| { + let c = a.chars().next(); + if c.is_some() { + c.unwrap().is_numeric() + } + }).map(|a| *a.parse()).collect(); + let members = usernames.filter(|a| { + let c = a.chars().next(); + if c.is_some() { + !c.unwrap().is_numeric() + } + }).collect::>(); + let seminar_id_vec = vec![id; usernames.len()]; + match query!("DELETE FROM freshman_seminar_attendance WHERE seminar_id = ($1::i32); DELETE FROM member_seminar_attendance WHERE seminar_id = ($2::i32); INSERT INTO freshman_seminar_attendance(fid, seminar_id) SELECT * FROM UNNEST($3::int4[], $4::int4[]); INSERT INTO member_seminar_attendance(uid, seminar_id) SELECT * FROM UNNEST($5::text[], $6::int4[]);", id, id, frosh, seminar_id_vec, members, seminar_id_vec) + .execute(&state.db) + .await + { + Ok(_) => HttpResponse::Ok(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } } #[get("/attendance/seminar/{user}")] @@ -27,7 +49,7 @@ pub async fn get_seminars_by_user(state: Data) -> impl Responder { if name.len() < 1 { return HttpResponse::BadRequest().body("No name found".to_string()); } - match query_as!(Seminar, format!("SELECT * FROM {} WHERE {} = $1 AND seminar_id IN (SELECT id FROM technical_seminars WHERE timestamp > ($2::timestamp))", if name.chars().next().is_numeric() { "freshman_seminar_attendance" } else { "member_seminar_attendance" }, if name.chars().next().is_numeric() { "fid" } else { "uid" }), body.name, &state.year_start) + match query_as!(Seminar, format!("SELECT * FROM {} WHERE approved = 'true' AND {} = $1 AND seminar_id IN (SELECT id FROM technical_seminars WHERE timestamp > ($2::timestamp))", if name.chars().next().is_numeric() { "freshman_seminar_attendance" } else { "member_seminar_attendance" }, if name.chars().next().is_numeric() { "fid" } else { "uid" }), body.name, &state.year_start) .fetch_all(&state.db) .await { @@ -51,7 +73,7 @@ pub async fn get_seminars(state: Data) -> impl Responder { #[put("/attendance/seminar/{id}")] pub async fn put_seminar(state: Data, body: Json) -> impl Responder { let (id,) = path.into_inner(); - let usernames: Vec<&String> = body.iter().map(|a| a.member_id); + let usernames: Vec<&String> = body.iter(); let frosh: Vec = usernames.filter(|a| { let c = a.chars().next(); if c.is_some() { @@ -77,7 +99,96 @@ pub async fn put_seminar(state: Data, body: Json) -> impl Resp #[delete("/attendance/seminar/{id}")] pub async fn delete_seminar(state: Data) -> impl Responder { let (id,) = path.into_inner(); - match query!("DELETE FROM technical_seminars WHERE id = (1)", id) + match query!("DELETE FROM technical_seminars WHERE id = ($1::int4); DELETE FROM freshman_seminar_attendance WHERE seminar_id = ($2::int4); DELETE FROM member_seminar_attendance WHERE seminar_id = ($3::int4);", id, id, id) + .execute(&state.db) + .await + { + Ok(_) => HttpResponse::Ok(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +// TODO: Joe: committee is used over directorship to maintain parity with db +#[post("/attendance/committee")] +pub async fn submit_committee_attendance(state: Data, body: SeminarAttendance) -> impl Responder { + // TODO: eboard should auto approve + let id = match query_as!(i32, "INSERT INTO committee_meetings(committee, timestamp, active, approved) OUTPUT INSERTED.id VALUES ($1::varchar(128), $2::timestamp, true, $3::bool", body.name, body.date, false) + .fetch_one(&state.db) + .await + { + Ok(_) => HttpResponse::Ok(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + }; + + let usernames: Vec<&String> = body.iter(); + let frosh: Vec = usernames.filter(|a| { + let c = a.chars().next(); + if c.is_some() { + c.unwrap().is_numeric() + } + }).map(|a| *a.parse()).collect(); + let members = usernames.filter(|a| { + let c = a.chars().next(); + if c.is_some() { + !c.unwrap().is_numeric() + } + }).collect::>(); + let committee_id_vec = vec![id; usernames.len()]; + match query!("DELETE FROM freshman_committee_attendance WHERE meeting_id = ($1::i32); DELETE FROM member_committee_attendance WHERE meeting_id = ($2::i32); INSERT INTO freshman_committee_attendance(fid, meeting_id) SELECT * FROM UNNEST($3::int4[], $4::int4[]); INSERT INTO member_committee_attendance(uid, meeting_id) SELECT * FROM UNNEST($5::text[], $6::int4[]);", id, id, frosh, committee_id_vec, members, committee_id_vec) + .execute(&state.db) + .await + { + Ok(_) => HttpResponse::Ok(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[get("/attendance/committee/{user}")] +pub async fn get_committees_by_user(state: Data) -> impl Responder { + // TODO: authenticate with token + let (name,) = path.into_inner(); + if name.len() < 1 { + return HttpResponse::BadRequest().body("No name found".to_string()); + } + match query_as!(Committee, format!("SELECT * FROM {} WHERE approved = 'true' AND {} = $1 AND committee_id IN (SELECT id FROM committee_meetings WHERE timestamp > ($2::timestamp))", if name.chars().next().is_numeric() { "freshman_committee_attendance" } else { "member_committee_attendance" }, if name.chars().next().is_numeric() { "fid" } else { "uid" }), body.name, &state.year_start) + .fetch_all(&state.db) + .await + { + Ok(committees) => HttpResponse::Ok().json(committees), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[get("/attendance/committee")] +pub async fn get_committee(state: Data) -> impl Responder { + // TODO: Joe: year_start should be the day the new year button was pressed by Evals, formatted for postgres + match query_as!(Committee, "SELECT * FROM committee_meetings WHERE timestamp > ($1::timestamp)", &state.year_start) + .fetch_all(&state.db) + .await + { + Ok(committees) => HttpResponse::Ok().json(committees), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[put("/attendance/committee/{id}")] +pub async fn put_committee(state: Data, body: Json) -> impl Responder { + let (id,) = path.into_inner(); + let usernames: Vec<&String> = body.iter(); + let frosh: Vec = usernames.filter(|a| { + let c = a.chars().next(); + if c.is_some() { + c.unwrap().is_numeric() + } + }).map(|a| *a.parse()).collect(); + let members = usernames.filter(|a| { + let c = a.chars().next(); + if c.is_some() { + !c.unwrap().is_numeric() + } + }).collect::>(); + let committee_id_vec = vec![id; usernames.len()]; + match query!("DELETE FROM freshman_committee_attendance WHERE meeting_id = ($1::i32); DELETE FROM member_committee_attendance WHERE meeting_id = ($2::i32); INSERT INTO freshman_committee_attendance(fid, meeting_id) SELECT * FROM UNNEST($3::int4[], $4::int4[]); INSERT INTO member_committee_attendance(uid, meeting_id) SELECT * FROM UNNEST($5::text[], $6::int4[]);", id, id, frosh, committee_id_vec, members, committee_id_vec) .execute(&state.db) .await { @@ -85,3 +196,15 @@ pub async fn delete_seminar(state: Data) -> impl Responder { Err(e) => HttpResponse::InternalServerError().body(e.to_string()), } } + +#[delete("/attendance/committee/{id}")] +pub async fn delete_committee(state: Data) -> impl Responder { + let (id,) = path.into_inner(); + match query!("DELETE FROM committee_meetings WHERE id = ($1::int4); DELETE FROM freshman_committee_attendance WHERE meeting_id = ($2::int4); DELETE FROM member_committee_attendance WHERE meeting_id = ($3::int4);", id, id, id) + .execute(&state.db) + .await + { + Ok(_) => HttpResponse::Ok(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} \ No newline at end of file diff --git a/src/schema/db.rs b/src/schema/db.rs index 05fd2dd..0f1120e 100644 --- a/src/schema/db.rs +++ b/src/schema/db.rs @@ -6,7 +6,7 @@ use sqlx::FromRow; /// Enum used for 'committee_meetings' to indicate directorship type #[derive(sqlx::Type, Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] #[sqlx(type_name = "committees_enum")] -pub enum DirectorshipType { +pub enum CommitteeType { Evaluations, History, Social, @@ -95,7 +95,7 @@ pub enum AttendanceStatus { /// /// Represents a row in the 'committee_meetings' table #[derive(FromRow, Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] -pub struct Directorship { +pub struct Committee { /// Unique id identifying a DirectorshipAttendance pub id: i32, /// The 'committee' or Directorship associated with this attendance. @@ -178,7 +178,7 @@ pub struct FreshmanAccount { /// Row in the 'freshman_committee_attendance' table #[derive(FromRow, Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] -pub struct FreshmanDirectorshipAttendance { +pub struct FreshmanCommitteeAttendance { /// Unique id identifying this freshman's attendance pub id: i32, /// Foreign key into 'freshman_accounts' table for freshman ids @@ -284,7 +284,7 @@ pub struct MajorProject { /// Row in 'member_committee_attendance' #[derive(FromRow, Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] -pub struct MemberDirectorshipAttendance { +pub struct MemberCommitteeAttendance { /// Unique id for this directorship attendance pub id: i32, /// Username of member who attended this meeting From 82d220c85245601b53c501bd6e551dc542477a76 Mon Sep 17 00:00:00 2001 From: Cecilia Lau <46845938+cecilialau6776@users.noreply.github.com> Date: Fri, 6 Oct 2023 05:47:43 -0400 Subject: [PATCH 3/4] I wrote this at bagels at 5:47 on my phone it's not going to work --- src/attendance/routes.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/attendance/routes.rs b/src/attendance/routes.rs index aa3a058..0d1052d 100644 --- a/src/attendance/routes.rs +++ b/src/attendance/routes.rs @@ -20,18 +20,14 @@ pub async fn submit_seminar_attendance(state: Data, body: SeminarAtten }; let usernames: Vec<&String> = body.body.iter(); - let frosh: Vec = usernames.filter(|a| { - let c = a.chars().next(); - if c.is_some() { - c.unwrap().is_numeric() - } - }).map(|a| *a.parse()).collect(); - let members = usernames.filter(|a| { - let c = a.chars().next(); - if c.is_some() { - !c.unwrap().is_numeric() - } - }).collect::>(); + let (frosh, members): (Vec<_>, Vec<_>) = usernames + .into_iter() + .partition(|username| { + let c = a.chars().next(); + if c.is_some() { + c.unwrap().is_numeric() + } + }); let seminar_id_vec = vec![id; usernames.len()]; match query!("DELETE FROM freshman_seminar_attendance WHERE seminar_id = ($1::i32); DELETE FROM member_seminar_attendance WHERE seminar_id = ($2::i32); INSERT INTO freshman_seminar_attendance(fid, seminar_id) SELECT * FROM UNNEST($3::int4[], $4::int4[]); INSERT INTO member_seminar_attendance(uid, seminar_id) SELECT * FROM UNNEST($5::text[], $6::int4[]);", id, id, frosh, seminar_id_vec, members, seminar_id_vec) .execute(&state.db) @@ -207,4 +203,4 @@ pub async fn delete_committee(state: Data) -> impl Responder { Ok(_) => HttpResponse::Ok(), Err(e) => HttpResponse::InternalServerError().body(e.to_string()), } -} \ No newline at end of file +} From 15f1e22c5f63664bb269993a5cb844382ae39777 Mon Sep 17 00:00:00 2001 From: Cecilia Lau Date: Fri, 6 Oct 2023 06:58:29 -0400 Subject: [PATCH 4/4] Add .rustfmt.toml --- .rustfmt.toml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .rustfmt.toml diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..dc97d29 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,6 @@ +format_code_in_doc_comments = true +format_strings = true +normalize_comments = true +tab_spaces = 4 +unstable_features = true +wrap_comments = true \ No newline at end of file