From 31088420aedbe4e58949dc19711b9693bef64699 Mon Sep 17 00:00:00 2001 From: Kavika Date: Thu, 2 Feb 2023 14:07:45 +1100 Subject: [PATCH 01/35] assign questions to one or all roles --- backend/server/src/campaigns.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/backend/server/src/campaigns.rs b/backend/server/src/campaigns.rs index 95c69d2b3..2256440ec 100644 --- a/backend/server/src/campaigns.rs +++ b/backend/server/src/campaigns.rs @@ -94,6 +94,7 @@ fn default_max_bytes() -> i32 { #[derive(Serialize, Deserialize)] pub struct QuestionInput { pub title: String, + pub common_question: bool, pub description: Option, #[serde(default = "default_max_bytes")] pub max_bytes: i32, @@ -143,6 +144,8 @@ pub async fn new( JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) })?; + let mut inserted_role_ids: Vec = vec![]; + for role in roles { let new_role = RoleUpdate { campaign_id: campaign.id, @@ -157,9 +160,18 @@ pub async fn new( JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) })?; - for question in role.questions_for_role { - if question < new_questions.len() { - new_questions[question].role_ids.push(inserted_role.id); + inserted_role_ids.push(inserted_role.id); + } + + for role in 0..inserted_role_ids.len() { + for question in roles[role].questions_for_role { + if questions[question].common_question && new_questions[question].role_ids.len() == 0 { + new_questions[question].role_ids == inserted_role_ids.clone(); + } else if new_questions[question].role_ids.len() == 0 { + new_questions[question].role_ids.push(inserted_role_ids[role]) + } else { + eprintln!("Question is not common, yet has multiple roles asking for it"); + JsonErr(CampaignError::UnableToCreate, Status::BadRequest) } } } From 329287f3c604bde9410a9856f0552ace2aa05050 Mon Sep 17 00:00:00 2001 From: Kavika Date: Thu, 2 Feb 2023 14:08:36 +1100 Subject: [PATCH 02/35] fix passing in reference to PgConnection --- backend/server/src/campaigns.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/server/src/campaigns.rs b/backend/server/src/campaigns.rs index 2256440ec..627921e60 100644 --- a/backend/server/src/campaigns.rs +++ b/backend/server/src/campaigns.rs @@ -71,7 +71,7 @@ pub async fn update( .check() .or_else(|_| Err(JsonErr(CampaignError::Unauthorized, Status::Forbidden)))?; - Campaign::update(conn, campaign_id, &update_campaign); + Campaign::update(&conn, campaign_id, &update_campaign); Ok(Json(())) }) @@ -139,7 +139,7 @@ pub async fn new( .check() .or_else(|_| Err(JsonErr(CampaignError::Unauthorized, Status::Forbidden)))?; - let campaign = Campaign::create(conn, &campaign).ok_or_else(|| { + let campaign = Campaign::create(&conn, &campaign).ok_or_else(|| { eprintln!("Failed to create campaign for some reason: {:?}", campaign); JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) })?; @@ -155,7 +155,7 @@ pub async fn new( max_available: role.max_available, finalised: campaign.published, }; - let inserted_role = new_role.insert(conn).ok_or_else(|| { + let inserted_role = new_role.insert(&conn).ok_or_else(|| { eprintln!("Failed to create role for some reason: {:?}", new_role); JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) })?; @@ -180,7 +180,7 @@ pub async fn new( if question.role_ids.len() == 0 { return Err(JsonErr(CampaignError::InvalidInput, Status::BadRequest)); } - question.insert(conn).ok_or_else(|| { + question.insert(&conn).ok_or_else(|| { eprintln!("Failed to create question for some reason"); JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) })?; @@ -204,7 +204,7 @@ pub async fn delete_campaign( .check() .or_else(|_| Err(JsonErr(CampaignError::Unauthorized, Status::Forbidden)))?; - Campaign::delete_deep(conn, campaign_id); + Campaign::delete_deep(&conn, campaign_id); Ok(Json(())) }) From 4fe45aae2fbb55c1030a2b0b9cec4b5d93bb7593 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 3 Feb 2023 12:00:22 +1100 Subject: [PATCH 03/35] Revert "fix passing in reference to PgConnection" This reverts commit 329287f3c604bde9410a9856f0552ace2aa05050. --- backend/server/src/campaigns.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/server/src/campaigns.rs b/backend/server/src/campaigns.rs index 627921e60..2256440ec 100644 --- a/backend/server/src/campaigns.rs +++ b/backend/server/src/campaigns.rs @@ -71,7 +71,7 @@ pub async fn update( .check() .or_else(|_| Err(JsonErr(CampaignError::Unauthorized, Status::Forbidden)))?; - Campaign::update(&conn, campaign_id, &update_campaign); + Campaign::update(conn, campaign_id, &update_campaign); Ok(Json(())) }) @@ -139,7 +139,7 @@ pub async fn new( .check() .or_else(|_| Err(JsonErr(CampaignError::Unauthorized, Status::Forbidden)))?; - let campaign = Campaign::create(&conn, &campaign).ok_or_else(|| { + let campaign = Campaign::create(conn, &campaign).ok_or_else(|| { eprintln!("Failed to create campaign for some reason: {:?}", campaign); JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) })?; @@ -155,7 +155,7 @@ pub async fn new( max_available: role.max_available, finalised: campaign.published, }; - let inserted_role = new_role.insert(&conn).ok_or_else(|| { + let inserted_role = new_role.insert(conn).ok_or_else(|| { eprintln!("Failed to create role for some reason: {:?}", new_role); JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) })?; @@ -180,7 +180,7 @@ pub async fn new( if question.role_ids.len() == 0 { return Err(JsonErr(CampaignError::InvalidInput, Status::BadRequest)); } - question.insert(&conn).ok_or_else(|| { + question.insert(conn).ok_or_else(|| { eprintln!("Failed to create question for some reason"); JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) })?; @@ -204,7 +204,7 @@ pub async fn delete_campaign( .check() .or_else(|_| Err(JsonErr(CampaignError::Unauthorized, Status::Forbidden)))?; - Campaign::delete_deep(&conn, campaign_id); + Campaign::delete_deep(conn, campaign_id); Ok(Json(())) }) From 5ff27d7c1d590213b082041d58744aa2d0cdb15a Mon Sep 17 00:00:00 2001 From: Kavika Date: Wed, 15 Feb 2023 14:36:52 +1100 Subject: [PATCH 04/35] added basic migration down.sql doesn't recover data (cannot see which roles the original question was meant to be asked for, if it was asked for more than one role) --- .../migrations/2023-02-08-081803_questions/down.sql | 2 ++ backend/migrations/2023-02-08-081803_questions/up.sql | 10 ++++++++++ 2 files changed, 12 insertions(+) create mode 100644 backend/migrations/2023-02-08-081803_questions/down.sql create mode 100644 backend/migrations/2023-02-08-081803_questions/up.sql diff --git a/backend/migrations/2023-02-08-081803_questions/down.sql b/backend/migrations/2023-02-08-081803_questions/down.sql new file mode 100644 index 000000000..ebe6feb7e --- /dev/null +++ b/backend/migrations/2023-02-08-081803_questions/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS questions + DROP COLUMN role_id; diff --git a/backend/migrations/2023-02-08-081803_questions/up.sql b/backend/migrations/2023-02-08-081803_questions/up.sql new file mode 100644 index 000000000..3a8bb45b7 --- /dev/null +++ b/backend/migrations/2023-02-08-081803_questions/up.sql @@ -0,0 +1,10 @@ +ALTER TABLE IF EXISTS questions + ADD COLUMN role_id INTEGER DEFAULT NULL; + +-- Make questions currently assigned to one role, uncommon +UPDATE questions SET role_id=role_ids[1] WHERE array_length(role_ids, 1) = 1; + +-- No need to change questions currently assigned to multiple roles, as role_id is NULL by default, thus making them common + +ALTER TABLE IF EXISTS questions + DROP COLUMN role_ids; From 809d1bfb9657d5fec4f7bcca3073b1925500fb54 Mon Sep 17 00:00:00 2001 From: Kavika Date: Mon, 27 Feb 2023 21:47:16 +1100 Subject: [PATCH 05/35] feat(backend): changed questions to common or unique --- backend/server/src/campaigns.rs | 35 ++++++++++++--------------- backend/server/src/database/models.rs | 26 +++++++++----------- backend/server/src/database/schema.rs | 2 +- 3 files changed, 28 insertions(+), 35 deletions(-) diff --git a/backend/server/src/campaigns.rs b/backend/server/src/campaigns.rs index 2256440ec..5dbdd563b 100644 --- a/backend/server/src/campaigns.rs +++ b/backend/server/src/campaigns.rs @@ -119,15 +119,15 @@ pub async fn new( let NewCampaignWithData { campaign, roles, - questions, + mut questions, } = inner; let mut new_questions: Vec = questions - .into_iter() + .iter_mut() .map(|x| NewQuestion { - role_ids: vec![], - title: x.title, - description: x.description, + role_id: None, + title: x.title.clone(), + description: x.description.clone(), max_bytes: x.max_bytes, required: x.required, }) @@ -144,8 +144,6 @@ pub async fn new( JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) })?; - let mut inserted_role_ids: Vec = vec![]; - for role in roles { let new_role = RoleUpdate { campaign_id: campaign.id, @@ -160,26 +158,23 @@ pub async fn new( JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) })?; - inserted_role_ids.push(inserted_role.id); - } - - for role in 0..inserted_role_ids.len() { - for question in roles[role].questions_for_role { - if questions[question].common_question && new_questions[question].role_ids.len() == 0 { - new_questions[question].role_ids == inserted_role_ids.clone(); - } else if new_questions[question].role_ids.len() == 0 { - new_questions[question].role_ids.push(inserted_role_ids[role]) + for question in role.questions_for_role { + if questions[question].common_question { + // If question is common + new_questions[question].role_id = None; + } else if let None = new_questions[question].role_id { + // If question is unique and no role_id assigned to it + new_questions[question].role_id = Option::from(inserted_role.id); } else { + // If question is meant to be unique, but already has a role_id assigned to it eprintln!("Question is not common, yet has multiple roles asking for it"); - JsonErr(CampaignError::UnableToCreate, Status::BadRequest) + return Err(JsonErr(CampaignError::UnableToCreate, Status::BadRequest)); } } + } for question in new_questions { - if question.role_ids.len() == 0 { - return Err(JsonErr(CampaignError::InvalidInput, Status::BadRequest)); - } question.insert(conn).ok_or_else(|| { eprintln!("Failed to create question for some reason"); JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) diff --git a/backend/server/src/database/models.rs b/backend/server/src/database/models.rs index b82864f64..2cef2cbbe 100644 --- a/backend/server/src/database/models.rs +++ b/backend/server/src/database/models.rs @@ -714,10 +714,8 @@ impl Role { } pub fn delete_children(conn: &PgConnection, role: Role) -> Option<()> { - use diesel::pg::expression::dsl::any; - let question_items: Vec = questions::table - .filter(any(questions::role_ids).eq(role.id)) + .filter(questions::role_id.eq(role.id)) .load(conn) .map_err(|x| { eprintln!("error in delete_children: {x:?}"); @@ -725,7 +723,7 @@ impl Role { }) .ok()?; - diesel::delete(questions::table.filter(any(questions::role_ids).eq(role.id))) + diesel::delete(questions::table.filter(questions::role_id.eq(role.id))) .execute(conn) .map_err(|x| { eprintln!("error in delete_children: {x:?}"); @@ -908,7 +906,7 @@ impl NewApplication { #[table_name = "questions"] pub struct Question { pub id: i32, - pub role_ids: Vec, + pub role_id: Option, pub title: String, pub description: Option, pub max_bytes: i32, @@ -920,7 +918,7 @@ pub struct Question { #[derive(Insertable, Serialize, Deserialize)] #[table_name = "questions"] pub struct NewQuestion { - pub role_ids: Vec, + pub role_id: Option, pub title: String, pub description: Option, #[serde(default)] @@ -931,7 +929,7 @@ pub struct NewQuestion { #[derive(Serialize)] pub struct QuestionResponse { pub id: i32, - pub role_ids: Vec, + pub role_id: Option, pub title: String, pub description: Option, pub max_bytes: i32, @@ -942,7 +940,7 @@ impl std::convert::From for QuestionResponse { fn from(question: Question) -> Self { Self { id: question.id, - role_ids: question.role_ids, + role_id: question.role_id, title: question.title, description: question.description, max_bytes: question.max_bytes, @@ -962,9 +960,8 @@ pub struct UpdateQuestionInput { impl Question { pub fn get_first_role(&self) -> i32 { - *self - .role_ids - .get(0) + self + .role_id .expect("Question should be for at least one role") } @@ -983,9 +980,10 @@ impl Question { Some(()) } - pub fn get_all_from_role_id(conn: &PgConnection, role_id_val: i32) -> Vec { + pub fn + get_all_from_role_id(conn: &PgConnection, role_id_val: i32) -> Vec { diesel::sql_query(&format!( - "select * from questions where {} = any(role_ids)", + "select * from questions where {} = role_id or role_id is null", role_id_val )) .load::(conn) @@ -994,7 +992,7 @@ impl Question { pub fn delete_all_from_role_id(conn: &PgConnection, role_id_val: i32) -> bool { diesel::sql_query(&format!( - "delete from questions where {} = any(role_ids)", + "delete from questions where {} = role_id", role_id_val )) .execute(conn) diff --git a/backend/server/src/database/schema.rs b/backend/server/src/database/schema.rs index a41af15a5..197b5da6a 100644 --- a/backend/server/src/database/schema.rs +++ b/backend/server/src/database/schema.rs @@ -106,7 +106,7 @@ table! { questions (id) { id -> Int4, - role_ids -> Array, + role_id -> Nullable, title -> Text, description -> Nullable, max_bytes -> Int4, From 26542171f54aed4e2768033819bbb2fddc2fab4d Mon Sep 17 00:00:00 2001 From: Kavika Date: Mon, 27 Feb 2023 21:56:20 +1100 Subject: [PATCH 06/35] fix(backend): seed.rs updated to match new questions schema --- backend/seed_data/src/seed.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/seed_data/src/seed.rs b/backend/seed_data/src/seed.rs index 108049f5a..6f244c858 100644 --- a/backend/seed_data/src/seed.rs +++ b/backend/seed_data/src/seed.rs @@ -175,7 +175,8 @@ pub fn seed() { let question_one = NewQuestion { title: "What is the meaning of life?".to_string(), max_bytes: 100, - role_ids: vec![senior_mentor_role.id], + role_id: Option::from(senior_mentor_role.id), + // role_ids: vec![senior_mentor_role.id], required: false, description: Some("Please ensure to go into great detail!".to_string()), } @@ -185,7 +186,7 @@ pub fn seed() { let question_two = NewQuestion { title: "Why do you want to be a Peer Mentor".to_string(), max_bytes: 300, - role_ids: vec![senior_mentor_role.id, mentor_role.id], + role_id: None, required: true, description: Some("Please explain why you would like to be a peer mentor!".to_string()), } From e662319b81c23479c42db8b3c85158b86b0c5d6c Mon Sep 17 00:00:00 2001 From: Kavika Date: Mon, 27 Feb 2023 22:19:58 +1100 Subject: [PATCH 07/35] style: ran cargo fmt ran `cargo fmt` on `campaigns.rs` & `models.rs` --- backend/server/src/campaigns.rs | 1 - backend/server/src/database/models.rs | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/server/src/campaigns.rs b/backend/server/src/campaigns.rs index 5dbdd563b..59e24d5f9 100644 --- a/backend/server/src/campaigns.rs +++ b/backend/server/src/campaigns.rs @@ -171,7 +171,6 @@ pub async fn new( return Err(JsonErr(CampaignError::UnableToCreate, Status::BadRequest)); } } - } for question in new_questions { diff --git a/backend/server/src/database/models.rs b/backend/server/src/database/models.rs index 2cef2cbbe..88172c14c 100644 --- a/backend/server/src/database/models.rs +++ b/backend/server/src/database/models.rs @@ -960,8 +960,7 @@ pub struct UpdateQuestionInput { impl Question { pub fn get_first_role(&self) -> i32 { - self - .role_id + self.role_id .expect("Question should be for at least one role") } @@ -980,8 +979,7 @@ impl Question { Some(()) } - pub fn - get_all_from_role_id(conn: &PgConnection, role_id_val: i32) -> Vec { + pub fn get_all_from_role_id(conn: &PgConnection, role_id_val: i32) -> Vec { diesel::sql_query(&format!( "select * from questions where {} = role_id or role_id is null", role_id_val From 334ddd6ed49ba114e742a43a8e4b13f4cfdabe35 Mon Sep 17 00:00:00 2001 From: Alex_Miao_WSL Date: Sun, 5 Mar 2023 16:23:15 +1100 Subject: [PATCH 08/35] feat: add new file to accomodate future additions of new question types. Marked where to implement future chanes for Question and Answer. Added new AnswerInput struct --- backend/server/src/application.rs | 27 +++++++++++++++++++++---- backend/server/src/campaigns.rs | 2 ++ backend/server/src/database/models.rs | 29 +++++++++++++++++++++++++++ backend/server/src/lib.rs | 2 ++ backend/server/src/question_types.rs | 15 ++++++++++++++ 5 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 backend/server/src/question_types.rs diff --git a/backend/server/src/application.rs b/backend/server/src/application.rs index 907984963..ea99cb241 100644 --- a/backend/server/src/application.rs +++ b/backend/server/src/application.rs @@ -1,13 +1,13 @@ use diesel::prelude::*; -use crate::database::{ +use crate::{database::{ models::{ Answer, Application, Campaign, Comment, NewAnswer, NewApplication, NewRating, OrganisationUser, Question, Rating, Role, User, }, schema::ApplicationStatus, Database, -}; +}, question_types::QuestionTypeEnum}; use crate::error::JsonErr; use rocket::{ get, @@ -119,13 +119,32 @@ pub async fn create_rating( .await } +#[derive(Serialize, Deserialize)] +pub struct AnswerInput { + pub application_id: i32, + pub question_id: i32, + pub description: String, + pub question_data: QuestionTypeEnum, +} + +impl From> for NewAnswer { + fn from(a: Json) -> Self { + NewAnswer { + application_id: a.application_id, + question_id: a.question_id, + description: a.into_inner().description, + } + } +} + #[post("/answer", data = "")] pub async fn submit_answer( user: User, db: Database, - answer: Json, + answer: Json, ) -> Result, JsonErr> { db.run(move |conn| { + let application = Application::get(answer.application_id, &conn) .ok_or(JsonErr(ApplicationError::AppNotFound, Status::NotFound))?; if application.user_id != user.id { @@ -140,7 +159,7 @@ pub async fn submit_answer( return Err(JsonErr(ApplicationError::InvalidInput, Status::BadRequest)); } - NewAnswer::insert(&answer, &conn).ok_or(JsonErr( + NewAnswer::insert(&answer.into(), &conn).ok_or(JsonErr( ApplicationError::UnableToCreate, Status::InternalServerError, ))?; diff --git a/backend/server/src/campaigns.rs b/backend/server/src/campaigns.rs index 95c69d2b3..860ac9066 100644 --- a/backend/server/src/campaigns.rs +++ b/backend/server/src/campaigns.rs @@ -5,6 +5,7 @@ use crate::database::{ }, Database, }; +use crate::question_types::QuestionTypeEnum; use crate::error::JsonErr; use rocket::{delete, get, http::Status, post, put, serde::json::Json}; use serde::{Deserialize, Serialize}; @@ -99,6 +100,7 @@ pub struct QuestionInput { pub max_bytes: i32, #[serde(default)] pub required: bool, + pub question_data: QuestionTypeEnum, } #[derive(Deserialize)] diff --git a/backend/server/src/database/models.rs b/backend/server/src/database/models.rs index b82864f64..8b643f19f 100644 --- a/backend/server/src/database/models.rs +++ b/backend/server/src/database/models.rs @@ -1,3 +1,4 @@ + use super::schema::AdminLevel; use super::schema::ApplicationStatus; use super::schema::{ @@ -904,6 +905,20 @@ impl NewApplication { } } + /** + * + * + * + * + * add New fields to Question sturct (also migration). + * We store the serailised struct in DB as TEXT + * + * + * + * + */ + + // pub what_ever_name_this_question_data_is_to_be: String #[derive(Identifiable, Queryable, PartialEq, Serialize, Debug, QueryableByName)] #[table_name = "questions"] pub struct Question { @@ -917,6 +932,19 @@ pub struct Question { pub updated_at: NaiveDateTime, } +/** + * + * + * + * Add enum field to NewQuestion + * + * + * + * + * This means FE must pass in correctly formated JSON struct for our question data + * + * pub question_data: String, + */ #[derive(Insertable, Serialize, Deserialize)] #[table_name = "questions"] pub struct NewQuestion { @@ -928,6 +956,7 @@ pub struct NewQuestion { pub required: bool, } + #[derive(Serialize)] pub struct QuestionResponse { pub id: i32, diff --git a/backend/server/src/lib.rs b/backend/server/src/lib.rs index 20370a226..5a8d7d264 100644 --- a/backend/server/src/lib.rs +++ b/backend/server/src/lib.rs @@ -15,4 +15,6 @@ pub mod permissions; pub mod question; pub mod role; pub mod state; +pub mod question_types; pub mod user; + diff --git a/backend/server/src/question_types.rs b/backend/server/src/question_types.rs new file mode 100644 index 000000000..15439b041 --- /dev/null +++ b/backend/server/src/question_types.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; +/** + * In this file, add new question types that we need to implement + * e.g. + * MultiSelect + */ + + +#[derive(Deserialize, Serialize)] +pub enum QuestionTypeEnum { + +} +pub struct MultiSelect { + +} From 28a3b915a3a0425d5a8ab1d33e51f9d7db29ee52 Mon Sep 17 00:00:00 2001 From: Alex_Miao_WSL Date: Wed, 29 Mar 2023 17:53:42 +1100 Subject: [PATCH 09/35] feat: added migration for updating questions table to accomodate question type field, updated campaign.rs for future implementations of inserting question data --- .../2023-03-29-025501_questions/down.sql | 6 +++++ .../2023-03-29-025501_questions/up.sql | 6 +++++ backend/server/src/application.rs | 24 +++--------------- backend/server/src/campaigns.rs | 23 ++++++++++++----- backend/server/src/database/models.rs | 25 ++++++++++++++++--- backend/server/src/database/schema.rs | 10 ++++++++ backend/server/src/question_types.rs | 10 +++++--- 7 files changed, 70 insertions(+), 34 deletions(-) create mode 100644 backend/migrations/2023-03-29-025501_questions/down.sql create mode 100644 backend/migrations/2023-03-29-025501_questions/up.sql diff --git a/backend/migrations/2023-03-29-025501_questions/down.sql b/backend/migrations/2023-03-29-025501_questions/down.sql new file mode 100644 index 000000000..2a43a8eba --- /dev/null +++ b/backend/migrations/2023-03-29-025501_questions/down.sql @@ -0,0 +1,6 @@ +-- This file should undo anything in `up.sql` + +ALTER TABLE IF EXISTS questions + DROP COLUMN question_type; + +DROP TYPE question_types; \ No newline at end of file diff --git a/backend/migrations/2023-03-29-025501_questions/up.sql b/backend/migrations/2023-03-29-025501_questions/up.sql new file mode 100644 index 000000000..231fbd57f --- /dev/null +++ b/backend/migrations/2023-03-29-025501_questions/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here + +CREATE TYPE question_types AS ENUM ('ShortAnswer', 'MultiSelect'); + +ALTER TABLE IF EXISTS questions + ADD COLUMN question_type question_types DEFAULT NULL; diff --git a/backend/server/src/application.rs b/backend/server/src/application.rs index ea99cb241..0de01ab5a 100644 --- a/backend/server/src/application.rs +++ b/backend/server/src/application.rs @@ -7,7 +7,7 @@ use crate::{database::{ }, schema::ApplicationStatus, Database, -}, question_types::QuestionTypeEnum}; +}}; use crate::error::JsonErr; use rocket::{ get, @@ -119,32 +119,14 @@ pub async fn create_rating( .await } -#[derive(Serialize, Deserialize)] -pub struct AnswerInput { - pub application_id: i32, - pub question_id: i32, - pub description: String, - pub question_data: QuestionTypeEnum, -} - -impl From> for NewAnswer { - fn from(a: Json) -> Self { - NewAnswer { - application_id: a.application_id, - question_id: a.question_id, - description: a.into_inner().description, - } - } -} #[post("/answer", data = "")] pub async fn submit_answer( user: User, db: Database, - answer: Json, + answer: Json, ) -> Result, JsonErr> { db.run(move |conn| { - let application = Application::get(answer.application_id, &conn) .ok_or(JsonErr(ApplicationError::AppNotFound, Status::NotFound))?; if application.user_id != user.id { @@ -159,7 +141,7 @@ pub async fn submit_answer( return Err(JsonErr(ApplicationError::InvalidInput, Status::BadRequest)); } - NewAnswer::insert(&answer.into(), &conn).ok_or(JsonErr( + NewAnswer::insert(&answer, &conn).ok_or(JsonErr( ApplicationError::UnableToCreate, Status::InternalServerError, ))?; diff --git a/backend/server/src/campaigns.rs b/backend/server/src/campaigns.rs index 860ac9066..4b9bc965d 100644 --- a/backend/server/src/campaigns.rs +++ b/backend/server/src/campaigns.rs @@ -1,11 +1,10 @@ -use crate::database::{ +use crate::{database::{ models::{ Campaign, CampaignWithRoles, NewCampaignInput, NewQuestion, OrganisationUser, Role, RoleUpdate, UpdateCampaignInput, User, }, - Database, -}; -use crate::question_types::QuestionTypeEnum; + Database, schema::QuestionTypes, +}, question_types::QuestionDataEnum}; use crate::error::JsonErr; use rocket::{delete, get, http::Status, post, put, serde::json::Json}; use serde::{Deserialize, Serialize}; @@ -100,7 +99,8 @@ pub struct QuestionInput { pub max_bytes: i32, #[serde(default)] pub required: bool, - pub question_data: QuestionTypeEnum, + pub question_data: QuestionDataEnum, + pub question_type: QuestionTypes, } #[derive(Deserialize)] @@ -123,6 +123,13 @@ pub async fn new( questions, } = inner; + let mut question_data:Vec = Vec::new(); + questions + .iter() + .for_each(|x| { + question_data.push(x.question_data.clone()) + }); + let mut new_questions: Vec = questions .into_iter() .map(|x| NewQuestion { @@ -131,6 +138,7 @@ pub async fn new( description: x.description, max_bytes: x.max_bytes, required: x.required, + question_type: x.question_type, }) .collect(); @@ -170,10 +178,13 @@ pub async fn new( if question.role_ids.len() == 0 { return Err(JsonErr(CampaignError::InvalidInput, Status::BadRequest)); } - question.insert(conn).ok_or_else(|| { + let inserted_question = question.insert(conn).ok_or_else(|| { eprintln!("Failed to create question for some reason"); JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) })?; + + // Insert QuestionDataEnum into db here + } Ok(Json(campaign)) diff --git a/backend/server/src/database/models.rs b/backend/server/src/database/models.rs index 8b643f19f..ea503c6d7 100644 --- a/backend/server/src/database/models.rs +++ b/backend/server/src/database/models.rs @@ -1,6 +1,6 @@ - use super::schema::AdminLevel; use super::schema::ApplicationStatus; +use super::schema::QuestionTypes; use super::schema::{ answers, applications, campaigns, comments, organisation_users, organisations, questions, ratings, roles, users, @@ -911,14 +911,19 @@ impl NewApplication { * * * add New fields to Question sturct (also migration). - * We store the serailised struct in DB as TEXT * + * New question_type which is a enum to identify which type it is + * pub question_type: QuestionTypeEnum * + * New question_data which will not be stored in db rather will provide info for separate specific + * question type table. The table to insert/query determined by question_type field + * pub question_data: QuestionDataEnum * + * id of QuestionData + * pub question_data_id: i32 * */ - // pub what_ever_name_this_question_data_is_to_be: String #[derive(Identifiable, Queryable, PartialEq, Serialize, Debug, QueryableByName)] #[table_name = "questions"] pub struct Question { @@ -930,6 +935,7 @@ pub struct Question { pub required: bool, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, + pub question_type: QuestionTypes, } /** @@ -943,7 +949,8 @@ pub struct Question { * * This means FE must pass in correctly formated JSON struct for our question data * - * pub question_data: String, + * pub question_type: QuestionTypeEnum + * pub question_data: QuestionDataEnum */ #[derive(Insertable, Serialize, Deserialize)] #[table_name = "questions"] @@ -954,6 +961,7 @@ pub struct NewQuestion { #[serde(default)] pub max_bytes: i32, pub required: bool, + pub question_type: QuestionTypes, } @@ -1059,6 +1067,15 @@ impl NewQuestion { pub fn insert(&self, conn: &PgConnection) -> Option { use crate::database::schema::questions::dsl::*; + /* + * + * + * CHANGEME! + * + * + * + */ + self.insert_into(questions).get_result(conn).ok() } } diff --git a/backend/server/src/database/schema.rs b/backend/server/src/database/schema.rs index a41af15a5..e1be045a6 100644 --- a/backend/server/src/database/schema.rs +++ b/backend/server/src/database/schema.rs @@ -11,6 +11,14 @@ pub enum ApplicationStatus { Success, } +#[derive(Debug, DbEnum, PartialEq, FromFormField, Serialize, Deserialize, Clone, Copy)] +#[DbValueStyle = "PascalCase"] +pub enum QuestionTypes { + ShortAnswer, + MultiSelect, +} + + #[derive(Debug, DbEnum, PartialEq, Serialize, Deserialize, Clone, Copy)] #[DbValueStyle = "PascalCase"] pub enum AdminLevel { @@ -103,6 +111,7 @@ table! { table! { use diesel::sql_types::*; + use super::QuestionTypesMapping; questions (id) { id -> Int4, @@ -113,6 +122,7 @@ table! { required -> Bool, created_at -> Timestamp, updated_at -> Timestamp, + question_type -> QuestionTypesMapping, } } diff --git a/backend/server/src/question_types.rs b/backend/server/src/question_types.rs index 15439b041..c1eb11a32 100644 --- a/backend/server/src/question_types.rs +++ b/backend/server/src/question_types.rs @@ -6,10 +6,14 @@ use serde::{Deserialize, Serialize}; */ -#[derive(Deserialize, Serialize)] -pub enum QuestionTypeEnum { - +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub enum QuestionDataEnum { + ShortAnswer, + MultiSelect(MultiSelect), } + + +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] pub struct MultiSelect { } From 6908e415557cc3b2a60ed1aa38eb9dd82ab35be3 Mon Sep 17 00:00:00 2001 From: Alex_Miao_WSL Date: Sat, 1 Apr 2023 20:00:29 +1100 Subject: [PATCH 10/35] feat: changed question type default from NULL to ShortAnswer, added some stubs for querying question data --- .../2023-03-29-025501_questions/up.sql | 2 +- backend/server/src/database/models.rs | 9 ---- backend/server/src/question_types.rs | 48 +++++++++++++++++++ 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/backend/migrations/2023-03-29-025501_questions/up.sql b/backend/migrations/2023-03-29-025501_questions/up.sql index 231fbd57f..139604848 100644 --- a/backend/migrations/2023-03-29-025501_questions/up.sql +++ b/backend/migrations/2023-03-29-025501_questions/up.sql @@ -3,4 +3,4 @@ CREATE TYPE question_types AS ENUM ('ShortAnswer', 'MultiSelect'); ALTER TABLE IF EXISTS questions - ADD COLUMN question_type question_types DEFAULT NULL; + ADD COLUMN question_type question_types DEFAULT 'ShortAnswer'; diff --git a/backend/server/src/database/models.rs b/backend/server/src/database/models.rs index ea503c6d7..62849d058 100644 --- a/backend/server/src/database/models.rs +++ b/backend/server/src/database/models.rs @@ -1067,15 +1067,6 @@ impl NewQuestion { pub fn insert(&self, conn: &PgConnection) -> Option { use crate::database::schema::questions::dsl::*; - /* - * - * - * CHANGEME! - * - * - * - */ - self.insert_into(questions).get_result(conn).ok() } } diff --git a/backend/server/src/question_types.rs b/backend/server/src/question_types.rs index c1eb11a32..7ddc73a88 100644 --- a/backend/server/src/question_types.rs +++ b/backend/server/src/question_types.rs @@ -1,4 +1,7 @@ +use diesel::PgConnection; use serde::{Deserialize, Serialize}; + +use crate::database::{schema::QuestionTypes, Database, models::Question}; /** * In this file, add new question types that we need to implement * e.g. @@ -12,6 +15,51 @@ pub enum QuestionDataEnum { MultiSelect(MultiSelect), } +impl QuestionDataEnum { + /** + * Insert the inner struct into its corresponding table according to the type given by question_type + */ + async fn insert_question_data( + self, + conn: &mut PgConnection, + question: &mut Question, + db: Database + ) -> Option { + db.run(move |conn| { + match self { + QuestionDataEnum::ShortAnswer => { + // do nothing, currently by default a question is of ShortAnswer Type + }, + QuestionDataEnum::MultiSelect(multi_select_data) => { + // Insert Multi Select Data into table + }, + } + + None + }) + .await + } + + async fn get_from_question_id(conn: &PgConnection, question_id: i32) -> Option { + + let question: Question; + + match Question::get_from_id(conn, question_id) { + Some(q) => { + question = q; + } + None => { + return None + } + } + + match question.question_type { + QuestionTypes::ShortAnswer => {None}, + QuestionTypes::MultiSelect => todo!(), + } + + } +} #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] pub struct MultiSelect { From 3ee61b7d8e36ecce7f298edc86282be380e2e792 Mon Sep 17 00:00:00 2001 From: Alex_Miao_WSL Date: Sun, 2 Apr 2023 17:18:55 +1000 Subject: [PATCH 11/35] feat: changed Question Response to include question Data; added Data for Answers as well, structred similar to Question; added function stubs to deal with inserting Answer Data; added migration for adding answer_type field to answer; changed schema of Answer --- .../2023-04-02-063755_answers/down.sql | 3 + .../2023-04-02-063755_answers/up.sql | 4 ++ backend/server/src/application.rs | 25 ++++++-- backend/server/src/database/models.rs | 34 +++++++--- backend/server/src/database/schema.rs | 4 ++ backend/server/src/question.rs | 11 ++-- backend/server/src/question_types.rs | 64 ++++++++++++++----- backend/server/src/role.rs | 15 ++++- 8 files changed, 123 insertions(+), 37 deletions(-) create mode 100644 backend/migrations/2023-04-02-063755_answers/down.sql create mode 100644 backend/migrations/2023-04-02-063755_answers/up.sql diff --git a/backend/migrations/2023-04-02-063755_answers/down.sql b/backend/migrations/2023-04-02-063755_answers/down.sql new file mode 100644 index 000000000..168e63cdd --- /dev/null +++ b/backend/migrations/2023-04-02-063755_answers/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE IF EXISTS answers + DROP COLUMN answer_type; diff --git a/backend/migrations/2023-04-02-063755_answers/up.sql b/backend/migrations/2023-04-02-063755_answers/up.sql new file mode 100644 index 000000000..41b3533b2 --- /dev/null +++ b/backend/migrations/2023-04-02-063755_answers/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here + +ALTER TABLE IF EXISTS answers + ADD COLUMN answer_type question_types DEFAULT 'ShortAnswer'; \ No newline at end of file diff --git a/backend/server/src/application.rs b/backend/server/src/application.rs index 0de01ab5a..9a6b072c8 100644 --- a/backend/server/src/application.rs +++ b/backend/server/src/application.rs @@ -7,7 +7,7 @@ use crate::{database::{ }, schema::ApplicationStatus, Database, -}}; +}, question_types::AnswerDataEnum}; use crate::error::JsonErr; use rocket::{ get, @@ -119,14 +119,24 @@ pub async fn create_rating( .await } +#[derive(Serialize, Deserialize)] +pub struct AnswerWithData { + pub answer: NewAnswer, + pub data: AnswerDataEnum, +} + -#[post("/answer", data = "")] +#[post("/answer", data = "")] pub async fn submit_answer( user: User, db: Database, - answer: Json, + answer_with_data: Json, ) -> Result, JsonErr> { db.run(move |conn| { + let answer_with_data = answer_with_data.into_inner(); + let answer = answer_with_data.answer; + let data = answer_with_data.data; + let application = Application::get(answer.application_id, &conn) .ok_or(JsonErr(ApplicationError::AppNotFound, Status::NotFound))?; if application.user_id != user.id { @@ -141,7 +151,14 @@ pub async fn submit_answer( return Err(JsonErr(ApplicationError::InvalidInput, Status::BadRequest)); } - NewAnswer::insert(&answer, &conn).ok_or(JsonErr( + let mut inserted_answer = NewAnswer::insert(&answer, &conn).ok_or(JsonErr( + ApplicationError::UnableToCreate, + Status::InternalServerError, + ))?; + + + // Insert the Answer Data UwU + AnswerDataEnum::insert_answer_data(data, conn, &inserted_answer).ok_or(JsonErr( ApplicationError::UnableToCreate, Status::InternalServerError, ))?; diff --git a/backend/server/src/database/models.rs b/backend/server/src/database/models.rs index 62849d058..28dc04de3 100644 --- a/backend/server/src/database/models.rs +++ b/backend/server/src/database/models.rs @@ -1,3 +1,5 @@ +use crate::question_types::QuestionDataEnum; + use super::schema::AdminLevel; use super::schema::ApplicationStatus; use super::schema::QuestionTypes; @@ -973,21 +975,33 @@ pub struct QuestionResponse { pub description: Option, pub max_bytes: i32, pub required: bool, + pub question_data: QuestionDataEnum, + pub question_type: QuestionTypes, } -impl std::convert::From for QuestionResponse { - fn from(question: Question) -> Self { +impl std::convert::From<(Question, QuestionDataEnum)> for QuestionResponse { + fn from(question_with_data: (Question, QuestionDataEnum)) -> Self { Self { - id: question.id, - role_ids: question.role_ids, - title: question.title, - description: question.description, - max_bytes: question.max_bytes, - required: question.required, + id: question_with_data.0.id, + role_ids: question_with_data.0.role_ids, + title: question_with_data.0.title, + description: question_with_data.0.description, + max_bytes: question_with_data.0.max_bytes, + required: question_with_data.0.required, + question_type: question_with_data.0.question_type, + question_data: question_with_data.1 } } } +// impl QuestionResponse { +// fn build_question_response() -> Self { +// Self { + +// } +// } +// } + #[derive(FromForm, AsChangeset, Deserialize)] #[table_name = "questions"] pub struct UpdateQuestionInput { @@ -1081,14 +1095,16 @@ pub struct Answer { pub description: String, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, + pub answer_type: QuestionTypes, } -#[derive(Insertable, Deserialize)] +#[derive(Insertable, Deserialize, Serialize)] #[table_name = "answers"] pub struct NewAnswer { pub application_id: i32, pub question_id: i32, pub description: String, + pub answer_type: QuestionTypes, } impl Answer { diff --git a/backend/server/src/database/schema.rs b/backend/server/src/database/schema.rs index e1be045a6..cfd1626f3 100644 --- a/backend/server/src/database/schema.rs +++ b/backend/server/src/database/schema.rs @@ -34,6 +34,9 @@ impl AdminLevel { } table! { + use diesel::sql_types::*; + use super::QuestionTypesMapping; + answers (id) { id -> Int4, application_id -> Int4, @@ -41,6 +44,7 @@ table! { description -> Text, created_at -> Timestamp, updated_at -> Timestamp, + answer_type -> QuestionTypesMapping, } } diff --git a/backend/server/src/question.rs b/backend/server/src/question.rs index d45bc56f3..a91a694e6 100644 --- a/backend/server/src/question.rs +++ b/backend/server/src/question.rs @@ -1,9 +1,9 @@ -use crate::database::{ +use crate::{database::{ models::{ Campaign, OrganisationUser, Question, QuestionResponse, Role, UpdateQuestionInput, User, }, Database, -}; +}, question_types::QuestionDataEnum}; use crate::error::JsonErr; use rocket::{ @@ -36,15 +36,18 @@ pub async fn get_question( .ok_or(JsonErr(QuestionError::QuestionNotFound, Status::NotFound))?; let c = Campaign::get_from_id(&conn, r.campaign_id) .ok_or(JsonErr(QuestionError::QuestionNotFound, Status::NotFound))?; + let d: QuestionDataEnum = QuestionDataEnum::get_from_question_id(&conn, question_id) + .ok_or(JsonErr(QuestionError::QuestionNotFound, Status::NotFound))?; OrganisationUser::role_admin_level(q.get_first_role(), user.id, conn) .is_at_least_director() .or(c.published) .check() .map_err(|_| JsonErr(QuestionError::InsufficientPermissions, Status::Forbidden))?; - Ok(q) + let question_with_data = (q,d); + Ok(question_with_data) }) .await - .map(|q| Json(QuestionResponse::from(q))) + .map(|question_with_data| Json(QuestionResponse::from(question_with_data))) } #[put("/", data = "")] diff --git a/backend/server/src/question_types.rs b/backend/server/src/question_types.rs index 7ddc73a88..8951ddeb4 100644 --- a/backend/server/src/question_types.rs +++ b/backend/server/src/question_types.rs @@ -1,7 +1,7 @@ use diesel::PgConnection; use serde::{Deserialize, Serialize}; -use crate::database::{schema::QuestionTypes, Database, models::Question}; +use crate::database::{schema::QuestionTypes, Database, models::{Question, Answer}}; /** * In this file, add new question types that we need to implement * e.g. @@ -15,32 +15,36 @@ pub enum QuestionDataEnum { MultiSelect(MultiSelect), } +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub enum AnswerDataEnum { + ShortAnswer, + MultiSelect(MultiSelectAnswer), +} + impl QuestionDataEnum { /** * Insert the inner struct into its corresponding table according to the type given by question_type */ - async fn insert_question_data( + pub fn insert_question_data( self, conn: &mut PgConnection, - question: &mut Question, - db: Database + question: &Question, ) -> Option { - db.run(move |conn| { - match self { - QuestionDataEnum::ShortAnswer => { - // do nothing, currently by default a question is of ShortAnswer Type - }, - QuestionDataEnum::MultiSelect(multi_select_data) => { - // Insert Multi Select Data into table - }, - } + + match self { + QuestionDataEnum::ShortAnswer => { + // do nothing, currently by default a question is of ShortAnswer Type + }, + QuestionDataEnum::MultiSelect(multi_select_data) => { + // Insert Multi Select Data into table + }, + } + + None - None - }) - .await } - async fn get_from_question_id(conn: &PgConnection, question_id: i32) -> Option { + pub fn get_from_question_id(conn: &PgConnection, question_id: i32) -> Option { let question: Question; @@ -61,7 +65,33 @@ impl QuestionDataEnum { } } + +impl AnswerDataEnum { + pub fn insert_answer_data( + self, + conn: &mut PgConnection, + answer: &Answer, + ) -> Option { + + match self { + AnswerDataEnum::ShortAnswer => { + // do nothing, currently by default a question is of ShortAnswer Type + }, + AnswerDataEnum::MultiSelect(multi_select_data) => { + // Insert Multi Select Data into table + }, + } + + None + } +} + #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] pub struct MultiSelect { } + +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub struct MultiSelectAnswer { + +} diff --git a/backend/server/src/role.rs b/backend/server/src/role.rs index 51426c84b..258f7672d 100644 --- a/backend/server/src/role.rs +++ b/backend/server/src/role.rs @@ -1,11 +1,11 @@ -use crate::database::{ +use crate::{database::{ models::{ Application, Campaign, GetQuestionsResponse, OrganisationUser, Question, Role, RoleUpdate, User, }, schema::ApplicationStatus, Database, -}; +}, question_types::QuestionDataEnum}; use chrono::NaiveDateTime; use diesel::PgConnection; use rocket::{ @@ -116,6 +116,7 @@ pub enum QuestionsError { CampaignNotFound, Unauthorized, UserNotFound, + QuestionDataNotFound, } #[get("//questions")] @@ -140,8 +141,16 @@ pub async fn get_questions( .or(campaign.published) .check() .map_err(|_| Json(QuestionsError::Unauthorized))?; + + let mut questions_with_data = Vec::new(); + + for question in Question::get_all_from_role_id(conn, role_id) { + let data = QuestionDataEnum::get_from_question_id(conn, question.id) + .ok_or(Json(QuestionsError::QuestionDataNotFound))?; + questions_with_data.push((question, data)); + } Ok(Json(GetQuestionsResponse { - questions: Question::get_all_from_role_id(conn, role_id) + questions: questions_with_data .into_iter() .map(|x| x.into()) .collect(), From 969d1052f9c7c07284a1998de06cb31339f8c9d9 Mon Sep 17 00:00:00 2001 From: Alex_Miao_WSL Date: Mon, 3 Apr 2023 11:11:24 +1000 Subject: [PATCH 12/35] feat: changed answer endpoint to send AnswerResponse type (answer + data); added new error types for Application End point; added new functions for finding AnswerData from borrow of a Answer struct (did the same to QuestionDataEnum), changed some of tsome of the cases where originally question_id is used to find data into using borrow of the Question struct already found. --- backend/server/src/application.rs | 21 ++++++++++++++++++--- backend/server/src/question.rs | 2 +- backend/server/src/question_types.rs | 25 ++++++++++++++++++++++++- backend/server/src/role.rs | 2 +- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/backend/server/src/application.rs b/backend/server/src/application.rs index 9a6b072c8..f19d5802c 100644 --- a/backend/server/src/application.rs +++ b/backend/server/src/application.rs @@ -28,6 +28,7 @@ pub enum ApplicationError { QuestionNotFound, InvalidInput, CampaignEnded, + AnswerDataNotFound, } #[derive(Deserialize)] @@ -151,7 +152,7 @@ pub async fn submit_answer( return Err(JsonErr(ApplicationError::InvalidInput, Status::BadRequest)); } - let mut inserted_answer = NewAnswer::insert(&answer, &conn).ok_or(JsonErr( + let inserted_answer = NewAnswer::insert(&answer, &conn).ok_or(JsonErr( ApplicationError::UnableToCreate, Status::InternalServerError, ))?; @@ -170,7 +171,13 @@ pub async fn submit_answer( #[derive(Serialize)] pub struct AnswersResponse { - answers: Vec, + answers: Vec, +} + +#[derive(Serialize)] +pub struct AnswerResponse { + answer: Answer, + data: AnswerDataEnum, } #[get("//answers")] @@ -188,8 +195,16 @@ pub async fn get_answers( .check() .map_err(|_| JsonErr(ApplicationError::Unauthorized, Status::Forbidden))?; + let mut response: Vec = Vec::new(); + + for answer in Answer::get_all_from_application_id(conn, application_id) { + let data = AnswerDataEnum::get_from_answer(conn, &answer) + .ok_or(JsonErr(ApplicationError::AnswerDataNotFound, Status::NotFound))?; + response.push(AnswerResponse { answer: answer, data: data }); + } + Ok(Json(AnswersResponse { - answers: Answer::get_all_from_application_id(conn, application_id), + answers: response, })) }) .await diff --git a/backend/server/src/question.rs b/backend/server/src/question.rs index a91a694e6..4238b5983 100644 --- a/backend/server/src/question.rs +++ b/backend/server/src/question.rs @@ -36,7 +36,7 @@ pub async fn get_question( .ok_or(JsonErr(QuestionError::QuestionNotFound, Status::NotFound))?; let c = Campaign::get_from_id(&conn, r.campaign_id) .ok_or(JsonErr(QuestionError::QuestionNotFound, Status::NotFound))?; - let d: QuestionDataEnum = QuestionDataEnum::get_from_question_id(&conn, question_id) + let d: QuestionDataEnum = QuestionDataEnum::get_from_question(&conn, &q) .ok_or(JsonErr(QuestionError::QuestionNotFound, Status::NotFound))?; OrganisationUser::role_admin_level(q.get_first_role(), user.id, conn) .is_at_least_director() diff --git a/backend/server/src/question_types.rs b/backend/server/src/question_types.rs index 8951ddeb4..d48d83f43 100644 --- a/backend/server/src/question_types.rs +++ b/backend/server/src/question_types.rs @@ -58,10 +58,22 @@ impl QuestionDataEnum { } match question.question_type { - QuestionTypes::ShortAnswer => {None}, + QuestionTypes::ShortAnswer => {Some(AnswerDataEnum::ShortAnswer);}, QuestionTypes::MultiSelect => todo!(), } + None + } + + pub fn get_from_question(conn: &PgConnection, question: &Question) -> Option { + let question_id = question.id; + + match question.question_type { + QuestionTypes::ShortAnswer => {Some(QuestionDataEnum::ShortAnswer);}, + QuestionTypes::MultiSelect => todo!(), + } + + None } } @@ -84,6 +96,17 @@ impl AnswerDataEnum { None } + + pub fn get_from_answer(conn: &PgConnection, answer: &Answer) -> Option { + let answer_id = answer.id; + + match answer.answer_type { + QuestionTypes::ShortAnswer => {Some(AnswerDataEnum::ShortAnswer);}, + QuestionTypes::MultiSelect => todo!(), + } + + None + } } #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] diff --git a/backend/server/src/role.rs b/backend/server/src/role.rs index 258f7672d..ff7b65db3 100644 --- a/backend/server/src/role.rs +++ b/backend/server/src/role.rs @@ -145,7 +145,7 @@ pub async fn get_questions( let mut questions_with_data = Vec::new(); for question in Question::get_all_from_role_id(conn, role_id) { - let data = QuestionDataEnum::get_from_question_id(conn, question.id) + let data = QuestionDataEnum::get_from_question(conn, &question) .ok_or(Json(QuestionsError::QuestionDataNotFound))?; questions_with_data.push((question, data)); } From 327d9a094f388d424b6c2b23d87793c55d6a31f3 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Sun, 11 Jun 2023 22:12:14 +1000 Subject: [PATCH 13/35] create migration to rename question_types enum Rename the enum to "question_type" --- backend/migrations/2023-05-16-105659_questions/down.sql | 3 +++ backend/migrations/2023-05-16-105659_questions/up.sql | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 backend/migrations/2023-05-16-105659_questions/down.sql create mode 100644 backend/migrations/2023-05-16-105659_questions/up.sql diff --git a/backend/migrations/2023-05-16-105659_questions/down.sql b/backend/migrations/2023-05-16-105659_questions/down.sql new file mode 100644 index 000000000..f6db8d2dd --- /dev/null +++ b/backend/migrations/2023-05-16-105659_questions/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +ALTER TYPE question_type RENAME TO question_types \ No newline at end of file diff --git a/backend/migrations/2023-05-16-105659_questions/up.sql b/backend/migrations/2023-05-16-105659_questions/up.sql new file mode 100644 index 000000000..7f6bf8b77 --- /dev/null +++ b/backend/migrations/2023-05-16-105659_questions/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here + +ALTER TYPE question_types RENAME TO question_type From f94306c63b3294a3d7dab8a1db87652fc56a85c7 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Sun, 11 Jun 2023 22:15:18 +1000 Subject: [PATCH 14/35] create migration for new multi select-based question types --- .../down.sql | 7 ++++ .../2023-05-16-111625_new_answer_types/up.sql | 42 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 backend/migrations/2023-05-16-111625_new_answer_types/down.sql create mode 100644 backend/migrations/2023-05-16-111625_new_answer_types/up.sql diff --git a/backend/migrations/2023-05-16-111625_new_answer_types/down.sql b/backend/migrations/2023-05-16-111625_new_answer_types/down.sql new file mode 100644 index 000000000..a489342aa --- /dev/null +++ b/backend/migrations/2023-05-16-111625_new_answer_types/down.sql @@ -0,0 +1,7 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE short_answer_answers; + +DROP TABLE multi_select_options; + +DROP TABLE multi_select_answers; diff --git a/backend/migrations/2023-05-16-111625_new_answer_types/up.sql b/backend/migrations/2023-05-16-111625_new_answer_types/up.sql new file mode 100644 index 000000000..c411bc2ad --- /dev/null +++ b/backend/migrations/2023-05-16-111625_new_answer_types/up.sql @@ -0,0 +1,42 @@ +-- Your SQL goes here +CREATE TABLE short_answer_answers ( -- TODO: Need to seed this table with data from the current answers table + id SERIAL PRIMARY KEY, + text TEXT NOT NULL, + answer_id INT NOT NULL, + CONSTRAINT fk_short_answer_answer_parent_answer + FOREIGN KEY(answer_id) + REFERENCES answers(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + + +CREATE TABLE multi_select_options( + id SERIAL PRIMARY KEY, + text TEXT NOT NULL, + question_id INT NOT NULL, + CONSTRAINT fk_multi_select_option_parent_question + FOREIGN KEY(question_id) + REFERENCES questions(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE TABLE multi_select_answers ( + id SERIAL PRIMARY KEY, + option_id INT NOT NULL, + answer_id INT NOT NULL, + + CONSTRAINT fk_multi_select_answer_parent_answer + FOREIGN KEY(answer_id) + REFERENCES answers(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + +-- If the option is deleted, then the selection should be too + CONSTRAINT fk_multi_select_option + FOREIGN KEY(option_id) + REFERENCES multi_select_options(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); \ No newline at end of file From 1595e3d9cda4b0a6d699b592c28a469d864d3531 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Wed, 14 Jun 2023 14:38:07 +1000 Subject: [PATCH 15/35] change answer_type to NOT NULL --- .../migrations/2023-06-14-042126_answer_type_not_null/down.sql | 2 ++ .../migrations/2023-06-14-042126_answer_type_not_null/up.sql | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 backend/migrations/2023-06-14-042126_answer_type_not_null/down.sql create mode 100644 backend/migrations/2023-06-14-042126_answer_type_not_null/up.sql diff --git a/backend/migrations/2023-06-14-042126_answer_type_not_null/down.sql b/backend/migrations/2023-06-14-042126_answer_type_not_null/down.sql new file mode 100644 index 000000000..e64fb005a --- /dev/null +++ b/backend/migrations/2023-06-14-042126_answer_type_not_null/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS answers + ALTER COLUMN answer_type DROP NOT NULL; diff --git a/backend/migrations/2023-06-14-042126_answer_type_not_null/up.sql b/backend/migrations/2023-06-14-042126_answer_type_not_null/up.sql new file mode 100644 index 000000000..45e743ce6 --- /dev/null +++ b/backend/migrations/2023-06-14-042126_answer_type_not_null/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS answers + ALTER COLUMN answer_type SET NOT NULL; \ No newline at end of file From 8ddc1f1e5bf73418f677c84a1af67875517c3fc4 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Wed, 14 Jun 2023 14:43:40 +1000 Subject: [PATCH 16/35] change question_type to NOT NULL --- .../2023-06-14-044219_question_type_not_null/down.sql | 2 ++ .../migrations/2023-06-14-044219_question_type_not_null/up.sql | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 backend/migrations/2023-06-14-044219_question_type_not_null/down.sql create mode 100644 backend/migrations/2023-06-14-044219_question_type_not_null/up.sql diff --git a/backend/migrations/2023-06-14-044219_question_type_not_null/down.sql b/backend/migrations/2023-06-14-044219_question_type_not_null/down.sql new file mode 100644 index 000000000..2e31efac7 --- /dev/null +++ b/backend/migrations/2023-06-14-044219_question_type_not_null/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS questions + ALTER COLUMN question_type DROP NOT NULL; diff --git a/backend/migrations/2023-06-14-044219_question_type_not_null/up.sql b/backend/migrations/2023-06-14-044219_question_type_not_null/up.sql new file mode 100644 index 000000000..3a90c14f2 --- /dev/null +++ b/backend/migrations/2023-06-14-044219_question_type_not_null/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS questions + ALTER COLUMN question_type SET NOT NULL; From ce1dc2d501f60fbbbef967f8896b554c534f6197 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Wed, 14 Jun 2023 14:46:14 +1000 Subject: [PATCH 17/35] updated schema.rs to reflect new question/answer types --- backend/server/src/database/schema.rs | 54 ++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/backend/server/src/database/schema.rs b/backend/server/src/database/schema.rs index efa2d8963..42c9b0a86 100644 --- a/backend/server/src/database/schema.rs +++ b/backend/server/src/database/schema.rs @@ -11,11 +11,17 @@ pub enum ApplicationStatus { Success, } -#[derive(Debug, DbEnum, PartialEq, FromFormField, Serialize, Deserialize, Clone, Copy)] -#[DbValueStyle = "PascalCase"] -pub enum QuestionTypes { - ShortAnswer, - MultiSelect, +// #[derive(Debug, DbEnum, PartialEq, FromFormField, Serialize, Deserialize, Clone, Copy)] +// #[DbValueStyle = "PascalCase"] +// pub enum QuestionTypes { +// ShortAnswer, +// MultiSelect, +// } + +pub mod sql_types { + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "question_type"))] + pub struct QuestionType; } @@ -35,7 +41,7 @@ impl AdminLevel { table! { use diesel::sql_types::*; - use super::QuestionTypesMapping; + use super::sql_types::QuestionType; answers (id) { id -> Int4, @@ -44,7 +50,23 @@ table! { description -> Text, created_at -> Timestamp, updated_at -> Timestamp, - answer_type -> QuestionTypesMapping, + answer_type -> QuestionType, + } +} + +table! { + multi_select_answers (id) { + id -> Int4, + option_id -> Int4, + answer_id -> Int4, + } +} + +table! { + short_answer_answers (id) { + id -> Int4, + text -> Text, + answer_id -> Int4, } } @@ -115,7 +137,6 @@ table! { table! { use diesel::sql_types::*; - use super::QuestionTypesMapping; questions (id) { id -> Int4, @@ -126,7 +147,15 @@ table! { required -> Bool, created_at -> Timestamp, updated_at -> Timestamp, - question_type -> QuestionTypesMapping, + question_type -> QuestionType, + } +} + +table! { + multi_select_options (id) { + id -> Int4, + text -> Text, + question_id -> Int4, } } @@ -183,15 +212,22 @@ joinable!(organisation_users -> users (user_id)); joinable!(ratings -> applications (application_id)); joinable!(ratings -> users (rater_user_id)); joinable!(roles -> campaigns (campaign_id)); +joinable!(multi_select_answers -> answers (answer_id)); +joinable!(multi_select_answers -> multi_select_options (option_id)); +joinable!(multi_select_options -> questions (question_id)); +joinable!(short_answer_answers -> answers (answer_id)); allow_tables_to_appear_in_same_query!( answers, + multi_select_answers, + short_answer_answers, applications, campaigns, comments, organisation_users, organisations, questions, + multi_select_options, ratings, roles, users, From c1d0212b815d0f06b1ed6dc9e11c9f498fbe8c6d Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Wed, 21 Jun 2023 13:30:56 +1000 Subject: [PATCH 18/35] feat(backend): multiple-select question family stubs --- backend/.idea/.gitignore | 8 + backend/.idea/backend.iml | 13 ++ backend/.idea/dataSources.xml | 12 ++ backend/.idea/modules.xml | 8 + backend/.idea/sqldialects.xml | 6 + backend/.idea/vcs.xml | 6 + backend/diesel.toml | 8 + backend/server/src/application.rs | 10 +- backend/server/src/campaigns.rs | 36 ++-- backend/server/src/database/models.rs | 131 +++++++++++--- backend/server/src/database/schema.rs | 19 ++ backend/server/src/database/schema_new.rs | 208 ++++++++++++++++++++++ backend/server/src/question.rs | 4 +- backend/server/src/question_types.rs | 107 +++++++---- backend/server/src/role.rs | 4 +- backend/src/schema.rs | 208 ++++++++++++++++++++++ 16 files changed, 705 insertions(+), 83 deletions(-) create mode 100644 backend/.idea/.gitignore create mode 100644 backend/.idea/backend.iml create mode 100644 backend/.idea/dataSources.xml create mode 100644 backend/.idea/modules.xml create mode 100644 backend/.idea/sqldialects.xml create mode 100644 backend/.idea/vcs.xml create mode 100644 backend/diesel.toml create mode 100644 backend/server/src/database/schema_new.rs create mode 100644 backend/src/schema.rs diff --git a/backend/.idea/.gitignore b/backend/.idea/.gitignore new file mode 100644 index 000000000..13566b81b --- /dev/null +++ b/backend/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/backend/.idea/backend.iml b/backend/.idea/backend.iml new file mode 100644 index 000000000..33e8580c4 --- /dev/null +++ b/backend/.idea/backend.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/.idea/dataSources.xml b/backend/.idea/dataSources.xml new file mode 100644 index 000000000..7703b9773 --- /dev/null +++ b/backend/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/chaos + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/backend/.idea/modules.xml b/backend/.idea/modules.xml new file mode 100644 index 000000000..e066844ef --- /dev/null +++ b/backend/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/backend/.idea/sqldialects.xml b/backend/.idea/sqldialects.xml new file mode 100644 index 000000000..6df4889b0 --- /dev/null +++ b/backend/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backend/.idea/vcs.xml b/backend/.idea/vcs.xml new file mode 100644 index 000000000..6c0b86358 --- /dev/null +++ b/backend/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backend/diesel.toml b/backend/diesel.toml new file mode 100644 index 000000000..35a12ff0d --- /dev/null +++ b/backend/diesel.toml @@ -0,0 +1,8 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" + +[migrations_directory] +dir = "migrations" diff --git a/backend/server/src/application.rs b/backend/server/src/application.rs index f19d5802c..61ef8f5d8 100644 --- a/backend/server/src/application.rs +++ b/backend/server/src/application.rs @@ -7,7 +7,7 @@ use crate::{database::{ }, schema::ApplicationStatus, Database, -}, question_types::AnswerDataEnum}; +}, question_types::AnswerData}; use crate::error::JsonErr; use rocket::{ get, @@ -123,7 +123,7 @@ pub async fn create_rating( #[derive(Serialize, Deserialize)] pub struct AnswerWithData { pub answer: NewAnswer, - pub data: AnswerDataEnum, + pub data: AnswerData, } @@ -159,7 +159,7 @@ pub async fn submit_answer( // Insert the Answer Data UwU - AnswerDataEnum::insert_answer_data(data, conn, &inserted_answer).ok_or(JsonErr( + AnswerData::insert_answer_data(data, conn, &inserted_answer).ok_or(JsonErr( ApplicationError::UnableToCreate, Status::InternalServerError, ))?; @@ -177,7 +177,7 @@ pub struct AnswersResponse { #[derive(Serialize)] pub struct AnswerResponse { answer: Answer, - data: AnswerDataEnum, + data: AnswerData, } #[get("//answers")] @@ -198,7 +198,7 @@ pub async fn get_answers( let mut response: Vec = Vec::new(); for answer in Answer::get_all_from_application_id(conn, application_id) { - let data = AnswerDataEnum::get_from_answer(conn, &answer) + let data = AnswerData::get_from_answer(conn, &answer) .ok_or(JsonErr(ApplicationError::AnswerDataNotFound, Status::NotFound))?; response.push(AnswerResponse { answer: answer, data: data }); } diff --git a/backend/server/src/campaigns.rs b/backend/server/src/campaigns.rs index 8072996d2..8f9b43801 100644 --- a/backend/server/src/campaigns.rs +++ b/backend/server/src/campaigns.rs @@ -5,10 +5,10 @@ use crate::{ Campaign, CampaignWithRoles, NewCampaignInput, NewQuestion, OrganisationUser, Role, RoleUpdate, UpdateCampaignInput, User, }, - Database, schema::QuestionTypes, + Database, schema::sql_types::QuestionType, }, images::{get_http_image_path, save_image, try_decode_data, ImageLocation}, - question_types::QuestionDataEnum + question_types::QuestionData }; use rocket::{data::Data, delete, get, http::Status, post, put, serde::json::Json}; use serde::{Deserialize, Serialize}; @@ -120,8 +120,8 @@ pub struct QuestionInput { pub max_bytes: i32, #[serde(default)] pub required: bool, - pub question_data: QuestionDataEnum, - pub question_type: QuestionTypes, + pub question_data: QuestionData, + pub question_type: QuestionType, } #[derive(Deserialize)] @@ -143,13 +143,13 @@ pub async fn new( roles, mut questions, } = inner; - - let mut question_data:Vec = Vec::new(); - questions + + let mut question_data: Vec = questions .iter() - .for_each(|x| { - question_data.push(x.question_data.clone()) - }); + .map(|x| { + x.question_data.clone() + }) + .collect(); let mut new_questions: Vec = questions .iter_mut() @@ -203,15 +203,19 @@ pub async fn new( } } - for question in new_questions { - question.insert(conn).ok_or_else(|| { + for (question, question_data) in new_questions.into_iter().zip(question_data.into_iter()) { + + // Insert question (skeleton) into database, and then insert it's data into + // corresponding table in database. + let inserted_id = question.insert(conn).ok_or_else(|| { eprintln!("Failed to create question for some reason"); JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) - })?; + })?.id; - // Insert QuestionDataEnum into db here - // Use the method insert_question_data() of QuestionDataEnum - todo!(); + question_data.insert_question_data(conn, &question, inserted_id).ok_or_else(|| { + eprintln!("Failed to create question data for some reason"); + JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) + })?; } Ok(Json(campaign)) diff --git a/backend/server/src/database/models.rs b/backend/server/src/database/models.rs index 85792898c..978ed5dc4 100644 --- a/backend/server/src/database/models.rs +++ b/backend/server/src/database/models.rs @@ -1,9 +1,8 @@ -use crate::question_types::QuestionDataEnum; +use crate::question_types::QuestionData; use crate::images::{get_http_image_path, ImageLocation}; use super::schema::AdminLevel; use super::schema::ApplicationStatus; -use super::schema::QuestionTypes; use super::schema::{ answers, applications, campaigns, comments, organisation_users, organisations, questions, ratings, roles, users, @@ -17,6 +16,10 @@ use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::fs::remove_file; use std::path::Path; +use crate::database::schema::multi_select_answers::dsl::multi_select_answers; +use crate::database::schema::multi_select_options::dsl::multi_select_options; +use crate::database::schema::short_answer_answers::dsl::short_answer_answers; +use crate::database::schema::sql_types::QuestionType; #[derive(Queryable)] pub struct User { @@ -972,22 +975,22 @@ impl NewApplication { } /** - * - * - * - * + * + * + * + * * add New fields to Question sturct (also migration). - * + * * New question_type which is a enum to identify which type it is * pub question_type: QuestionTypeEnum - * + * * New question_data which will not be stored in db rather will provide info for separate specific * question type table. The table to insert/query determined by question_type field * pub question_data: QuestionDataEnum - * + * * id of QuestionData * pub question_data_id: i32 - * + * */ #[derive(Identifiable, Queryable, PartialEq, Serialize, Debug, QueryableByName)] @@ -1001,20 +1004,20 @@ pub struct Question { pub required: bool, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, - pub question_type: QuestionTypes, + pub question_type: QuestionType, } /** - * - * - * + * + * + * * Add enum field to NewQuestion - * - * - * - * + * + * + * + * * This means FE must pass in correctly formated JSON struct for our question data - * + * * pub question_type: QuestionTypeEnum * pub question_data: QuestionDataEnum */ @@ -1027,7 +1030,7 @@ pub struct NewQuestion { #[serde(default)] pub max_bytes: i32, pub required: bool, - pub question_type: QuestionTypes, + pub question_type: QuestionType, } @@ -1039,12 +1042,12 @@ pub struct QuestionResponse { pub description: Option, pub max_bytes: i32, pub required: bool, - pub question_data: QuestionDataEnum, - pub question_type: QuestionTypes, + pub question_data: QuestionData, + pub question_type: QuestionType, } -impl std::convert::From<(Question, QuestionDataEnum)> for QuestionResponse { - fn from(question_with_data: (Question, QuestionDataEnum)) -> Self { +impl std::convert::From<(Question, QuestionData)> for QuestionResponse { + fn from(question_with_data: (Question, QuestionData)) -> Self { Self { id: question_with_data.0.id, role_id: question_with_data.0.role_id, @@ -1075,6 +1078,29 @@ pub struct UpdateQuestionInput { pub required: bool, } +#[derive(Insertable, Queryable, Deserialize, Serialize, PartialEq, Debug, Clone)] +#[table_name = "multi_select_options"] +pub struct MultiSelectOption { + id: i32, + text: String, + question_id: i32, +} + +#[derive(Insertable, Deserialize, Serialize, PartialEq, Debug, Clone)] +#[table_name = "multi_select_options"] +pub struct NewMultiSelectOption { + text: String, + question_id: i32, +} + +impl NewMultiSelectOption { + pub fn insert(&self, conn: &PgConnection) -> Option { + use crate::database::schema::multi_select_options::dsl::*; + + self.insert_into(multi_select_options).get_result(conn).ok() + } +} + impl Question { pub fn get_first_role(&self) -> i32 { self.role_id @@ -1157,7 +1183,7 @@ pub struct Answer { pub description: String, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, - pub answer_type: QuestionTypes, + pub answer_type: QuestionType, } #[derive(Insertable, Deserialize, Serialize)] @@ -1166,7 +1192,60 @@ pub struct NewAnswer { pub application_id: i32, pub question_id: i32, pub description: String, - pub answer_type: QuestionTypes, + pub answer_type: QuestionType, +} + +#[derive(Queryable, Deserialize, Serialize, PartialEq, Debug, Clone)] +#[table_name = "short_answer_answers"] +pub struct ShortAnswerAnswer { + id: i32, + text: String, + answer_id: i32, +} + +#[derive(Insertable, Deserialize, Serialize, PartialEq, Debug, Clone)] +#[table_name = "short_answer_answers"] +pub struct NewShortAnswerAnswer { + text: String, + answer_id: i32, +} + +impl NewShortAnswerAnswer { + pub fn insert(&self, conn: &PgConnection) -> Option { + use crate::database::schema::short_answer_answers::dsl::*; + + self.insert_into(short_answer_answers).get_result(conn).ok() + } +} + +/// A struct to store answers for multi-choice, multi-select and drop-down +/// question types, as these work the same way in the backend. The only +/// difference is the restriction on number of answers for each type, +/// with multi-choice only having one unique answer (vector length 1) +/// +/// \ +/// The vector will store the id's of each option selected. +#[derive(Queryable, Deserialize, Serialize, PartialEq, Debug, Clone)] +#[table_name = "multi_select_answers"] +pub struct MultiSelectAnswer { + id: i32, + option_id: i32, + answer_id: i32, +} + +#[derive(Insertable, Deserialize, Serialize, PartialEq, Debug, Clone)] +#[table_name = "multi_select_answers"] +pub struct NewMultiSelectAnswer { + option_id: i32, + answer_id: i32, +} + +impl NewMultiSelectAnswer { + pub fn insert(&self, conn: &PgConnection) -> Option { + use crate::database::schema::multi_select_answers::dsl::*; + + self.insert_into(multi_select_answers).get_result(conn).ok() + } } impl Answer { diff --git a/backend/server/src/database/schema.rs b/backend/server/src/database/schema.rs index efa2d8963..1ad083efc 100644 --- a/backend/server/src/database/schema.rs +++ b/backend/server/src/database/schema.rs @@ -11,11 +11,26 @@ pub enum ApplicationStatus { Success, } +<<<<<<< Updated upstream #[derive(Debug, DbEnum, PartialEq, FromFormField, Serialize, Deserialize, Clone, Copy)] #[DbValueStyle = "PascalCase"] pub enum QuestionTypes { ShortAnswer, MultiSelect, +======= +// #[derive(Debug, DbEnum, PartialEq, FromFormField, Serialize, Deserialize, Clone, Copy)] +// #[DbValueStyle = "PascalCase"] +// pub enum QuestionTypes { +// ShortAnswer, +// MultiSelect, +// } + +// TODO: Might have to change this to an enum (diesel auto-generated this struct, but enum is better? maybe for use within rust) +pub mod sql_types { + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "question_type"))] + pub struct QuestionType; +>>>>>>> Stashed changes } @@ -35,7 +50,11 @@ impl AdminLevel { table! { use diesel::sql_types::*; +<<<<<<< Updated upstream use super::QuestionTypesMapping; +======= + use crate::database::sql_types::QuestionType; +>>>>>>> Stashed changes answers (id) { id -> Int4, diff --git a/backend/server/src/database/schema_new.rs b/backend/server/src/database/schema_new.rs new file mode 100644 index 000000000..b4b972d78 --- /dev/null +++ b/backend/server/src/database/schema_new.rs @@ -0,0 +1,208 @@ +// @generated automatically by Diesel CLI. + +pub mod sql_types { + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "admin_level"))] + pub struct AdminLevel; + + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "application_status"))] + pub struct ApplicationStatus; + + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "question_type"))] + pub struct QuestionType; +} + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::QuestionType; + + answers (id) { + id -> Int4, + application_id -> Int4, + question_id -> Int4, + description -> Text, + created_at -> Timestamp, + updated_at -> Timestamp, + answer_type -> QuestionType, + } +} + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::ApplicationStatus; + + applications (id) { + id -> Int4, + user_id -> Int4, + role_id -> Int4, + status -> ApplicationStatus, + created_at -> Timestamp, + updated_at -> Timestamp, + private_status -> Nullable, + } +} + +diesel::table! { + campaigns (id) { + id -> Int4, + organisation_id -> Int4, + name -> Text, + cover_image -> Nullable, + description -> Text, + starts_at -> Timestamp, + ends_at -> Timestamp, + published -> Bool, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::table! { + comments (id) { + id -> Int4, + application_id -> Int4, + commenter_user_id -> Int4, + description -> Text, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::table! { + multi_select_answers (id) { + id -> Int4, + option_id -> Int4, + answer_id -> Int4, + } +} + +diesel::table! { + multi_select_options (id) { + id -> Int4, + text -> Text, + question_id -> Int4, + } +} + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::AdminLevel; + + organisation_users (id) { + id -> Int4, + user_id -> Int4, + organisation_id -> Int4, + admin_level -> AdminLevel, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::table! { + organisations (id) { + id -> Int4, + name -> Text, + logo -> Nullable, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::QuestionType; + + questions (id) { + id -> Int4, + title -> Text, + description -> Nullable, + max_bytes -> Int4, + required -> Bool, + created_at -> Timestamp, + updated_at -> Timestamp, + role_id -> Nullable, + question_type -> Nullable, + } +} + +diesel::table! { + ratings (id) { + id -> Int4, + application_id -> Int4, + rater_user_id -> Int4, + rating -> Int4, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::table! { + roles (id) { + id -> Int4, + campaign_id -> Int4, + name -> Text, + description -> Nullable, + min_available -> Int4, + max_available -> Int4, + finalised -> Bool, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::table! { + short_answer_answers (id) { + id -> Int4, + text -> Text, + answer_id -> Int4, + } +} + +diesel::table! { + users (id) { + id -> Int4, + email -> Text, + zid -> Text, + display_name -> Text, + degree_name -> Text, + degree_starting_year -> Int4, + superuser -> Bool, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::joinable!(answers -> applications (application_id)); +diesel::joinable!(answers -> questions (question_id)); +diesel::joinable!(applications -> roles (role_id)); +diesel::joinable!(applications -> users (user_id)); +diesel::joinable!(campaigns -> organisations (organisation_id)); +diesel::joinable!(comments -> applications (application_id)); +diesel::joinable!(comments -> users (commenter_user_id)); +diesel::joinable!(multi_select_answers -> answers (answer_id)); +diesel::joinable!(multi_select_answers -> multi_select_options (option_id)); +diesel::joinable!(multi_select_options -> questions (question_id)); +diesel::joinable!(organisation_users -> organisations (organisation_id)); +diesel::joinable!(organisation_users -> users (user_id)); +diesel::joinable!(ratings -> applications (application_id)); +diesel::joinable!(ratings -> users (rater_user_id)); +diesel::joinable!(roles -> campaigns (campaign_id)); +diesel::joinable!(short_answer_answers -> answers (answer_id)); + +diesel::allow_tables_to_appear_in_same_query!( + answers, + applications, + campaigns, + comments, + multi_select_answers, + multi_select_options, + organisation_users, + organisations, + questions, + ratings, + roles, + short_answer_answers, + users, +); diff --git a/backend/server/src/question.rs b/backend/server/src/question.rs index 4238b5983..ded1ddf8c 100644 --- a/backend/server/src/question.rs +++ b/backend/server/src/question.rs @@ -3,7 +3,7 @@ use crate::{database::{ Campaign, OrganisationUser, Question, QuestionResponse, Role, UpdateQuestionInput, User, }, Database, -}, question_types::QuestionDataEnum}; +}, question_types::QuestionData}; use crate::error::JsonErr; use rocket::{ @@ -36,7 +36,7 @@ pub async fn get_question( .ok_or(JsonErr(QuestionError::QuestionNotFound, Status::NotFound))?; let c = Campaign::get_from_id(&conn, r.campaign_id) .ok_or(JsonErr(QuestionError::QuestionNotFound, Status::NotFound))?; - let d: QuestionDataEnum = QuestionDataEnum::get_from_question(&conn, &q) + let d: QuestionData = QuestionData::get_from_question(&conn, &q) .ok_or(JsonErr(QuestionError::QuestionNotFound, Status::NotFound))?; OrganisationUser::role_admin_level(q.get_first_role(), user.id, conn) .is_at_least_director() diff --git a/backend/server/src/question_types.rs b/backend/server/src/question_types.rs index e0cbadd1a..7e7a2ffdb 100644 --- a/backend/server/src/question_types.rs +++ b/backend/server/src/question_types.rs @@ -1,42 +1,72 @@ use diesel::PgConnection; use serde::{Deserialize, Serialize}; -use crate::database::{schema::QuestionTypes, models::{Question, Answer}}; -/** - * In this file, add new question types that we need to implement - * e.g. - * MultiSelect - */ - - +use crate::database::{models::{Question, Answer}}; +use crate::database::models::{MultiSelectOption, NewQuestion}; +use crate::database::schema::sql_types::QuestionType; +// QUESTION TYPES +// In this file, add new question types that we need to implement +// e.g. +// MultiSelect +// ShortAnswer +// + +/// An enum that represents all the types of questions that CHAOS can handle. +/// This stores all the data for each question type. +/// +/// \ +/// Some question types are stored in-memory and JSON using the same struct, and only differ +/// in their implementation when inserting to the database and in their restrictions +/// (e.g. max answers allowed in single multi-choice) #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] -pub enum QuestionDataEnum { +pub enum QuestionData { ShortAnswer, - MultiSelect(MultiSelect), + MultiSelect(MultiSelectQuestion), + MultiChoice(MultiSelectQuestion), + DropDown(MultiSelectQuestion), } +/// An enum that represents all the types of questions answers that CHAOS can handle. +/// This stores all the data for each answer type. +/// +/// \ +/// Some answers types are stored in-memory and JSON using the same struct, and only differ +/// in their implementation when inserting to the database and in their restrictions +/// (e.g. max answers allowed in single multi-choice) #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] -pub enum AnswerDataEnum { - ShortAnswer, +pub enum AnswerData { + ShortAnswer(String), MultiSelect(MultiSelectAnswer), + MultiChoice(MultiSelectAnswer), // TODO: Is there a better way to name these, without duplicating the structs? Traits? + DropDown(MultiSelectAnswer), } -impl QuestionDataEnum { +impl QuestionData { /** * Insert the inner struct into its corresponding table according to the type given by question_type */ pub fn insert_question_data( self, - conn: &mut PgConnection, - question: &Question, + conn: &PgConnection, + question: &NewQuestion, + question_id: i32, ) -> Option { match self { - QuestionDataEnum::ShortAnswer => { - // do nothing, currently by default a question is of ShortAnswer Type + QuestionData::ShortAnswer => { + // No need for any question data insertion, as short-answer + // questions only need a title (contained in parent table) }, - QuestionDataEnum::MultiSelect(multi_select_data) => { + QuestionData::MultiSelect(multi_select_data) => { // Insert Multi Select Data into table + + // TODO: insert into db using question_id - I think nothing has to be done for this one!!! + }, + QuestionData::MultiChoice(multi_choice_data) => { + // Insert Multi Choice Data into table + }, + QuestionData::DropDown(drop_down_data) => { + // Insert Drop Down Data into table }, } @@ -58,8 +88,10 @@ impl QuestionDataEnum { } match question.question_type { - QuestionTypes::ShortAnswer => {Some(AnswerDataEnum::ShortAnswer);}, - QuestionTypes::MultiSelect => todo!(), + QuestionType::ShortAnswer => {Some(AnswerData::ShortAnswer);}, + QuestionType::MultiSelect => todo!(), + QuestionType::MultiChoice => todo!(), + QuestionType::DropDown => todo!(), } None @@ -69,8 +101,10 @@ impl QuestionDataEnum { let question_id = question.id; match question.question_type { - QuestionTypes::ShortAnswer => {Some(QuestionDataEnum::ShortAnswer);}, - QuestionTypes::MultiSelect => todo!(), + QuestionType::ShortAnswer => {Some(QuestionData::ShortAnswer);}, + QuestionType::MultiSelect => todo!(), + QuestionType::MultiChoice => todo!(), + QuestionType::DropDown => todo!(), } None @@ -78,7 +112,7 @@ impl QuestionDataEnum { } -impl AnswerDataEnum { +impl AnswerData { pub fn insert_answer_data( self, conn: &mut PgConnection, @@ -86,12 +120,18 @@ impl AnswerDataEnum { ) -> Option { match self { - AnswerDataEnum::ShortAnswer => { + AnswerData::ShortAnswer(short_answer_data) => { // do nothing, currently by default a question is of ShortAnswer Type }, - AnswerDataEnum::MultiSelect(multi_select_data) => { + AnswerData::MultiSelect(multi_select_data) => { // Insert Multi Select Data into table }, + AnswerData::MultiChoice(multi_choice_data) => { + // Insert Multi Choice Data into table + }, + AnswerData::DropDown(drop_down_data) => { + // Insert Drop Down Data into table + } } None @@ -101,20 +141,23 @@ impl AnswerDataEnum { let answer_id = answer.id; match answer.answer_type { - QuestionTypes::ShortAnswer => {Some(AnswerDataEnum::ShortAnswer);}, - QuestionTypes::MultiSelect => todo!(), + QuestionType::ShortAnswer => {Some(AnswerData::ShortAnswer);}, + QuestionType::MultiSelect => todo!(), + QuestionType::MultiChoice => todo!(), + QuestionType::DropDown => todo!(), } None } } -#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] -pub struct MultiSelect { +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub struct MultiSelectQuestion { + options: Vec } #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] -pub struct MultiSelectAnswer { - -} +pub struct NewMultiSelectAnswer { + options_selected: Vec, +} \ No newline at end of file diff --git a/backend/server/src/role.rs b/backend/server/src/role.rs index ff7b65db3..81357be08 100644 --- a/backend/server/src/role.rs +++ b/backend/server/src/role.rs @@ -5,7 +5,7 @@ use crate::{database::{ }, schema::ApplicationStatus, Database, -}, question_types::QuestionDataEnum}; +}, question_types::QuestionData}; use chrono::NaiveDateTime; use diesel::PgConnection; use rocket::{ @@ -145,7 +145,7 @@ pub async fn get_questions( let mut questions_with_data = Vec::new(); for question in Question::get_all_from_role_id(conn, role_id) { - let data = QuestionDataEnum::get_from_question(conn, &question) + let data = QuestionData::get_from_question(conn, &question) .ok_or(Json(QuestionsError::QuestionDataNotFound))?; questions_with_data.push((question, data)); } diff --git a/backend/src/schema.rs b/backend/src/schema.rs new file mode 100644 index 000000000..b4b972d78 --- /dev/null +++ b/backend/src/schema.rs @@ -0,0 +1,208 @@ +// @generated automatically by Diesel CLI. + +pub mod sql_types { + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "admin_level"))] + pub struct AdminLevel; + + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "application_status"))] + pub struct ApplicationStatus; + + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "question_type"))] + pub struct QuestionType; +} + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::QuestionType; + + answers (id) { + id -> Int4, + application_id -> Int4, + question_id -> Int4, + description -> Text, + created_at -> Timestamp, + updated_at -> Timestamp, + answer_type -> QuestionType, + } +} + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::ApplicationStatus; + + applications (id) { + id -> Int4, + user_id -> Int4, + role_id -> Int4, + status -> ApplicationStatus, + created_at -> Timestamp, + updated_at -> Timestamp, + private_status -> Nullable, + } +} + +diesel::table! { + campaigns (id) { + id -> Int4, + organisation_id -> Int4, + name -> Text, + cover_image -> Nullable, + description -> Text, + starts_at -> Timestamp, + ends_at -> Timestamp, + published -> Bool, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::table! { + comments (id) { + id -> Int4, + application_id -> Int4, + commenter_user_id -> Int4, + description -> Text, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::table! { + multi_select_answers (id) { + id -> Int4, + option_id -> Int4, + answer_id -> Int4, + } +} + +diesel::table! { + multi_select_options (id) { + id -> Int4, + text -> Text, + question_id -> Int4, + } +} + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::AdminLevel; + + organisation_users (id) { + id -> Int4, + user_id -> Int4, + organisation_id -> Int4, + admin_level -> AdminLevel, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::table! { + organisations (id) { + id -> Int4, + name -> Text, + logo -> Nullable, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::QuestionType; + + questions (id) { + id -> Int4, + title -> Text, + description -> Nullable, + max_bytes -> Int4, + required -> Bool, + created_at -> Timestamp, + updated_at -> Timestamp, + role_id -> Nullable, + question_type -> Nullable, + } +} + +diesel::table! { + ratings (id) { + id -> Int4, + application_id -> Int4, + rater_user_id -> Int4, + rating -> Int4, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::table! { + roles (id) { + id -> Int4, + campaign_id -> Int4, + name -> Text, + description -> Nullable, + min_available -> Int4, + max_available -> Int4, + finalised -> Bool, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::table! { + short_answer_answers (id) { + id -> Int4, + text -> Text, + answer_id -> Int4, + } +} + +diesel::table! { + users (id) { + id -> Int4, + email -> Text, + zid -> Text, + display_name -> Text, + degree_name -> Text, + degree_starting_year -> Int4, + superuser -> Bool, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::joinable!(answers -> applications (application_id)); +diesel::joinable!(answers -> questions (question_id)); +diesel::joinable!(applications -> roles (role_id)); +diesel::joinable!(applications -> users (user_id)); +diesel::joinable!(campaigns -> organisations (organisation_id)); +diesel::joinable!(comments -> applications (application_id)); +diesel::joinable!(comments -> users (commenter_user_id)); +diesel::joinable!(multi_select_answers -> answers (answer_id)); +diesel::joinable!(multi_select_answers -> multi_select_options (option_id)); +diesel::joinable!(multi_select_options -> questions (question_id)); +diesel::joinable!(organisation_users -> organisations (organisation_id)); +diesel::joinable!(organisation_users -> users (user_id)); +diesel::joinable!(ratings -> applications (application_id)); +diesel::joinable!(ratings -> users (rater_user_id)); +diesel::joinable!(roles -> campaigns (campaign_id)); +diesel::joinable!(short_answer_answers -> answers (answer_id)); + +diesel::allow_tables_to_appear_in_same_query!( + answers, + applications, + campaigns, + comments, + multi_select_answers, + multi_select_options, + organisation_users, + organisations, + questions, + ratings, + roles, + short_answer_answers, + users, +); From 05fbbf55098ef1dacb0773e24c5bf41dbe938a11 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Wed, 21 Jun 2023 14:38:55 +1000 Subject: [PATCH 19/35] feat(backend): create new data types for question data input --- backend/server/src/campaigns.rs | 5 +++-- backend/server/src/database/models.rs | 17 +++++++++++------ backend/server/src/database/schema.rs | 19 ++++--------------- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/backend/server/src/campaigns.rs b/backend/server/src/campaigns.rs index 8f9b43801..dcf0225cd 100644 --- a/backend/server/src/campaigns.rs +++ b/backend/server/src/campaigns.rs @@ -14,6 +14,7 @@ use rocket::{data::Data, delete, get, http::Status, post, put, serde::json::Json use serde::{Deserialize, Serialize}; use uuid::Uuid; +use crate::question_types::QuestionDataInput; #[derive(Serialize)] pub enum CampaignError { @@ -120,7 +121,7 @@ pub struct QuestionInput { pub max_bytes: i32, #[serde(default)] pub required: bool, - pub question_data: QuestionData, + pub question_data: QuestionDataInput, pub question_type: QuestionType, } @@ -144,7 +145,7 @@ pub async fn new( mut questions, } = inner; - let mut question_data: Vec = questions + let mut question_data: Vec = questions .iter() .map(|x| { x.question_data.clone() diff --git a/backend/server/src/database/models.rs b/backend/server/src/database/models.rs index 978ed5dc4..761d7da73 100644 --- a/backend/server/src/database/models.rs +++ b/backend/server/src/database/models.rs @@ -1078,19 +1078,24 @@ pub struct UpdateQuestionInput { pub required: bool, } -#[derive(Insertable, Queryable, Deserialize, Serialize, PartialEq, Debug, Clone)] +#[derive(Queryable, Deserialize, Serialize, PartialEq, Debug, Clone)] #[table_name = "multi_select_options"] pub struct MultiSelectOption { - id: i32, - text: String, - question_id: i32, + pub id: i32, + pub text: String, + pub question_id: i32, +} + +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub struct MultiSelectOptionInput { + pub text: String, } #[derive(Insertable, Deserialize, Serialize, PartialEq, Debug, Clone)] #[table_name = "multi_select_options"] pub struct NewMultiSelectOption { - text: String, - question_id: i32, + pub text: String, + pub question_id: i32, } impl NewMultiSelectOption { diff --git a/backend/server/src/database/schema.rs b/backend/server/src/database/schema.rs index 1ad083efc..f2a0f45d9 100644 --- a/backend/server/src/database/schema.rs +++ b/backend/server/src/database/schema.rs @@ -11,26 +11,11 @@ pub enum ApplicationStatus { Success, } -<<<<<<< Updated upstream #[derive(Debug, DbEnum, PartialEq, FromFormField, Serialize, Deserialize, Clone, Copy)] #[DbValueStyle = "PascalCase"] pub enum QuestionTypes { ShortAnswer, MultiSelect, -======= -// #[derive(Debug, DbEnum, PartialEq, FromFormField, Serialize, Deserialize, Clone, Copy)] -// #[DbValueStyle = "PascalCase"] -// pub enum QuestionTypes { -// ShortAnswer, -// MultiSelect, -// } - -// TODO: Might have to change this to an enum (diesel auto-generated this struct, but enum is better? maybe for use within rust) -pub mod sql_types { - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "question_type"))] - pub struct QuestionType; ->>>>>>> Stashed changes } @@ -50,8 +35,12 @@ impl AdminLevel { table! { use diesel::sql_types::*; +<<<<<<< Updated upstream <<<<<<< Updated upstream use super::QuestionTypesMapping; +======= + use crate::database::sql_types::QuestionType; +>>>>>>> Stashed changes ======= use crate::database::sql_types::QuestionType; >>>>>>> Stashed changes From 508ccf8218d5bf1af38b596fc876676566869809 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Wed, 21 Jun 2023 14:39:08 +1000 Subject: [PATCH 20/35] feat(backend): handle insertion of question data into DB --- backend/server/src/question_types.rs | 79 +++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/backend/server/src/question_types.rs b/backend/server/src/question_types.rs index 7e7a2ffdb..2840e075c 100644 --- a/backend/server/src/question_types.rs +++ b/backend/server/src/question_types.rs @@ -1,9 +1,10 @@ use diesel::PgConnection; +use rocket::http::Status; use serde::{Deserialize, Serialize}; use crate::database::{models::{Question, Answer}}; -use crate::database::models::{MultiSelectOption, NewQuestion}; -use crate::database::schema::sql_types::QuestionType; +use crate::database::models::{MultiSelectOption, NewMultiSelectOption, NewQuestion}; +use crate::error::JsonErr; // QUESTION TYPES // In this file, add new question types that we need to implement // e.g. @@ -21,11 +22,19 @@ use crate::database::schema::sql_types::QuestionType; #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] pub enum QuestionData { ShortAnswer, - MultiSelect(MultiSelectQuestion), + MultiSelect(MultiSelectQuestion), // Vector of option text MultiChoice(MultiSelectQuestion), DropDown(MultiSelectQuestion), } +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub enum QuestionDataInput { + ShortAnswer, + MultiSelect(MultiSelectQuestionInput), // Vector of option text + MultiChoice(MultiSelectQuestionInput), + DropDown(MultiSelectQuestionInput), +} + /// An enum that represents all the types of questions answers that CHAOS can handle. /// This stores all the data for each answer type. /// @@ -41,7 +50,7 @@ pub enum AnswerData { DropDown(MultiSelectAnswer), } -impl QuestionData { +impl QuestionDataInput { /** * Insert the inner struct into its corresponding table according to the type given by question_type */ @@ -51,28 +60,66 @@ impl QuestionData { question: &NewQuestion, question_id: i32, ) -> Option { - + match self { - QuestionData::ShortAnswer => { + QuestionDataInput::ShortAnswer => { // No need for any question data insertion, as short-answer // questions only need a title (contained in parent table) }, - QuestionData::MultiSelect(multi_select_data) => { + QuestionDataInput::MultiSelect(multi_select_data) => { // Insert Multi Select Data into table - - // TODO: insert into db using question_id - I think nothing has to be done for this one!!! + let new_data= multi_select_data.map(|x| { + NewMultiSelectOption { + text: x, + question_id, + } + }).collect(); + + for option in new_data { + option.insert(conn).ok_or_else(|| { + eprintln!("Failed to create question data for some reason"); + JsonErr(todo!(), Status::InternalServerError); + })?; + } }, - QuestionData::MultiChoice(multi_choice_data) => { - // Insert Multi Choice Data into table + QuestionDataInput::MultiChoice(multi_choice_data) => { + let new_data = multi_select_data.map(|x| { + NewMultiSelectOption { + text: x, + question_id, + } + }).collect(); + + for option in new_data { + option.insert(conn).ok_or_else(|| { + eprintln!("Failed to create question data for some reason"); + JsonErr(todo!(), Status::InternalServerError); + })?; + } }, - QuestionData::DropDown(drop_down_data) => { - // Insert Drop Down Data into table + QuestionDataInput::DropDown(drop_down_data) => { + let new_data = multi_select_data.map(|x| { + NewMultiSelectOption { + text: x, + question_id, + } + }).collect(); + + for option in new_data { + option.insert(conn).ok_or_else(|| { + eprintln!("Failed to create question data for some reason"); + JsonErr(todo!(), Status::InternalServerError); + })?; + } }, } None } +} + +impl QuestionData { pub fn get_from_question_id(conn: &PgConnection, question_id: i32) -> Option { @@ -157,6 +204,12 @@ pub struct MultiSelectQuestion { options: Vec } +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub struct MultiSelectQuestionInput { + options: Vec +} + + #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] pub struct NewMultiSelectAnswer { options_selected: Vec, From e230f5bd275d304b997da3a09d1ef85bf24ca3f3 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Sat, 24 Jun 2023 11:40:31 +1000 Subject: [PATCH 21/35] feat(backend): updated schema.rs to handle new question types --- backend/server/src/database/schema.rs | 45 +++++++++++++++++++-------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/backend/server/src/database/schema.rs b/backend/server/src/database/schema.rs index f2a0f45d9..414138ae8 100644 --- a/backend/server/src/database/schema.rs +++ b/backend/server/src/database/schema.rs @@ -13,7 +13,7 @@ pub enum ApplicationStatus { #[derive(Debug, DbEnum, PartialEq, FromFormField, Serialize, Deserialize, Clone, Copy)] #[DbValueStyle = "PascalCase"] -pub enum QuestionTypes { +pub enum QuestionType { ShortAnswer, MultiSelect, } @@ -35,15 +35,7 @@ impl AdminLevel { table! { use diesel::sql_types::*; -<<<<<<< Updated upstream -<<<<<<< Updated upstream - use super::QuestionTypesMapping; -======= - use crate::database::sql_types::QuestionType; ->>>>>>> Stashed changes -======= - use crate::database::sql_types::QuestionType; ->>>>>>> Stashed changes + use super::QuestionTypeMapping; answers (id) { id -> Int4, @@ -52,7 +44,7 @@ table! { description -> Text, created_at -> Timestamp, updated_at -> Timestamp, - answer_type -> QuestionTypesMapping, + answer_type -> QuestionTypeMapping, } } @@ -97,6 +89,22 @@ table! { } } +table! { + multi_select_answers (id) { + id -> Int4, + option_id -> Int4, + answer_id -> Int4, + } +} + +table! { + multi_select_options (id) { + id -> Int4, + text -> Text, + question_id -> Int4, + } +} + table! { use diesel::sql_types::*; use super::AdminLevelMapping; @@ -123,7 +131,7 @@ table! { table! { use diesel::sql_types::*; - use super::QuestionTypesMapping; + use super::QuestionTypeMapping; questions (id) { id -> Int4, @@ -134,7 +142,7 @@ table! { required -> Bool, created_at -> Timestamp, updated_at -> Timestamp, - question_type -> QuestionTypesMapping, + question_type -> QuestionTypeMapping, } } @@ -163,6 +171,14 @@ table! { } } +table! { + short_answer_answers (id) { + id -> Int4, + text -> Text, + answer_id -> Int4, + } +} + table! { users (id) { id -> Int4, @@ -197,10 +213,13 @@ allow_tables_to_appear_in_same_query!( applications, campaigns, comments, + multi_select_answers, + multi_select_options, organisation_users, organisations, questions, ratings, roles, + short_answer_answers, users, ); From b181d33f5f32827316fb8d87b6d6898fd756dd1a Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Sat, 24 Jun 2023 11:46:47 +1000 Subject: [PATCH 22/35] feat(backend): insert and get question data from db --- backend/server/src/question.rs | 3 ++- backend/server/src/question_types.rs | 34 ++++++++++++++-------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/backend/server/src/question.rs b/backend/server/src/question.rs index ded1ddf8c..89ab681ab 100644 --- a/backend/server/src/question.rs +++ b/backend/server/src/question.rs @@ -18,6 +18,7 @@ use std::convert::From; #[derive(Serialize)] pub enum QuestionError { QuestionNotFound, + QuestionDataNotFound, UpdateFailed, InsufficientPermissions, } @@ -37,7 +38,7 @@ pub async fn get_question( let c = Campaign::get_from_id(&conn, r.campaign_id) .ok_or(JsonErr(QuestionError::QuestionNotFound, Status::NotFound))?; let d: QuestionData = QuestionData::get_from_question(&conn, &q) - .ok_or(JsonErr(QuestionError::QuestionNotFound, Status::NotFound))?; + .ok_or(JsonErr(QuestionError::QuestionDataNotFound, Status::NotFound))?; OrganisationUser::role_admin_level(q.get_first_role(), user.id, conn) .is_at_least_director() .or(c.published) diff --git a/backend/server/src/question_types.rs b/backend/server/src/question_types.rs index 2840e075c..d1a10c629 100644 --- a/backend/server/src/question_types.rs +++ b/backend/server/src/question_types.rs @@ -1,9 +1,11 @@ -use diesel::PgConnection; +use diesel::{PgConnection, RunQueryDsl}; use rocket::http::Status; use serde::{Deserialize, Serialize}; use crate::database::{models::{Question, Answer}}; use crate::database::models::{MultiSelectOption, NewMultiSelectOption, NewQuestion}; +use crate::database::schema::multi_select_options::dsl::multi_select_options; +use crate::database::schema::QuestionType; use crate::error::JsonErr; // QUESTION TYPES // In this file, add new question types that we need to implement @@ -121,24 +123,22 @@ impl QuestionDataInput { impl QuestionData { - pub fn get_from_question_id(conn: &PgConnection, question_id: i32) -> Option { - - let question: Question; - - match Question::get_from_id(conn, question_id) { - Some(q) => { - question = q; - } - None => { - return None - } - } + pub fn get_from_question_id(conn: &PgConnection, q_id: i32) -> Option { match question.question_type { QuestionType::ShortAnswer => {Some(AnswerData::ShortAnswer);}, - QuestionType::MultiSelect => todo!(), - QuestionType::MultiChoice => todo!(), - QuestionType::DropDown => todo!(), + QuestionType::MultiSelect => { + use crate::database::schema::multi_select_options::dsl::*; + return multi_select_options.filter(question_id.eq(q_id)).first(conn).ok(); + }, + QuestionType::MultiChoice => { + use crate::database::schema::multi_select_options::dsl::*; + return multi_select_options.filter(question_id.eq(q_id)).first(conn).ok(); + }, + QuestionType::DropDown => { + use crate::database::schema::multi_select_options::dsl::*; + return multi_select_options.filter(question_id.eq(q_id)).first(conn).ok(); + }, } None @@ -171,7 +171,7 @@ impl AnswerData { // do nothing, currently by default a question is of ShortAnswer Type }, AnswerData::MultiSelect(multi_select_data) => { - // Insert Multi Select Data into table + // TODO: Insert Multi Select Data into table }, AnswerData::MultiChoice(multi_choice_data) => { // Insert Multi Choice Data into table From 13f0dfe5f43f8d7d14d393b6b45e0d0fcad53baa Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Sat, 24 Jun 2023 11:50:52 +1000 Subject: [PATCH 23/35] feat(backend): fix not searching for question using question id --- backend/server/src/question_types.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/backend/server/src/question_types.rs b/backend/server/src/question_types.rs index d1a10c629..d3e9ada60 100644 --- a/backend/server/src/question_types.rs +++ b/backend/server/src/question_types.rs @@ -125,6 +125,17 @@ impl QuestionData { pub fn get_from_question_id(conn: &PgConnection, q_id: i32) -> Option { + let question: Question; + + match Question::get_from_id(conn, q_id) { + Some(q) => { + question = q; + } + None => { + return None + } + } + match question.question_type { QuestionType::ShortAnswer => {Some(AnswerData::ShortAnswer);}, QuestionType::MultiSelect => { @@ -148,10 +159,19 @@ impl QuestionData { let question_id = question.id; match question.question_type { - QuestionType::ShortAnswer => {Some(QuestionData::ShortAnswer);}, - QuestionType::MultiSelect => todo!(), - QuestionType::MultiChoice => todo!(), - QuestionType::DropDown => todo!(), + QuestionType::ShortAnswer => { Some(AnswerData::ShortAnswer); }, + QuestionType::MultiSelect => { + use crate::database::schema::multi_select_options::dsl::*; + return multi_select_options.filter(question_id.eq(question.id)).first(conn).ok(); + }, + QuestionType::MultiChoice => { + use crate::database::schema::multi_select_options::dsl::*; + return multi_select_options.filter(question_id.eq(question.id)).first(conn).ok(); + }, + QuestionType::DropDown => { + use crate::database::schema::multi_select_options::dsl::*; + return multi_select_options.filter(question_id.eq(question.id)).first(conn).ok(); + }, } None From bad8666c9c113b0a736f9b0386d9971a618c2226 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Wed, 19 Jul 2023 15:06:41 +1000 Subject: [PATCH 24/35] feat(backend): answer and question data input/fetch --- backend/server/src/application.rs | 9 +- backend/server/src/campaigns.rs | 2 +- backend/server/src/database/models.rs | 28 +++--- backend/server/src/question_types.rs | 138 +++++++++++++++++++++----- 4 files changed, 133 insertions(+), 44 deletions(-) diff --git a/backend/server/src/application.rs b/backend/server/src/application.rs index 61ef8f5d8..01e19ab75 100644 --- a/backend/server/src/application.rs +++ b/backend/server/src/application.rs @@ -16,6 +16,7 @@ use rocket::{ serde::{json::Json, Deserialize, Serialize}, FromForm, }; +use crate::question_types::AnswerDataInput; #[derive(Serialize)] pub enum ApplicationError { @@ -123,7 +124,7 @@ pub async fn create_rating( #[derive(Serialize, Deserialize)] pub struct AnswerWithData { pub answer: NewAnswer, - pub data: AnswerData, + pub data: AnswerDataInput, } @@ -159,7 +160,7 @@ pub async fn submit_answer( // Insert the Answer Data UwU - AnswerData::insert_answer_data(data, conn, &inserted_answer).ok_or(JsonErr( + AnswerDataInput::insert_answer_data(data, conn, &inserted_answer).ok_or(JsonErr( ApplicationError::UnableToCreate, Status::InternalServerError, ))?; @@ -197,8 +198,8 @@ pub async fn get_answers( let mut response: Vec = Vec::new(); - for answer in Answer::get_all_from_application_id(conn, application_id) { - let data = AnswerData::get_from_answer(conn, &answer) + for answer in Answer::get_all_from_application_id(&conn, application_id) { + let data = AnswerData::get_from_answer(&conn, &answer) .ok_or(JsonErr(ApplicationError::AnswerDataNotFound, Status::NotFound))?; response.push(AnswerResponse { answer: answer, data: data }); } diff --git a/backend/server/src/campaigns.rs b/backend/server/src/campaigns.rs index dcf0225cd..5741b4608 100644 --- a/backend/server/src/campaigns.rs +++ b/backend/server/src/campaigns.rs @@ -5,7 +5,7 @@ use crate::{ Campaign, CampaignWithRoles, NewCampaignInput, NewQuestion, OrganisationUser, Role, RoleUpdate, UpdateCampaignInput, User, }, - Database, schema::sql_types::QuestionType, + Database, schema::QuestionType, }, images::{get_http_image_path, save_image, try_decode_data, ImageLocation}, question_types::QuestionData diff --git a/backend/server/src/database/models.rs b/backend/server/src/database/models.rs index 761d7da73..3f8a62419 100644 --- a/backend/server/src/database/models.rs +++ b/backend/server/src/database/models.rs @@ -1,4 +1,4 @@ -use crate::question_types::QuestionData; +use crate::question_types::{AnswerData, QuestionData}; use crate::images::{get_http_image_path, ImageLocation}; use super::schema::AdminLevel; @@ -19,7 +19,7 @@ use std::path::Path; use crate::database::schema::multi_select_answers::dsl::multi_select_answers; use crate::database::schema::multi_select_options::dsl::multi_select_options; use crate::database::schema::short_answer_answers::dsl::short_answer_answers; -use crate::database::schema::sql_types::QuestionType; +use crate::database::schema::QuestionType; #[derive(Queryable)] pub struct User { @@ -1100,7 +1100,7 @@ pub struct NewMultiSelectOption { impl NewMultiSelectOption { pub fn insert(&self, conn: &PgConnection) -> Option { - use crate::database::schema::multi_select_options::dsl::*; + use crate::dabase::schema::multi_select_options::dsl::*; self.insert_into(multi_select_options).get_result(conn).ok() } @@ -1189,6 +1189,7 @@ pub struct Answer { pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub answer_type: QuestionType, + pub answer_data: AnswerData, } #[derive(Insertable, Deserialize, Serialize)] @@ -1198,21 +1199,22 @@ pub struct NewAnswer { pub question_id: i32, pub description: String, pub answer_type: QuestionType, + pub answer_data: AnswerData, } #[derive(Queryable, Deserialize, Serialize, PartialEq, Debug, Clone)] #[table_name = "short_answer_answers"] pub struct ShortAnswerAnswer { - id: i32, - text: String, - answer_id: i32, + pub id: i32, + pub text: String, + pub answer_id: i32, } #[derive(Insertable, Deserialize, Serialize, PartialEq, Debug, Clone)] #[table_name = "short_answer_answers"] pub struct NewShortAnswerAnswer { - text: String, - answer_id: i32, + pub text: String, + pub answer_id: i32, } impl NewShortAnswerAnswer { @@ -1233,16 +1235,16 @@ impl NewShortAnswerAnswer { #[derive(Queryable, Deserialize, Serialize, PartialEq, Debug, Clone)] #[table_name = "multi_select_answers"] pub struct MultiSelectAnswer { - id: i32, - option_id: i32, - answer_id: i32, + pub id: i32, + pub option_id: i32, + pub answer_id: i32, } #[derive(Insertable, Deserialize, Serialize, PartialEq, Debug, Clone)] #[table_name = "multi_select_answers"] pub struct NewMultiSelectAnswer { - option_id: i32, - answer_id: i32, + pub option_id: i32, + pub answer_id: i32, } impl NewMultiSelectAnswer { diff --git a/backend/server/src/question_types.rs b/backend/server/src/question_types.rs index d3e9ada60..254e77bef 100644 --- a/backend/server/src/question_types.rs +++ b/backend/server/src/question_types.rs @@ -3,9 +3,11 @@ use rocket::http::Status; use serde::{Deserialize, Serialize}; use crate::database::{models::{Question, Answer}}; -use crate::database::models::{MultiSelectOption, NewMultiSelectOption, NewQuestion}; +use crate::database::models::{MultiSelectAnswer, MultiSelectOption, MultiSelectOptionInput, NewMultiSelectAnswer, NewMultiSelectOption, NewQuestion, NewShortAnswerAnswer, ShortAnswerAnswer}; +use crate::database::schema::multi_select_answers::dsl::multi_select_answers; use crate::database::schema::multi_select_options::dsl::multi_select_options; use crate::database::schema::QuestionType; +use crate::database::schema::short_answer_answers::dsl::short_answer_answers; use crate::error::JsonErr; // QUESTION TYPES // In this file, add new question types that we need to implement @@ -47,9 +49,17 @@ pub enum QuestionDataInput { #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] pub enum AnswerData { ShortAnswer(String), - MultiSelect(MultiSelectAnswer), - MultiChoice(MultiSelectAnswer), // TODO: Is there a better way to name these, without duplicating the structs? Traits? - DropDown(MultiSelectAnswer), + MultiSelect(Vec), + MultiChoice(i32), // TODO: START FROM HERE: change this to multichoice answer, which returns a single i32 + DropDown(i32), +} + +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub enum AnswerDataInput { + ShortAnswer(String), + MultiSelect(Vec), // Vector of option text + MultiChoice(i32), + DropDown(i32), } impl QuestionDataInput { @@ -70,7 +80,7 @@ impl QuestionDataInput { }, QuestionDataInput::MultiSelect(multi_select_data) => { // Insert Multi Select Data into table - let new_data= multi_select_data.map(|x| { + let new_data = multi_select_data.map(|x| { NewMultiSelectOption { text: x, question_id, @@ -85,7 +95,7 @@ impl QuestionDataInput { } }, QuestionDataInput::MultiChoice(multi_choice_data) => { - let new_data = multi_select_data.map(|x| { + let new_data = multi_choice_data.map(|x| { NewMultiSelectOption { text: x, question_id, @@ -100,7 +110,7 @@ impl QuestionDataInput { } }, QuestionDataInput::DropDown(drop_down_data) => { - let new_data = multi_select_data.map(|x| { + let new_data = drop_down_data.map(|x| { NewMultiSelectOption { text: x, question_id, @@ -178,47 +188,122 @@ impl QuestionData { } } - -impl AnswerData { +impl AnswerDataInput { pub fn insert_answer_data( self, conn: &mut PgConnection, answer: &Answer, ) -> Option { - match self { - AnswerData::ShortAnswer(short_answer_data) => { - // do nothing, currently by default a question is of ShortAnswer Type + AnswerDataInput::ShortAnswer(short_answer_data) => { + let answer = NewShortAnswerAnswer { + text: short_answer_data, + answer_id: answer.id, + }; + + answer.insert(conn).ok_or_else(|| { + eprintln!("Failed to create answer data for some reason"); + JsonErr(todo!(), Status::InternalServerError); + })?; }, - AnswerData::MultiSelect(multi_select_data) => { - // TODO: Insert Multi Select Data into table + AnswerDataInput::MultiSelect(multi_select_data) => { + let new_answers = multi_select_data.map(|x| { + NewMultiSelectAnswer { + option_id: x, + answer_id: answer.id, + } + }).collect(); + + for answer in new_answers { + answer.insert(conn).ok_or_else(|| { + eprintln!("Failed to create answer data for some reason"); + JsonErr(todo!(), Status::InternalServerError); + })?; + } }, - AnswerData::MultiChoice(multi_choice_data) => { - // Insert Multi Choice Data into table + AnswerDataInput::MultiChoice(option_id) => { + NewMultiSelectAnswer { + option_id, + answer_id: answer.id, + }.insert(conn).ok_or_else(|| { + eprintln!("Failed to create answer data for some reason"); + JsonErr(todo!(), Status::InternalServerError); + })?; }, - AnswerData::DropDown(drop_down_data) => { - // Insert Drop Down Data into table + AnswerDataInput::DropDown(option_id) => { + NewMultiSelectAnswer { + option_id, + answer_id: answer.id, + }.insert(conn).ok_or_else(|| { + eprintln!("Failed to create answer data for some reason"); + JsonErr(todo!(), Status::InternalServerError); + })?; } } None } +} +impl AnswerData { pub fn get_from_answer(conn: &PgConnection, answer: &Answer) -> Option { let answer_id = answer.id; match answer.answer_type { - QuestionType::ShortAnswer => {Some(AnswerData::ShortAnswer);}, - QuestionType::MultiSelect => todo!(), - QuestionType::MultiChoice => todo!(), - QuestionType::DropDown => todo!(), + QuestionType::ShortAnswer => { + use crate::database::schema::short_answer_answers::dsl::*; + + let answer_data: ShortAnswerAnswer = short_answer_answers.filter( + answer_id + .eq(answer.id) + ).first(conn).ok_or_else(|| { + eprintln!("Failed to fetch answer data for some reason"); + JsonErr(todo!(), Status::InternalServerError); + }); + + Some(answer_data.text) + }, + QuestionType::MultiSelect => { + use crate::database::schema::multi_select_answers::dsl::*; + + let answers: Vec = multi_select_answers.filter( + answer_id.eq(answer.id) + ).load(conn).unwrap_or_else(|_| vec![]); + + Some(answer.map(|x| { + x.option_id + }).collect()) + }, + QuestionType::MultiChoice => { + use crate::database::schema::multi_select_answers::dsl::*; + + let answer_data: MultiSelectAnswer = multi_select_answers.filter( + answer_id.eq(answer.id) + ).first(conn).ok_or_else(|| { + eprintln!("Failed to fetch answer data for some reason"); + JsonErr(todo!(), Status::InternalServerError); + }); + + Some(answer_data.option_id) + }, + QuestionType::DropDown => { + use crate::database::schema::multi_select_answers::dsl::*; + + let answer_data: MultiSelectAnswer = multi_select_answers.filter( + answer_id.eq(answer.id) + ).first(conn).ok_or_else(|| { + eprintln!("Failed to fetch answer data for some reason"); + JsonErr(todo!(), Status::InternalServerError); + }); + + Some(answer_data.option_id) + }, } None } } - #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] pub struct MultiSelectQuestion { options: Vec @@ -226,11 +311,12 @@ pub struct MultiSelectQuestion { #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] pub struct MultiSelectQuestionInput { - options: Vec + options: Vec } #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] -pub struct NewMultiSelectAnswer { +pub struct MultiSelectAnswerInput { options_selected: Vec, -} \ No newline at end of file +} + From 20da953766c9e0bd57c86bf4fdd6dcfa2709bf4d Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Wed, 19 Jul 2023 16:39:27 +1000 Subject: [PATCH 25/35] feat(fix-partial): partially fixed returns and queries in new question/answer types --- backend/server/src/campaigns.rs | 3 +- backend/server/src/database/models.rs | 21 +++--- backend/server/src/database/schema.rs | 8 ++ backend/server/src/question_types.rs | 101 +++++++++++--------------- backend/src/schema.rs | 2 +- 5 files changed, 63 insertions(+), 72 deletions(-) diff --git a/backend/server/src/campaigns.rs b/backend/server/src/campaigns.rs index 5741b4608..2a319e5ea 100644 --- a/backend/server/src/campaigns.rs +++ b/backend/server/src/campaigns.rs @@ -8,7 +8,6 @@ use crate::{ Database, schema::QuestionType, }, images::{get_http_image_path, save_image, try_decode_data, ImageLocation}, - question_types::QuestionData }; use rocket::{data::Data, delete, get, http::Status, post, put, serde::json::Json}; use serde::{Deserialize, Serialize}; @@ -145,7 +144,7 @@ pub async fn new( mut questions, } = inner; - let mut question_data: Vec = questions + let question_data: Vec = questions .iter() .map(|x| { x.question_data.clone() diff --git a/backend/server/src/database/models.rs b/backend/server/src/database/models.rs index 3f8a62419..a878c3cd1 100644 --- a/backend/server/src/database/models.rs +++ b/backend/server/src/database/models.rs @@ -1,11 +1,11 @@ -use crate::question_types::{AnswerData, QuestionData}; +use crate::question_types::QuestionData; use crate::images::{get_http_image_path, ImageLocation}; use super::schema::AdminLevel; use super::schema::ApplicationStatus; use super::schema::{ answers, applications, campaigns, comments, organisation_users, organisations, questions, - ratings, roles, users, + ratings, roles, users, multi_select_answers, multi_select_options, short_answer_answers }; use chrono::NaiveDateTime; use chrono::Utc; @@ -16,9 +16,6 @@ use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::fs::remove_file; use std::path::Path; -use crate::database::schema::multi_select_answers::dsl::multi_select_answers; -use crate::database::schema::multi_select_options::dsl::multi_select_options; -use crate::database::schema::short_answer_answers::dsl::short_answer_answers; use crate::database::schema::QuestionType; #[derive(Queryable)] @@ -1079,7 +1076,7 @@ pub struct UpdateQuestionInput { } #[derive(Queryable, Deserialize, Serialize, PartialEq, Debug, Clone)] -#[table_name = "multi_select_options"] +// #[table_name = "multi_select_options"] pub struct MultiSelectOption { pub id: i32, pub text: String, @@ -1099,8 +1096,8 @@ pub struct NewMultiSelectOption { } impl NewMultiSelectOption { - pub fn insert(&self, conn: &PgConnection) -> Option { - use crate::dabase::schema::multi_select_options::dsl::*; + pub fn insert(&self, conn: &PgConnection) -> Option { + use crate::database::schema::multi_select_options::dsl::*; self.insert_into(multi_select_options).get_result(conn).ok() } @@ -1189,7 +1186,7 @@ pub struct Answer { pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub answer_type: QuestionType, - pub answer_data: AnswerData, + // pub answer_data: AnswerData, } #[derive(Insertable, Deserialize, Serialize)] @@ -1199,11 +1196,11 @@ pub struct NewAnswer { pub question_id: i32, pub description: String, pub answer_type: QuestionType, - pub answer_data: AnswerData, + // pub answer_data: AnswerData, } #[derive(Queryable, Deserialize, Serialize, PartialEq, Debug, Clone)] -#[table_name = "short_answer_answers"] +// #[table_name = "short_answer_answers"] pub struct ShortAnswerAnswer { pub id: i32, pub text: String, @@ -1233,7 +1230,7 @@ impl NewShortAnswerAnswer { /// \ /// The vector will store the id's of each option selected. #[derive(Queryable, Deserialize, Serialize, PartialEq, Debug, Clone)] -#[table_name = "multi_select_answers"] +// #[table_name = "multi_select_answers"] pub struct MultiSelectAnswer { pub id: i32, pub option_id: i32, diff --git a/backend/server/src/database/schema.rs b/backend/server/src/database/schema.rs index 414138ae8..51115f504 100644 --- a/backend/server/src/database/schema.rs +++ b/backend/server/src/database/schema.rs @@ -16,6 +16,8 @@ pub enum ApplicationStatus { pub enum QuestionType { ShortAnswer, MultiSelect, + MultiChoice, + DropDown, } @@ -90,6 +92,8 @@ table! { } table! { + use diesel::sql_types::*; + multi_select_answers (id) { id -> Int4, option_id -> Int4, @@ -98,6 +102,8 @@ table! { } table! { + use diesel::sql_types::*; + multi_select_options (id) { id -> Int4, text -> Text, @@ -172,6 +178,8 @@ table! { } table! { + use diesel::sql_types::*; + short_answer_answers (id) { id -> Int4, text -> Text, diff --git a/backend/server/src/question_types.rs b/backend/server/src/question_types.rs index 254e77bef..b60f4f8eb 100644 --- a/backend/server/src/question_types.rs +++ b/backend/server/src/question_types.rs @@ -1,14 +1,11 @@ use diesel::{PgConnection, RunQueryDsl}; -use rocket::http::Status; use serde::{Deserialize, Serialize}; -use crate::database::{models::{Question, Answer}}; +use crate::diesel::QueryDsl; +use diesel::expression_methods::ExpressionMethods; +use crate::database::{Database, models::{Question, Answer}}; use crate::database::models::{MultiSelectAnswer, MultiSelectOption, MultiSelectOptionInput, NewMultiSelectAnswer, NewMultiSelectOption, NewQuestion, NewShortAnswerAnswer, ShortAnswerAnswer}; -use crate::database::schema::multi_select_answers::dsl::multi_select_answers; -use crate::database::schema::multi_select_options::dsl::multi_select_options; use crate::database::schema::QuestionType; -use crate::database::schema::short_answer_answers::dsl::short_answer_answers; -use crate::error::JsonErr; // QUESTION TYPES // In this file, add new question types that we need to implement // e.g. @@ -34,9 +31,9 @@ pub enum QuestionData { #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] pub enum QuestionDataInput { ShortAnswer, - MultiSelect(MultiSelectQuestionInput), // Vector of option text - MultiChoice(MultiSelectQuestionInput), - DropDown(MultiSelectQuestionInput), + MultiSelect(Vec), // Vector of option text + MultiChoice(Vec), + DropDown(Vec), } /// An enum that represents all the types of questions answers that CHAOS can handle. @@ -80,7 +77,7 @@ impl QuestionDataInput { }, QuestionDataInput::MultiSelect(multi_select_data) => { // Insert Multi Select Data into table - let new_data = multi_select_data.map(|x| { + let new_data: Vec = multi_select_data.into_iter().map(|x| { NewMultiSelectOption { text: x, question_id, @@ -90,12 +87,12 @@ impl QuestionDataInput { for option in new_data { option.insert(conn).ok_or_else(|| { eprintln!("Failed to create question data for some reason"); - JsonErr(todo!(), Status::InternalServerError); - })?; + // JsonErr(todo!(), Status::InternalServerError); + }).ok(); } }, QuestionDataInput::MultiChoice(multi_choice_data) => { - let new_data = multi_choice_data.map(|x| { + let new_data: Vec = multi_choice_data.into_iter().map(|x| { NewMultiSelectOption { text: x, question_id, @@ -105,12 +102,12 @@ impl QuestionDataInput { for option in new_data { option.insert(conn).ok_or_else(|| { eprintln!("Failed to create question data for some reason"); - JsonErr(todo!(), Status::InternalServerError); - })?; + // JsonErr(todo!(), Status::InternalServerError); + }).ok(); } }, QuestionDataInput::DropDown(drop_down_data) => { - let new_data = drop_down_data.map(|x| { + let new_data: Vec = drop_down_data.into_iter().map(|x| { NewMultiSelectOption { text: x, question_id, @@ -120,8 +117,8 @@ impl QuestionDataInput { for option in new_data { option.insert(conn).ok_or_else(|| { eprintln!("Failed to create question data for some reason"); - JsonErr(todo!(), Status::InternalServerError); - })?; + // JsonErr(todo!(), Status::InternalServerError); + }).ok(); } }, } @@ -133,7 +130,7 @@ impl QuestionDataInput { impl QuestionData { - pub fn get_from_question_id(conn: &PgConnection, q_id: i32) -> Option { + pub fn get_from_question_id(conn: &PgConnection, q_id: i32, db: Database) -> Option { let question: Question; @@ -146,11 +143,12 @@ impl QuestionData { } } - match question.question_type { - QuestionType::ShortAnswer => {Some(AnswerData::ShortAnswer);}, + return match question.question_type { + QuestionType::ShortAnswer => { Some(QuestionData::ShortAnswer) }, QuestionType::MultiSelect => { use crate::database::schema::multi_select_options::dsl::*; - return multi_select_options.filter(question_id.eq(q_id)).first(conn).ok(); + // TODO: Need to convert the options a questiondata struct and return them! + Some(QuestionData::MultiSelect(multi_select_options.filter(question_id.eq(q_id)).first(conn).ok().unwrap())); }, QuestionType::MultiChoice => { use crate::database::schema::multi_select_options::dsl::*; @@ -160,9 +158,8 @@ impl QuestionData { use crate::database::schema::multi_select_options::dsl::*; return multi_select_options.filter(question_id.eq(q_id)).first(conn).ok(); }, - } - - None + }; + // None } pub fn get_from_question(conn: &PgConnection, question: &Question) -> Option { @@ -203,11 +200,11 @@ impl AnswerDataInput { answer.insert(conn).ok_or_else(|| { eprintln!("Failed to create answer data for some reason"); - JsonErr(todo!(), Status::InternalServerError); - })?; + // JsonErr(todo!(), Status::InternalServerError); + }).ok(); }, AnswerDataInput::MultiSelect(multi_select_data) => { - let new_answers = multi_select_data.map(|x| { + let new_answers: Vec = multi_select_data.into_iter().map(|x| { NewMultiSelectAnswer { option_id: x, answer_id: answer.id, @@ -217,8 +214,8 @@ impl AnswerDataInput { for answer in new_answers { answer.insert(conn).ok_or_else(|| { eprintln!("Failed to create answer data for some reason"); - JsonErr(todo!(), Status::InternalServerError); - })?; + // JsonErr(todo!(), Status::InternalServerError); + }).ok(); } }, AnswerDataInput::MultiChoice(option_id) => { @@ -227,8 +224,8 @@ impl AnswerDataInput { answer_id: answer.id, }.insert(conn).ok_or_else(|| { eprintln!("Failed to create answer data for some reason"); - JsonErr(todo!(), Status::InternalServerError); - })?; + // JsonErr(todo!(), Status::InternalServerError); + }).ok(); }, AnswerDataInput::DropDown(option_id) => { NewMultiSelectAnswer { @@ -236,8 +233,8 @@ impl AnswerDataInput { answer_id: answer.id, }.insert(conn).ok_or_else(|| { eprintln!("Failed to create answer data for some reason"); - JsonErr(todo!(), Status::InternalServerError); - })?; + // JsonErr(todo!(), Status::InternalServerError); + }).ok(); } } @@ -249,19 +246,15 @@ impl AnswerData { pub fn get_from_answer(conn: &PgConnection, answer: &Answer) -> Option { let answer_id = answer.id; - match answer.answer_type { + return match answer.answer_type { QuestionType::ShortAnswer => { use crate::database::schema::short_answer_answers::dsl::*; let answer_data: ShortAnswerAnswer = short_answer_answers.filter( - answer_id - .eq(answer.id) - ).first(conn).ok_or_else(|| { - eprintln!("Failed to fetch answer data for some reason"); - JsonErr(todo!(), Status::InternalServerError); - }); - - Some(answer_data.text) + answer_id.eq(answer.id) + ).first(conn).ok()?; + + Some(AnswerData::ShortAnswer(answer_data.text)) }, QuestionType::MultiSelect => { use crate::database::schema::multi_select_answers::dsl::*; @@ -270,37 +263,31 @@ impl AnswerData { answer_id.eq(answer.id) ).load(conn).unwrap_or_else(|_| vec![]); - Some(answer.map(|x| { + Some(AnswerData::MultiSelect(answers.into_iter().map(|x| { x.option_id - }).collect()) + }).collect())) }, QuestionType::MultiChoice => { use crate::database::schema::multi_select_answers::dsl::*; let answer_data: MultiSelectAnswer = multi_select_answers.filter( answer_id.eq(answer.id) - ).first(conn).ok_or_else(|| { - eprintln!("Failed to fetch answer data for some reason"); - JsonErr(todo!(), Status::InternalServerError); - }); + ).first(conn).ok().unwrap(); - Some(answer_data.option_id) + Some(AnswerData::MultiChoice(answer_data.option_id)) }, QuestionType::DropDown => { use crate::database::schema::multi_select_answers::dsl::*; let answer_data: MultiSelectAnswer = multi_select_answers.filter( answer_id.eq(answer.id) - ).first(conn).ok_or_else(|| { - eprintln!("Failed to fetch answer data for some reason"); - JsonErr(todo!(), Status::InternalServerError); - }); + ).first(conn).ok(); - Some(answer_data.option_id) + Some(AnswerData::DropDown(answer_data.option_id)) }, - } + }; - None + // None } } diff --git a/backend/src/schema.rs b/backend/src/schema.rs index b4b972d78..046e18592 100644 --- a/backend/src/schema.rs +++ b/backend/src/schema.rs @@ -123,7 +123,7 @@ diesel::table! { created_at -> Timestamp, updated_at -> Timestamp, role_id -> Nullable, - question_type -> Nullable, + question_type -> QuestionType, } } From 3529a3d9fb6ae278fb03387ac27d5ea060054e69 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Fri, 21 Jul 2023 21:25:20 +1000 Subject: [PATCH 26/35] feat(backend): removed unneeded passing-in of question into insert_question_data --- backend/server/src/campaigns.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/server/src/campaigns.rs b/backend/server/src/campaigns.rs index 2a319e5ea..ed5a2568a 100644 --- a/backend/server/src/campaigns.rs +++ b/backend/server/src/campaigns.rs @@ -212,7 +212,7 @@ pub async fn new( JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) })?.id; - question_data.insert_question_data(conn, &question, inserted_id).ok_or_else(|| { + question_data.insert_question_data(conn, inserted_id).ok_or_else(|| { eprintln!("Failed to create question data for some reason"); JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) })?; From c11dcef7c54474020345074e4ca522c3c3027542 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Fri, 21 Jul 2023 21:25:40 +1000 Subject: [PATCH 27/35] feat(backend): getting/setting question data and answer data should be fully implemented... --- backend/server/src/question_types.rs | 98 +++++++++++++++++++--------- 1 file changed, 66 insertions(+), 32 deletions(-) diff --git a/backend/server/src/question_types.rs b/backend/server/src/question_types.rs index b60f4f8eb..8790e4cb6 100644 --- a/backend/server/src/question_types.rs +++ b/backend/server/src/question_types.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; use crate::diesel::QueryDsl; use diesel::expression_methods::ExpressionMethods; -use crate::database::{Database, models::{Question, Answer}}; -use crate::database::models::{MultiSelectAnswer, MultiSelectOption, MultiSelectOptionInput, NewMultiSelectAnswer, NewMultiSelectOption, NewQuestion, NewShortAnswerAnswer, ShortAnswerAnswer}; +use crate::database::models::{Question, Answer}; +use crate::database::models::{MultiSelectAnswer, MultiSelectOption, MultiSelectOptionInput, NewMultiSelectAnswer, NewMultiSelectOption, NewShortAnswerAnswer, ShortAnswerAnswer}; use crate::database::schema::QuestionType; // QUESTION TYPES // In this file, add new question types that we need to implement @@ -23,9 +23,9 @@ use crate::database::schema::QuestionType; #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] pub enum QuestionData { ShortAnswer, - MultiSelect(MultiSelectQuestion), // Vector of option text - MultiChoice(MultiSelectQuestion), - DropDown(MultiSelectQuestion), + MultiSelect(Vec), // Vector of option text + MultiChoice(Vec), + DropDown(Vec), } #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] @@ -47,7 +47,7 @@ pub enum QuestionDataInput { pub enum AnswerData { ShortAnswer(String), MultiSelect(Vec), - MultiChoice(i32), // TODO: START FROM HERE: change this to multichoice answer, which returns a single i32 + MultiChoice(i32), DropDown(i32), } @@ -66,7 +66,6 @@ impl QuestionDataInput { pub fn insert_question_data( self, conn: &PgConnection, - question: &NewQuestion, question_id: i32, ) -> Option { @@ -87,7 +86,6 @@ impl QuestionDataInput { for option in new_data { option.insert(conn).ok_or_else(|| { eprintln!("Failed to create question data for some reason"); - // JsonErr(todo!(), Status::InternalServerError); }).ok(); } }, @@ -102,7 +100,6 @@ impl QuestionDataInput { for option in new_data { option.insert(conn).ok_or_else(|| { eprintln!("Failed to create question data for some reason"); - // JsonErr(todo!(), Status::InternalServerError); }).ok(); } }, @@ -117,7 +114,6 @@ impl QuestionDataInput { for option in new_data { option.insert(conn).ok_or_else(|| { eprintln!("Failed to create question data for some reason"); - // JsonErr(todo!(), Status::InternalServerError); }).ok(); } }, @@ -130,7 +126,7 @@ impl QuestionDataInput { impl QuestionData { - pub fn get_from_question_id(conn: &PgConnection, q_id: i32, db: Database) -> Option { + pub fn get_from_question_id(conn: &PgConnection, q_id: i32) -> Option { let question: Question; @@ -147,41 +143,85 @@ impl QuestionData { QuestionType::ShortAnswer => { Some(QuestionData::ShortAnswer) }, QuestionType::MultiSelect => { use crate::database::schema::multi_select_options::dsl::*; - // TODO: Need to convert the options a questiondata struct and return them! - Some(QuestionData::MultiSelect(multi_select_options.filter(question_id.eq(q_id)).first(conn).ok().unwrap())); + + let data: Vec = multi_select_options + .filter(question_id.eq(q_id)) + .load(conn) + .unwrap_or_else(|_| vec![]); + + Some(QuestionData::MultiSelect(data.into_iter().map(|x| { + x.text + }).collect())) }, QuestionType::MultiChoice => { use crate::database::schema::multi_select_options::dsl::*; - return multi_select_options.filter(question_id.eq(q_id)).first(conn).ok(); + + let data: Vec = multi_select_options + .filter(question_id.eq(q_id)) + .load(conn) + .unwrap_or_else(|_| vec![]); + + Some(QuestionData::MultiChoice(data.into_iter().map(|x| { + x.text + }).collect())) }, QuestionType::DropDown => { use crate::database::schema::multi_select_options::dsl::*; - return multi_select_options.filter(question_id.eq(q_id)).first(conn).ok(); + + let data: Vec = multi_select_options + .filter(question_id.eq(q_id)) + .load(conn) + .unwrap_or_else(|_| vec![]); + + Some(QuestionData::DropDown(data.into_iter().map(|x| { + x.text + }).collect())) }, }; - // None } pub fn get_from_question(conn: &PgConnection, question: &Question) -> Option { - let question_id = question.id; + let q_id = question.id; - match question.question_type { - QuestionType::ShortAnswer => { Some(AnswerData::ShortAnswer); }, + return match question.question_type { + QuestionType::ShortAnswer => { Some(QuestionData::ShortAnswer) }, QuestionType::MultiSelect => { use crate::database::schema::multi_select_options::dsl::*; - return multi_select_options.filter(question_id.eq(question.id)).first(conn).ok(); + + let data: Vec = multi_select_options + .filter(question_id.eq(q_id)) + .load(conn) + .unwrap_or_else(|_| vec![]); + + Some(QuestionData::MultiSelect(data.into_iter().map(|x| { + x.text + }).collect())) }, QuestionType::MultiChoice => { use crate::database::schema::multi_select_options::dsl::*; - return multi_select_options.filter(question_id.eq(question.id)).first(conn).ok(); + + let data: Vec = multi_select_options + .filter(question_id.eq(q_id)) + .load(conn) + .unwrap_or_else(|_| vec![]); + + Some(QuestionData::MultiChoice(data.into_iter().map(|x| { + x.text + }).collect())) }, QuestionType::DropDown => { use crate::database::schema::multi_select_options::dsl::*; - return multi_select_options.filter(question_id.eq(question.id)).first(conn).ok(); - }, - } - None + let data: Vec = multi_select_options + .filter(question_id.eq(q_id)) + .load(conn) + .unwrap_or_else(|_| vec![]); + + Some(QuestionData::DropDown(data.into_iter().map(|x| { + x.text + }).collect())) + }, + }; } } @@ -200,7 +240,6 @@ impl AnswerDataInput { answer.insert(conn).ok_or_else(|| { eprintln!("Failed to create answer data for some reason"); - // JsonErr(todo!(), Status::InternalServerError); }).ok(); }, AnswerDataInput::MultiSelect(multi_select_data) => { @@ -214,7 +253,6 @@ impl AnswerDataInput { for answer in new_answers { answer.insert(conn).ok_or_else(|| { eprintln!("Failed to create answer data for some reason"); - // JsonErr(todo!(), Status::InternalServerError); }).ok(); } }, @@ -224,7 +262,6 @@ impl AnswerDataInput { answer_id: answer.id, }.insert(conn).ok_or_else(|| { eprintln!("Failed to create answer data for some reason"); - // JsonErr(todo!(), Status::InternalServerError); }).ok(); }, AnswerDataInput::DropDown(option_id) => { @@ -233,7 +270,6 @@ impl AnswerDataInput { answer_id: answer.id, }.insert(conn).ok_or_else(|| { eprintln!("Failed to create answer data for some reason"); - // JsonErr(todo!(), Status::InternalServerError); }).ok(); } } @@ -244,8 +280,6 @@ impl AnswerDataInput { impl AnswerData { pub fn get_from_answer(conn: &PgConnection, answer: &Answer) -> Option { - let answer_id = answer.id; - return match answer.answer_type { QuestionType::ShortAnswer => { use crate::database::schema::short_answer_answers::dsl::*; @@ -281,7 +315,7 @@ impl AnswerData { let answer_data: MultiSelectAnswer = multi_select_answers.filter( answer_id.eq(answer.id) - ).first(conn).ok(); + ).first(conn).ok().unwrap(); Some(AnswerData::DropDown(answer_data.option_id)) }, From eed7880237c6e114c81af5abf35993eb4b029e73 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Fri, 21 Jul 2023 21:43:42 +1000 Subject: [PATCH 28/35] fix(backend): removed accidental inclusion of .idea folder --- backend/.idea/.gitignore | 8 -------- backend/.idea/backend.iml | 13 ------------- backend/.idea/dataSources.xml | 12 ------------ backend/.idea/modules.xml | 8 -------- backend/.idea/sqldialects.xml | 6 ------ backend/.idea/vcs.xml | 6 ------ 6 files changed, 53 deletions(-) delete mode 100644 backend/.idea/.gitignore delete mode 100644 backend/.idea/backend.iml delete mode 100644 backend/.idea/dataSources.xml delete mode 100644 backend/.idea/modules.xml delete mode 100644 backend/.idea/sqldialects.xml delete mode 100644 backend/.idea/vcs.xml diff --git a/backend/.idea/.gitignore b/backend/.idea/.gitignore deleted file mode 100644 index 13566b81b..000000000 --- a/backend/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/backend/.idea/backend.iml b/backend/.idea/backend.iml deleted file mode 100644 index 33e8580c4..000000000 --- a/backend/.idea/backend.iml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/backend/.idea/dataSources.xml b/backend/.idea/dataSources.xml deleted file mode 100644 index 7703b9773..000000000 --- a/backend/.idea/dataSources.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - postgresql - true - org.postgresql.Driver - jdbc:postgresql://localhost:5432/chaos - $ProjectFileDir$ - - - \ No newline at end of file diff --git a/backend/.idea/modules.xml b/backend/.idea/modules.xml deleted file mode 100644 index e066844ef..000000000 --- a/backend/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/backend/.idea/sqldialects.xml b/backend/.idea/sqldialects.xml deleted file mode 100644 index 6df4889b0..000000000 --- a/backend/.idea/sqldialects.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/backend/.idea/vcs.xml b/backend/.idea/vcs.xml deleted file mode 100644 index 6c0b86358..000000000 --- a/backend/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 063167aa88cdecd8804998b71abbfd5624a98d6d Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Wed, 26 Jul 2023 13:28:28 +1000 Subject: [PATCH 29/35] feat(fix): delete unused src/schema.rs --- backend/src/schema.rs | 208 ------------------------------------------ 1 file changed, 208 deletions(-) delete mode 100644 backend/src/schema.rs diff --git a/backend/src/schema.rs b/backend/src/schema.rs deleted file mode 100644 index 046e18592..000000000 --- a/backend/src/schema.rs +++ /dev/null @@ -1,208 +0,0 @@ -// @generated automatically by Diesel CLI. - -pub mod sql_types { - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "admin_level"))] - pub struct AdminLevel; - - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "application_status"))] - pub struct ApplicationStatus; - - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "question_type"))] - pub struct QuestionType; -} - -diesel::table! { - use diesel::sql_types::*; - use super::sql_types::QuestionType; - - answers (id) { - id -> Int4, - application_id -> Int4, - question_id -> Int4, - description -> Text, - created_at -> Timestamp, - updated_at -> Timestamp, - answer_type -> QuestionType, - } -} - -diesel::table! { - use diesel::sql_types::*; - use super::sql_types::ApplicationStatus; - - applications (id) { - id -> Int4, - user_id -> Int4, - role_id -> Int4, - status -> ApplicationStatus, - created_at -> Timestamp, - updated_at -> Timestamp, - private_status -> Nullable, - } -} - -diesel::table! { - campaigns (id) { - id -> Int4, - organisation_id -> Int4, - name -> Text, - cover_image -> Nullable, - description -> Text, - starts_at -> Timestamp, - ends_at -> Timestamp, - published -> Bool, - created_at -> Timestamp, - updated_at -> Timestamp, - } -} - -diesel::table! { - comments (id) { - id -> Int4, - application_id -> Int4, - commenter_user_id -> Int4, - description -> Text, - created_at -> Timestamp, - updated_at -> Timestamp, - } -} - -diesel::table! { - multi_select_answers (id) { - id -> Int4, - option_id -> Int4, - answer_id -> Int4, - } -} - -diesel::table! { - multi_select_options (id) { - id -> Int4, - text -> Text, - question_id -> Int4, - } -} - -diesel::table! { - use diesel::sql_types::*; - use super::sql_types::AdminLevel; - - organisation_users (id) { - id -> Int4, - user_id -> Int4, - organisation_id -> Int4, - admin_level -> AdminLevel, - created_at -> Timestamp, - updated_at -> Timestamp, - } -} - -diesel::table! { - organisations (id) { - id -> Int4, - name -> Text, - logo -> Nullable, - created_at -> Timestamp, - updated_at -> Timestamp, - } -} - -diesel::table! { - use diesel::sql_types::*; - use super::sql_types::QuestionType; - - questions (id) { - id -> Int4, - title -> Text, - description -> Nullable, - max_bytes -> Int4, - required -> Bool, - created_at -> Timestamp, - updated_at -> Timestamp, - role_id -> Nullable, - question_type -> QuestionType, - } -} - -diesel::table! { - ratings (id) { - id -> Int4, - application_id -> Int4, - rater_user_id -> Int4, - rating -> Int4, - created_at -> Timestamp, - updated_at -> Timestamp, - } -} - -diesel::table! { - roles (id) { - id -> Int4, - campaign_id -> Int4, - name -> Text, - description -> Nullable, - min_available -> Int4, - max_available -> Int4, - finalised -> Bool, - created_at -> Timestamp, - updated_at -> Timestamp, - } -} - -diesel::table! { - short_answer_answers (id) { - id -> Int4, - text -> Text, - answer_id -> Int4, - } -} - -diesel::table! { - users (id) { - id -> Int4, - email -> Text, - zid -> Text, - display_name -> Text, - degree_name -> Text, - degree_starting_year -> Int4, - superuser -> Bool, - created_at -> Timestamp, - updated_at -> Timestamp, - } -} - -diesel::joinable!(answers -> applications (application_id)); -diesel::joinable!(answers -> questions (question_id)); -diesel::joinable!(applications -> roles (role_id)); -diesel::joinable!(applications -> users (user_id)); -diesel::joinable!(campaigns -> organisations (organisation_id)); -diesel::joinable!(comments -> applications (application_id)); -diesel::joinable!(comments -> users (commenter_user_id)); -diesel::joinable!(multi_select_answers -> answers (answer_id)); -diesel::joinable!(multi_select_answers -> multi_select_options (option_id)); -diesel::joinable!(multi_select_options -> questions (question_id)); -diesel::joinable!(organisation_users -> organisations (organisation_id)); -diesel::joinable!(organisation_users -> users (user_id)); -diesel::joinable!(ratings -> applications (application_id)); -diesel::joinable!(ratings -> users (rater_user_id)); -diesel::joinable!(roles -> campaigns (campaign_id)); -diesel::joinable!(short_answer_answers -> answers (answer_id)); - -diesel::allow_tables_to_appear_in_same_query!( - answers, - applications, - campaigns, - comments, - multi_select_answers, - multi_select_options, - organisation_users, - organisations, - questions, - ratings, - roles, - short_answer_answers, - users, -); From 62f3053460730cc8811592cef7bd02c940888148 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Wed, 26 Jul 2023 13:28:47 +1000 Subject: [PATCH 30/35] feat(fix): remove unused returns --- backend/server/src/question_types.rs | 56 +--------------------------- 1 file changed, 2 insertions(+), 54 deletions(-) diff --git a/backend/server/src/question_types.rs b/backend/server/src/question_types.rs index 8790e4cb6..0f2eed4f7 100644 --- a/backend/server/src/question_types.rs +++ b/backend/server/src/question_types.rs @@ -67,7 +67,7 @@ impl QuestionDataInput { self, conn: &PgConnection, question_id: i32, - ) -> Option { + ) { match self { QuestionDataInput::ShortAnswer => { @@ -118,14 +118,10 @@ impl QuestionDataInput { } }, } - - None - } } impl QuestionData { - pub fn get_from_question_id(conn: &PgConnection, q_id: i32) -> Option { let question: Question; @@ -179,50 +175,6 @@ impl QuestionData { }, }; } - - pub fn get_from_question(conn: &PgConnection, question: &Question) -> Option { - let q_id = question.id; - - return match question.question_type { - QuestionType::ShortAnswer => { Some(QuestionData::ShortAnswer) }, - QuestionType::MultiSelect => { - use crate::database::schema::multi_select_options::dsl::*; - - let data: Vec = multi_select_options - .filter(question_id.eq(q_id)) - .load(conn) - .unwrap_or_else(|_| vec![]); - - Some(QuestionData::MultiSelect(data.into_iter().map(|x| { - x.text - }).collect())) - }, - QuestionType::MultiChoice => { - use crate::database::schema::multi_select_options::dsl::*; - - let data: Vec = multi_select_options - .filter(question_id.eq(q_id)) - .load(conn) - .unwrap_or_else(|_| vec![]); - - Some(QuestionData::MultiChoice(data.into_iter().map(|x| { - x.text - }).collect())) - }, - QuestionType::DropDown => { - use crate::database::schema::multi_select_options::dsl::*; - - let data: Vec = multi_select_options - .filter(question_id.eq(q_id)) - .load(conn) - .unwrap_or_else(|_| vec![]); - - Some(QuestionData::DropDown(data.into_iter().map(|x| { - x.text - }).collect())) - }, - }; - } } impl AnswerDataInput { @@ -230,7 +182,7 @@ impl AnswerDataInput { self, conn: &mut PgConnection, answer: &Answer, - ) -> Option { + ) { match self { AnswerDataInput::ShortAnswer(short_answer_data) => { let answer = NewShortAnswerAnswer { @@ -273,8 +225,6 @@ impl AnswerDataInput { }).ok(); } } - - None } } @@ -320,8 +270,6 @@ impl AnswerData { Some(AnswerData::DropDown(answer_data.option_id)) }, }; - - // None } } From 818c2342042192d225d5abd75a50b8ee5c08df1f Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Wed, 26 Jul 2023 13:31:13 +1000 Subject: [PATCH 31/35] feat(fix): remove deprecated comments in models.rs --- backend/server/src/database/models.rs | 45 --------------------------- 1 file changed, 45 deletions(-) diff --git a/backend/server/src/database/models.rs b/backend/server/src/database/models.rs index a878c3cd1..0fa9c8173 100644 --- a/backend/server/src/database/models.rs +++ b/backend/server/src/database/models.rs @@ -971,25 +971,6 @@ impl NewApplication { } } - /** - * - * - * - * - * add New fields to Question sturct (also migration). - * - * New question_type which is a enum to identify which type it is - * pub question_type: QuestionTypeEnum - * - * New question_data which will not be stored in db rather will provide info for separate specific - * question type table. The table to insert/query determined by question_type field - * pub question_data: QuestionDataEnum - * - * id of QuestionData - * pub question_data_id: i32 - * - */ - #[derive(Identifiable, Queryable, PartialEq, Serialize, Debug, QueryableByName)] #[table_name = "questions"] pub struct Question { @@ -1004,20 +985,6 @@ pub struct Question { pub question_type: QuestionType, } -/** - * - * - * - * Add enum field to NewQuestion - * - * - * - * - * This means FE must pass in correctly formated JSON struct for our question data - * - * pub question_type: QuestionTypeEnum - * pub question_data: QuestionDataEnum - */ #[derive(Insertable, Serialize, Deserialize)] #[table_name = "questions"] pub struct NewQuestion { @@ -1030,7 +997,6 @@ pub struct NewQuestion { pub question_type: QuestionType, } - #[derive(Serialize)] pub struct QuestionResponse { pub id: i32, @@ -1058,14 +1024,6 @@ impl std::convert::From<(Question, QuestionData)> for QuestionResponse { } } -// impl QuestionResponse { -// fn build_question_response() -> Self { -// Self { - -// } -// } -// } - #[derive(FromForm, AsChangeset, Deserialize)] #[table_name = "questions"] pub struct UpdateQuestionInput { @@ -1076,7 +1034,6 @@ pub struct UpdateQuestionInput { } #[derive(Queryable, Deserialize, Serialize, PartialEq, Debug, Clone)] -// #[table_name = "multi_select_options"] pub struct MultiSelectOption { pub id: i32, pub text: String, @@ -1200,7 +1157,6 @@ pub struct NewAnswer { } #[derive(Queryable, Deserialize, Serialize, PartialEq, Debug, Clone)] -// #[table_name = "short_answer_answers"] pub struct ShortAnswerAnswer { pub id: i32, pub text: String, @@ -1230,7 +1186,6 @@ impl NewShortAnswerAnswer { /// \ /// The vector will store the id's of each option selected. #[derive(Queryable, Deserialize, Serialize, PartialEq, Debug, Clone)] -// #[table_name = "multi_select_answers"] pub struct MultiSelectAnswer { pub id: i32, pub option_id: i32, From 9e5f36333018ca5dd89fc0812c76f16c4030bbde Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Wed, 26 Jul 2023 13:47:05 +1000 Subject: [PATCH 32/35] feat(fix): fix broken things from removing returns in insert answer/question data functions --- backend/server/src/application.rs | 5 +---- backend/server/src/campaigns.rs | 5 +---- backend/server/src/database/models.rs | 4 ++-- backend/server/src/question.rs | 6 +++--- backend/server/src/role.rs | 6 +++--- 5 files changed, 10 insertions(+), 16 deletions(-) diff --git a/backend/server/src/application.rs b/backend/server/src/application.rs index 01e19ab75..70a959e14 100644 --- a/backend/server/src/application.rs +++ b/backend/server/src/application.rs @@ -160,10 +160,7 @@ pub async fn submit_answer( // Insert the Answer Data UwU - AnswerDataInput::insert_answer_data(data, conn, &inserted_answer).ok_or(JsonErr( - ApplicationError::UnableToCreate, - Status::InternalServerError, - ))?; + AnswerDataInput::insert_answer_data(data, conn, &inserted_answer); Ok(Json(())) }) diff --git a/backend/server/src/campaigns.rs b/backend/server/src/campaigns.rs index ed5a2568a..19ad9946f 100644 --- a/backend/server/src/campaigns.rs +++ b/backend/server/src/campaigns.rs @@ -212,10 +212,7 @@ pub async fn new( JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) })?.id; - question_data.insert_question_data(conn, inserted_id).ok_or_else(|| { - eprintln!("Failed to create question data for some reason"); - JsonErr(CampaignError::UnableToCreate, Status::InternalServerError) - })?; + question_data.insert_question_data(conn, inserted_id); } Ok(Json(campaign)) diff --git a/backend/server/src/database/models.rs b/backend/server/src/database/models.rs index 0fa9c8173..9843f1396 100644 --- a/backend/server/src/database/models.rs +++ b/backend/server/src/database/models.rs @@ -1009,7 +1009,7 @@ pub struct QuestionResponse { pub question_type: QuestionType, } -impl std::convert::From<(Question, QuestionData)> for QuestionResponse { +impl From<(Question, QuestionData)> for QuestionResponse { fn from(question_with_data: (Question, QuestionData)) -> Self { Self { id: question_with_data.0.id, @@ -1429,7 +1429,7 @@ pub struct CampaignInfo { pub ends_at: NaiveDateTime, } -impl std::convert::From for CampaignInfo { +impl From for CampaignInfo { fn from(campaign: Campaign) -> Self { Self { id: campaign.id, diff --git a/backend/server/src/question.rs b/backend/server/src/question.rs index 89ab681ab..a07ef241f 100644 --- a/backend/server/src/question.rs +++ b/backend/server/src/question.rs @@ -37,14 +37,14 @@ pub async fn get_question( .ok_or(JsonErr(QuestionError::QuestionNotFound, Status::NotFound))?; let c = Campaign::get_from_id(&conn, r.campaign_id) .ok_or(JsonErr(QuestionError::QuestionNotFound, Status::NotFound))?; - let d: QuestionData = QuestionData::get_from_question(&conn, &q) - .ok_or(JsonErr(QuestionError::QuestionDataNotFound, Status::NotFound))?; + let d: Option = QuestionData::get_from_question_id(&conn, q.id); + if let None = d { return Err(JsonErr(QuestionError::QuestionNotFound, Status::NotFound)); } OrganisationUser::role_admin_level(q.get_first_role(), user.id, conn) .is_at_least_director() .or(c.published) .check() .map_err(|_| JsonErr(QuestionError::InsufficientPermissions, Status::Forbidden))?; - let question_with_data = (q,d); + let question_with_data = (q,d.unwrap()); Ok(question_with_data) }) .await diff --git a/backend/server/src/role.rs b/backend/server/src/role.rs index 81357be08..a9946ba9f 100644 --- a/backend/server/src/role.rs +++ b/backend/server/src/role.rs @@ -145,9 +145,9 @@ pub async fn get_questions( let mut questions_with_data = Vec::new(); for question in Question::get_all_from_role_id(conn, role_id) { - let data = QuestionData::get_from_question(conn, &question) - .ok_or(Json(QuestionsError::QuestionDataNotFound))?; - questions_with_data.push((question, data)); + let data = QuestionData::get_from_question_id(conn, question.id); + if let None = data { return Err(Json(QuestionsError::QuestionDataNotFound)); } + questions_with_data.push((question, data.unwrap())); } Ok(Json(GetQuestionsResponse { questions: questions_with_data From a93c77e3d8a89b472cbf8fe7bd64699f59fc7e91 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Wed, 26 Jul 2023 15:23:14 +1000 Subject: [PATCH 33/35] feat(fix): removed duplicated images module in lib.rs --- backend/server/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/server/src/lib.rs b/backend/server/src/lib.rs index 3fb3ebc2c..87008b041 100644 --- a/backend/server/src/lib.rs +++ b/backend/server/src/lib.rs @@ -18,5 +18,4 @@ pub mod role; pub mod state; pub mod question_types; pub mod user; -pub mod images; pub mod static_resources; From c3d4053fce81ff2b9a50c38d3ef3d78e42581bd6 Mon Sep 17 00:00:00 2001 From: Kavika Date: Mon, 2 Oct 2023 20:04:21 +1100 Subject: [PATCH 34/35] feat(fix): update seed.rs to include question_type field in NewQuestion struct --- backend/seed_data/src/seed.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/seed_data/src/seed.rs b/backend/seed_data/src/seed.rs index 3cac6f610..be15690dc 100644 --- a/backend/seed_data/src/seed.rs +++ b/backend/seed_data/src/seed.rs @@ -1,7 +1,7 @@ #![allow(unused_variables)] use backend::database::models::*; -use backend::database::schema::{AdminLevel, ApplicationStatus}; +use backend::database::schema::{AdminLevel, ApplicationStatus, QuestionType}; use backend::images::{save_image, try_decode_bytes}; use chrono::naive::NaiveDate; use diesel::pg::PgConnection; @@ -190,6 +190,7 @@ pub fn seed() { // role_ids: vec![senior_mentor_role.id], required: false, description: Some("Please ensure to go into great detail!".to_string()), + question_type: QuestionType::ShortAnswer, } .insert(&connection) .expect("Failed to insert question"); @@ -200,6 +201,7 @@ pub fn seed() { role_id: None, required: true, description: Some("Please explain why you would like to be a peer mentor!".to_string()), + question_type: QuestionType::ShortAnswer, } .insert(&connection) .expect("Failed to insert question"); @@ -236,6 +238,7 @@ pub fn seed() { question_id: question_one.id, application_id: application.id, description: "42".to_string(), + answer_type: QuestionType::ShortAnswer, } .insert(&connection) .expect("Failed to insert answer"); From f6ef94cf4e8fc4fa3825bfd9a4335d559dfe4dc0 Mon Sep 17 00:00:00 2001 From: Kavika Date: Mon, 2 Oct 2023 20:21:18 +1100 Subject: [PATCH 35/35] feat(fix): add missing import of diesel::sql_types::* for users table in schema.rs --- backend/server/src/database/schema.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/server/src/database/schema.rs b/backend/server/src/database/schema.rs index cf4608367..d91f3bd9e 100644 --- a/backend/server/src/database/schema.rs +++ b/backend/server/src/database/schema.rs @@ -196,6 +196,7 @@ table! { } table! { + use diesel::sql_types::*; use super::UserGenderMapping; users (id) {