diff --git a/services/headless-lms/migrations/20240823123921_add_code_giveaways.up.sql b/services/headless-lms/migrations/20240823123921_add_code_giveaways.up.sql index 625806c9fd5..77049b69188 100644 --- a/services/headless-lms/migrations/20240823123921_add_code_giveaways.up.sql +++ b/services/headless-lms/migrations/20240823123921_add_code_giveaways.up.sql @@ -4,6 +4,8 @@ CREATE TABLE code_giveaways ( updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), deleted_at TIMESTAMP WITH TIME ZONE, course_id UUID NOT NULL REFERENCES courses(id), + course_module_id UUID REFERENCES course_modules(id), + enabled BOOLEAN NOT NULL DEFAULT FALSE ); CREATE TRIGGER set_timestamp BEFORE UPDATE ON code_giveaways FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); @@ -13,6 +15,8 @@ COMMENT ON COLUMN code_giveaways.created_at IS 'Timestamp when the record was cr COMMENT ON COLUMN code_giveaways.updated_at IS 'Timestamp when the record was last updated. The field is updated automatically by the set_timestamp trigger.'; COMMENT ON COLUMN code_giveaways.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN code_giveaways.course_id IS 'The course the code giveaway is available on.'; +COMMENT ON COLUMN code_giveaways.course_module_id IS 'The course module the code giveaway is available on. If null, the giveaway has not been placed on a course module on the CMS.'; +COMMENT ON COLUMN code_giveaways.enabled IS 'If the giveaway is enabled, the codes can be given to students.'; CREATE TABLE code_giveaway_codes ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, @@ -22,10 +26,17 @@ CREATE TABLE code_giveaway_codes ( code_giveaway_id UUID NOT NULL REFERENCES code_giveaways(id), code_given_to_user_id UUID REFERENCES users(id), added_by_user_id UUID NOT NULL REFERENCES users(id), + code VARCHAR(2048) NOT NULL, + -- A user can only receive one code from a giveaway. + UNIQUE NULLS NOT DISTINCT ( + code_giveaway_id, + code_given_to_user_id, + deleted_at + ) ); CREATE TRIGGER set_timestamp BEFORE UPDATE ON code_giveaway_codes FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); -COMMENT ON TABLE code_giveaway_codes IS 'A code that is available in a code giveaway.'; +COMMENT ON TABLE code_giveaway_codes IS 'A code that is available in a code giveaway. A user can only receive one code from a giveaway.'; COMMENT ON COLUMN code_giveaway_codes.id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN code_giveaway_codes.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN code_giveaway_codes.updated_at IS 'Timestamp when the record was last updated. The field is updated automatically by the set_timestamp trigger.'; @@ -33,3 +44,4 @@ COMMENT ON COLUMN code_giveaway_codes.deleted_at IS 'Timestamp when the record w COMMENT ON COLUMN code_giveaway_codes.code_giveaway_id IS 'The code giveaway the code is available in.'; COMMENT ON COLUMN code_giveaway_codes.code_given_to_user_id IS 'The user the code was given to. If null, the code has not been given to a user.'; COMMENT ON COLUMN code_giveaway_codes.added_by_user_id IS 'The user that added the code to the giveaway.'; +COMMENT ON COLUMN code_giveaway_codes.code IS 'The code that is given to a user.'; diff --git a/services/headless-lms/models/.sqlx/query-25128916d3132e8c6a18b311ff0bf41c6f456c0442074005c37cf8c4f5fe3742.json b/services/headless-lms/models/.sqlx/query-25128916d3132e8c6a18b311ff0bf41c6f456c0442074005c37cf8c4f5fe3742.json new file mode 100644 index 00000000000..183aac8c7b7 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-25128916d3132e8c6a18b311ff0bf41c6f456c0442074005c37cf8c4f5fe3742.json @@ -0,0 +1,48 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT *\nFROM code_giveaways\nWHERE course_id = $1\n AND deleted_at IS NULL\n", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "course_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "course_module_id", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "enabled", + "type_info": "Bool" + } + ], + "parameters": { + "Left": ["Uuid"] + }, + "nullable": [false, false, false, true, false, true, false] + }, + "hash": "25128916d3132e8c6a18b311ff0bf41c6f456c0442074005c37cf8c4f5fe3742" +} diff --git a/services/headless-lms/models/.sqlx/query-36b71b769e5b8c7a058ffed11832fb83c88ca543b071e71dfe39aa31a9ffecda.json b/services/headless-lms/models/.sqlx/query-36b71b769e5b8c7a058ffed11832fb83c88ca543b071e71dfe39aa31a9ffecda.json new file mode 100644 index 00000000000..f9ef3968947 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-36b71b769e5b8c7a058ffed11832fb83c88ca543b071e71dfe39aa31a9ffecda.json @@ -0,0 +1,53 @@ +{ + "db_name": "PostgreSQL", + "query": "\nUPDATE code_giveaway_codes\nSET code_given_to_user_id = $2\nWHERE code_giveaway_id = $1\n AND code_given_to_user_id IS NULL\n AND deleted_at IS NULL\nRETURNING *\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "code_giveaway_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "code_given_to_user_id", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "added_by_user_id", + "type_info": "Uuid" + }, + { + "ordinal": 7, + "name": "code", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": ["Uuid", "Uuid"] + }, + "nullable": [false, false, false, true, false, true, false, false] + }, + "hash": "36b71b769e5b8c7a058ffed11832fb83c88ca543b071e71dfe39aa31a9ffecda" +} diff --git a/services/headless-lms/models/.sqlx/query-4aa7a72a1675fa71a4b6dab793f9652f79777360691d0b97c998c17f40d37270.json b/services/headless-lms/models/.sqlx/query-4aa7a72a1675fa71a4b6dab793f9652f79777360691d0b97c998c17f40d37270.json new file mode 100644 index 00000000000..d3499333f68 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-4aa7a72a1675fa71a4b6dab793f9652f79777360691d0b97c998c17f40d37270.json @@ -0,0 +1,48 @@ +{ + "db_name": "PostgreSQL", + "query": "\nINSERT INTO code_giveaways (course_id)\nVALUES ($1)\nRETURNING *\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "course_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "course_module_id", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "enabled", + "type_info": "Bool" + } + ], + "parameters": { + "Left": ["Uuid"] + }, + "nullable": [false, false, false, true, false, true, false] + }, + "hash": "4aa7a72a1675fa71a4b6dab793f9652f79777360691d0b97c998c17f40d37270" +} diff --git a/services/headless-lms/models/.sqlx/query-a2e874db1ab1f1cab908956c4cb51339a7cbf22c5e214c5e7b15ea83298280eb.json b/services/headless-lms/models/.sqlx/query-a2e874db1ab1f1cab908956c4cb51339a7cbf22c5e214c5e7b15ea83298280eb.json new file mode 100644 index 00000000000..044d2b3257d --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-a2e874db1ab1f1cab908956c4cb51339a7cbf22c5e214c5e7b15ea83298280eb.json @@ -0,0 +1,53 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT *\nFROM code_giveaway_codes\nWHERE code_giveaway_id = $1\n AND added_by_user_id = $2\n AND id = ANY($3)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "code_giveaway_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "code_given_to_user_id", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "added_by_user_id", + "type_info": "Uuid" + }, + { + "ordinal": 7, + "name": "code", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": ["Uuid", "Uuid", "UuidArray"] + }, + "nullable": [false, false, false, true, false, true, false, false] + }, + "hash": "a2e874db1ab1f1cab908956c4cb51339a7cbf22c5e214c5e7b15ea83298280eb" +} diff --git a/services/headless-lms/models/.sqlx/query-c15d5b1c07d8bfc8e76610b3162c8b8da71726f84ddb2591777bbb3a4361c14d.json b/services/headless-lms/models/.sqlx/query-c15d5b1c07d8bfc8e76610b3162c8b8da71726f84ddb2591777bbb3a4361c14d.json new file mode 100644 index 00000000000..2386a1377d8 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-c15d5b1c07d8bfc8e76610b3162c8b8da71726f84ddb2591777bbb3a4361c14d.json @@ -0,0 +1,53 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT *\nFROM code_giveaway_codes\nWHERE code_giveaway_id = $1\n AND deleted_at IS NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "code_giveaway_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "code_given_to_user_id", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "added_by_user_id", + "type_info": "Uuid" + }, + { + "ordinal": 7, + "name": "code", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": ["Uuid"] + }, + "nullable": [false, false, false, true, false, true, false, false] + }, + "hash": "c15d5b1c07d8bfc8e76610b3162c8b8da71726f84ddb2591777bbb3a4361c14d" +} diff --git a/services/headless-lms/models/.sqlx/query-cfff68ad0ab3cac139a34e02f42133ec1aa81d264982b0381c4fe42f08eceb81.json b/services/headless-lms/models/.sqlx/query-cfff68ad0ab3cac139a34e02f42133ec1aa81d264982b0381c4fe42f08eceb81.json new file mode 100644 index 00000000000..42e732598df --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-cfff68ad0ab3cac139a34e02f42133ec1aa81d264982b0381c4fe42f08eceb81.json @@ -0,0 +1,48 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT *\nFROM code_giveaways\nWHERE id = $1\n", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "course_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "course_module_id", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "enabled", + "type_info": "Bool" + } + ], + "parameters": { + "Left": ["Uuid"] + }, + "nullable": [false, false, false, true, false, true, false] + }, + "hash": "cfff68ad0ab3cac139a34e02f42133ec1aa81d264982b0381c4fe42f08eceb81" +} diff --git a/services/headless-lms/models/.sqlx/query-d1b0bc1c3dcd19c64d5546b2f4b5f8af92c200ff2d5f54a65ae0a7bcb2876500.json b/services/headless-lms/models/.sqlx/query-d1b0bc1c3dcd19c64d5546b2f4b5f8af92c200ff2d5f54a65ae0a7bcb2876500.json new file mode 100644 index 00000000000..90382225dd5 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-d1b0bc1c3dcd19c64d5546b2f4b5f8af92c200ff2d5f54a65ae0a7bcb2876500.json @@ -0,0 +1,53 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT *\nFROM code_giveaway_codes\nWHERE code_giveaway_id = $1\n AND code_given_to_user_id = $2\n AND deleted_at IS NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "code_giveaway_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "code_given_to_user_id", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "added_by_user_id", + "type_info": "Uuid" + }, + { + "ordinal": 7, + "name": "code", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": ["Uuid", "Uuid"] + }, + "nullable": [false, false, false, true, false, true, false, false] + }, + "hash": "d1b0bc1c3dcd19c64d5546b2f4b5f8af92c200ff2d5f54a65ae0a7bcb2876500" +} diff --git a/services/headless-lms/models/.sqlx/query-f38ec981811e12386c476b429aa0efa8040d0b5e91f71ccff66f5cadb4dd634e.json b/services/headless-lms/models/.sqlx/query-f38ec981811e12386c476b429aa0efa8040d0b5e91f71ccff66f5cadb4dd634e.json new file mode 100644 index 00000000000..b123792814b --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-f38ec981811e12386c476b429aa0efa8040d0b5e91f71ccff66f5cadb4dd634e.json @@ -0,0 +1,48 @@ +{ + "db_name": "PostgreSQL", + "query": "\nUPDATE code_giveaways\nSET enabled = $2\nWHERE id = $1\nRETURNING *\n", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "course_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "course_module_id", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "enabled", + "type_info": "Bool" + } + ], + "parameters": { + "Left": ["Uuid", "Bool"] + }, + "nullable": [false, false, false, true, false, true, false] + }, + "hash": "f38ec981811e12386c476b429aa0efa8040d0b5e91f71ccff66f5cadb4dd634e" +} diff --git a/services/headless-lms/models/src/code_giveaway_codes.rs b/services/headless-lms/models/src/code_giveaway_codes.rs new file mode 100644 index 00000000000..cd02d13b89a --- /dev/null +++ b/services/headless-lms/models/src/code_giveaway_codes.rs @@ -0,0 +1,159 @@ +use sqlx::{QueryBuilder, Row}; + +use crate::prelude::*; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[cfg_attr(feature = "ts_rs", derive(TS))] +pub struct CodeGiveawayCode { + pub id: Uuid, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option>, + pub code_giveaway_id: Uuid, + pub code_given_to_user_id: Option, + pub added_by_user_id: Uuid, + pub code: String, +} + +pub async fn insert_many( + conn: &mut PgConnection, + code_giveaway_id: Uuid, + input: &[String], + added_by_user_id: Uuid, +) -> ModelResult> { + let mut query_builder = QueryBuilder::new( + "INSERT INTO code_giveaway_codes (code_giveaway_id, code, added_by_user_id) ", + ); + + query_builder.push_values(input, |mut b, code| { + b.push_bind(code_giveaway_id) + .push_bind(code) + .push_bind(added_by_user_id); + }); + + query_builder.push(" RETURNING id"); + + let query = query_builder.build(); + + let ids: Vec = query + .fetch_all(&mut *conn) + .await? + .iter() + .map(|row| row.get("id")) + .collect(); + + // Fetch the inserted rows to return them + let res = sqlx::query_as!( + CodeGiveawayCode, + r#" +SELECT * +FROM code_giveaway_codes +WHERE code_giveaway_id = $1 + AND added_by_user_id = $2 + AND id = ANY($3) + "#, + code_giveaway_id, + added_by_user_id, + &ids + ) + .fetch_all(&mut *conn) + .await?; + + Ok(res) +} + +pub async fn get_all_by_code_giveaway_id( + conn: &mut PgConnection, + code_giveaway_id: Uuid, +) -> ModelResult> { + let res = sqlx::query_as!( + CodeGiveawayCode, + r#" +SELECT * +FROM code_giveaway_codes +WHERE code_giveaway_id = $1 + AND deleted_at IS NULL + "#, + code_giveaway_id + ) + .fetch_all(conn) + .await?; + Ok(res) +} + +pub async fn get_code_given_to_user( + conn: &mut PgConnection, + code_giveaway_id: Uuid, + user_id: Uuid, +) -> ModelResult> { + let res = sqlx::query_as!( + CodeGiveawayCode, + r#" +SELECT * +FROM code_giveaway_codes +WHERE code_giveaway_id = $1 + AND code_given_to_user_id = $2 + AND deleted_at IS NULL + "#, + code_giveaway_id, + user_id + ) + .fetch_optional(conn) + .await?; + Ok(res) +} + +pub async fn give_some_code_to_user( + conn: &mut PgConnection, + code_giveaway_id: Uuid, + user_id: Uuid, +) -> ModelResult> { + let res = sqlx::query_as!( + CodeGiveawayCode, + r#" +UPDATE code_giveaway_codes +SET code_given_to_user_id = $2 +WHERE code_giveaway_id = $1 + AND code_given_to_user_id IS NULL + AND deleted_at IS NULL +RETURNING * + "#, + code_giveaway_id, + user_id + ) + .fetch_optional(conn) + .await?; + Ok(res) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_helper::*; + + #[tokio::test] + async fn test_insert_many_empty() { + insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module); + + let insert_result = insert_many(tx.as_mut(), &[]).await.unwrap(); + + let inserted_data = get_inserted_data(tx.as_mut()).await.unwrap(); + + assert!(insert_result.is_empty()); + assert!(inserted_data.is_empty()); + } + + #[tokio::test] + async fn test_insert_many_with_data() { + insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, chapter: _chapter, page: _page, exercise: exercise_id); + + let data_to_insert = vec![/* your data here */]; + + let insert_result = insert_many(tx.as_mut(), &data_to_insert).await.unwrap(); + + let inserted_data = get_inserted_data(tx.as_mut()).await.unwrap(); + + assert_eq!(insert_result.len(), data_to_insert.len()); + assert_eq!(inserted_data.len(), data_to_insert.len()); + } +} diff --git a/services/headless-lms/models/src/code_giveaways.rs b/services/headless-lms/models/src/code_giveaways.rs new file mode 100644 index 00000000000..ae3209cd9c4 --- /dev/null +++ b/services/headless-lms/models/src/code_giveaways.rs @@ -0,0 +1,87 @@ +use crate::prelude::*; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[cfg_attr(feature = "ts_rs", derive(TS))] +pub struct CodeGiveaway { + pub id: Uuid, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option>, + pub course_id: Uuid, + pub course_module_id: Option, + pub enabled: bool, +} + +async fn insert(conn: &mut PgConnection, course_id: Uuid) -> ModelResult { + let res = sqlx::query_as!( + CodeGiveaway, + r#" +INSERT INTO code_giveaways (course_id) +VALUES ($1) +RETURNING * + "#, + course_id + ) + .fetch_one(&mut *conn) + .await?; + + Ok(res) +} + +async fn get_all_for_course( + conn: &mut PgConnection, + course_id: Uuid, +) -> ModelResult> { + let res = sqlx::query_as!( + CodeGiveaway, + r#" +SELECT * +FROM code_giveaways +WHERE course_id = $1 + AND deleted_at IS NULL +"#, + course_id + ) + .fetch_all(&mut *conn) + .await?; + + Ok(res) +} + +async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult> { + let res = sqlx::query_as!( + CodeGiveaway, + r#" +SELECT * +FROM code_giveaways +WHERE id = $1 +"#, + id + ) + .fetch_optional(&mut *conn) + .await?; + + Ok(res) +} + +async fn set_enabled( + conn: &mut PgConnection, + id: Uuid, + enabled: bool, +) -> ModelResult { + let res = sqlx::query_as!( + CodeGiveaway, + r#" +UPDATE code_giveaways +SET enabled = $2 +WHERE id = $1 +RETURNING * +"#, + id, + enabled + ) + .fetch_one(&mut *conn) + .await?; + + Ok(res) +} diff --git a/services/headless-lms/models/src/lib.rs b/services/headless-lms/models/src/lib.rs index dfe3d06c75f..6bd9ba40e59 100644 --- a/services/headless-lms/models/src/lib.rs +++ b/services/headless-lms/models/src/lib.rs @@ -12,6 +12,8 @@ pub mod chapters; pub mod chatbot_configurations; pub mod chatbot_conversation_messages; pub mod chatbot_conversations; +pub mod code_giveaway_codes; +pub mod code_giveaways; pub mod course_background_question_answers; pub mod course_background_questions; pub mod course_exams;