diff --git a/services/headless-lms/models/.sqlx/query-628aff63ebc08b7b29ad86df1bb98589cba402715bdce6f60d3ee1b9e358507c.json b/services/headless-lms/models/.sqlx/query-628aff63ebc08b7b29ad86df1bb98589cba402715bdce6f60d3ee1b9e358507c.json new file mode 100644 index 000000000000..51124fc98e09 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-628aff63ebc08b7b29ad86df1bb98589cba402715bdce6f60d3ee1b9e358507c.json @@ -0,0 +1,48 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT DISTINCT ON (ci.id)\n c.id AS course_id,\n c.slug AS course_slug,\n c.name AS course_name,\n c.description AS course_description,\n ci.id AS course_instance_id,\n ci.name AS course_instance_name,\n ci.description AS course_instance_description\nFROM course_instances AS ci\n JOIN course_instance_enrollments AS cie ON ci.id = cie.course_instance_id\n LEFT JOIN courses AS c ON ci.course_id = c.id\n LEFT JOIN exercises AS e ON e.course_id = c.id\n LEFT JOIN exercise_slides AS es ON es.exercise_id = e.id\n LEFT JOIN exercise_tasks AS et ON et.exercise_slide_id = es.id\nWHERE cie.user_id = $1\n AND et.exercise_type = $2\n AND ci.deleted_at IS NULL\n AND cie.deleted_at IS NULL\n AND c.deleted_at IS NULL\n AND e.deleted_at IS NULL\n AND es.deleted_at IS NULL\n AND et.deleted_at IS NULL\n", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "course_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "course_slug", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "course_name", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "course_description", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "course_instance_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "course_instance_name", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "course_instance_description", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": ["Uuid", "Text"] + }, + "nullable": [false, false, false, true, false, true, true] + }, + "hash": "628aff63ebc08b7b29ad86df1bb98589cba402715bdce6f60d3ee1b9e358507c" +} diff --git a/services/headless-lms/models/src/course_instances.rs b/services/headless-lms/models/src/course_instances.rs index 3491ca8ba40b..53ecccf4f927 100644 --- a/services/headless-lms/models/src/course_instances.rs +++ b/services/headless-lms/models/src/course_instances.rs @@ -541,6 +541,45 @@ WHERE cie.user_id = $1 Ok(course_instances) } +pub async fn get_enrolled_course_instances_for_user_with_exercise_type( + conn: &mut PgConnection, + user_id: Uuid, + exercise_type: &str, +) -> ModelResult> { + let course_instances = sqlx::query_as!( + CourseInstanceWithCourseInfo, + r#" +SELECT DISTINCT ON (ci.id) + c.id AS course_id, + c.slug AS course_slug, + c.name AS course_name, + c.description AS course_description, + ci.id AS course_instance_id, + ci.name AS course_instance_name, + ci.description AS course_instance_description +FROM course_instances AS ci + JOIN course_instance_enrollments AS cie ON ci.id = cie.course_instance_id + LEFT JOIN courses AS c ON ci.course_id = c.id + LEFT JOIN exercises AS e ON e.course_id = c.id + LEFT JOIN exercise_slides AS es ON es.exercise_id = e.id + LEFT JOIN exercise_tasks AS et ON et.exercise_slide_id = es.id +WHERE cie.user_id = $1 + AND et.exercise_type = $2 + AND ci.deleted_at IS NULL + AND cie.deleted_at IS NULL + AND c.deleted_at IS NULL + AND e.deleted_at IS NULL + AND es.deleted_at IS NULL + AND et.deleted_at IS NULL +"#, + user_id, + exercise_type, + ) + .fetch_all(conn) + .await?; + Ok(course_instances) +} + /// Deletes submissions, peer reviews, points and etc. for a course and user. Main purpose is for teachers who are testing their course with their own accounts. pub async fn reset_progress_on_course_instance_for_user( conn: &mut PgConnection, @@ -749,7 +788,10 @@ WHERE user_id = $1 #[cfg(test)] mod test { use super::*; - use crate::test_helper::*; + use crate::{ + course_instance_enrollments::NewCourseInstanceEnrollment, exercise_tasks::NewExerciseTask, + test_helper::*, + }; #[tokio::test] async fn allows_only_one_instance_per_course_without_name() { @@ -779,4 +821,56 @@ mod test { .await .unwrap(); } + + #[tokio::test] + async fn gets_enrolled_course_instances_for_user_with_exercise_type() { + insert_data!(:tx, user:user_id, :org, course:course_id, :instance, course_module:_course_module_id, chapter:chapter_id, page:page_id, :exercise, slide:exercise_slide_id); + + // enroll user on course + crate::course_instance_enrollments::insert_enrollment_and_set_as_current( + tx.as_mut(), + NewCourseInstanceEnrollment { + course_id, + user_id, + course_instance_id: instance.id, + }, + ) + .await + .unwrap(); + let course_instances = + get_enrolled_course_instances_for_user_with_exercise_type(tx.as_mut(), user_id, "tmc") + .await + .unwrap(); + assert!( + course_instances.is_empty(), + "user should not be enrolled on any course with tmc exercises" + ); + + // insert tmc exercise task + crate::exercise_tasks::insert( + tx.as_mut(), + PKeyPolicy::Generate, + NewExerciseTask { + assignment: Vec::new(), + exercise_slide_id, + exercise_type: "tmc".to_string(), + model_solution_spec: None, + private_spec: None, + public_spec: None, + order_number: 1, + }, + ) + .await + .unwrap(); + let course_instances = + get_enrolled_course_instances_for_user_with_exercise_type(tx.as_mut(), user_id, "tmc") + .await + .unwrap(); + assert_eq!( + course_instances.len(), + 1, + "user should be enrolled on one course with tmc exercises" + ); + tx.rollback().await; + } } diff --git a/services/headless-lms/models/src/test_helper.rs b/services/headless-lms/models/src/test_helper.rs index 81fc1555c36a..65127ce10fd1 100644 --- a/services/headless-lms/models/src/test_helper.rs +++ b/services/headless-lms/models/src/test_helper.rs @@ -90,7 +90,7 @@ pub const TEST_HELPER_EXERCISE_SERVICE_NAME: &str = "exercise_type"; /// Helper macro that can be used to conveniently insert data that has some prerequisites. /// The macro accepts variable arguments in the following order: /// -/// tx, user, org, course, instance, course_module, page, chapter, exercise, slide, task +/// tx, user, org, course, instance, course_module, chapter, page, exercise, slide, task /// /// Arguments can be given in either of two forms: /// @@ -103,7 +103,7 @@ pub const TEST_HELPER_EXERCISE_SERVICE_NAME: &str = "exercise_type"; /// would use existing variables tx and u to insert and declare variables for an organization and course named org and course. macro_rules! insert_data { // these rules transform individual arguments like "user" into "user: user" - // arg before ; has no name + // arg before potential ; has no name ($($name:ident: $var:ident, )* :$ident:ident, $($tt:tt)*) => { insert_data!($($name: $var, )* $ident: $ident, $($tt)*); }; @@ -291,7 +291,8 @@ macro_rules! insert_data { pub use crate::insert_data; // checks that correct usage of the macro compiles +#[allow(unused)] async fn _test() { - insert_data!(:tx, user:u, org:o, course: c, instance: _instance, course_module: m, chapter: c, :page, exercise: e, :slide, task: task); - println!("{task}") + insert_data!(tx:t, user:u, org:o, course:c, instance:i, course_module:m, chapter:c, page:p, exercise:e, slide:s, task:tsk); + insert_data!(:tx, :user, :org, :course, :instance, :course_module, :chapter, :page, :exercise, :slide, :task); } diff --git a/services/headless-lms/server/src/controllers/langs.rs b/services/headless-lms/server/src/controllers/langs.rs index 94d3ccb05025..36806be99945 100644 --- a/services/headless-lms/server/src/controllers/langs.rs +++ b/services/headless-lms/server/src/controllers/langs.rs @@ -15,7 +15,7 @@ use std::collections::HashSet; /** * GET /api/v0/langs/course-instances * - * Returns the course instances that the user is currently enrolled on. + * Returns the course instances that the user is currently enrolled on that contain TMC exercises. */ #[instrument(skip(pool))] async fn get_course_instances( @@ -25,9 +25,11 @@ async fn get_course_instances( let mut conn = pool.acquire().await?; let course_instances = - models::course_instances::get_enrolled_course_instances_for_user(&mut conn, user.id) - .await? - .convert(); + models::course_instances::get_enrolled_course_instances_for_user_with_exercise_type( + &mut conn, user.id, "tmc", + ) + .await? + .convert(); // if the user is enrolled on the course, they should be able to view it regardless of permissions let token = skip_authorize(); 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 fe74f722624f..98f9d9cd89c4 100644 --- a/services/headless-lms/server/src/programs/seed/seed_courses.rs +++ b/services/headless-lms/server/src/programs/seed/seed_courses.rs @@ -49,6 +49,7 @@ pub struct CommonCourseData { pub organization_id: Uuid, pub admin_user_id: Uuid, pub student_user_id: Uuid, + pub langs_user_id: Uuid, pub example_normal_user_ids: Arc>, pub jwt_key: Arc, pub base_url: String, @@ -65,6 +66,7 @@ pub async fn seed_sample_course( organization_id: org, admin_user_id: admin, student_user_id: student, + langs_user_id, example_normal_user_ids: users, jwt_key, base_url, @@ -1783,6 +1785,15 @@ pub async fn seed_sample_course( ) .await?; } + course_instance_enrollments::insert_enrollment_and_set_as_current( + &mut conn, + NewCourseInstanceEnrollment { + course_id, + course_instance_id: default_instance.id, + user_id: langs_user_id, + }, + ) + .await?; // feedback info!("sample feedback"); @@ -1978,6 +1989,7 @@ pub async fn create_glossary_course( organization_id: org_id, admin_user_id: admin, student_user_id: _, + langs_user_id: _, example_normal_user_ids: _, jwt_key, base_url, @@ -2100,6 +2112,7 @@ pub async fn seed_cs_course_material( db_pool: &Pool, org: Uuid, admin: Uuid, + langs_user_id: Uuid, base_url: String, jwt_key: Arc, ) -> Result { @@ -2119,7 +2132,7 @@ pub async fn seed_cs_course_material( is_test_mode: false, copy_user_permissions: false, }; - let (course, front_page, _default_instance, default_module) = + let (course, front_page, default_instance, default_module) = library::content_management::create_new_course( &mut conn, PKeyPolicy::Fixed(CreateNewCourseFixedIds { @@ -2953,6 +2966,17 @@ pub async fn seed_cs_course_material( ) .await?; + // enrollments + course_instance_enrollments::insert_enrollment_and_set_as_current( + &mut conn, + NewCourseInstanceEnrollment { + course_id: course.id, + course_instance_id: default_instance.id, + user_id: langs_user_id, + }, + ) + .await?; + Ok(course.id) } @@ -4367,6 +4391,7 @@ pub async fn seed_peer_review_course_without_submissions( organization_id: org, admin_user_id: admin, student_user_id: _, + langs_user_id: _, example_normal_user_ids: _, jwt_key, base_url, 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 47a5df80f367..7492a381c8d1 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 @@ -7,9 +7,9 @@ use headless_lms_models::{ course_instances::{self, NewCourseInstance}, course_modules::{self, AutomaticCompletionRequirements, CompletionPolicy}, courses::NewCourse, - library::content_management::CreateNewCourseFixedIds, library::{ self, + content_management::CreateNewCourseFixedIds, progressing::{TeacherManualCompletion, TeacherManualCompletionRequest}, }, open_university_registration_links, organizations, @@ -64,7 +64,7 @@ pub async fn seed_organization_uh_cs( student_3_user_id, student_4_user_id: _, student_5_user_id: _, - langs_user_id: _, + langs_user_id, } = seed_users_result; let _ = seed_file_storage_result; @@ -95,6 +95,7 @@ pub async fn seed_organization_uh_cs( organization_id: uh_cs_organization_id, admin_user_id, student_user_id: student_3_user_id, + langs_user_id, example_normal_user_ids: Arc::new(example_normal_user_ids.clone()), jwt_key: Arc::clone(&jwt_key), base_url: base_url.clone(), @@ -434,6 +435,7 @@ pub async fn seed_organization_uh_cs( &db_pool, uh_cs_organization_id, admin_user_id, + langs_user_id, base_url.clone(), Arc::clone(&jwt_key), ) 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 af45e7bdd83b..cae72e956a3b 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 @@ -50,7 +50,7 @@ pub async fn seed_organization_uh_mathstat( student_3_user_id, student_4_user_id: _, student_5_user_id: _, - langs_user_id: _, + langs_user_id, } = seed_users_result; let _ = seed_file_storage_result; @@ -148,6 +148,7 @@ pub async fn seed_organization_uh_mathstat( organization_id: uh_mathstat_id, admin_user_id, student_user_id: student_3_user_id, + langs_user_id, example_normal_user_ids: Arc::new(example_normal_user_ids.clone()), jwt_key: Arc::clone(&jwt_key), base_url, diff --git a/services/tmc/README.md b/services/tmc/README.md index 306585b5a14d..d595eee6cceb 100644 --- a/services/tmc/README.md +++ b/services/tmc/README.md @@ -1,4 +1,4 @@ -The TestMyCode exercise service is used for programming exercises. +The TestMyCode exercise service is used for programming exercises. Handles exercises of the `"tmc"` exercise type. ## Setup