Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/target
.env
.idea
.vscode
6 changes: 6 additions & 0 deletions .rustfmt.toml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
206 changes: 206 additions & 0 deletions src/attendance/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
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_seminar_attendance(state: Data<AppState>, body: SeminarAttendance) -> impl Responder {
// TODO: eboard should auto approve
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(id) => id,
Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
};

let usernames: Vec<&String> = body.body.iter();
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)
.await
{
Ok(_) => HttpResponse::Ok(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
}
}

#[get("/attendance/seminar/{user}")]
pub async fn get_seminars_by_user(state: Data<AppState>) -> 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 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
{
Ok(seminars) => HttpResponse::Ok().json(seminars),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
}
}

#[get("/attendance/seminar")]
pub async fn get_seminars(state: Data<AppState>) -> 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<AppState>, body: Json<String>) -> impl Responder {
let (id,) = path.into_inner();
let usernames: Vec<&String> = body.iter();
let frosh: Vec<u32> = 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::<Vec<_>>();
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<Appstate>) -> impl Responder {
let (id,) = path.into_inner();
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<AppState>, 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<u32> = 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::<Vec<_>>();
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<AppState>) -> 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<AppState>) -> 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<AppState>, body: Json<String>) -> impl Responder {
let (id,) = path.into_inner();
let usernames: Vec<&String> = body.iter();
let frosh: Vec<u32> = 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::<Vec<_>>();
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()),
}
}

#[delete("/attendance/committee/{id}")]
pub async fn delete_committee(state: Data<Appstate>) -> 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()),
}
}
9 changes: 9 additions & 0 deletions src/schema/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
}
8 changes: 4 additions & 4 deletions src/schema/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down