From 41f50dfa74bde21114103ba7bb1998a02f1115c9 Mon Sep 17 00:00:00 2001 From: Maija Y Date: Mon, 16 Sep 2024 10:48:14 +0300 Subject: [PATCH 1/9] Regeneratable code for joining course --- ...add-join-code-to-course-instances.down.sql | 1 + ...4_add-join-code-to-course-instances.up.sql | 3 ++ ...a175b1b78795d36cd486e18ca8d80f591cdf.json} | 11 ++++-- ...97175420552b591ab92b80df6bb88c996bf6.json} | 11 ++++-- ...46b7d27d42c453ed774a6afa34f955d085c3c.json | 12 ++++++ ...f34113788a0500a36b6553c6a119807ec8c8.json} | 11 ++++-- ...c8b9fb2620fac9a78fe0f99d7372a60c6ccd3.json | 7 +++- ...b865831429e12d26bed1786210abb23598dd.json} | 11 ++++-- ...5b052cb4c4c3b82e27817ae5a059685fa088b.json | 7 +++- ...768266211b1612168108695b61f23dc7175f.json} | 11 ++++-- ...9aecf88d4a09675adb64401c0cd2d51e2373.json} | 11 ++++-- .../models/src/course_instances.rs | 38 ++++++++++++++++--- .../main_frontend/course_instances.rs | 26 +++++++++++++ .../manage/course-instances/[id]/index.tsx | 27 +++++++++++++ .../src/services/backend/course-instances.ts | 6 +++ .../packages/common/src/bindings.guard.ts | 3 +- shared-module/packages/common/src/bindings.ts | 1 + .../common/src/locales/en/main-frontend.json | 1 + .../common/src/locales/fi/main-frontend.json | 1 + 19 files changed, 172 insertions(+), 27 deletions(-) create mode 100644 services/headless-lms/migrations/20240913132004_add-join-code-to-course-instances.down.sql create mode 100644 services/headless-lms/migrations/20240913132004_add-join-code-to-course-instances.up.sql rename services/headless-lms/models/.sqlx/{query-a9e6bf7e1afec5191482ce2cfaca22c670d50759540e166270fa2881e5db1dc4.json => query-2d109f8e298dfd10b8f6f35d9375a175b1b78795d36cd486e18ca8d80f591cdf.json} (76%) rename services/headless-lms/models/.sqlx/{query-fb18b61c85778ef2bffb9dc1d388e1f5d1a7f53af4ffbe102a731c39a353afe8.json => query-4b07d43b9b4b925b2659e06521d797175420552b591ab92b80df6bb88c996bf6.json} (77%) create mode 100644 services/headless-lms/models/.sqlx/query-5f4303393e9944d6f985866d65646b7d27d42c453ed774a6afa34f955d085c3c.json rename services/headless-lms/models/.sqlx/{query-2327eff57553a33838e6dcdddca821ffd3cee0c0637332d1942eea2bac657834.json => query-6b9b3b2a77be5b4d9eea6bca8115f34113788a0500a36b6553c6a119807ec8c8.json} (83%) rename services/headless-lms/models/.sqlx/{query-7074164915eca8a60f6f1d36ba2dfaf2df3b8b430c01f15a37b4a8ec4fd62470.json => query-b9ebc7e3b77c2f52d67720ae5bcfb865831429e12d26bed1786210abb23598dd.json} (83%) rename services/headless-lms/models/.sqlx/{query-1e26900e33b2a6f1baafb787523f3539beb5c20be6a2a0857d6510e597217134.json => query-f16f9deef31c89ce215409477de2768266211b1612168108695b61f23dc7175f.json} (84%) rename services/headless-lms/models/.sqlx/{query-ad44e66896540a92d76d8b584ae7142955baf3641d83702179e92fe60f8e3836.json => query-fa6df49c2e89141f0f60ffa73fd39aecf88d4a09675adb64401c0cd2d51e2373.json} (87%) diff --git a/services/headless-lms/migrations/20240913132004_add-join-code-to-course-instances.down.sql b/services/headless-lms/migrations/20240913132004_add-join-code-to-course-instances.down.sql new file mode 100644 index 000000000000..d2be27b09c1b --- /dev/null +++ b/services/headless-lms/migrations/20240913132004_add-join-code-to-course-instances.down.sql @@ -0,0 +1 @@ +ALTER TABLE course_instances DROP COLUMN join_code; diff --git a/services/headless-lms/migrations/20240913132004_add-join-code-to-course-instances.up.sql b/services/headless-lms/migrations/20240913132004_add-join-code-to-course-instances.up.sql new file mode 100644 index 000000000000..0db5d6ac7955 --- /dev/null +++ b/services/headless-lms/migrations/20240913132004_add-join-code-to-course-instances.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE course_instances +ADD COLUMN join_code varchar(1024); +COMMENT ON COLUMN course_instances.join_code IS 'Regeneratable code that is used to join the course'; diff --git a/services/headless-lms/models/.sqlx/query-a9e6bf7e1afec5191482ce2cfaca22c670d50759540e166270fa2881e5db1dc4.json b/services/headless-lms/models/.sqlx/query-2d109f8e298dfd10b8f6f35d9375a175b1b78795d36cd486e18ca8d80f591cdf.json similarity index 76% rename from services/headless-lms/models/.sqlx/query-a9e6bf7e1afec5191482ce2cfaca22c670d50759540e166270fa2881e5db1dc4.json rename to services/headless-lms/models/.sqlx/query-2d109f8e298dfd10b8f6f35d9375a175b1b78795d36cd486e18ca8d80f591cdf.json index ce03478ffe4b..5b389ae4d63b 100644 --- a/services/headless-lms/models/.sqlx/query-a9e6bf7e1afec5191482ce2cfaca22c670d50759540e166270fa2881e5db1dc4.json +++ b/services/headless-lms/models/.sqlx/query-2d109f8e298dfd10b8f6f35d9375a175b1b78795d36cd486e18ca8d80f591cdf.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT i.id,\n i.created_at,\n i.updated_at,\n i.deleted_at,\n i.course_id,\n i.starts_at,\n i.ends_at,\n i.name,\n i.description,\n i.teacher_in_charge_name,\n i.teacher_in_charge_email,\n i.support_email\nFROM course_instances i\n JOIN course_instance_enrollments ie ON (i.id = ie.course_id)\nWHERE i.course_id = $1\n AND i.deleted_at IS NULL\n AND ie.user_id = $2\n AND ie.deleted_at IS NULL\nORDER BY ie.created_at DESC;\n ", + "query": "\nSELECT i.id,\n i.created_at,\n i.updated_at,\n i.deleted_at,\n i.course_id,\n i.starts_at,\n i.ends_at,\n i.name,\n i.description,\n i.teacher_in_charge_name,\n i.teacher_in_charge_email,\n i.support_email,\n i.join_code\nFROM course_instances i\n JOIN course_instance_enrollments ie ON (i.id = ie.course_id)\nWHERE i.course_id = $1\n AND i.deleted_at IS NULL\n AND ie.user_id = $2\n AND ie.deleted_at IS NULL\nORDER BY ie.created_at DESC;\n ", "describe": { "columns": [ { @@ -62,12 +62,17 @@ "ordinal": 11, "name": "support_email", "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { "Left": ["Uuid", "Uuid"] }, - "nullable": [false, false, false, true, false, true, true, true, true, false, false, true] + "nullable": [false, false, false, true, false, true, true, true, true, false, false, true, true] }, - "hash": "a9e6bf7e1afec5191482ce2cfaca22c670d50759540e166270fa2881e5db1dc4" + "hash": "2d109f8e298dfd10b8f6f35d9375a175b1b78795d36cd486e18ca8d80f591cdf" } diff --git a/services/headless-lms/models/.sqlx/query-fb18b61c85778ef2bffb9dc1d388e1f5d1a7f53af4ffbe102a731c39a353afe8.json b/services/headless-lms/models/.sqlx/query-4b07d43b9b4b925b2659e06521d797175420552b591ab92b80df6bb88c996bf6.json similarity index 77% rename from services/headless-lms/models/.sqlx/query-fb18b61c85778ef2bffb9dc1d388e1f5d1a7f53af4ffbe102a731c39a353afe8.json rename to services/headless-lms/models/.sqlx/query-4b07d43b9b4b925b2659e06521d797175420552b591ab92b80df6bb88c996bf6.json index fefb737988e0..30f618df15ac 100644 --- a/services/headless-lms/models/.sqlx/query-fb18b61c85778ef2bffb9dc1d388e1f5d1a7f53af4ffbe102a731c39a353afe8.json +++ b/services/headless-lms/models/.sqlx/query-4b07d43b9b4b925b2659e06521d797175420552b591ab92b80df6bb88c996bf6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT i.id,\n i.created_at,\n i.updated_at,\n i.deleted_at,\n i.course_id,\n i.starts_at,\n i.ends_at,\n i.name,\n i.description,\n i.teacher_in_charge_name,\n i.teacher_in_charge_email,\n i.support_email\nFROM user_course_settings ucs\n JOIN course_instances i ON (ucs.current_course_instance_id = i.id)\nWHERE ucs.user_id = $1\n AND ucs.current_course_id = $2\n AND ucs.deleted_at IS NULL;\n ", + "query": "\nSELECT i.id,\n i.created_at,\n i.updated_at,\n i.deleted_at,\n i.course_id,\n i.starts_at,\n i.ends_at,\n i.name,\n i.description,\n i.teacher_in_charge_name,\n i.teacher_in_charge_email,\n i.support_email,\n i.join_code\nFROM user_course_settings ucs\n JOIN course_instances i ON (ucs.current_course_instance_id = i.id)\nWHERE ucs.user_id = $1\n AND ucs.current_course_id = $2\n AND ucs.deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -62,12 +62,17 @@ "ordinal": 11, "name": "support_email", "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { "Left": ["Uuid", "Uuid"] }, - "nullable": [false, false, false, true, false, true, true, true, true, false, false, true] + "nullable": [false, false, false, true, false, true, true, true, true, false, false, true, true] }, - "hash": "fb18b61c85778ef2bffb9dc1d388e1f5d1a7f53af4ffbe102a731c39a353afe8" + "hash": "4b07d43b9b4b925b2659e06521d797175420552b591ab92b80df6bb88c996bf6" } diff --git a/services/headless-lms/models/.sqlx/query-5f4303393e9944d6f985866d65646b7d27d42c453ed774a6afa34f955d085c3c.json b/services/headless-lms/models/.sqlx/query-5f4303393e9944d6f985866d65646b7d27d42c453ed774a6afa34f955d085c3c.json new file mode 100644 index 000000000000..b0582ac5ae5b --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-5f4303393e9944d6f985866d65646b7d27d42c453ed774a6afa34f955d085c3c.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "\nUPDATE course_instances\nSET join_code = $2\nWHERE id = $1\n", + "describe": { + "columns": [], + "parameters": { + "Left": ["Uuid", "Varchar"] + }, + "nullable": [] + }, + "hash": "5f4303393e9944d6f985866d65646b7d27d42c453ed774a6afa34f955d085c3c" +} diff --git a/services/headless-lms/models/.sqlx/query-2327eff57553a33838e6dcdddca821ffd3cee0c0637332d1942eea2bac657834.json b/services/headless-lms/models/.sqlx/query-6b9b3b2a77be5b4d9eea6bca8115f34113788a0500a36b6553c6a119807ec8c8.json similarity index 83% rename from services/headless-lms/models/.sqlx/query-2327eff57553a33838e6dcdddca821ffd3cee0c0637332d1942eea2bac657834.json rename to services/headless-lms/models/.sqlx/query-6b9b3b2a77be5b4d9eea6bca8115f34113788a0500a36b6553c6a119807ec8c8.json index 377c63232ea9..4ea13c1f0c94 100644 --- a/services/headless-lms/models/.sqlx/query-2327eff57553a33838e6dcdddca821ffd3cee0c0637332d1942eea2bac657834.json +++ b/services/headless-lms/models/.sqlx/query-6b9b3b2a77be5b4d9eea6bca8115f34113788a0500a36b6553c6a119807ec8c8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n starts_at,\n ends_at,\n name,\n description,\n teacher_in_charge_name,\n teacher_in_charge_email,\n support_email\nFROM course_instances\nWHERE id = $1\n AND deleted_at IS NULL;\n ", + "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n starts_at,\n ends_at,\n name,\n description,\n teacher_in_charge_name,\n teacher_in_charge_email,\n support_email,\n join_code\nFROM course_instances\nWHERE id = $1\n AND deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -62,12 +62,17 @@ "ordinal": 11, "name": "support_email", "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { "Left": ["Uuid"] }, - "nullable": [false, false, false, true, false, true, true, true, true, false, false, true] + "nullable": [false, false, false, true, false, true, true, true, true, false, false, true, true] }, - "hash": "2327eff57553a33838e6dcdddca821ffd3cee0c0637332d1942eea2bac657834" + "hash": "6b9b3b2a77be5b4d9eea6bca8115f34113788a0500a36b6553c6a119807ec8c8" } diff --git a/services/headless-lms/models/.sqlx/query-b27922e66ed8dd5b05c89d85f50c8b9fb2620fac9a78fe0f99d7372a60c6ccd3.json b/services/headless-lms/models/.sqlx/query-b27922e66ed8dd5b05c89d85f50c8b9fb2620fac9a78fe0f99d7372a60c6ccd3.json index b028262fe6dd..33cb4788770b 100644 --- a/services/headless-lms/models/.sqlx/query-b27922e66ed8dd5b05c89d85f50c8b9fb2620fac9a78fe0f99d7372a60c6ccd3.json +++ b/services/headless-lms/models/.sqlx/query-b27922e66ed8dd5b05c89d85f50c8b9fb2620fac9a78fe0f99d7372a60c6ccd3.json @@ -62,12 +62,17 @@ "ordinal": 11, "name": "teacher_in_charge_email", "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { "Left": ["Uuid"] }, - "nullable": [false, false, false, true, false, true, true, true, true, true, false, false] + "nullable": [false, false, false, true, false, true, true, true, true, true, false, false, true] }, "hash": "b27922e66ed8dd5b05c89d85f50c8b9fb2620fac9a78fe0f99d7372a60c6ccd3" } diff --git a/services/headless-lms/models/.sqlx/query-7074164915eca8a60f6f1d36ba2dfaf2df3b8b430c01f15a37b4a8ec4fd62470.json b/services/headless-lms/models/.sqlx/query-b9ebc7e3b77c2f52d67720ae5bcfb865831429e12d26bed1786210abb23598dd.json similarity index 83% rename from services/headless-lms/models/.sqlx/query-7074164915eca8a60f6f1d36ba2dfaf2df3b8b430c01f15a37b4a8ec4fd62470.json rename to services/headless-lms/models/.sqlx/query-b9ebc7e3b77c2f52d67720ae5bcfb865831429e12d26bed1786210abb23598dd.json index c6e765af349d..e384e7f210ae 100644 --- a/services/headless-lms/models/.sqlx/query-7074164915eca8a60f6f1d36ba2dfaf2df3b8b430c01f15a37b4a8ec4fd62470.json +++ b/services/headless-lms/models/.sqlx/query-b9ebc7e3b77c2f52d67720ae5bcfb865831429e12d26bed1786210abb23598dd.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n starts_at,\n ends_at,\n name,\n description,\n teacher_in_charge_name,\n teacher_in_charge_email,\n support_email\nFROM course_instances\nWHERE course_id = $1\n AND deleted_at IS NULL;\n ", + "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n starts_at,\n ends_at,\n name,\n description,\n teacher_in_charge_name,\n teacher_in_charge_email,\n support_email,\n join_code\nFROM course_instances\nWHERE course_id = $1\n AND deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -62,12 +62,17 @@ "ordinal": 11, "name": "support_email", "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { "Left": ["Uuid"] }, - "nullable": [false, false, false, true, false, true, true, true, true, false, false, true] + "nullable": [false, false, false, true, false, true, true, true, true, false, false, true, true] }, - "hash": "7074164915eca8a60f6f1d36ba2dfaf2df3b8b430c01f15a37b4a8ec4fd62470" + "hash": "b9ebc7e3b77c2f52d67720ae5bcfb865831429e12d26bed1786210abb23598dd" } diff --git a/services/headless-lms/models/.sqlx/query-dd8c271c3a0901cc8c2fdecfbae5b052cb4c4c3b82e27817ae5a059685fa088b.json b/services/headless-lms/models/.sqlx/query-dd8c271c3a0901cc8c2fdecfbae5b052cb4c4c3b82e27817ae5a059685fa088b.json index 602a7189c655..8fcd8b3c7af4 100644 --- a/services/headless-lms/models/.sqlx/query-dd8c271c3a0901cc8c2fdecfbae5b052cb4c4c3b82e27817ae5a059685fa088b.json +++ b/services/headless-lms/models/.sqlx/query-dd8c271c3a0901cc8c2fdecfbae5b052cb4c4c3b82e27817ae5a059685fa088b.json @@ -62,12 +62,17 @@ "ordinal": 11, "name": "teacher_in_charge_email", "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { "Left": ["UuidArray"] }, - "nullable": [false, false, false, true, false, true, true, true, true, true, false, false] + "nullable": [false, false, false, true, false, true, true, true, true, true, false, false, true] }, "hash": "dd8c271c3a0901cc8c2fdecfbae5b052cb4c4c3b82e27817ae5a059685fa088b" } diff --git a/services/headless-lms/models/.sqlx/query-1e26900e33b2a6f1baafb787523f3539beb5c20be6a2a0857d6510e597217134.json b/services/headless-lms/models/.sqlx/query-f16f9deef31c89ce215409477de2768266211b1612168108695b61f23dc7175f.json similarity index 84% rename from services/headless-lms/models/.sqlx/query-1e26900e33b2a6f1baafb787523f3539beb5c20be6a2a0857d6510e597217134.json rename to services/headless-lms/models/.sqlx/query-f16f9deef31c89ce215409477de2768266211b1612168108695b61f23dc7175f.json index 97ff7d957b28..e7e3a766fa0a 100644 --- a/services/headless-lms/models/.sqlx/query-1e26900e33b2a6f1baafb787523f3539beb5c20be6a2a0857d6510e597217134.json +++ b/services/headless-lms/models/.sqlx/query-f16f9deef31c89ce215409477de2768266211b1612168108695b61f23dc7175f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n starts_at,\n ends_at,\n name,\n description,\n teacher_in_charge_name,\n teacher_in_charge_email,\n support_email\nFROM course_instances\nWHERE deleted_at IS NULL\n", + "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n starts_at,\n ends_at,\n name,\n description,\n teacher_in_charge_name,\n teacher_in_charge_email,\n support_email,\n join_code\nFROM course_instances\nWHERE deleted_at IS NULL\n", "describe": { "columns": [ { @@ -62,12 +62,17 @@ "ordinal": 11, "name": "support_email", "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { "Left": [] }, - "nullable": [false, false, false, true, false, true, true, true, true, false, false, true] + "nullable": [false, false, false, true, false, true, true, true, true, false, false, true, true] }, - "hash": "1e26900e33b2a6f1baafb787523f3539beb5c20be6a2a0857d6510e597217134" + "hash": "f16f9deef31c89ce215409477de2768266211b1612168108695b61f23dc7175f" } diff --git a/services/headless-lms/models/.sqlx/query-ad44e66896540a92d76d8b584ae7142955baf3641d83702179e92fe60f8e3836.json b/services/headless-lms/models/.sqlx/query-fa6df49c2e89141f0f60ffa73fd39aecf88d4a09675adb64401c0cd2d51e2373.json similarity index 87% rename from services/headless-lms/models/.sqlx/query-ad44e66896540a92d76d8b584ae7142955baf3641d83702179e92fe60f8e3836.json rename to services/headless-lms/models/.sqlx/query-fa6df49c2e89141f0f60ffa73fd39aecf88d4a09675adb64401c0cd2d51e2373.json index 6ad2adecdcab..861b119e8c1a 100644 --- a/services/headless-lms/models/.sqlx/query-ad44e66896540a92d76d8b584ae7142955baf3641d83702179e92fe60f8e3836.json +++ b/services/headless-lms/models/.sqlx/query-fa6df49c2e89141f0f60ffa73fd39aecf88d4a09675adb64401c0cd2d51e2373.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nINSERT INTO course_instances (\n id,\n course_id,\n name,\n description,\n teacher_in_charge_name,\n teacher_in_charge_email,\n support_email\n )\nVALUES ($1, $2, $3, $4, $5, $6, $7)\nRETURNING id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n starts_at,\n ends_at,\n name,\n description,\n teacher_in_charge_name,\n teacher_in_charge_email,\n support_email\n", + "query": "\nINSERT INTO course_instances (\n id,\n course_id,\n name,\n description,\n teacher_in_charge_name,\n teacher_in_charge_email,\n support_email\n )\nVALUES ($1, $2, $3, $4, $5, $6, $7)\nRETURNING id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n starts_at,\n ends_at,\n name,\n description,\n teacher_in_charge_name,\n teacher_in_charge_email,\n support_email,\n join_code\n", "describe": { "columns": [ { @@ -62,12 +62,17 @@ "ordinal": 11, "name": "support_email", "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { "Left": ["Uuid", "Uuid", "Varchar", "Varchar", "Varchar", "Varchar", "Varchar"] }, - "nullable": [false, false, false, true, false, true, true, true, true, false, false, true] + "nullable": [false, false, false, true, false, true, true, true, true, false, false, true, true] }, - "hash": "ad44e66896540a92d76d8b584ae7142955baf3641d83702179e92fe60f8e3836" + "hash": "fa6df49c2e89141f0f60ffa73fd39aecf88d4a09675adb64401c0cd2d51e2373" } diff --git a/services/headless-lms/models/src/course_instances.rs b/services/headless-lms/models/src/course_instances.rs index 0f7208fcfcc4..1b7d7f1c6562 100644 --- a/services/headless-lms/models/src/course_instances.rs +++ b/services/headless-lms/models/src/course_instances.rs @@ -24,6 +24,7 @@ pub struct CourseInstance { pub teacher_in_charge_name: String, pub teacher_in_charge_email: String, pub support_email: Option, + pub join_code: Option, } impl CourseInstance { @@ -85,7 +86,8 @@ RETURNING id, description, teacher_in_charge_name, teacher_in_charge_email, - support_email + support_email, + join_code "#, pkey_policy.into_uuid(), new_course_instance.course_id, @@ -118,7 +120,8 @@ SELECT id, description, teacher_in_charge_name, teacher_in_charge_email, - support_email + support_email, + join_code FROM course_instances WHERE id = $1 AND deleted_at IS NULL; @@ -187,7 +190,8 @@ SELECT i.id, i.description, i.teacher_in_charge_name, i.teacher_in_charge_email, - i.support_email + i.support_email, + i.join_code FROM user_course_settings ucs JOIN course_instances i ON (ucs.current_course_instance_id = i.id) WHERE ucs.user_id = $1 @@ -221,7 +225,8 @@ SELECT i.id, i.description, i.teacher_in_charge_name, i.teacher_in_charge_email, - i.support_email + i.support_email, + i.join_code FROM course_instances i JOIN course_instance_enrollments ie ON (i.id = ie.course_id) WHERE i.course_id = $1 @@ -253,7 +258,8 @@ SELECT id, description, teacher_in_charge_name, teacher_in_charge_email, - support_email + support_email, + join_code FROM course_instances WHERE deleted_at IS NULL "# @@ -281,7 +287,8 @@ SELECT id, description, teacher_in_charge_name, teacher_in_charge_email, - support_email + support_email, + join_code FROM course_instances WHERE course_id = $1 AND deleted_at IS NULL; @@ -852,6 +859,25 @@ WHERE ce.course_instance_id = $1 Ok(res.map(|r| r.student_duration_seconds).unwrap_or_default()) } +pub async fn generate_join_code_for_course_instance( + conn: &mut PgConnection, + course_instance_id: Uuid, + join_code: String, +) -> ModelResult<()> { + sqlx::query!( + " +UPDATE course_instances +SET join_code = $2 +WHERE id = $1 +", + course_instance_id, + join_code + ) + .execute(conn) + .await?; + Ok(()) +} + #[cfg(test)] mod test { use super::*; diff --git a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs index f8b4d27333b4..4fc80b37855e 100644 --- a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs +++ b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs @@ -443,6 +443,28 @@ async fn get_user_progress_for_course_instance( token.authorized_ok(web::Json(user_course_instance_progress)) } +/** + POST /api/v0/main-frontend/course-instance/:course_instance_id/generate-join-code - Generates a code that is used as a part of URL to join course +*/ +#[instrument(skip(pool))] +async fn generate_join_code_for_course_instance( + id: web::Path, + pool: web::Data, + user: AuthUser, +) -> ControllerResult { + let mut conn = pool.acquire().await?; + let token = authorize( + &mut conn, + Act::Edit, + Some(user.id), + Res::CourseInstance(*id), + ) + .await?; + let code = Uuid::new_v4().to_string(); + + models::course_instances::generate_join_code_for_course_instance(&mut conn, *id, code).await?; + token.authorized_ok(HttpResponse::Ok().finish()) +} /** Add a route for each controller in this module. @@ -502,5 +524,9 @@ pub fn _add_routes(cfg: &mut ServiceConfig) { .route( "/{course_instance_id}/default-certificate-configurations", web::get().to(certificate_configurations), + ) + .route( + "/{course_instance_id}/generate-join-code", + web::post().to(generate_join_code_for_course_instance), ); } diff --git a/services/main-frontend/src/pages/manage/course-instances/[id]/index.tsx b/services/main-frontend/src/pages/manage/course-instances/[id]/index.tsx index 20556ced636d..a2323b5d9c38 100644 --- a/services/main-frontend/src/pages/manage/course-instances/[id]/index.tsx +++ b/services/main-frontend/src/pages/manage/course-instances/[id]/index.tsx @@ -10,6 +10,7 @@ import { deleteCourseInstance, editCourseInstance, fetchCourseInstance, + generateJoinCourseLinkForCourseInstance, } from "../../../../services/backend/course-instances" import { CourseInstanceForm } from "@/shared-module/common/bindings" @@ -69,6 +70,21 @@ const ManageCourseInstances: React.FC { + await generateJoinCourseLinkForCourseInstance(courseInstanceId) + }, + { + notify: true, + method: "POST", + }, + { + onSuccess: () => { + getCourseInstances.refetch() + }, + }, + ) + let instanceInfo if (getCourseInstances.isSuccess) { const data = getCourseInstances.data @@ -131,6 +147,17 @@ const ManageCourseInstances: React.FC{t("support-email-description")} {schedule} + +
{data.join_code}
+
+ +
diff --git a/services/main-frontend/src/services/backend/course-instances.ts b/services/main-frontend/src/services/backend/course-instances.ts index 05c0b4405804..b9335d585f25 100644 --- a/services/main-frontend/src/services/backend/course-instances.ts +++ b/services/main-frontend/src/services/backend/course-instances.ts @@ -146,3 +146,9 @@ export const fetchDefaultCertificateConfigurations = async ( ) return validateResponse(res, isArray(isCertificateConfigurationAndRequirements)) } + +export const generateJoinCourseLinkForCourseInstance = async ( + courseInstanceId: string, +): Promise => { + await mainFrontendClient.post(`/course-instances/${courseInstanceId}/generate-join-code`) +} diff --git a/shared-module/packages/common/src/bindings.guard.ts b/shared-module/packages/common/src/bindings.guard.ts index 5cc45b242a23..35e777c4a16e 100644 --- a/shared-module/packages/common/src/bindings.guard.ts +++ b/shared-module/packages/common/src/bindings.guard.ts @@ -793,7 +793,8 @@ export function isCourseInstance(obj: unknown): obj is CourseInstance { (typedObj["description"] === null || typeof typedObj["description"] === "string") && typeof typedObj["teacher_in_charge_name"] === "string" && typeof typedObj["teacher_in_charge_email"] === "string" && - (typedObj["support_email"] === null || typeof typedObj["support_email"] === "string") + (typedObj["support_email"] === null || typeof typedObj["support_email"] === "string") && + (typedObj["join_code"] === null || typeof typedObj["join_code"] === "string") ) } diff --git a/shared-module/packages/common/src/bindings.ts b/shared-module/packages/common/src/bindings.ts index 66e9796d0b3f..320807366b64 100644 --- a/shared-module/packages/common/src/bindings.ts +++ b/shared-module/packages/common/src/bindings.ts @@ -333,6 +333,7 @@ export interface CourseInstance { teacher_in_charge_name: string teacher_in_charge_email: string support_email: string | null + join_code: string | null } export interface CourseInstanceForm { diff --git a/shared-module/packages/common/src/locales/en/main-frontend.json b/shared-module/packages/common/src/locales/en/main-frontend.json index 47a10edaaffb..b326c39aa7b7 100644 --- a/shared-module/packages/common/src/locales/en/main-frontend.json +++ b/shared-module/packages/common/src/locales/en/main-frontend.json @@ -50,6 +50,7 @@ "button-text-edit-page-details": "Edit page details", "button-text-flag-as-plagiarism": "Flag as plagiarism", "button-text-full-points": "Full points", + "button-text-generate-join-course-link": "Generate join course link", "button-text-give-custom-points": "Give custom points", "button-text-import": "Import", "button-text-move-down": "Move down", diff --git a/shared-module/packages/common/src/locales/fi/main-frontend.json b/shared-module/packages/common/src/locales/fi/main-frontend.json index 30e59f35fb7f..1d8a96a1c406 100644 --- a/shared-module/packages/common/src/locales/fi/main-frontend.json +++ b/shared-module/packages/common/src/locales/fi/main-frontend.json @@ -50,6 +50,7 @@ "button-text-edit-page-details": "Muokkaa sivun tietoja", "button-text-flag-as-plagiarism": "Merkitse plagioinniksi", "button-text-full-points": "Täydet pisteet", + "button-text-generate-join-course-link": "Luo liity kurssille -linkki", "button-text-give-custom-points": "Anna mukautetut pisteet", "button-text-import": "Tuo", "button-text-move-down": "Siirrä alas", From b3f48db485b1dc0c635659e95e96f00939bd3310 Mon Sep 17 00:00:00 2001 From: Maija Y Date: Mon, 16 Sep 2024 15:59:44 +0300 Subject: [PATCH 2/9] Course accesses table --- .../20240916122556_course_accesses.down.sql | 1 + .../20240916122556_course_accesses.up.sql | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 services/headless-lms/migrations/20240916122556_course_accesses.down.sql create mode 100644 services/headless-lms/migrations/20240916122556_course_accesses.up.sql diff --git a/services/headless-lms/migrations/20240916122556_course_accesses.down.sql b/services/headless-lms/migrations/20240916122556_course_accesses.down.sql new file mode 100644 index 000000000000..1eee1cc555a8 --- /dev/null +++ b/services/headless-lms/migrations/20240916122556_course_accesses.down.sql @@ -0,0 +1 @@ +DROP TABLE course_accesses; diff --git a/services/headless-lms/migrations/20240916122556_course_accesses.up.sql b/services/headless-lms/migrations/20240916122556_course_accesses.up.sql new file mode 100644 index 000000000000..a3ffd54ed9eb --- /dev/null +++ b/services/headless-lms/migrations/20240916122556_course_accesses.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE course_accesses ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + course_instance_id UUID NOT NULL REFERENCES course_instances(id), + user_id UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMP WITH TIME ZONE +); +COMMENT ON TABLE course_accesses IS 'This table is used to check if user has access to a course that is only joinable by a special code'; +COMMENT ON COLUMN course_accesses.id IS 'A unique, stable identifier for the record.'; +COMMENT ON COLUMN course_accesses.course_instance_id IS 'Course instance that the user has access to.'; +COMMENT ON COLUMN course_accesses.user_id IS 'User who has the access to the course.'; +COMMENT ON COLUMN course_accesses.created_at IS 'Timestamp of when the record was created'; +COMMENT ON COLUMN course_accesses.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; From ba2235634c27dd3014635ed84c6d2c430894f302 Mon Sep 17 00:00:00 2001 From: Maija Y Date: Wed, 18 Sep 2024 15:11:54 +0300 Subject: [PATCH 3/9] Join page for joining the course via join code --- ...4_add-join-code-to-course-instances.up.sql | 2 +- .../20240916122556_course_accesses.up.sql | 18 ++-- ...5679c5a19f27cfbb0671983e0386113d71efc.json | 18 ++++ ...e6625a2e23244efb11434a60e4210ee311c5d.json | 78 ++++++++++++++++++ .../models/src/course_instances.rs | 31 +++++++ .../headless-lms/models/src/join_code_uses.rs | 32 ++++++++ services/headless-lms/models/src/lib.rs | 1 + .../main_frontend/course_instances.rs | 41 +++++++++- .../src/controllers/main_frontend/courses.rs | 23 ++++++ services/main-frontend/src/pages/join.tsx | 82 +++++++++++++++++++ .../manage/course-instances/[id]/index.tsx | 2 +- .../src/services/backend/course-instances.ts | 9 +- .../src/services/backend/courses.ts | 7 ++ .../common/src/locales/en/main-frontend.json | 2 + .../common/src/locales/fi/main-frontend.json | 2 + 15 files changed, 337 insertions(+), 11 deletions(-) create mode 100644 services/headless-lms/models/.sqlx/query-4ea55478c8ea88545ab1823c9275679c5a19f27cfbb0671983e0386113d71efc.json create mode 100644 services/headless-lms/models/.sqlx/query-8e7e18dd643612d66463b290887e6625a2e23244efb11434a60e4210ee311c5d.json create mode 100644 services/headless-lms/models/src/join_code_uses.rs create mode 100644 services/main-frontend/src/pages/join.tsx diff --git a/services/headless-lms/migrations/20240913132004_add-join-code-to-course-instances.up.sql b/services/headless-lms/migrations/20240913132004_add-join-code-to-course-instances.up.sql index 0db5d6ac7955..ee4c6330d175 100644 --- a/services/headless-lms/migrations/20240913132004_add-join-code-to-course-instances.up.sql +++ b/services/headless-lms/migrations/20240913132004_add-join-code-to-course-instances.up.sql @@ -1,3 +1,3 @@ ALTER TABLE course_instances ADD COLUMN join_code varchar(1024); -COMMENT ON COLUMN course_instances.join_code IS 'Regeneratable code that is used to join the course'; +COMMENT ON COLUMN course_instances.join_code IS 'Regeneratable code that is used to join the course. If a user uses the code they will be added to join_code_uses -table to get access to the course'; diff --git a/services/headless-lms/migrations/20240916122556_course_accesses.up.sql b/services/headless-lms/migrations/20240916122556_course_accesses.up.sql index a3ffd54ed9eb..b4612ae7a280 100644 --- a/services/headless-lms/migrations/20240916122556_course_accesses.up.sql +++ b/services/headless-lms/migrations/20240916122556_course_accesses.up.sql @@ -1,13 +1,17 @@ -CREATE TABLE course_accesses ( +CREATE TABLE join_code_uses ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, course_instance_id UUID NOT NULL REFERENCES course_instances(id), user_id UUID NOT NULL REFERENCES users(id), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), deleted_at TIMESTAMP WITH TIME ZONE ); -COMMENT ON TABLE course_accesses IS 'This table is used to check if user has access to a course that is only joinable by a special code'; -COMMENT ON COLUMN course_accesses.id IS 'A unique, stable identifier for the record.'; -COMMENT ON COLUMN course_accesses.course_instance_id IS 'Course instance that the user has access to.'; -COMMENT ON COLUMN course_accesses.user_id IS 'User who has the access to the course.'; -COMMENT ON COLUMN course_accesses.created_at IS 'Timestamp of when the record was created'; -COMMENT ON COLUMN course_accesses.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; +CREATE TRIGGER set_timestamp BEFORE +UPDATE ON join_code_uses FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); +COMMENT ON TABLE join_code_uses IS 'This table is used to check if user has access to a course that is only joinable by a join code. The join code is located in course_instances table'; +COMMENT ON COLUMN join_code_uses.id IS 'A unique, stable identifier for the record.'; +COMMENT ON COLUMN join_code_uses.course_instance_id IS 'Course instance that the user has access to.'; +COMMENT ON COLUMN join_code_uses.user_id IS 'User who has the access to the course.'; +COMMENT ON COLUMN join_code_uses.created_at IS 'Timestamp of when the record was created'; +COMMENT ON COLUMN join_code_uses.updated_at IS 'Timestamp when the record was last updated. The field is updated automatically by the set_timestamp trigger.'; +COMMENT ON COLUMN join_code_uses.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; diff --git a/services/headless-lms/models/.sqlx/query-4ea55478c8ea88545ab1823c9275679c5a19f27cfbb0671983e0386113d71efc.json b/services/headless-lms/models/.sqlx/query-4ea55478c8ea88545ab1823c9275679c5a19f27cfbb0671983e0386113d71efc.json new file mode 100644 index 000000000000..23f9199e4e75 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-4ea55478c8ea88545ab1823c9275679c5a19f27cfbb0671983e0386113d71efc.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\nINSERT INTO join_code_uses (id, user_id, course_instance_id)\nVALUES ($1, $2, $3)\nRETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": ["Uuid", "Uuid", "Uuid"] + }, + "nullable": [false] + }, + "hash": "4ea55478c8ea88545ab1823c9275679c5a19f27cfbb0671983e0386113d71efc" +} diff --git a/services/headless-lms/models/.sqlx/query-8e7e18dd643612d66463b290887e6625a2e23244efb11434a60e4210ee311c5d.json b/services/headless-lms/models/.sqlx/query-8e7e18dd643612d66463b290887e6625a2e23244efb11434a60e4210ee311c5d.json new file mode 100644 index 000000000000..0ee4abaaa7f3 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-8e7e18dd643612d66463b290887e6625a2e23244efb11434a60e4210ee311c5d.json @@ -0,0 +1,78 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n starts_at,\n ends_at,\n name,\n description,\n teacher_in_charge_name,\n teacher_in_charge_email,\n support_email,\n join_code\nFROM course_instances\nWHERE join_code = $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": "starts_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "ends_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "teacher_in_charge_name", + "type_info": "Varchar" + }, + { + "ordinal": 10, + "name": "teacher_in_charge_email", + "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "support_email", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "join_code", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": ["Text"] + }, + "nullable": [false, false, false, true, false, true, true, true, true, false, false, true, true] + }, + "hash": "8e7e18dd643612d66463b290887e6625a2e23244efb11434a60e4210ee311c5d" +} diff --git a/services/headless-lms/models/src/course_instances.rs b/services/headless-lms/models/src/course_instances.rs index 1b7d7f1c6562..830ee0e3c649 100644 --- a/services/headless-lms/models/src/course_instances.rs +++ b/services/headless-lms/models/src/course_instances.rs @@ -878,6 +878,37 @@ WHERE id = $1 Ok(()) } +pub async fn get_course_instance_with_join_code( + conn: &mut PgConnection, + join_code: String, +) -> ModelResult { + let course_instance = sqlx::query_as!( + CourseInstance, + r#" +SELECT id, + created_at, + updated_at, + deleted_at, + course_id, + starts_at, + ends_at, + name, + description, + teacher_in_charge_name, + teacher_in_charge_email, + support_email, + join_code +FROM course_instances +WHERE join_code = $1 + AND deleted_at IS NULL; + "#, + join_code, + ) + .fetch_one(conn) + .await?; + Ok(course_instance) +} + #[cfg(test)] mod test { use super::*; diff --git a/services/headless-lms/models/src/join_code_uses.rs b/services/headless-lms/models/src/join_code_uses.rs new file mode 100644 index 000000000000..ce66d50db2b9 --- /dev/null +++ b/services/headless-lms/models/src/join_code_uses.rs @@ -0,0 +1,32 @@ +use crate::prelude::*; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct JoinCodeUses { + pub id: Uuid, + pub user_id: Uuid, + pub course_instance_id: Uuid, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option>, +} + +pub async fn insert( + conn: &mut PgConnection, + pkey_policy: PKeyPolicy, + user_id: Uuid, + course_instance_id: Uuid, +) -> ModelResult { + let res = sqlx::query!( + " +INSERT INTO join_code_uses (id, user_id, course_instance_id) +VALUES ($1, $2, $3) +RETURNING id + ", + pkey_policy.into_uuid(), + user_id, + course_instance_id + ) + .fetch_one(conn) + .await?; + Ok(res.id) +} diff --git a/services/headless-lms/models/src/lib.rs b/services/headless-lms/models/src/lib.rs index 6bd9ba40e598..7956dc092ea4 100644 --- a/services/headless-lms/models/src/lib.rs +++ b/services/headless-lms/models/src/lib.rs @@ -43,6 +43,7 @@ pub mod feedback; pub mod file_uploads; pub mod generated_certificates; pub mod glossary; +pub mod join_code_uses; pub mod library; pub mod material_references; pub mod offered_answers_to_peer_review_temporary; diff --git a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs index 4fc80b37855e..97b4f84bd413 100644 --- a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs +++ b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs @@ -17,6 +17,7 @@ use models::{ }, user_exercise_states::UserCourseInstanceProgress, }; +use rand::Rng; use crate::{ domain::csv_export::{ @@ -460,11 +461,45 @@ async fn generate_join_code_for_course_instance( Res::CourseInstance(*id), ) .await?; - let code = Uuid::new_v4().to_string(); + + const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\ + abcdefghjkmnpqrstuvwxyz"; + const PASSWORD_LEN: usize = 64; + let mut rng = rand::thread_rng(); + + let code: String = (0..PASSWORD_LEN) + .map(|_| { + let idx = rng.gen_range(0..CHARSET.len()); + CHARSET[idx] as char + }) + .collect(); models::course_instances::generate_join_code_for_course_instance(&mut conn, *id, code).await?; token.authorized_ok(HttpResponse::Ok().finish()) } + +/** +POST /course_instances/:course_instance_id/join-course-with-join-code - Adds the user to join_code_uses so the user gets access to the course +*/ +#[instrument(skip(pool))] +async fn add_user_to_course_with_join_code( + course_instance_id: web::Path, + user: AuthUser, + pool: web::Data, +) -> ControllerResult> { + let mut conn = pool.acquire().await?; + let token = skip_authorize(); + + let joined = models::join_code_uses::insert( + &mut conn, + PKeyPolicy::Generate, + user.id, + *course_instance_id, + ) + .await?; + token.authorized_ok(web::Json(joined)) +} + /** Add a route for each controller in this module. @@ -528,5 +563,9 @@ pub fn _add_routes(cfg: &mut ServiceConfig) { .route( "/{course_instance_id}/generate-join-code", web::post().to(generate_join_code_for_course_instance), + ) + .route( + "/{course_instance_id}/join-course-with-join-code", + web::post().to(add_user_to_course_with_join_code), ); } diff --git a/services/headless-lms/server/src/controllers/main_frontend/courses.rs b/services/headless-lms/server/src/controllers/main_frontend/courses.rs index 4a6a56c7783b..95b86afb054e 100644 --- a/services/headless-lms/server/src/controllers/main_frontend/courses.rs +++ b/services/headless-lms/server/src/controllers/main_frontend/courses.rs @@ -1449,6 +1449,25 @@ async fn teacher_approve_suspected_cheater( token.authorized_ok(web::Json(())) } +/** +GET /courses/join/:join_code - Gets the course instance related to join code +*/ +#[instrument(skip(pool))] +async fn get_course_instance_with_join_code( + join_code: web::Path, + user: AuthUser, + pool: web::Data, +) -> ControllerResult> { + let mut conn = pool.acquire().await?; + let token = skip_authorize(); + let course_instance = models::course_instances::get_course_instance_with_join_code( + &mut conn, + join_code.to_string(), + ) + .await?; + + token.authorized_ok(web::Json(course_instance)) +} /** Add a route for each controller in this module. @@ -1618,5 +1637,9 @@ pub fn _add_routes(cfg: &mut ServiceConfig) { .route( "/{course_id}/teacher-reset-course-progress-for-everyone", web::delete().to(teacher_reset_course_progress_for_everyone), + ) + .route( + "/join/{join_code}", + web::get().to(get_course_instance_with_join_code), ); } diff --git a/services/main-frontend/src/pages/join.tsx b/services/main-frontend/src/pages/join.tsx new file mode 100644 index 000000000000..ca6fe39b9ca7 --- /dev/null +++ b/services/main-frontend/src/pages/join.tsx @@ -0,0 +1,82 @@ +import { useQuery } from "@tanstack/react-query" +import { useTranslation } from "react-i18next" + +import { addUserToCourseWithJoinCode } from "@/services/backend/course-instances" +import { + fetchCourseInstanceWithJoinCode, + getCourseBreadCrumbInfo, +} from "@/services/backend/courses" +import Button from "@/shared-module/common/components/Button" +import ErrorBanner from "@/shared-module/common/components/ErrorBanner" +import Spinner from "@/shared-module/common/components/Spinner" +import useQueryParameter from "@/shared-module/common/hooks/useQueryParameter" +import useToastMutation from "@/shared-module/common/hooks/useToastMutation" + +const JoinCoursePage: React.FC> = () => { + const { t } = useTranslation() + const joinCode = useQueryParameter("code") + + const courseInstance = useQuery({ + queryKey: [`/course-instances/join/${joinCode}/`, joinCode], + queryFn: () => fetchCourseInstanceWithJoinCode(joinCode ?? ""), + }) + + const courseId = courseInstance.data?.course_id + + const courseBreadcrumbs = useQuery({ + queryKey: [`/courses/${courseId}/breadcrumb-info`, courseId], + queryFn: () => getCourseBreadCrumbInfo(courseId ?? ""), + enabled: !!courseId, + }) + + const handleRedirectMutation = useToastMutation( + async (courseInstanceId: string) => { + await addUserToCourseWithJoinCode(courseInstanceId) + }, + { + notify: true, + method: "POST", + }, + { + onSuccess: () => { + if (courseBreadcrumbs.isSuccess) { + // eslint-disable-next-line i18next/no-literal-string + location.href = `/org/${courseBreadcrumbs.data.organization_slug}/courses/${courseBreadcrumbs.data?.course_slug}` + } + }, + }, + ) + + const handleReturn = () => { + if (courseBreadcrumbs.isSuccess) { + location.href = `/` + } + } + return ( +
+ {courseInstance.isError && ( + + )} + {courseInstance.isPending && } + {courseInstance.isSuccess && courseBreadcrumbs.isSuccess && ( +
+

{courseBreadcrumbs.data?.course_name}

+ +
{t("do-you-want-to-join-this-course")}
+ + +
+ )} +
+ ) +} + +export default JoinCoursePage diff --git a/services/main-frontend/src/pages/manage/course-instances/[id]/index.tsx b/services/main-frontend/src/pages/manage/course-instances/[id]/index.tsx index a2323b5d9c38..4c57da18a911 100644 --- a/services/main-frontend/src/pages/manage/course-instances/[id]/index.tsx +++ b/services/main-frontend/src/pages/manage/course-instances/[id]/index.tsx @@ -148,7 +148,7 @@ const ManageCourseInstances: React.FC{t("support-email-description")} {schedule} -
{data.join_code}
+ {data.join_code}
+ {course.is_joinable_by_code_only && ( +
+ {/*eslint-disable-next-line i18next/no-literal-string */} + {`/join?code=${course.join_code}`} +
+ +
+
+ )} ) } diff --git a/services/main-frontend/src/components/page-specific/manage/courses/id/index/UpdateCourseForm.tsx b/services/main-frontend/src/components/page-specific/manage/courses/id/index/UpdateCourseForm.tsx index c47d36442f83..2bda738fb86c 100644 --- a/services/main-frontend/src/components/page-specific/manage/courses/id/index/UpdateCourseForm.tsx +++ b/services/main-frontend/src/components/page-specific/manage/courses/id/index/UpdateCourseForm.tsx @@ -32,7 +32,9 @@ const UpdateCourseForm: React.FC> const [draftStatus, setDraftStatus] = useState(course.is_draft) const [testStatus, setTestStatus] = useState(course.is_test_mode) const [isUnlisted, setIsUnlisted] = useState(course.is_unlisted) - const [joinableByCodeOnlyStatus, setjoinableByCodeOnlyStatus] = useState(course.is_unlisted) + const [joinableByCodeOnlyStatus, setjoinableByCodeOnlyStatus] = useState( + course.is_joinable_by_code_only, + ) const [canAddChatbot, setCanAddChatbot] = useState(course.can_add_chatbot) @@ -92,15 +94,7 @@ const UpdateCourseForm: React.FC> checked={draftStatus} /> - - { - setjoinableByCodeOnlyStatus(!joinableByCodeOnlyStatus) - }} - checked={joinableByCodeOnlyStatus} - /> - + {!draftStatus && ( > checked={canAddChatbot} /> + + { + setjoinableByCodeOnlyStatus(!joinableByCodeOnlyStatus) + }} + checked={joinableByCodeOnlyStatus} + /> +
diff --git a/services/main-frontend/src/pages/join.tsx b/services/main-frontend/src/pages/join.tsx index ca6fe39b9ca7..b5e4d6e6df7a 100644 --- a/services/main-frontend/src/pages/join.tsx +++ b/services/main-frontend/src/pages/join.tsx @@ -1,9 +1,9 @@ import { useQuery } from "@tanstack/react-query" import { useTranslation } from "react-i18next" -import { addUserToCourseWithJoinCode } from "@/services/backend/course-instances" import { - fetchCourseInstanceWithJoinCode, + addUserToCourseWithJoinCode, + fetchCourseWithJoinCode, getCourseBreadCrumbInfo, } from "@/services/backend/courses" import Button from "@/shared-module/common/components/Button" @@ -16,12 +16,12 @@ const JoinCoursePage: React.FC> = () => { const { t } = useTranslation() const joinCode = useQueryParameter("code") - const courseInstance = useQuery({ - queryKey: [`/course-instances/join/${joinCode}/`, joinCode], - queryFn: () => fetchCourseInstanceWithJoinCode(joinCode ?? ""), + const course = useQuery({ + queryKey: [`/courses/join/${joinCode}/`, joinCode], + queryFn: () => fetchCourseWithJoinCode(joinCode ?? ""), }) - const courseId = courseInstance.data?.course_id + const courseId = course.data?.id const courseBreadcrumbs = useQuery({ queryKey: [`/courses/${courseId}/breadcrumb-info`, courseId], @@ -30,8 +30,8 @@ const JoinCoursePage: React.FC> = () => { }) const handleRedirectMutation = useToastMutation( - async (courseInstanceId: string) => { - await addUserToCourseWithJoinCode(courseInstanceId) + async (courseId: string) => { + await addUserToCourseWithJoinCode(courseId) }, { notify: true, @@ -39,6 +39,7 @@ const JoinCoursePage: React.FC> = () => { }, { onSuccess: () => { + courseBreadcrumbs.refetch() if (courseBreadcrumbs.isSuccess) { // eslint-disable-next-line i18next/no-literal-string location.href = `/org/${courseBreadcrumbs.data.organization_slug}/courses/${courseBreadcrumbs.data?.course_slug}` @@ -54,19 +55,17 @@ const JoinCoursePage: React.FC> = () => { } return (
- {courseInstance.isError && ( - - )} - {courseInstance.isPending && } - {courseInstance.isSuccess && courseBreadcrumbs.isSuccess && ( + {course.isError && } + {course.isPending && } + {course.isSuccess && (
-

{courseBreadcrumbs.data?.course_name}

+

{course.data.name}

{t("do-you-want-to-join-this-course")}
diff --git a/services/main-frontend/src/pages/manage/course-instances/[id]/index.tsx b/services/main-frontend/src/pages/manage/course-instances/[id]/index.tsx index 4c57da18a911..20556ced636d 100644 --- a/services/main-frontend/src/pages/manage/course-instances/[id]/index.tsx +++ b/services/main-frontend/src/pages/manage/course-instances/[id]/index.tsx @@ -10,7 +10,6 @@ import { deleteCourseInstance, editCourseInstance, fetchCourseInstance, - generateJoinCourseLinkForCourseInstance, } from "../../../../services/backend/course-instances" import { CourseInstanceForm } from "@/shared-module/common/bindings" @@ -70,21 +69,6 @@ const ManageCourseInstances: React.FC { - await generateJoinCourseLinkForCourseInstance(courseInstanceId) - }, - { - notify: true, - method: "POST", - }, - { - onSuccess: () => { - getCourseInstances.refetch() - }, - }, - ) - let instanceInfo if (getCourseInstances.isSuccess) { const data = getCourseInstances.data @@ -147,17 +131,6 @@ const ManageCourseInstances: React.FC{t("support-email-description")}
{schedule} - - {data.join_code} -
- -
diff --git a/services/main-frontend/src/services/backend/course-instances.ts b/services/main-frontend/src/services/backend/course-instances.ts index 4b31ef389404..05c0b4405804 100644 --- a/services/main-frontend/src/services/backend/course-instances.ts +++ b/services/main-frontend/src/services/backend/course-instances.ts @@ -1,4 +1,4 @@ -import { isBoolean, isString } from "lodash" +import { isBoolean } from "lodash" import { mainFrontendClient } from "../mainFrontendClient" @@ -146,16 +146,3 @@ export const fetchDefaultCertificateConfigurations = async ( ) return validateResponse(res, isArray(isCertificateConfigurationAndRequirements)) } - -export const generateJoinCourseLinkForCourseInstance = async ( - courseInstanceId: string, -): Promise => { - await mainFrontendClient.post(`/course-instances/${courseInstanceId}/generate-join-code`) -} - -export const addUserToCourseWithJoinCode = async (courseInstanceId: string): Promise => { - const response = await mainFrontendClient.post( - `/course-instances/${courseInstanceId}/join-course-with-join-code`, - ) - return validateResponse(response, isString) -} diff --git a/services/main-frontend/src/services/backend/courses.ts b/services/main-frontend/src/services/backend/courses.ts index 6bf344db6353..bac2c4e318e7 100644 --- a/services/main-frontend/src/services/backend/courses.ts +++ b/services/main-frontend/src/services/backend/courses.ts @@ -308,9 +308,16 @@ export const approveSuspectedCheaters = async (courseId: string, id: string): Pr await mainFrontendClient.post(`/courses/${courseId}/suspected-cheaters/approve/${id}`) } -export const fetchCourseInstanceWithJoinCode = async ( - joinCode: string, -): Promise => { +export const fetchCourseWithJoinCode = async (joinCode: string): Promise => { const response = await mainFrontendClient.get(`/courses/join/${joinCode}`) - return validateResponse(response, isCourseInstance) + return validateResponse(response, isCourse) +} + +export const addUserToCourseWithJoinCode = async (courseId: string): Promise => { + const response = await mainFrontendClient.post(`/courses/${courseId}/join-course-with-join-code`) + return validateResponse(response, isString) +} + +export const setJoinCourseLinkForCourse = async (courseId: string): Promise => { + await mainFrontendClient.post(`/courses/${courseId}/set-join-code`) } diff --git a/shared-module/packages/common/src/bindings.guard.ts b/shared-module/packages/common/src/bindings.guard.ts index 87d11d7ab313..0e3214544c58 100644 --- a/shared-module/packages/common/src/bindings.guard.ts +++ b/shared-module/packages/common/src/bindings.guard.ts @@ -793,8 +793,7 @@ export function isCourseInstance(obj: unknown): obj is CourseInstance { (typedObj["description"] === null || typeof typedObj["description"] === "string") && typeof typedObj["teacher_in_charge_name"] === "string" && typeof typedObj["teacher_in_charge_email"] === "string" && - (typedObj["support_email"] === null || typeof typedObj["support_email"] === "string") && - (typedObj["join_code"] === null || typeof typedObj["join_code"] === "string") + (typedObj["support_email"] === null || typeof typedObj["support_email"] === "string") ) } @@ -1023,7 +1022,8 @@ export function isCourse(obj: unknown): obj is Course { typeof typedObj["is_unlisted"] === "boolean" && typeof typedObj["base_module_completion_requires_n_submodule_completions"] === "number" && typeof typedObj["can_add_chatbot"] === "boolean" && - typeof typedObj["is_joinable_by_code_only"] === "boolean" + typeof typedObj["is_joinable_by_code_only"] === "boolean" && + (typedObj["join_code"] === null || typeof typedObj["join_code"] === "string") ) } @@ -1090,7 +1090,8 @@ export function isNewCourse(obj: unknown): obj is NewCourse { typeof typedObj["is_test_mode"] === "boolean" && typeof typedObj["is_unlisted"] === "boolean" && typeof typedObj["copy_user_permissions"] === "boolean" && - typeof typedObj["is_joinable_by_code_only"] === "boolean" + typeof typedObj["is_joinable_by_code_only"] === "boolean" && + (typedObj["join_code"] === null || typeof typedObj["join_code"] === "string") ) } diff --git a/shared-module/packages/common/src/bindings.ts b/shared-module/packages/common/src/bindings.ts index a0ab9e5d4b15..9c7b1b8b5805 100644 --- a/shared-module/packages/common/src/bindings.ts +++ b/shared-module/packages/common/src/bindings.ts @@ -333,7 +333,6 @@ export interface CourseInstance { teacher_in_charge_name: string teacher_in_charge_email: string support_email: string | null - join_code: string | null } export interface CourseInstanceForm { @@ -474,6 +473,7 @@ export interface Course { base_module_completion_requires_n_submodule_completions: number can_add_chatbot: boolean is_joinable_by_code_only: boolean + join_code: string | null } export interface CourseBreadcrumbInfo { @@ -518,6 +518,7 @@ export interface NewCourse { is_unlisted: boolean copy_user_permissions: boolean is_joinable_by_code_only: boolean + join_code: string | null } export interface EmailTemplate { From 12f64e98eb3ce6aa0be79426508a02152da90ef5 Mon Sep 17 00:00:00 2001 From: Maija Y Date: Mon, 14 Oct 2024 15:24:04 +0300 Subject: [PATCH 6/9] Test and new course to seed --- ...e0ac57830d1fbfe8178a3c88f81188daaed2b.json | 43 --------- ...3680cd9381921f3017f3413c61875548dc92b.json | 18 ---- ...cdae210fce2baf7b9d3612f77b6548a6df68c.json | 30 ++++++ services/headless-lms/models/src/courses.rs | 12 ++- .../seed/seed_organizations/uh_mathstat.rs | 30 ++++++ .../manage/courses/id/index/ManageCourse.tsx | 2 +- .../courses/id/index/UpdateCourseForm.tsx | 18 ++-- services/main-frontend/src/pages/join.tsx | 23 +++-- .../tests/join-course-only-by-code.spec.ts | 96 +++++++++++++++++++ 9 files changed, 189 insertions(+), 83 deletions(-) delete mode 100644 services/headless-lms/models/.sqlx/query-27a76128d44ccd4f10baeec939be0ac57830d1fbfe8178a3c88f81188daaed2b.json delete mode 100644 services/headless-lms/models/.sqlx/query-31935ca7a5eb235ff25de151ade3680cd9381921f3017f3413c61875548dc92b.json create mode 100644 services/headless-lms/models/.sqlx/query-cc2fceb2f75d704b551705f15ffcdae210fce2baf7b9d3612f77b6548a6df68c.json create mode 100644 system-tests/src/tests/join-course-only-by-code.spec.ts diff --git a/services/headless-lms/models/.sqlx/query-27a76128d44ccd4f10baeec939be0ac57830d1fbfe8178a3c88f81188daaed2b.json b/services/headless-lms/models/.sqlx/query-27a76128d44ccd4f10baeec939be0ac57830d1fbfe8178a3c88f81188daaed2b.json deleted file mode 100644 index c83544ad2d1c..000000000000 --- a/services/headless-lms/models/.sqlx/query-27a76128d44ccd4f10baeec939be0ac57830d1fbfe8178a3c88f81188daaed2b.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nSELECT *\nFROM course_instance_enrollments\nWHERE user_id = $1\n AND course_id = $2\n AND deleted_at IS NULL\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "user_id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "course_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "course_instance_id", - "type_info": "Uuid" - }, - { - "ordinal": 3, - "name": "created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 4, - "name": "updated_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 5, - "name": "deleted_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": ["Uuid", "Uuid"] - }, - "nullable": [false, false, false, false, false, true] - }, - "hash": "27a76128d44ccd4f10baeec939be0ac57830d1fbfe8178a3c88f81188daaed2b" -} diff --git a/services/headless-lms/models/.sqlx/query-31935ca7a5eb235ff25de151ade3680cd9381921f3017f3413c61875548dc92b.json b/services/headless-lms/models/.sqlx/query-31935ca7a5eb235ff25de151ade3680cd9381921f3017f3413c61875548dc92b.json deleted file mode 100644 index 1b35c7a65711..000000000000 --- a/services/headless-lms/models/.sqlx/query-31935ca7a5eb235ff25de151ade3680cd9381921f3017f3413c61875548dc92b.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nINSERT INTO courses(\n id,\n name,\n description,\n slug,\n organization_id,\n language_code,\n course_language_group_id,\n is_draft,\n is_test_mode\n )\nVALUES(\n $1,\n $2,\n $3,\n $4,\n $5,\n $6,\n $7,\n $8,\n $9\n )\nRETURNING id\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": ["Uuid", "Varchar", "Text", "Varchar", "Uuid", "Varchar", "Uuid", "Bool", "Bool"] - }, - "nullable": [false] - }, - "hash": "31935ca7a5eb235ff25de151ade3680cd9381921f3017f3413c61875548dc92b" -} diff --git a/services/headless-lms/models/.sqlx/query-cc2fceb2f75d704b551705f15ffcdae210fce2baf7b9d3612f77b6548a6df68c.json b/services/headless-lms/models/.sqlx/query-cc2fceb2f75d704b551705f15ffcdae210fce2baf7b9d3612f77b6548a6df68c.json new file mode 100644 index 000000000000..dd16b88f2db8 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-cc2fceb2f75d704b551705f15ffcdae210fce2baf7b9d3612f77b6548a6df68c.json @@ -0,0 +1,30 @@ +{ + "db_name": "PostgreSQL", + "query": "\nINSERT INTO courses(\n id,\n name,\n description,\n slug,\n organization_id,\n language_code,\n course_language_group_id,\n is_draft,\n is_test_mode,\n is_joinable_by_code_only,\n join_code\n )\nVALUES(\n $1,\n $2,\n $3,\n $4,\n $5,\n $6,\n $7,\n $8,\n $9,\n $10,\n $11\n )\nRETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Varchar", + "Text", + "Varchar", + "Uuid", + "Varchar", + "Uuid", + "Bool", + "Bool", + "Bool", + "Varchar" + ] + }, + "nullable": [false] + }, + "hash": "cc2fceb2f75d704b551705f15ffcdae210fce2baf7b9d3612f77b6548a6df68c" +} diff --git a/services/headless-lms/models/src/courses.rs b/services/headless-lms/models/src/courses.rs index a97c79e3bfce..ed903a8ae4df 100644 --- a/services/headless-lms/models/src/courses.rs +++ b/services/headless-lms/models/src/courses.rs @@ -99,7 +99,9 @@ INSERT INTO courses( language_code, course_language_group_id, is_draft, - is_test_mode + is_test_mode, + is_joinable_by_code_only, + join_code ) VALUES( $1, @@ -110,7 +112,9 @@ VALUES( $6, $7, $8, - $9 + $9, + $10, + $11 ) RETURNING id ", @@ -122,7 +126,9 @@ RETURNING id new_course.language_code, course_language_group_id, new_course.is_draft, - new_course.is_test_mode + new_course.is_test_mode, + new_course.is_joinable_by_code_only, + new_course.join_code ) .fetch_one(conn) .await?; diff --git a/services/headless-lms/server/src/programs/seed/seed_organizations/uh_mathstat.rs b/services/headless-lms/server/src/programs/seed/seed_organizations/uh_mathstat.rs index 31d33d51d34a..9a2b3966b69d 100644 --- a/services/headless-lms/server/src/programs/seed/seed_organizations/uh_mathstat.rs +++ b/services/headless-lms/server/src/programs/seed/seed_organizations/uh_mathstat.rs @@ -150,6 +150,36 @@ pub async fn seed_organization_uh_mathstat( ) .await?; + let cody_only_course = NewCourse { + name: "Joinable by code only".to_string(), + slug: "joinable-by-code-only".to_string(), + organization_id: uh_mathstat_id, + language_code: "en-US".to_string(), + teacher_in_charge_name: "admin".to_string(), + teacher_in_charge_email: "admin@example.com".to_string(), + description: "Just a draft.".to_string(), + is_draft: false, + is_test_mode: false, + is_unlisted: false, + copy_user_permissions: false, + is_joinable_by_code_only: true, + join_code: Some( + "zARvZARjYhESMPVceEgZyJGQZZuUHVVgcUepyzEqzSqCMdbSCDrTaFhkJTxBshWU".to_string(), + ), + }; + library::content_management::create_new_course( + &mut conn, + PKeyPolicy::Fixed(CreateNewCourseFixedIds { + course_id: Uuid::parse_str("39a52e8c-ebbf-4b9a-a900-09aa344f3691")?, + default_course_instance_id: Uuid::parse_str("5b7286ce-22c5-4874-ade1-262949c4a604")?, + }), + cody_only_course, + admin_user_id, + models_requests::make_spec_fetcher(base_url.clone(), Uuid::new_v4(), Arc::clone(&jwt_key)), + models_requests::fetch_service_info, + ) + .await?; + let uh_data = CommonCourseData { db_pool: db_pool.clone(), organization_id: uh_mathstat_id, diff --git a/services/main-frontend/src/components/page-specific/manage/courses/id/index/ManageCourse.tsx b/services/main-frontend/src/components/page-specific/manage/courses/id/index/ManageCourse.tsx index 0e735631cf2c..1ccc11c716e3 100644 --- a/services/main-frontend/src/components/page-specific/manage/courses/id/index/ManageCourse.tsx +++ b/services/main-frontend/src/components/page-specific/manage/courses/id/index/ManageCourse.tsx @@ -316,7 +316,7 @@ const ManageCourse: React.FC> = ({ course, refetc {`/join?code=${course.join_code}`}