Skip to content

Commit

Permalink
Question types framework + ShortAnswer & MultiChoice implementations (#…
Browse files Browse the repository at this point in the history
…483)

* Move initial migration to one file

* Update initial schema NOT NULL columns

* Fix question_id ref column being int not bigint

* Question framework and impl for MultiOption and ShortAnswer

* answer framework for short answer and multichoice

* update `Question` documentation to fit `serde` representation

* fix duplicates in question framework

* remove redundancy in answer framework

* make question option id unique snowflake

* re-separate schema migrations

* set timestamps fields to `NOT NULL`

* add answer to models module file

* fix enum errors

* fix always true/false case in answer length

* add `Ranking` question type

* rename multi option data "rank" to "order"

* update question json example

* fix use of postgres keyword `order`

---------

Co-authored-by: skye_blair <[email protected]>
  • Loading branch information
KavikaPalletenne and skye-blair authored Nov 5, 2024
1 parent 15aa828 commit f1994a7
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 45 deletions.
6 changes: 3 additions & 3 deletions backend/migrations/20240406023149_create_users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ CREATE TABLE users (
degree_name TEXT,
degree_starting_year INTEGER,
role user_role NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE UNIQUE INDEX IDX_users_email_lower on users ((lower(email)));
CREATE UNIQUE INDEX IDX_users_email_lower on users ((lower(email)));
7 changes: 4 additions & 3 deletions backend/migrations/20240406024211_create_organisations.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ CREATE TABLE organisations (
id BIGINT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
logo UUID,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TYPE organisation_role AS ENUM ('User', 'Admin');
Expand All @@ -12,12 +12,13 @@ CREATE TABLE organisation_members (
id BIGSERIAL PRIMARY KEY,
organisation_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
role organisation_role DEFAULT 'User' NOT NULL,
role organisation_role NOT NULL DEFAULT 'User',
CONSTRAINT FK_organisation_members_organisation
FOREIGN KEY(organisation_id)
REFERENCES organisations(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);


CREATE INDEX IDX_organisation_admins_organisation on organisation_members (organisation_id);
18 changes: 9 additions & 9 deletions backend/migrations/20240406025537_create_campaigns.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ CREATE TABLE campaigns (
description TEXT,
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT FK_campaigns_organisations
FOREIGN KEY(organisation_id)
REFERENCES organisations(id)
ON DELETE CASCADE
ON UPDATE CASCADE
FOREIGN KEY(organisation_id)
REFERENCES organisations(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);

CREATE TABLE campaign_roles (
Expand All @@ -23,13 +23,13 @@ CREATE TABLE campaign_roles (
min_available INTEGER NOT NULL,
max_available INTEGER NOT NULL,
finalised BOOLEAN NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT FK_campaign_roles_campaign
FOREIGN KEY(campaign_id)
REFERENCES campaigns(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);

CREATE INDEX IDX_campaign_roles_campaign on campaign_roles (campaign_id);
CREATE INDEX IDX_campaign_roles_campaign on campaign_roles (campaign_id);
22 changes: 12 additions & 10 deletions backend/migrations/20240406031400_create_questions.sql
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
CREATE TYPE question_type AS ENUM ('ShortAnswer', 'MultiChoice', 'MultiSelect', 'DropDown');
CREATE TYPE question_type AS ENUM ('ShortAnswer', 'MultiChoice', 'MultiSelect', 'DropDown', 'Ranking');

CREATE TABLE questions (
id BIGINT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
common BOOLEAN NOT NULL,
required BOOLEAN,
question_type question_type NOT NULL,
campaign_id BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT FK_questions_campaigns
FOREIGN KEY(campaign_id)
REFERENCES campaigns(id)
Expand All @@ -17,14 +18,15 @@ CREATE TABLE questions (
);

CREATE TABLE multi_option_question_options (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
text TEXT NOT NULL,
question_id INTEGER NOT NULL,
question_id BIGINT NOT NULL,
display_order INTEGER NOT NULL,
CONSTRAINT FK_multi_option_question_options_questions
FOREIGN KEY(question_id)
REFERENCES questions(id)
ON DELETE CASCADE
ON UPDATE CASCADE
FOREIGN KEY(question_id)
REFERENCES questions(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);

CREATE INDEX IDX_multi_option_question_options_questions on multi_option_question_options (question_id);
CREATE INDEX IDX_multi_option_question_options_questions on multi_option_question_options (question_id);
57 changes: 37 additions & 20 deletions backend/migrations/20240406031915_create_applications.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ CREATE TABLE applications (
user_id BIGINT NOT NULL,
status application_status NOT NULL DEFAULT 'Pending',
private_status application_status NOT NULL DEFAULT 'Pending',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT FK_applications_campaigns
FOREIGN KEY(campaign_id)
REFERENCES campaigns(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY(campaign_id)
REFERENCES campaigns(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT FK_applications_users
FOREIGN KEY(user_id)
REFERENCES users(id)
Expand Down Expand Up @@ -40,7 +40,7 @@ CREATE INDEX IDX_application_roles_applications on application_roles (applicatio
CREATE INDEX IDX_application_roles_campaign_roles on application_roles (campaign_role_id);

CREATE TABLE answers (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
application_id BIGINT NOT NULL,
question_id BIGINT NOT NULL,
CONSTRAINT FK_answers_applications
Expand All @@ -61,7 +61,7 @@ CREATE INDEX IDX_answers_questions on answers (question_id);
CREATE TABLE short_answer_answers (
id BIGSERIAL PRIMARY KEY,
text TEXT NOT NULL,
answer_id INTEGER NOT NULL,
answer_id BIGINT NOT NULL,
CONSTRAINT FK_short_answer_answers_answers
FOREIGN KEY(answer_id)
REFERENCES answers(id)
Expand All @@ -74,17 +74,34 @@ CREATE INDEX IDX_short_answer_answers_answers on short_answer_answers (answer_id
CREATE TABLE multi_option_answer_options (
id BIGSERIAL PRIMARY KEY,
option_id BIGINT NOT NULL,
answer_id INTEGER NOT NULL,
answer_id BIGINT NOT NULL,
CONSTRAINT FK_multi_option_answer_options_question_options
FOREIGN KEY(option_id)
REFERENCES multi_option_question_options(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT FK_multi_option_answer_options_answers
FOREIGN KEY(answer_id)
REFERENCES answers(id)
ON DELETE CASCADE
ON UPDATE CASCADE
FOREIGN KEY(answer_id)
REFERENCES answers(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);

CREATE TABLE ranking_answer_rankings (
id BIGSERIAL PRIMARY KEY,
option_id BIGINT NOT NULL,
rank INTEGER NOT NULL,
answer_id BIGINT NOT NULL,
CONSTRAINT FK_ranking_answer_rankings_question_options
FOREIGN KEY(option_id)
REFERENCES multi_option_question_options(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT FK_ranking_answer_rankings_answers
FOREIGN KEY(answer_id)
REFERENCES answers(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);

CREATE INDEX IDX_multi_option_answer_options_question_options on multi_option_answer_options (option_id);
Expand All @@ -95,13 +112,13 @@ CREATE TABLE application_ratings (
application_id BIGINT NOT NULL,
rater_id BIGINT NOT NULL,
rating INTEGER NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT FK_application_ratings_applications
FOREIGN KEY(application_id)
REFERENCES applications(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY(application_id)
REFERENCES applications(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT FK_application_ratings_users
FOREIGN KEY(rater_id)
REFERENCES users(id)
Expand All @@ -110,4 +127,4 @@ CREATE TABLE application_ratings (
);

CREATE INDEX IDX_application_ratings_applications on application_ratings (application_id);
CREATE INDEX IDX_application_ratings_users on application_ratings (rater_id);
CREATE INDEX IDX_application_ratings_users on application_ratings (rater_id);
110 changes: 110 additions & 0 deletions backend/server/src/models/answer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use crate::models::error::ChaosError;
use anyhow::{bail, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres, QueryBuilder, Row};

/// The `Answer` type that will be sent in API responses.
///
///
/// With the chosen `serde` representation and the use of `#[serde(flatten)]`, the JSON for a
/// `Answer` will look like this:
/// ```json
/// {
/// "id": 7233828375289773948,
/// "application_id": 7233828375289125398,
/// "question_id": 7233828375289139200,
/// "answer_type": "MultiChoice",
/// "data": 7233828393325384908,
/// "created_at": "2024-06-28T16:29:04.644008111Z",
/// "updated_at": "2024-06-30T12:14:12.458390190Z"
/// }
/// ```
#[derive(Deserialize, Serialize)]
pub struct Answer {
id: i64,
application_id: i64,
question_id: i64,

#[serde(flatten)]
answer_data: AnswerData,

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

#[derive(Deserialize, Serialize)]
#[serde( tag = "answer_type", content = "data")]
pub enum AnswerData {
ShortAnswer(String),
MultiChoice(i64),
MultiSelect(Vec<i64>),
DropDown(i64),
Ranking(Vec<i64>)
}

impl AnswerData {
pub async fn validate(self) -> Result<()> {
match self {
Self::ShortAnswer(text) => if text.len() == 0 { bail!("Empty answer") },
Self::MultiSelect(data) => if data.len() == 0 { bail!("Empty answer") },
_ => {},
}

Ok(())
}

pub async fn insert_into_db(self, answer_id: i64, pool: &Pool<Postgres>) -> Result<()> {
match self {
Self::ShortAnswer(text) => {
let result = sqlx::query!(
"INSERT INTO short_answer_answers (text, answer_id) VALUES ($1, $2)",
text,
answer_id
)
.execute(pool)
.await?;

Ok(())
},
Self::MultiChoice(option_id)
| Self::DropDown(option_id) => {
let result = sqlx::query!(
"INSERT INTO multi_option_answer_options (option_id, answer_id) VALUES ($1, $2)",
option_id,
answer_id
)
.execute(pool)
.await?;

Ok(())
},
Self::MultiSelect(option_ids) => {
let mut query_builder = sqlx::QueryBuilder::new("INSERT INTO multi_option_answer_options (option_id, answer_id)");

query_builder.push_values(option_ids, |mut b, option_id| {
b.push_bind(option_id).push_bind(answer_id);
});

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

Ok(())
},
Self::Ranking(option_ids) => {
let mut query_builder = sqlx::QueryBuilder::new("INSERT INTO ranking_answer_rankings (option_id, rank, answer_id)");

let mut rank = 1;
query_builder.push_values(option_ids, |mut b, option_id| {
b.push_bind(option_id).push_bind(rank).push_bind(answer_id);
rank += 1;
});

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

Ok(())
}
}
}
}
3 changes: 3 additions & 0 deletions backend/server/src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
pub mod answer;
pub mod app;
pub mod auth;
pub mod campaign;
pub mod error;
pub mod question;
pub mod organisation;
pub mod role;
pub mod storage;
pub mod transaction;
pub mod user;
pub mod application;

Loading

0 comments on commit f1994a7

Please sign in to comment.