Skip to content

Commit

Permalink
Question model implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
KavikaPalletenne committed Nov 12, 2024
1 parent 097dd1b commit 02dddf0
Showing 1 changed file with 345 additions and 5 deletions.
350 changes: 345 additions & 5 deletions backend/server/src/models/question.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::ops::DerefMut;
use crate::models::error::ChaosError;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres, QueryBuilder, Row};
use sqlx::{Postgres, QueryBuilder, Transaction};
use snowflake::SnowflakeIdGenerator;

/// The `Question` type that will be sent in API responses.
Expand Down Expand Up @@ -53,6 +54,179 @@ pub struct Question {
updated_at: DateTime<Utc>,
}

#[derive(Deserialize, Serialize, sqlx::FromRow)]
pub struct QuestionRawData {
id: i64,
title: String,
description: Option<String>,
common: bool, // Common question are shown at the start
required: bool,

question_type: QuestionType,
multi_option_data: Option<sqlx::types::Json<Vec<MultiOptionQuestionOption>>>,

created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}

impl Question {
pub async fn create(campaign_id: i64, title: String, description: Option<String>, common: bool, required: bool, question_data: QuestionData, mut snowflake_generator: SnowflakeIdGenerator, transaction: &mut Transaction<'_, Postgres>) -> Result<i64, ChaosError> {
question_data.validate()?;

let id = snowflake_generator.generate();

sqlx::query!(
"
INSERT INTO questions (
id, title, description, common,
required, question_type, campaign_id
) VALUES ($1, $2, $3, $4, $5, $6, $7)
",
id, title, description, common, required, QuestionType::from_question_data(&question_data) as QuestionType, campaign_id
)
.execute(transaction.deref_mut())
.await?;

question_data.insert_into_db(id, transaction, snowflake_generator).await?;

Ok(id)
}

pub async fn get(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result<Question, ChaosError> {
let question_raw_data: QuestionRawData = sqlx::query_as(
"
SELECT
q.id,
q.title,
q.description,
q.common,
q.required,
q.question_type AS \"question_type: QuestionType\",
q.created_at,
q.updated_at,
array_agg(
jsonb_build_object(
'id', mod.id,
'display_order', mod.display_order,
'text', mod.text
) ORDER BY mod.display_order
) FILTER (WHERE mod.id IS NOT NULL) AS \"multi_option_data: Option<sqlx::types::Json<Vec<MultiOptionQuestionOption>>>\"
FROM
questions q
LEFT JOIN
multi_option_question_options mod ON q.id = mod.question_id
AND q.question_type IN ('MultiChoice', 'MultiSelect', 'DropDown', 'Ranking')
WHERE q.id = $1
GROUP BY
q.id
"
)
.bind(id)
.fetch_one(transaction.deref_mut())
.await?;

let question_data = QuestionData::from_question_raw_data(question_raw_data.question_type, question_raw_data.multi_option_data);

Ok(
Question {
id,
title: question_raw_data.title,
description: question_raw_data.description,
common: question_raw_data.common,
required: question_raw_data.required,
question_data,
created_at: question_raw_data.created_at,
updated_at: question_raw_data.updated_at,
}
)
}

pub async fn get_all_by_campaign(campaign_id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result<Vec<Question>, ChaosError> {
let question_raw_data: Vec<QuestionRawData> = sqlx::query_as(
"
SELECT
q.id,
q.title,
q.description,
q.common,
q.required,
q.question_type AS \"question_type: QuestionType\",
q.created_at,
q.updated_at,
array_agg(
jsonb_build_object(
'id', mod.id,
'display_order', mod.display_order,
'text', mod.text
) ORDER BY mod.display_order
) FILTER (WHERE mod.id IS NOT NULL) AS \"multi_option_data: Option<sqlx::types::Json<Vec<MultiOptionQuestionOption>>>\"
FROM
questions q
LEFT JOIN
multi_option_question_options mod ON q.id = mod.question_id
AND q.question_type IN ('MultiChoice', 'MultiSelect', 'DropDown', 'Ranking')
WHERE q.campaign_id = $1
GROUP BY
q.id
"
)
.bind(campaign_id)
.fetch_all(transaction.deref_mut())
.await?;

let questions = question_raw_data.into_iter().map(|question_raw_data| {
let question_data = QuestionData::from_question_raw_data(question_raw_data.question_type, question_raw_data.multi_option_data);

Question {
id: question_raw_data.id,
title: question_raw_data.title,
description: question_raw_data.description,
common: question_raw_data.common,
required: question_raw_data.required,
question_data,
created_at: question_raw_data.created_at,
updated_at: question_raw_data.updated_at,
}
}).collect();

Ok(questions)
}

pub async fn update(id: i64, title: String, description: Option<String>, common: bool, required: bool, question_data: QuestionData, transaction: &mut Transaction<'_, Postgres>, snowflake_generator: SnowflakeIdGenerator) -> Result<(), ChaosError> {
question_data.validate()?;

let question_type_parent: QuestionTypeParent = sqlx::query_as!(
QuestionTypeParent,
"
UPDATE questions SET
title = $2, description = $3, common = $4,
required = $5, question_type = $6, updated_at = $7
WHERE id = $1
RETURNING question_type AS \"question_type: QuestionType\"
",
id, title, description, common, required,
QuestionType::from_question_data(&question_data) as QuestionType, Utc::now()
)
.fetch_one(transaction.deref_mut())
.await?;

let old_data = QuestionData::from_question_type(&question_type_parent.question_type);
old_data.delete_from_db(id, transaction).await?;

question_data.insert_into_db(id, transaction, snowflake_generator).await?;

Ok(())
}

pub async fn delete(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result<(), ChaosError> {
sqlx::query!("DELETE FROM questions WHERE id = $1 RETURNING id", id)
.fetch_one(transaction.deref_mut())
.await?;

Ok(())
}
}

/// An enum that represents all the data types of question data that CHAOS can handle.
/// This stores all the data for each question type.
///
Expand All @@ -70,23 +244,95 @@ pub enum QuestionData {
Ranking(MultiOptionData),
}

/// An enum needed to track QuestionType in the database,
/// as DB enum does not contain the inner data.
#[derive(Deserialize, Serialize, PartialEq, sqlx::Type)]
#[sqlx(type_name = "question_type", rename_all = "PascalCase")]
pub enum QuestionType {
ShortAnswer,
MultiChoice,
MultiSelect,
DropDown,
Ranking,
}

#[derive(Deserialize)]
pub struct QuestionTypeParent {
question_type: QuestionType
}

impl QuestionType {
fn from_question_data(question_data: &QuestionData) -> Self {
match question_data {
QuestionData::ShortAnswer => QuestionType::ShortAnswer,
QuestionData::MultiChoice(_) => QuestionType::MultiChoice,
QuestionData::MultiSelect(_) => QuestionType::MultiSelect,
QuestionData::DropDown(_) => QuestionType::DropDown,
QuestionData::Ranking(_) => QuestionType::Ranking,
}
}
}

#[derive(Deserialize, Serialize)]
pub struct MultiOptionData {
options: Vec<MultiOptionQuestionOption>,
}

impl Default for MultiOptionData {
fn default() -> Self {
Self {
// Return an empty vector to be replaced by real data later on.
options: vec![],
}
}
}

/// Each of these structs represent a row in the `multi_option_question_options`
/// table. For a `MultiChoice` question like "What is your favourite programming
/// language?", there would be rows for "Rust", "Java" and "TypeScript".
#[derive(Deserialize, Serialize)]
pub struct MultiOptionQuestionOption {
id: i32,
id: i64,
display_order: i32,
text: String,
}

impl QuestionData {
pub async fn validate(self) -> Result<(), ChaosError> {
fn from_question_type(question_type: &QuestionType) -> Self {
match question_type {
QuestionType::ShortAnswer => QuestionData::ShortAnswer,
QuestionType::MultiChoice => QuestionData::MultiChoice(MultiOptionData::default()),
QuestionType::MultiSelect => QuestionData::MultiSelect(MultiOptionData::default()),
QuestionType::DropDown => QuestionData::DropDown(MultiOptionData::default()),
QuestionType::Ranking => QuestionData::Ranking(MultiOptionData::default()),
}
}

fn from_question_raw_data(question_type: QuestionType, multi_option_data: Option<sqlx::types::Json<Vec<MultiOptionQuestionOption>>>) -> Self {
return if question_type == QuestionType::ShortAnswer {
QuestionData::ShortAnswer
} else if
question_type == QuestionType::MultiChoice ||
question_type == QuestionType::MultiSelect ||
question_type == QuestionType::DropDown ||
question_type == QuestionType::Ranking
{
let options = multi_option_data.expect("Data should exist for MultiOptionData variants").0;
let data = MultiOptionData { options };

match question_type {
QuestionType::MultiChoice => QuestionData::MultiChoice(data),
QuestionType::MultiSelect => QuestionData::MultiSelect(data),
QuestionType::DropDown => QuestionData::DropDown(data),
QuestionType::Ranking => QuestionData::Ranking(data),
_ => QuestionData::ShortAnswer // Should never be reached, hence return ShortAnswer
}
} else {
QuestionData::ShortAnswer // Should never be reached, hence return ShortAnswer
}
}

pub fn validate(&self) -> Result<(), ChaosError> {
match self {
Self::ShortAnswer => Ok(()),
Self::MultiChoice(data)
Expand All @@ -102,7 +348,7 @@ impl QuestionData {
}
}

pub async fn insert_into_db(self, question_id: i64, pool: &Pool<Postgres>, mut snowflake_generator: SnowflakeIdGenerator) -> Result<(), ChaosError> {
pub async fn insert_into_db(self, question_id: i64, transaction: &mut Transaction<'_,Postgres>, mut snowflake_generator: SnowflakeIdGenerator) -> Result<(), ChaosError> {
match self {
Self::ShortAnswer => Ok(()),
Self::MultiChoice(data)
Expand All @@ -118,7 +364,101 @@ impl QuestionData {
});

let query = query_builder.build();
let result = query.execute(pool).await?;
query.execute(transaction.deref_mut()).await?;

Ok(())
}
}
}

pub async fn get_from_db(self, question_id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result<Self, ChaosError> {
match self {
Self::ShortAnswer => Ok(Self::ShortAnswer),
Self::MultiChoice(_) => {
let data_vec = sqlx::query_as!(
MultiOptionQuestionOption,
"
SELECT id, display_order, text FROM multi_option_question_options
WHERE question_id = $1
",
question_id
)
.fetch_all(transaction.deref_mut())
.await?;

let data = MultiOptionData {
options: data_vec
};

Ok(Self::MultiChoice(data))
},
Self::MultiSelect(_) => {
let data_vec = sqlx::query_as!(
MultiOptionQuestionOption,
"
SELECT id, display_order, text FROM multi_option_question_options
WHERE question_id = $1
",
question_id
)
.fetch_all(transaction.deref_mut())
.await?;

let data = MultiOptionData {
options: data_vec
};

Ok(Self::MultiSelect(data))
}
Self::DropDown(_) => {
let data_vec = sqlx::query_as!(
MultiOptionQuestionOption,
"
SELECT id, display_order, text FROM multi_option_question_options
WHERE question_id = $1
",
question_id
)
.fetch_all(transaction.deref_mut())
.await?;

let data = MultiOptionData {
options: data_vec
};

Ok(Self::DropDown(data))
}
Self::Ranking(_) => {
let data_vec = sqlx::query_as!(
MultiOptionQuestionOption,
"
SELECT id, display_order, text FROM multi_option_question_options
WHERE question_id = $1
",
question_id
)
.fetch_all(transaction.deref_mut())
.await?;

let data = MultiOptionData {
options: data_vec
};

Ok(Self::Ranking(data))
}
}
}

pub async fn delete_from_db(self, question_id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result<(), ChaosError> {
match self {
Self::ShortAnswer => Ok(()),
Self::MultiChoice(_)
| Self::MultiSelect(_)
| Self::DropDown(_)
| Self::Ranking(_) => {
sqlx::query!("DELETE FROM multi_option_question_options WHERE question_id = $1", question_id)
.execute(transaction.deref_mut())
.await?;

Ok(())
}
Expand Down

0 comments on commit 02dddf0

Please sign in to comment.