diff --git a/services/headless-lms/migrations/20240930042528_add-join-code-uses.down.sql b/services/headless-lms/migrations/20240930042528_add-join-code-uses.down.sql new file mode 100644 index 000000000000..251f89424dcc --- /dev/null +++ b/services/headless-lms/migrations/20240930042528_add-join-code-uses.down.sql @@ -0,0 +1,3 @@ +DROP TABLE join_code_uses; +ALTER TABLE courses DROP COLUMN is_joinable_by_code_only; +ALTER TABLE courses DROP COLUMN join_code; diff --git a/services/headless-lms/migrations/20240930042528_add-join-code-uses.up.sql b/services/headless-lms/migrations/20240930042528_add-join-code-uses.up.sql new file mode 100644 index 000000000000..fc7a268abbf4 --- /dev/null +++ b/services/headless-lms/migrations/20240930042528_add-join-code-uses.up.sql @@ -0,0 +1,25 @@ +CREATE TABLE join_code_uses ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + course_id UUID NOT NULL REFERENCES courses(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 +); + + +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 courses table'; +COMMENT ON COLUMN join_code_uses.id IS 'A unique, stable identifier for the record.'; +COMMENT ON COLUMN join_code_uses.course_id IS 'Course 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.'; + +ALTER TABLE courses +ADD COLUMN join_code varchar(1024), + ADD COLUMN is_joinable_by_code_only BOOLEAN NOT NULL DEFAULT FALSE; +COMMENT ON COLUMN courses.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'; +COMMENT ON COLUMN courses.is_joinable_by_code_only IS 'Whether this course is only joinable by a join code that can be generated for a course instance' diff --git a/services/headless-lms/models/.sqlx/query-2078d5df8e383597f68566f92be652f6183d12a66a522164dc6238a80becf00e.json b/services/headless-lms/models/.sqlx/query-032e00af20a43472c6e91d155e130a6e0790e330a6d943c1b4871eef476e26d0.json similarity index 79% rename from services/headless-lms/models/.sqlx/query-2078d5df8e383597f68566f92be652f6183d12a66a522164dc6238a80becf00e.json rename to services/headless-lms/models/.sqlx/query-032e00af20a43472c6e91d155e130a6e0790e330a6d943c1b4871eef476e26d0.json index d20caea4d840..715a90b38505 100644 --- a/services/headless-lms/models/.sqlx/query-2078d5df8e383597f68566f92be652f6183d12a66a522164dc6238a80becf00e.json +++ b/services/headless-lms/models/.sqlx/query-032e00af20a43472c6e91d155e130a6e0790e330a6d943c1b4871eef476e26d0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n can_add_chatbot,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions\nFROM courses\nWHERE courses.deleted_at IS NULL\n AND (\n id IN (\n SELECT course_id\n FROM roles\n WHERE deleted_at IS NULL\n AND user_id = $1\n AND course_id IS NOT NULL\n )\n OR (\n id IN (\n SELECT ci.course_id\n FROM course_instances ci\n JOIN ROLES r ON r.course_instance_id = ci.id\n WHERE r.user_id = $1\n AND r.deleted_at IS NULL\n AND ci.deleted_at IS NULL\n )\n )\n ) ", + "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n can_add_chatbot,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions,\n is_joinable_by_code_only,\n join_code\nFROM courses\nWHERE join_code = $1\n AND deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -87,10 +87,20 @@ "ordinal": 16, "name": "base_module_completion_requires_n_submodule_completions", "type_info": "Int4" + }, + { + "ordinal": 17, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { - "Left": ["Uuid"] + "Left": ["Text"] }, "nullable": [ false, @@ -109,8 +119,10 @@ false, false, false, - false + false, + false, + true ] }, - "hash": "2078d5df8e383597f68566f92be652f6183d12a66a522164dc6238a80becf00e" + "hash": "032e00af20a43472c6e91d155e130a6e0790e330a6d943c1b4871eef476e26d0" } diff --git a/services/headless-lms/models/.sqlx/query-a9158120a6668a2b904d37e9e8d5bec64b7ab544181836426f82ea6cac1c4cdd.json b/services/headless-lms/models/.sqlx/query-13554c9b1e813c29066c90df0e04a3b84ed33fc39fadae2253c80a9761e39903.json similarity index 84% rename from services/headless-lms/models/.sqlx/query-a9158120a6668a2b904d37e9e8d5bec64b7ab544181836426f82ea6cac1c4cdd.json rename to services/headless-lms/models/.sqlx/query-13554c9b1e813c29066c90df0e04a3b84ed33fc39fadae2253c80a9761e39903.json index ea14167742de..e5e2739dbf36 100644 --- a/services/headless-lms/models/.sqlx/query-a9158120a6668a2b904d37e9e8d5bec64b7ab544181836426f82ea6cac1c4cdd.json +++ b/services/headless-lms/models/.sqlx/query-13554c9b1e813c29066c90df0e04a3b84ed33fc39fadae2253c80a9761e39903.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n can_add_chatbot,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions\nFROM courses\nWHERE id IN (SELECT * FROM UNNEST($1::uuid[]))\n ", + "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n can_add_chatbot,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions,\n is_joinable_by_code_only,\n join_code\nFROM courses\nWHERE id IN (SELECT * FROM UNNEST($1::uuid[]))\n ", "describe": { "columns": [ { @@ -87,6 +87,16 @@ "ordinal": 16, "name": "base_module_completion_requires_n_submodule_completions", "type_info": "Int4" + }, + { + "ordinal": 17, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { @@ -109,8 +119,10 @@ false, false, false, - false + false, + false, + true ] }, - "hash": "a9158120a6668a2b904d37e9e8d5bec64b7ab544181836426f82ea6cac1c4cdd" + "hash": "13554c9b1e813c29066c90df0e04a3b84ed33fc39fadae2253c80a9761e39903" } diff --git a/services/headless-lms/models/.sqlx/query-b6af36b075da7711db12a6e045b808f26862ce729ff99891e7f9823ec7d51420.json b/services/headless-lms/models/.sqlx/query-2f227e2bf3a4dd0828def3caa59b00246bf72c3a2bc0b40a9ff94e14ce61615f.json similarity index 84% rename from services/headless-lms/models/.sqlx/query-b6af36b075da7711db12a6e045b808f26862ce729ff99891e7f9823ec7d51420.json rename to services/headless-lms/models/.sqlx/query-2f227e2bf3a4dd0828def3caa59b00246bf72c3a2bc0b40a9ff94e14ce61615f.json index 9dccb9f5e6b6..90ecc8bfa63a 100644 --- a/services/headless-lms/models/.sqlx/query-b6af36b075da7711db12a6e045b808f26862ce729ff99891e7f9823ec7d51420.json +++ b/services/headless-lms/models/.sqlx/query-2f227e2bf3a4dd0828def3caa59b00246bf72c3a2bc0b40a9ff94e14ce61615f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n can_add_chatbot,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions\nFROM courses\nWHERE slug = $1\n AND deleted_at IS NULL\n", + "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n can_add_chatbot,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions,\n is_joinable_by_code_only,\n join_code\nFROM courses\nWHERE slug = $1\n AND deleted_at IS NULL\n", "describe": { "columns": [ { @@ -87,6 +87,16 @@ "ordinal": 16, "name": "base_module_completion_requires_n_submodule_completions", "type_info": "Int4" + }, + { + "ordinal": 17, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { @@ -109,8 +119,10 @@ false, false, false, - false + false, + false, + true ] }, - "hash": "b6af36b075da7711db12a6e045b808f26862ce729ff99891e7f9823ec7d51420" + "hash": "2f227e2bf3a4dd0828def3caa59b00246bf72c3a2bc0b40a9ff94e14ce61615f" } 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-1d169e68f5cc3734a8b921b470d6e66dda1c5b6c2e8e7bfa8b0ff0c6db7bc5c5.json b/services/headless-lms/models/.sqlx/query-50d7e5768bc24ec9a2c18d11ce77ac35db826c01ec7bc05cb905aab1c51ba7e4.json similarity index 84% rename from services/headless-lms/models/.sqlx/query-1d169e68f5cc3734a8b921b470d6e66dda1c5b6c2e8e7bfa8b0ff0c6db7bc5c5.json rename to services/headless-lms/models/.sqlx/query-50d7e5768bc24ec9a2c18d11ce77ac35db826c01ec7bc05cb905aab1c51ba7e4.json index 8c9ae673910a..c626d4e923bb 100644 --- a/services/headless-lms/models/.sqlx/query-1d169e68f5cc3734a8b921b470d6e66dda1c5b6c2e8e7bfa8b0ff0c6db7bc5c5.json +++ b/services/headless-lms/models/.sqlx/query-50d7e5768bc24ec9a2c18d11ce77ac35db826c01ec7bc05cb905aab1c51ba7e4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n is_unlisted\nFROM courses\nWHERE deleted_at IS NULL;\n", + "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n is_unlisted,\n is_joinable_by_code_only,\n join_code\nFROM courses\nWHERE deleted_at IS NULL;\n", "describe": { "columns": [ { @@ -87,6 +87,16 @@ "ordinal": 16, "name": "is_unlisted", "type_info": "Bool" + }, + { + "ordinal": 17, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { @@ -109,8 +119,10 @@ false, false, false, - false + false, + false, + true ] }, - "hash": "1d169e68f5cc3734a8b921b470d6e66dda1c5b6c2e8e7bfa8b0ff0c6db7bc5c5" + "hash": "50d7e5768bc24ec9a2c18d11ce77ac35db826c01ec7bc05cb905aab1c51ba7e4" } diff --git a/services/headless-lms/models/.sqlx/query-17ed6560ca746324d1dea979be7ebe950ffed1e832c1ecf8cacee45bd0eeceef.json b/services/headless-lms/models/.sqlx/query-5303f036fecb3efa8dce1756eaf2193d7e6c2f5cb7c9267b14e193daf525d997.json similarity index 73% rename from services/headless-lms/models/.sqlx/query-17ed6560ca746324d1dea979be7ebe950ffed1e832c1ecf8cacee45bd0eeceef.json rename to services/headless-lms/models/.sqlx/query-5303f036fecb3efa8dce1756eaf2193d7e6c2f5cb7c9267b14e193daf525d997.json index cc3968039099..a7f5f5a27a01 100644 --- a/services/headless-lms/models/.sqlx/query-17ed6560ca746324d1dea979be7ebe950ffed1e832c1ecf8cacee45bd0eeceef.json +++ b/services/headless-lms/models/.sqlx/query-5303f036fecb3efa8dce1756eaf2193d7e6c2f5cb7c9267b14e193daf525d997.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nINSERT INTO courses (\n name,\n organization_id,\n slug,\n content_search_language,\n language_code,\n copied_from,\n course_language_group_id,\n is_draft,\n base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n is_unlisted\n )\nVALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)\nRETURNING id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n is_unlisted\n ", + "query": "\nINSERT INTO courses (\n name,\n organization_id,\n slug,\n content_search_language,\n language_code,\n copied_from,\n course_language_group_id,\n is_draft,\n base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n is_unlisted,\n is_joinable_by_code_only,\n join_code\n )\nVALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)\nRETURNING id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n is_unlisted,\n is_joinable_by_code_only,\n join_code\n ", "describe": { "columns": [ { @@ -87,6 +87,16 @@ "ordinal": 16, "name": "is_unlisted", "type_info": "Bool" + }, + { + "ordinal": 17, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { @@ -101,7 +111,9 @@ "Bool", "Int4", "Bool", - "Bool" + "Bool", + "Bool", + "Varchar" ] }, "nullable": [ @@ -121,8 +133,10 @@ false, false, false, - false + false, + false, + true ] }, - "hash": "17ed6560ca746324d1dea979be7ebe950ffed1e832c1ecf8cacee45bd0eeceef" + "hash": "5303f036fecb3efa8dce1756eaf2193d7e6c2f5cb7c9267b14e193daf525d997" } diff --git a/services/headless-lms/models/.sqlx/query-73a34b358da09a5a1940816bd1e429d25d2175154df666c449f062d1bf969089.json b/services/headless-lms/models/.sqlx/query-63132d1b35ab7549158a6f5e330221a49cd06eb066e344d338efee8289d68e35.json similarity index 73% rename from services/headless-lms/models/.sqlx/query-73a34b358da09a5a1940816bd1e429d25d2175154df666c449f062d1bf969089.json rename to services/headless-lms/models/.sqlx/query-63132d1b35ab7549158a6f5e330221a49cd06eb066e344d338efee8289d68e35.json index 65d73b7d9c9f..f6fe7417df8c 100644 --- a/services/headless-lms/models/.sqlx/query-73a34b358da09a5a1940816bd1e429d25d2175154df666c449f062d1bf969089.json +++ b/services/headless-lms/models/.sqlx/query-63132d1b35ab7549158a6f5e330221a49cd06eb066e344d338efee8289d68e35.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nUPDATE courses\nSET name = $1,\n description = $2,\n is_draft = $3,\n is_test_mode = $4,\n can_add_chatbot = $5,\n is_unlisted = $6\nWHERE id = $7\nRETURNING id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n can_add_chatbot,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions\n ", + "query": "\nUPDATE courses\nSET name = $1,\n description = $2,\n is_draft = $3,\n is_test_mode = $4,\n can_add_chatbot = $5,\n is_unlisted = $6,\n is_joinable_by_code_only = $7\nWHERE id = $8\nRETURNING id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n can_add_chatbot,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions,\n is_joinable_by_code_only,\n join_code\n ", "describe": { "columns": [ { @@ -87,10 +87,20 @@ "ordinal": 16, "name": "base_module_completion_requires_n_submodule_completions", "type_info": "Int4" + }, + { + "ordinal": 17, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { - "Left": ["Varchar", "Text", "Bool", "Bool", "Bool", "Bool", "Uuid"] + "Left": ["Varchar", "Text", "Bool", "Bool", "Bool", "Bool", "Bool", "Uuid"] }, "nullable": [ false, @@ -109,8 +119,10 @@ false, false, false, - false + false, + false, + true ] }, - "hash": "73a34b358da09a5a1940816bd1e429d25d2175154df666c449f062d1bf969089" + "hash": "63132d1b35ab7549158a6f5e330221a49cd06eb066e344d338efee8289d68e35" } diff --git a/services/headless-lms/models/.sqlx/query-2879a1ea6e31d652e50d2f037521a36ed2de0a2a0ffdbc1bf3d4ad6598b8537e.json b/services/headless-lms/models/.sqlx/query-659d8635c8511990904c11ca7a200ae7559ffeca619e7a8ecc975b19fe933f2e.json similarity index 84% rename from services/headless-lms/models/.sqlx/query-2879a1ea6e31d652e50d2f037521a36ed2de0a2a0ffdbc1bf3d4ad6598b8537e.json rename to services/headless-lms/models/.sqlx/query-659d8635c8511990904c11ca7a200ae7559ffeca619e7a8ecc975b19fe933f2e.json index 0a690994662e..b6f0a6ccca3d 100644 --- a/services/headless-lms/models/.sqlx/query-2879a1ea6e31d652e50d2f037521a36ed2de0a2a0ffdbc1bf3d4ad6598b8537e.json +++ b/services/headless-lms/models/.sqlx/query-659d8635c8511990904c11ca7a200ae7559ffeca619e7a8ecc975b19fe933f2e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n can_add_chatbot,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions\nFROM courses\nWHERE id = $1;\n ", + "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n can_add_chatbot,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions,\n is_joinable_by_code_only,\n join_code\nFROM courses\nWHERE id = $1;\n ", "describe": { "columns": [ { @@ -87,6 +87,16 @@ "ordinal": 16, "name": "base_module_completion_requires_n_submodule_completions", "type_info": "Int4" + }, + { + "ordinal": 17, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { @@ -109,8 +119,10 @@ false, false, false, - false + false, + false, + true ] }, - "hash": "2879a1ea6e31d652e50d2f037521a36ed2de0a2a0ffdbc1bf3d4ad6598b8537e" + "hash": "659d8635c8511990904c11ca7a200ae7559ffeca619e7a8ecc975b19fe933f2e" } diff --git a/services/headless-lms/models/.sqlx/query-786d41394659b910e6833a0ae0876193fe5ec3089cbe4da15565174322edbf9f.json b/services/headless-lms/models/.sqlx/query-786d41394659b910e6833a0ae0876193fe5ec3089cbe4da15565174322edbf9f.json new file mode 100644 index 000000000000..e1a78038769e --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-786d41394659b910e6833a0ae0876193fe5ec3089cbe4da15565174322edbf9f.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\nINSERT INTO join_code_uses (id, user_id, course_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": "786d41394659b910e6833a0ae0876193fe5ec3089cbe4da15565174322edbf9f" +} diff --git a/services/headless-lms/models/.sqlx/query-8202eb97a2b1f388a51758e7fcf8d69b2e68544fae551e3da0a41bc6e9a2de9c.json b/services/headless-lms/models/.sqlx/query-8202eb97a2b1f388a51758e7fcf8d69b2e68544fae551e3da0a41bc6e9a2de9c.json new file mode 100644 index 000000000000..de6ce5ee1345 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-8202eb97a2b1f388a51758e7fcf8d69b2e68544fae551e3da0a41bc6e9a2de9c.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "\nUPDATE courses\nSET join_code = $2\nWHERE id = $1\n", + "describe": { + "columns": [], + "parameters": { + "Left": ["Uuid", "Varchar"] + }, + "nullable": [] + }, + "hash": "8202eb97a2b1f388a51758e7fcf8d69b2e68544fae551e3da0a41bc6e9a2de9c" +} diff --git a/services/headless-lms/models/.sqlx/query-0a2d78334809e740bb9b9af6b81f0e88d9cc5eb4c2379b87f8738afec0306c76.json b/services/headless-lms/models/.sqlx/query-82fe4061970937715a5e2d15682c8aed17f804b6a1823dad140ee6f5f4ff5ce6.json similarity index 86% rename from services/headless-lms/models/.sqlx/query-0a2d78334809e740bb9b9af6b81f0e88d9cc5eb4c2379b87f8738afec0306c76.json rename to services/headless-lms/models/.sqlx/query-82fe4061970937715a5e2d15682c8aed17f804b6a1823dad140ee6f5f4ff5ce6.json index 9e6085771b22..ab5b9a6555d6 100644 --- a/services/headless-lms/models/.sqlx/query-0a2d78334809e740bb9b9af6b81f0e88d9cc5eb4c2379b87f8738afec0306c76.json +++ b/services/headless-lms/models/.sqlx/query-82fe4061970937715a5e2d15682c8aed17f804b6a1823dad140ee6f5f4ff5ce6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nUPDATE courses\nSET deleted_at = now()\nWHERE id = $1\nRETURNING id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n can_add_chatbot,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions\n ", + "query": "\nUPDATE courses\nSET deleted_at = now()\nWHERE id = $1\nRETURNING id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n can_add_chatbot,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions,\n is_joinable_by_code_only,\n join_code\n ", "describe": { "columns": [ { @@ -87,6 +87,16 @@ "ordinal": 16, "name": "base_module_completion_requires_n_submodule_completions", "type_info": "Int4" + }, + { + "ordinal": 17, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { @@ -109,8 +119,10 @@ false, false, false, - false + false, + false, + true ] }, - "hash": "0a2d78334809e740bb9b9af6b81f0e88d9cc5eb4c2379b87f8738afec0306c76" + "hash": "82fe4061970937715a5e2d15682c8aed17f804b6a1823dad140ee6f5f4ff5ce6" } diff --git a/services/headless-lms/models/.sqlx/query-c54b8b02fc33933e1fbbb5ea4a698825a1d815d475a5706fa750d9d20fb324af.json b/services/headless-lms/models/.sqlx/query-ae1ed9d41861ec3cd6038dc2aa8c5a86234832d8bcce20b54ae05ffe643e6f99.json similarity index 83% rename from services/headless-lms/models/.sqlx/query-c54b8b02fc33933e1fbbb5ea4a698825a1d815d475a5706fa750d9d20fb324af.json rename to services/headless-lms/models/.sqlx/query-ae1ed9d41861ec3cd6038dc2aa8c5a86234832d8bcce20b54ae05ffe643e6f99.json index 87e119ef7012..1dd13c29dcff 100644 --- a/services/headless-lms/models/.sqlx/query-c54b8b02fc33933e1fbbb5ea4a698825a1d815d475a5706fa750d9d20fb324af.json +++ b/services/headless-lms/models/.sqlx/query-ae1ed9d41861ec3cd6038dc2aa8c5a86234832d8bcce20b54ae05ffe643e6f99.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n is_unlisted\nFROM courses\nWHERE course_language_group_id = $1\nAND deleted_at IS NULL\n ", + "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n is_unlisted,\n is_joinable_by_code_only,\n join_code\nFROM courses\nWHERE course_language_group_id = $1\nAND deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -87,6 +87,16 @@ "ordinal": 16, "name": "is_unlisted", "type_info": "Bool" + }, + { + "ordinal": 17, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { @@ -109,8 +119,10 @@ false, false, false, - false + false, + false, + true ] }, - "hash": "c54b8b02fc33933e1fbbb5ea4a698825a1d815d475a5706fa750d9d20fb324af" + "hash": "ae1ed9d41861ec3cd6038dc2aa8c5a86234832d8bcce20b54ae05ffe643e6f99" } 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/.sqlx/query-d0bedcc2c41efb7bff7b70e6e48d690bfd173347719f3ba48c4bbf489b52b38d.json b/services/headless-lms/models/.sqlx/query-d0bedcc2c41efb7bff7b70e6e48d690bfd173347719f3ba48c4bbf489b52b38d.json new file mode 100644 index 000000000000..112615a55d3d --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-d0bedcc2c41efb7bff7b70e6e48d690bfd173347719f3ba48c4bbf489b52b38d.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT is_joinable_by_code_only\nFROM courses\nWHERE id = $1\n", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + } + ], + "parameters": { + "Left": ["Uuid"] + }, + "nullable": [false] + }, + "hash": "d0bedcc2c41efb7bff7b70e6e48d690bfd173347719f3ba48c4bbf489b52b38d" +} diff --git a/services/headless-lms/models/.sqlx/query-d7d3c14b4d8d900248d80589c9dc73bf98ec7672563931ae546c7fbbf01463e0.json b/services/headless-lms/models/.sqlx/query-d7d3c14b4d8d900248d80589c9dc73bf98ec7672563931ae546c7fbbf01463e0.json new file mode 100644 index 000000000000..b559342fd767 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-d7d3c14b4d8d900248d80589c9dc73bf98ec7672563931ae546c7fbbf01463e0.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT id\nFROM join_code_uses\nWHERE user_id = $1\nAND course_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": ["Uuid", "Uuid"] + }, + "nullable": [false] + }, + "hash": "d7d3c14b4d8d900248d80589c9dc73bf98ec7672563931ae546c7fbbf01463e0" +} diff --git a/services/headless-lms/models/.sqlx/query-37d99e2343ab0466b42f792cde4c79713a11147c57114c60b5338e89461059aa.json b/services/headless-lms/models/.sqlx/query-dc6b04142ac276503be3b714a535f56e008e8516a2c38f2f149fbeaa1571c5ed.json similarity index 78% rename from services/headless-lms/models/.sqlx/query-37d99e2343ab0466b42f792cde4c79713a11147c57114c60b5338e89461059aa.json rename to services/headless-lms/models/.sqlx/query-dc6b04142ac276503be3b714a535f56e008e8516a2c38f2f149fbeaa1571c5ed.json index 8f92fe88f92e..376c847197cd 100644 --- a/services/headless-lms/models/.sqlx/query-37d99e2343ab0466b42f792cde4c79713a11147c57114c60b5338e89461059aa.json +++ b/services/headless-lms/models/.sqlx/query-dc6b04142ac276503be3b714a535f56e008e8516a2c38f2f149fbeaa1571c5ed.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT\n DISTINCT(c.id),\n c.name,\n c.created_at,\n c.updated_at,\n c.organization_id,\n c.deleted_at,\n c.slug,\n c.content_search_language::text,\n c.language_code,\n c.copied_from,\n c.course_language_group_id,\n c.description,\n c.is_draft,\n c.is_test_mode,\n c.base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n c.is_unlisted\nFROM courses as c\n LEFT JOIN course_instances as ci on c.id = ci.course_id\nWHERE\n c.organization_id = $1 AND\n ci.starts_at < NOW() AND ci.ends_at > NOW() AND\n c.deleted_at IS NULL AND ci.deleted_at IS NULL\n LIMIT $2 OFFSET $3;\n ", + "query": "\nSELECT\n DISTINCT(c.id),\n c.name,\n c.created_at,\n c.updated_at,\n c.organization_id,\n c.deleted_at,\n c.slug,\n c.content_search_language::text,\n c.language_code,\n c.copied_from,\n c.course_language_group_id,\n c.description,\n c.is_draft,\n c.is_test_mode,\n c.base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n c.is_unlisted,\n c.is_joinable_by_code_only,\n c.join_code\nFROM courses as c\n LEFT JOIN course_instances as ci on c.id = ci.course_id\nWHERE\n c.organization_id = $1 AND\n ci.starts_at < NOW() AND ci.ends_at > NOW() AND\n c.deleted_at IS NULL AND ci.deleted_at IS NULL\n LIMIT $2 OFFSET $3;\n ", "describe": { "columns": [ { @@ -87,6 +87,16 @@ "ordinal": 16, "name": "is_unlisted", "type_info": "Bool" + }, + { + "ordinal": 17, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { @@ -109,8 +119,10 @@ false, false, false, - false + false, + false, + true ] }, - "hash": "37d99e2343ab0466b42f792cde4c79713a11147c57114c60b5338e89461059aa" + "hash": "dc6b04142ac276503be3b714a535f56e008e8516a2c38f2f149fbeaa1571c5ed" } diff --git a/services/headless-lms/models/.sqlx/query-dc7313994c18a88543cde9bb43521f7ca63b67950784ce377055eecbd0af0102.json b/services/headless-lms/models/.sqlx/query-dc7313994c18a88543cde9bb43521f7ca63b67950784ce377055eecbd0af0102.json new file mode 100644 index 000000000000..371729edf235 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-dc7313994c18a88543cde9bb43521f7ca63b67950784ce377055eecbd0af0102.json @@ -0,0 +1,128 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n can_add_chatbot,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions,\n is_joinable_by_code_only,\n join_code\nFROM courses\nWHERE courses.deleted_at IS NULL\n AND (\n id IN (\n SELECT course_id\n FROM roles\n WHERE deleted_at IS NULL\n AND user_id = $1\n AND course_id IS NOT NULL\n )\n OR (\n id IN (\n SELECT ci.course_id\n FROM course_instances ci\n JOIN ROLES r ON r.course_instance_id = ci.id\n WHERE r.user_id = $1\n AND r.deleted_at IS NULL\n AND ci.deleted_at IS NULL\n )\n )\n ) ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "organization_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "content_search_language", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "language_code", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "copied_from", + "type_info": "Uuid" + }, + { + "ordinal": 10, + "name": "course_language_group_id", + "type_info": "Uuid" + }, + { + "ordinal": 11, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "is_draft", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "is_test_mode", + "type_info": "Bool" + }, + { + "ordinal": 14, + "name": "can_add_chatbot", + "type_info": "Bool" + }, + { + "ordinal": 15, + "name": "is_unlisted", + "type_info": "Bool" + }, + { + "ordinal": 16, + "name": "base_module_completion_requires_n_submodule_completions", + "type_info": "Int4" + }, + { + "ordinal": 17, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "join_code", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": ["Uuid"] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + null, + false, + true, + false, + true, + false, + false, + false, + false, + false, + false, + true + ] + }, + "hash": "dc7313994c18a88543cde9bb43521f7ca63b67950784ce377055eecbd0af0102" +} diff --git a/services/headless-lms/models/.sqlx/query-20f105fdfd06708154041458ac950351f637c790b6e76778d59dbc327d56b2ea.json b/services/headless-lms/models/.sqlx/query-e45f9d2d34d5e905575efd5c61b0519ddeb78673c0df31a3e078c642f2b6106d.json similarity index 80% rename from services/headless-lms/models/.sqlx/query-20f105fdfd06708154041458ac950351f637c790b6e76778d59dbc327d56b2ea.json rename to services/headless-lms/models/.sqlx/query-e45f9d2d34d5e905575efd5c61b0519ddeb78673c0df31a3e078c642f2b6106d.json index 56574aec6703..7a736c6584c7 100644 --- a/services/headless-lms/models/.sqlx/query-20f105fdfd06708154041458ac950351f637c790b6e76778d59dbc327d56b2ea.json +++ b/services/headless-lms/models/.sqlx/query-e45f9d2d34d5e905575efd5c61b0519ddeb78673c0df31a3e078c642f2b6106d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions,\n can_add_chatbot\nFROM courses\nWHERE courses.deleted_at IS NULL\n AND id IN (\n SELECT current_course_id\n FROM user_course_settings\n WHERE deleted_at IS NULL\n AND user_id = $1\n )\n", + "query": "\nSELECT id,\n name,\n created_at,\n updated_at,\n organization_id,\n deleted_at,\n slug,\n content_search_language::text,\n language_code,\n copied_from,\n course_language_group_id,\n description,\n is_draft,\n is_test_mode,\n is_unlisted,\n base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n is_joinable_by_code_only,\n join_code\nFROM courses\nWHERE courses.deleted_at IS NULL\n AND id IN (\n SELECT current_course_id\n FROM user_course_settings\n WHERE deleted_at IS NULL\n AND user_id = $1\n )\n", "describe": { "columns": [ { @@ -87,6 +87,16 @@ "ordinal": 16, "name": "can_add_chatbot", "type_info": "Bool" + }, + { + "ordinal": 17, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { @@ -109,8 +119,10 @@ false, false, false, - false + false, + false, + true ] }, - "hash": "20f105fdfd06708154041458ac950351f637c790b6e76778d59dbc327d56b2ea" + "hash": "e45f9d2d34d5e905575efd5c61b0519ddeb78673c0df31a3e078c642f2b6106d" } diff --git a/services/headless-lms/models/.sqlx/query-1dbcda9a3e6ade02dda88660125599f8fefb022a8f91554a679927f818b48b49.json b/services/headless-lms/models/.sqlx/query-ebcc56fdcf585b87b73a715bc114a8c5809204b3f3c5a1e6b08de8bcd54b7e44.json similarity index 80% rename from services/headless-lms/models/.sqlx/query-1dbcda9a3e6ade02dda88660125599f8fefb022a8f91554a679927f818b48b49.json rename to services/headless-lms/models/.sqlx/query-ebcc56fdcf585b87b73a715bc114a8c5809204b3f3c5a1e6b08de8bcd54b7e44.json index 38fa80d96e1c..2e2ab4675f52 100644 --- a/services/headless-lms/models/.sqlx/query-1dbcda9a3e6ade02dda88660125599f8fefb022a8f91554a679927f818b48b49.json +++ b/services/headless-lms/models/.sqlx/query-ebcc56fdcf585b87b73a715bc114a8c5809204b3f3c5a1e6b08de8bcd54b7e44.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n slug,\n courses.created_at,\n courses.updated_at,\n courses.deleted_at,\n name,\n description,\n organization_id,\n language_code,\n copied_from,\n content_search_language::text,\n course_language_group_id,\n is_draft,\n is_test_mode,\n base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n is_unlisted\nFROM courses\n JOIN course_exams ON courses.id = course_exams.course_id\nWHERE course_exams.exam_id = $1\n AND courses.deleted_at IS NULL\n AND course_exams.deleted_at IS NULL\n", + "query": "\nSELECT id,\n slug,\n courses.created_at,\n courses.updated_at,\n courses.deleted_at,\n name,\n description,\n organization_id,\n language_code,\n copied_from,\n content_search_language::text,\n course_language_group_id,\n is_draft,\n is_test_mode,\n base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n is_unlisted,\n is_joinable_by_code_only,\n join_code\nFROM courses\n JOIN course_exams ON courses.id = course_exams.course_id\nWHERE course_exams.exam_id = $1\n AND courses.deleted_at IS NULL\n AND course_exams.deleted_at IS NULL\n", "describe": { "columns": [ { @@ -87,6 +87,16 @@ "ordinal": 16, "name": "is_unlisted", "type_info": "Bool" + }, + { + "ordinal": 17, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { @@ -109,8 +119,10 @@ false, false, false, - false + false, + false, + true ] }, - "hash": "1dbcda9a3e6ade02dda88660125599f8fefb022a8f91554a679927f818b48b49" + "hash": "ebcc56fdcf585b87b73a715bc114a8c5809204b3f3c5a1e6b08de8bcd54b7e44" } diff --git a/services/headless-lms/models/.sqlx/query-be250a0d597512e5eb52be037fa57e6fc143e9e693bf5614fee6d538484d4e50.json b/services/headless-lms/models/.sqlx/query-f85e90f73c74ed500cec06a100a1196db665c91c4b8805d70fa94ffca87da53a.json similarity index 74% rename from services/headless-lms/models/.sqlx/query-be250a0d597512e5eb52be037fa57e6fc143e9e693bf5614fee6d538484d4e50.json rename to services/headless-lms/models/.sqlx/query-f85e90f73c74ed500cec06a100a1196db665c91c4b8805d70fa94ffca87da53a.json index fad516a939b1..0befed3a0998 100644 --- a/services/headless-lms/models/.sqlx/query-be250a0d597512e5eb52be037fa57e6fc143e9e693bf5614fee6d538484d4e50.json +++ b/services/headless-lms/models/.sqlx/query-f85e90f73c74ed500cec06a100a1196db665c91c4b8805d70fa94ffca87da53a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT courses.id,\n courses.name,\n courses.created_at,\n courses.updated_at,\n courses.organization_id,\n courses.deleted_at,\n courses.slug,\n courses.content_search_language::text,\n courses.language_code,\n courses.copied_from,\n courses.course_language_group_id,\n courses.description,\n courses.is_draft,\n courses.is_test_mode,\n base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n courses.is_unlisted\nFROM courses\nWHERE courses.organization_id = $1\n AND (\n (\n courses.is_draft IS FALSE\n AND courses.is_unlisted IS FALSE\n )\n OR EXISTS (\n SELECT id\n FROM roles\n WHERE user_id = $2\n AND (\n course_id = courses.id\n OR roles.organization_id = courses.organization_id\n OR roles.is_global IS TRUE\n )\n )\n )\n AND courses.deleted_at IS NULL\nORDER BY courses.name\nLIMIT $3 OFFSET $4;\n", + "query": "\nSELECT courses.id,\n courses.name,\n courses.created_at,\n courses.updated_at,\n courses.organization_id,\n courses.deleted_at,\n courses.slug,\n courses.content_search_language::text,\n courses.language_code,\n courses.copied_from,\n courses.course_language_group_id,\n courses.description,\n courses.is_draft,\n courses.is_test_mode,\n base_module_completion_requires_n_submodule_completions,\n can_add_chatbot,\n courses.is_unlisted,\n courses.is_joinable_by_code_only,\n courses.join_code\nFROM courses\nWHERE courses.organization_id = $1\n AND (\n (\n courses.is_draft IS FALSE\n AND courses.is_unlisted IS FALSE\n )\n OR EXISTS (\n SELECT id\n FROM roles\n WHERE user_id = $2\n AND (\n course_id = courses.id\n OR roles.organization_id = courses.organization_id\n OR roles.is_global IS TRUE\n )\n )\n )\n AND courses.deleted_at IS NULL\nORDER BY courses.name\nLIMIT $3 OFFSET $4;\n", "describe": { "columns": [ { @@ -87,6 +87,16 @@ "ordinal": 16, "name": "is_unlisted", "type_info": "Bool" + }, + { + "ordinal": 17, + "name": "is_joinable_by_code_only", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "join_code", + "type_info": "Varchar" } ], "parameters": { @@ -109,8 +119,10 @@ false, false, false, - false + false, + false, + true ] }, - "hash": "be250a0d597512e5eb52be037fa57e6fc143e9e693bf5614fee6d538484d4e50" + "hash": "f85e90f73c74ed500cec06a100a1196db665c91c4b8805d70fa94ffca87da53a" } diff --git a/services/headless-lms/models/src/chapters.rs b/services/headless-lms/models/src/chapters.rs index 30c795d653f0..3bbde4796ad6 100644 --- a/services/headless-lms/models/src/chapters.rs +++ b/services/headless-lms/models/src/chapters.rs @@ -618,6 +618,8 @@ mod tests { is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, }, user, |_, _, _| unimplemented!(), diff --git a/services/headless-lms/models/src/courses.rs b/services/headless-lms/models/src/courses.rs index 130fba420cd5..ed903a8ae4df 100644 --- a/services/headless-lms/models/src/courses.rs +++ b/services/headless-lms/models/src/courses.rs @@ -46,6 +46,8 @@ pub struct Course { pub is_unlisted: bool, pub base_module_completion_requires_n_submodule_completions: i32, pub can_add_chatbot: bool, + pub is_joinable_by_code_only: bool, + pub join_code: Option, } #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] @@ -76,6 +78,8 @@ pub struct NewCourse { pub is_unlisted: bool, /// If true, copies all user permissions from the original course to the new one. pub copy_user_permissions: bool, + pub is_joinable_by_code_only: bool, + pub join_code: Option, } pub async fn insert( @@ -95,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, @@ -106,7 +112,9 @@ VALUES( $6, $7, $8, - $9 + $9, + $10, + $11 ) RETURNING id ", @@ -118,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?; @@ -154,7 +164,9 @@ SELECT id, is_test_mode, base_module_completion_requires_n_submodule_completions, can_add_chatbot, - is_unlisted + is_unlisted, + is_joinable_by_code_only, + join_code FROM courses WHERE deleted_at IS NULL; "# @@ -187,7 +199,9 @@ SELECT id, is_test_mode, is_unlisted, base_module_completion_requires_n_submodule_completions, - can_add_chatbot + can_add_chatbot, + is_joinable_by_code_only, + join_code FROM courses WHERE courses.deleted_at IS NULL AND id IN ( @@ -227,7 +241,9 @@ SELECT id, is_test_mode, can_add_chatbot, is_unlisted, - base_module_completion_requires_n_submodule_completions + base_module_completion_requires_n_submodule_completions, + is_joinable_by_code_only, + join_code FROM courses WHERE courses.deleted_at IS NULL AND ( @@ -279,7 +295,9 @@ SELECT id, is_test_mode, base_module_completion_requires_n_submodule_completions, can_add_chatbot, - is_unlisted + is_unlisted, + is_joinable_by_code_only, + join_code FROM courses WHERE course_language_group_id = $1 AND deleted_at IS NULL @@ -316,7 +334,9 @@ SELECT c.is_test_mode, c.base_module_completion_requires_n_submodule_completions, can_add_chatbot, - c.is_unlisted + c.is_unlisted, + c.is_joinable_by_code_only, + c.join_code FROM courses as c LEFT JOIN course_instances as ci on c.id = ci.course_id WHERE @@ -378,7 +398,9 @@ SELECT id, is_test_mode, can_add_chatbot, is_unlisted, - base_module_completion_requires_n_submodule_completions + base_module_completion_requires_n_submodule_completions, + is_joinable_by_code_only, + join_code FROM courses WHERE id = $1; "#, @@ -482,7 +504,9 @@ SELECT courses.id, courses.is_test_mode, base_module_completion_requires_n_submodule_completions, can_add_chatbot, - courses.is_unlisted + courses.is_unlisted, + courses.is_joinable_by_code_only, + courses.join_code FROM courses WHERE courses.organization_id = $1 AND ( @@ -545,6 +569,7 @@ pub struct CourseUpdate { pub is_test_mode: bool, pub can_add_chatbot: bool, pub is_unlisted: bool, + pub is_joinable_by_code_only: bool, } pub async fn update_course( @@ -561,8 +586,9 @@ SET name = $1, is_draft = $3, is_test_mode = $4, can_add_chatbot = $5, - is_unlisted = $6 -WHERE id = $7 + is_unlisted = $6, + is_joinable_by_code_only = $7 +WHERE id = $8 RETURNING id, name, created_at, @@ -579,7 +605,9 @@ RETURNING id, is_test_mode, can_add_chatbot, is_unlisted, - base_module_completion_requires_n_submodule_completions + base_module_completion_requires_n_submodule_completions, + is_joinable_by_code_only, + join_code "#, course_update.name, course_update.description, @@ -587,6 +615,7 @@ RETURNING id, course_update.is_test_mode, course_update.can_add_chatbot, course_update.is_unlisted, + course_update.is_joinable_by_code_only, course_id ) .fetch_one(conn) @@ -637,7 +666,9 @@ RETURNING id, is_test_mode, can_add_chatbot, is_unlisted, - base_module_completion_requires_n_submodule_completions + base_module_completion_requires_n_submodule_completions, + is_joinable_by_code_only, + join_code "#, course_id ) @@ -666,7 +697,9 @@ SELECT id, is_test_mode, can_add_chatbot, is_unlisted, - base_module_completion_requires_n_submodule_completions + base_module_completion_requires_n_submodule_completions, + is_joinable_by_code_only, + join_code FROM courses WHERE slug = $1 AND deleted_at IS NULL @@ -717,6 +750,20 @@ WHERE id = $1 Ok(res.is_draft) } +pub async fn is_joinable_by_code_only(conn: &mut PgConnection, id: Uuid) -> ModelResult { + let res = sqlx::query!( + " +SELECT is_joinable_by_code_only +FROM courses +WHERE id = $1 +", + id + ) + .fetch_one(conn) + .await?; + Ok(res.is_joinable_by_code_only) +} + pub(crate) async fn get_by_ids( conn: &mut PgConnection, course_ids: &[Uuid], @@ -740,7 +787,9 @@ SELECT id, is_test_mode, can_add_chatbot, is_unlisted, - base_module_completion_requires_n_submodule_completions + base_module_completion_requires_n_submodule_completions, + is_joinable_by_code_only, + join_code FROM courses WHERE id IN (SELECT * FROM UNNEST($1::uuid[])) ", @@ -751,6 +800,62 @@ WHERE id IN (SELECT * FROM UNNEST($1::uuid[])) Ok(courses) } +pub async fn set_join_code_for_course( + conn: &mut PgConnection, + course_id: Uuid, + join_code: String, +) -> ModelResult<()> { + sqlx::query!( + " +UPDATE courses +SET join_code = $2 +WHERE id = $1 +", + course_id, + join_code + ) + .execute(conn) + .await?; + Ok(()) +} + +pub async fn get_course_with_join_code( + conn: &mut PgConnection, + join_code: String, +) -> ModelResult { + let course = sqlx::query_as!( + Course, + r#" +SELECT id, + name, + created_at, + updated_at, + organization_id, + deleted_at, + slug, + content_search_language::text, + language_code, + copied_from, + course_language_group_id, + description, + is_draft, + is_test_mode, + can_add_chatbot, + is_unlisted, + base_module_completion_requires_n_submodule_completions, + is_joinable_by_code_only, + join_code +FROM courses +WHERE join_code = $1 + AND deleted_at IS NULL; + "#, + join_code, + ) + .fetch_one(conn) + .await?; + Ok(course) +} + #[cfg(test)] mod test { use super::*; @@ -852,6 +957,8 @@ mod test { is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, } } } diff --git a/services/headless-lms/models/src/exams.rs b/services/headless-lms/models/src/exams.rs index 6c160fc3a3a5..ec1fd136abda 100644 --- a/services/headless-lms/models/src/exams.rs +++ b/services/headless-lms/models/src/exams.rs @@ -95,7 +95,9 @@ SELECT id, is_test_mode, base_module_completion_requires_n_submodule_completions, can_add_chatbot, - is_unlisted + is_unlisted, + is_joinable_by_code_only, + join_code FROM courses JOIN course_exams ON courses.id = course_exams.course_id WHERE course_exams.exam_id = $1 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..41e76240ec72 --- /dev/null +++ b/services/headless-lms/models/src/join_code_uses.rs @@ -0,0 +1,52 @@ +use crate::prelude::*; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct JoinCodeUses { + pub id: Uuid, + pub user_id: Uuid, + pub course_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_id: Uuid, +) -> ModelResult { + let res = sqlx::query!( + " +INSERT INTO join_code_uses (id, user_id, course_id) +VALUES ($1, $2, $3) +RETURNING id + ", + pkey_policy.into_uuid(), + user_id, + course_id + ) + .fetch_one(conn) + .await?; + Ok(res.id) +} + +pub async fn check_if_user_has_access_to_course( + conn: &mut PgConnection, + user_id: Uuid, + course_id: Uuid, +) -> ModelResult { + let res = sqlx::query!( + " +SELECT id +FROM join_code_uses +WHERE user_id = $1 +AND course_id = $2 + ", + user_id, + course_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/models/src/library/copying.rs b/services/headless-lms/models/src/library/copying.rs index 91779f6c299f..48bf87d37de7 100644 --- a/services/headless-lms/models/src/library/copying.rs +++ b/services/headless-lms/models/src/library/copying.rs @@ -48,9 +48,11 @@ INSERT INTO courses ( is_draft, base_module_completion_requires_n_submodule_completions, can_add_chatbot, - is_unlisted + is_unlisted, + is_joinable_by_code_only, + join_code ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, name, created_at, @@ -67,7 +69,9 @@ RETURNING id, is_test_mode, base_module_completion_requires_n_submodule_completions, can_add_chatbot, - is_unlisted + is_unlisted, + is_joinable_by_code_only, + join_code ", new_course.name, new_course.organization_id, @@ -80,6 +84,8 @@ RETURNING id, parent_course.base_module_completion_requires_n_submodule_completions, parent_course.can_add_chatbot, new_course.is_unlisted, + new_course.is_joinable_by_code_only, + new_course.join_code ) .fetch_one(&mut *tx) .await?; @@ -1164,6 +1170,8 @@ mod tests { is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, } } } diff --git a/services/headless-lms/models/src/test_helper.rs b/services/headless-lms/models/src/test_helper.rs index 0eb30cad5af3..c746eeee78af 100644 --- a/services/headless-lms/models/src/test_helper.rs +++ b/services/headless-lms/models/src/test_helper.rs @@ -174,6 +174,8 @@ macro_rules! insert_data { is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, }, $user, |_, _, _| unimplemented!(), 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..3263b5e5d981 100644 --- a/services/headless-lms/server/src/controllers/main_frontend/courses.rs +++ b/services/headless-lms/server/src/controllers/main_frontend/courses.rs @@ -3,6 +3,7 @@ use chrono::Utc; use domain::csv_export::user_exericse_states_export::UserExerciseStatesExportOperation; use headless_lms_models::suspected_cheaters::{SuspectedCheaters, ThresholdData}; +use rand::Rng; use std::sync::Arc; use headless_lms_utils::strings::is_ietf_language_code_like; @@ -1449,6 +1450,69 @@ async fn teacher_approve_suspected_cheater( token.authorized_ok(web::Json(())) } +/** +POST /courses/:course_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_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_id) + .await?; + token.authorized_ok(web::Json(joined)) +} + +/** + POST /api/v0/main-frontend/courses/:course_id/generate-join-code - Generates a code that is used as a part of URL to join course +*/ +#[instrument(skip(pool))] +async fn set_join_code_for_course( + 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::Course(*id)).await?; + + 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::courses::set_join_code_for_course(&mut conn, *id, code).await?; + token.authorized_ok(HttpResponse::Ok().finish()) +} + +/** +GET /courses/join/:join_code - Gets the course related to join code +*/ +#[instrument(skip(pool))] +async fn get_course_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 = + models::courses::get_course_with_join_code(&mut conn, join_code.to_string()).await?; + + token.authorized_ok(web::Json(course)) +} + /** Add a route for each controller in this module. @@ -1618,5 +1682,17 @@ 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( + "/{course_id}/join-course-with-join-code", + web::post().to(add_user_to_course_with_join_code), + ) + .route( + "/{course_id}/set-join-code", + web::post().to(set_join_code_for_course), + ) + .route( + "/join/{join_code}", + web::get().to(get_course_with_join_code), ); } diff --git a/services/headless-lms/server/src/domain/authorization.rs b/services/headless-lms/server/src/domain/authorization.rs index 5418d785bf9b..f6397bd8e786 100644 --- a/services/headless-lms/server/src/domain/authorization.rs +++ b/services/headless-lms/server/src/domain/authorization.rs @@ -289,9 +289,22 @@ pub async fn authorize_access_to_course_material( )); } authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await? + } else if models::courses::is_joinable_by_code_only(conn, course_id).await? { + if models::join_code_uses::check_if_user_has_access_to_course( + conn, + user_id.unwrap(), + course_id, + ) + .await + .is_err() + { + authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await?; + } + skip_authorize() } else { skip_authorize() }; + Ok(token) } diff --git a/services/headless-lms/server/src/programs/seed/seed_courses.rs b/services/headless-lms/server/src/programs/seed/seed_courses.rs index 08ed9e36c1fd..f03b0b96235c 100644 --- a/services/headless-lms/server/src/programs/seed/seed_courses.rs +++ b/services/headless-lms/server/src/programs/seed/seed_courses.rs @@ -94,6 +94,8 @@ pub async fn seed_sample_course( is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, }; let (course, _front_page, default_instance, default_module) = library::content_management::create_new_course( @@ -2026,6 +2028,8 @@ pub async fn create_glossary_course( is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, }; let (course, _front_page, _default_instance, default_module) = @@ -2150,6 +2154,8 @@ pub async fn seed_cs_course_material( is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, }; let (course, front_page, default_instance, default_module) = library::content_management::create_new_course( @@ -3027,6 +3033,8 @@ pub async fn seed_course_without_submissions( is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, }; let (course, _front_page, _, default_module) = library::content_management::create_new_course( &mut conn, @@ -4432,6 +4440,8 @@ pub async fn seed_peer_review_course_without_submissions( is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, }; let (course, _front_page, _, default_module) = library::content_management::create_new_course( diff --git a/services/headless-lms/server/src/programs/seed/seed_organizations/uh_cs.rs b/services/headless-lms/server/src/programs/seed/seed_organizations/uh_cs.rs index 1802f4b3597b..1dc59ebd0767 100644 --- a/services/headless-lms/server/src/programs/seed/seed_organizations/uh_cs.rs +++ b/services/headless-lms/server/src/programs/seed/seed_organizations/uh_cs.rs @@ -495,6 +495,8 @@ pub async fn seed_organization_uh_cs( is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, }; let (cs_course, _cs_front_page, _cs_default_course_instance, _cs_default_course_module) = library::content_management::create_new_course( 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 de5a92a3f0d0..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 @@ -86,6 +86,8 @@ pub async fn seed_organization_uh_mathstat( is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, }; let ( statistics_course, @@ -132,6 +134,8 @@ pub async fn seed_organization_uh_mathstat( is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, }; library::content_management::create_new_course( &mut conn, @@ -146,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, @@ -180,6 +214,8 @@ pub async fn seed_organization_uh_mathstat( is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, }, true, admin_user_id, diff --git a/services/headless-lms/server/src/test_helper.rs b/services/headless-lms/server/src/test_helper.rs index b666531dadf7..4e18dd90ec7a 100644 --- a/services/headless-lms/server/src/test_helper.rs +++ b/services/headless-lms/server/src/test_helper.rs @@ -188,6 +188,8 @@ macro_rules! insert_data { is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, }, $user, |_, _, _| unimplemented!(), diff --git a/services/headless-lms/server/tests/study_registry_test.rs b/services/headless-lms/server/tests/study_registry_test.rs index f7af82c052b6..410c5d3980f8 100644 --- a/services/headless-lms/server/tests/study_registry_test.rs +++ b/services/headless-lms/server/tests/study_registry_test.rs @@ -162,6 +162,8 @@ async fn insert_data( is_test_mode: false, is_unlisted: false, copy_user_permissions: false, + is_joinable_by_code_only: false, + join_code: None, }, user_1, models_requests::make_spec_fetcher( diff --git a/services/main-frontend/src/components/forms/NewCourseForm.tsx b/services/main-frontend/src/components/forms/NewCourseForm.tsx index 9787d00527eb..2d4a10db6e54 100644 --- a/services/main-frontend/src/components/forms/NewCourseForm.tsx +++ b/services/main-frontend/src/components/forms/NewCourseForm.tsx @@ -84,6 +84,8 @@ const NewCourseForm: React.FC> = ({ is_test_mode: false, is_unlisted: false, copy_user_permissions: copyCourseUserPermissions, + is_joinable_by_code_only: false, + join_code: null, } if (courseId) { await onSubmitDuplicateCourseForm(courseId, newCourse) @@ -118,6 +120,8 @@ const NewCourseForm: React.FC> = ({ is_test_mode: false, is_unlisted: false, copy_user_permissions: copyCourseUserPermissions, + is_joinable_by_code_only: false, + join_code: null, }) setName("") setSlug("") 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 a8141758be58..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 @@ -13,6 +13,7 @@ import { import UpdateCourseForm from "./UpdateCourseForm" import UpdatePeerReviewQueueReviewsReceivedButton from "./UpdatePeerReviewQueueReviewsReceivedButton" +import { setJoinCourseLinkForCourse } from "@/services/backend/courses" import { Course } from "@/shared-module/common/bindings" import Button from "@/shared-module/common/components/Button" import Dialog from "@/shared-module/common/components/Dialog" @@ -77,6 +78,21 @@ const ManageCourse: React.FC> = ({ course, refetc await refetch() } + const setJoinCourseLinkMutation = useToastMutation( + async (courseId: string) => { + await setJoinCourseLinkForCourse(courseId) + }, + { + notify: true, + method: "POST", + }, + { + onSuccess: async () => { + await refetch() + }, + }, + ) + return ( <>
> = ({ course, refetc + {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 0bf7b5dc6b63..9d2f93aa8c1f 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,6 +32,10 @@ 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_joinable_by_code_only, + ) + const [canAddChatbot, setCanAddChatbot] = useState(course.can_add_chatbot) const updateCourseMutation = useToastMutation( @@ -48,6 +52,7 @@ const UpdateCourseForm: React.FC> is_test_mode: testStatus, is_unlisted: unlisted, can_add_chatbot: canAddChatbot, + is_joinable_by_code_only: joinableByCodeOnlyStatus, }) onSubmitForm() }, @@ -89,6 +94,7 @@ const UpdateCourseForm: React.FC> checked={draftStatus} /> + {!draftStatus && ( > /> + + { + setjoinableByCodeOnlyStatus(!joinableByCodeOnlyStatus) + }} + checked={joinableByCodeOnlyStatus} + /> +
+ +
+ )} + + ) +} + +export default JoinCoursePage diff --git a/services/main-frontend/src/services/backend/courses.ts b/services/main-frontend/src/services/backend/courses.ts index 64e79d46cd6d..bac2c4e318e7 100644 --- a/services/main-frontend/src/services/backend/courses.ts +++ b/services/main-frontend/src/services/backend/courses.ts @@ -307,3 +307,17 @@ export const archiveSuspectedCheaters = async (courseId: string, id: string): Pr export const approveSuspectedCheaters = async (courseId: string, id: string): Promise => { await mainFrontendClient.post(`/courses/${courseId}/suspected-cheaters/approve/${id}`) } + +export const fetchCourseWithJoinCode = async (joinCode: string): Promise => { + const response = await mainFrontendClient.get(`/courses/join/${joinCode}`) + 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 5cc45b242a23..0e3214544c58 100644 --- a/shared-module/packages/common/src/bindings.guard.ts +++ b/shared-module/packages/common/src/bindings.guard.ts @@ -1021,7 +1021,9 @@ export function isCourse(obj: unknown): obj is Course { typeof typedObj["is_test_mode"] === "boolean" && typeof typedObj["is_unlisted"] === "boolean" && typeof typedObj["base_module_completion_requires_n_submodule_completions"] === "number" && - typeof typedObj["can_add_chatbot"] === "boolean" + typeof typedObj["can_add_chatbot"] === "boolean" && + typeof typedObj["is_joinable_by_code_only"] === "boolean" && + (typedObj["join_code"] === null || typeof typedObj["join_code"] === "string") ) } @@ -1068,7 +1070,8 @@ export function isCourseUpdate(obj: unknown): obj is CourseUpdate { typeof typedObj["is_draft"] === "boolean" && typeof typedObj["is_test_mode"] === "boolean" && typeof typedObj["can_add_chatbot"] === "boolean" && - typeof typedObj["is_unlisted"] === "boolean" + typeof typedObj["is_unlisted"] === "boolean" && + typeof typedObj["is_joinable_by_code_only"] === "boolean" ) } @@ -1086,7 +1089,9 @@ export function isNewCourse(obj: unknown): obj is NewCourse { typeof typedObj["is_draft"] === "boolean" && typeof typedObj["is_test_mode"] === "boolean" && typeof typedObj["is_unlisted"] === "boolean" && - typeof typedObj["copy_user_permissions"] === "boolean" + typeof typedObj["copy_user_permissions"] === "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 66e9796d0b3f..9c7b1b8b5805 100644 --- a/shared-module/packages/common/src/bindings.ts +++ b/shared-module/packages/common/src/bindings.ts @@ -472,6 +472,8 @@ export interface Course { is_unlisted: boolean 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 { @@ -500,6 +502,7 @@ export interface CourseUpdate { is_test_mode: boolean can_add_chatbot: boolean is_unlisted: boolean + is_joinable_by_code_only: boolean } export interface NewCourse { @@ -514,6 +517,8 @@ export interface NewCourse { is_test_mode: boolean is_unlisted: boolean copy_user_permissions: boolean + is_joinable_by_code_only: boolean + join_code: string | null } export interface EmailTemplate { 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..59129f106362 100644 --- a/shared-module/packages/common/src/locales/en/main-frontend.json +++ b/shared-module/packages/common/src/locales/en/main-frontend.json @@ -48,8 +48,10 @@ "button-text-edit-image": "Edit image", "button-text-edit-page": "Edit page", "button-text-edit-page-details": "Edit page details", + "button-text-enroll-me": "Enroll me", "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", @@ -155,6 +157,7 @@ "disable-generating-certificates": "Disable generating certificates", "disable-sandbox": "Disable sandbox", "do-not-add-duplicate-completions-for-these-users": "Skip these users without adding duplicate completions.", + "do-you-want-to-join-this-course": "Do you want to join this course", "draft": "Draft", "duplicate": "Duplicate", "duration": "Duration", @@ -291,6 +294,7 @@ "instance-opens-at-time": "Instance opens at {{time}}", "invalid-service-info": "Invalid service info", "invalid-url": "Invalid URL", + "joinable-by-code-only": "Joinable by code only", "label-action": "Action", "label-actions": "Actions", "label-add-user": "Add user", 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..1dd7fd4f86ca 100644 --- a/shared-module/packages/common/src/locales/fi/main-frontend.json +++ b/shared-module/packages/common/src/locales/fi/main-frontend.json @@ -48,8 +48,10 @@ "button-text-edit-image": "Muokkaa kuvaa", "button-text-edit-page": "Muokkaa sivua", "button-text-edit-page-details": "Muokkaa sivun tietoja", + "button-text-enroll-me": "Liitä minut", "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", @@ -157,6 +159,7 @@ "disable-generating-certificates": "Estä sertifikaattien luonti", "disable-sandbox": "Poista hiekkalaatikko käytöstä", "do-not-add-duplicate-completions-for-these-users": "Ohita nämä käyttäjät antamatta heille monistuneita suorituksia.", + "do-you-want-to-join-this-course": "Haluatko liittyä tälle kurssille", "draft": "Luonnos", "duplicate": "Monista", "duration": "Kesto", @@ -295,6 +298,7 @@ "instance-opens-at-time": "Kurssiversio aukeaa {{time}}", "invalid-service-info": "Virheellinen palvelun tieto", "invalid-url": "Epäkelpo osoite", + "joinable-by-code-only": "Liittyminen vain koodilla", "label-action": "Toiminta", "label-actions": "Toiminnot", "label-add-user": "Lisää käyttäjä", diff --git a/system-tests/src/tests/join-course-only-by-code.spec.ts b/system-tests/src/tests/join-course-only-by-code.spec.ts new file mode 100644 index 000000000000..a6f0a12318f2 --- /dev/null +++ b/system-tests/src/tests/join-course-only-by-code.spec.ts @@ -0,0 +1,96 @@ +import { BrowserContext, expect, test } from "@playwright/test" + +import { selectCourseInstanceIfPrompted } from "@/utils/courseMaterialActions" + +test.use({ + storageState: "src/states/admin@example.com.json", +}) + +let context1: BrowserContext +let context2: BrowserContext +let context3: BrowserContext + +test.beforeEach(async ({ browser }) => { + context1 = await browser.newContext({ storageState: "src/states/student1@example.com.json" }) + context2 = await browser.newContext({ storageState: "src/states/student2@example.com.json" }) + context3 = await browser.newContext({ storageState: "src/states/teacher@example.com.json" }) +}) + +test.afterEach(async () => { + await context1.close() + await context2.close() + await context3.close() +}) + +test("Join course by code only", async ({}) => { + test.slow() + const student1Page = await context1.newPage() + const student2Page = await context2.newPage() + const teacherPage = await context3.newPage() + + // Check that student can't see to the course + await student1Page.goto("http://project-331.local/org/uh-mathstat/courses/joinable-by-code-only") + await student1Page.getByText("Unauthorized", { exact: true }).waitFor() + + // Go to join page and add student to the course + await student1Page.goto( + "http://project-331.local/join?code=zARvZARjYhESMPVceEgZyJGQZZuUHVVgcUepyzEqzSqCMdbSCDrTaFhkJTxBshWU", + ) + await student1Page.getByRole("heading", { name: "Joinable by code only" }).click() + await student1Page.getByRole("button", { name: "Yes" }).click() + + // Check that student can see the course + await selectCourseInstanceIfPrompted(student1Page) + await student1Page.getByRole("heading", { name: "Welcome to..." }).click() + + // Check that student can't see to the course + await student2Page.goto("http://project-331.local/org/uh-mathstat/courses/joinable-by-code-only") + await student2Page.getByText("Unauthorized", { exact: true }).waitFor() + + // Go to join page and add student to the course + await student2Page.goto( + "http://project-331.local/join?code=zARvZARjYhESMPVceEgZyJGQZZuUHVVgcUepyzEqzSqCMdbSCDrTaFhkJTxBshWU", + ) + await student2Page.getByRole("heading", { name: "Joinable by code only" }).click() + await student2Page.getByRole("button", { name: "Yes" }).click() + + // Check that student can see the course + await selectCourseInstanceIfPrompted(student2Page) + await student2Page.getByRole("heading", { name: "Welcome to..." }).click() + + // Check that teacher can change the course to join by code only + await teacherPage.goto( + "http://project-331.local/manage/courses/049061ba-ac30-49f1-aa9d-b7566dc22b78", + ) + + // Check that generate join code button is not visible if the feature is not enabled + await expect(teacherPage.getByText("Generate join course link", { exact: true })).toBeHidden() + + // Change course to be joinable by code only + await teacherPage.getByRole("button", { name: "Edit", exact: true }).click() + await teacherPage.getByText("Joinable by code only").click() + await teacherPage.getByRole("button", { name: "Update", exact: true }).click() + await expect(teacherPage.getByText("Success", { exact: true })).toBeVisible() + + // Chech that code can be generated + await expect(teacherPage.getByText("/join?code=null")).toBeVisible() + + await teacherPage.getByRole("button", { name: "Generate join course link" }).click() + await expect(teacherPage.getByText("/join?code=null")).toBeHidden() + await expect(teacherPage.getByText("/join?code=")).toBeVisible() + + // Check that teacher can see the course page normally when the feature is enabled + await teacherPage.goto( + "http://project-331.local/org/uh-mathstat/courses/introduction-to-citations", + ) + await selectCourseInstanceIfPrompted(teacherPage) + await teacherPage.getByRole("heading", { name: "Welcome to..." }).click() + + // Change the course back to not joinable by code only + await teacherPage.goto( + "http://project-331.local/manage/courses/049061ba-ac30-49f1-aa9d-b7566dc22b78", + ) + await teacherPage.getByRole("button", { name: "Edit", exact: true }).click() + await teacherPage.getByText("Joinable by code only").click() + await teacherPage.getByRole("button", { name: "Update", exact: true }).click() +}) diff --git a/system-tests/src/tests/peer-reviews/giving-extra-reviews.spec.ts b/system-tests/src/tests/peer-reviews/giving-extra-reviews.spec.ts index 88e4b1ae101a..1b12e17a32e7 100644 --- a/system-tests/src/tests/peer-reviews/giving-extra-reviews.spec.ts +++ b/system-tests/src/tests/peer-reviews/giving-extra-reviews.spec.ts @@ -82,7 +82,7 @@ test.describe("Students should be able to give extra peer reviews to receive pri .getByRole("row", { name: "02364d40-2aac-4763-8a06-" }) .getByRole("button") .click() - await adminPage.getByRole("button", { name: "Course status summary" }).click() + adminPage.getByRole("button", { name: "Course status summary" }).nth(0).click() await adminPage.getByText("Exercise: Can give extra").click() await expect(adminPage.locator("main")).toContainText("Priority: 4") })