1pub mod chatbots;
4pub mod stats;
5pub mod students;
6
7use chrono::Utc;
8use domain::csv_export::user_exercise_states_export::UserExerciseStatesExportOperation;
9use headless_lms_chatbot::course_description_summary::SisuDescriptionResponse;
10use headless_lms_models::{
11 application_task_default_language_models::ApplicationTask,
12 course_audiences::CourseAudience,
13 course_prerequisites::CoursePrerequisite,
14 courses::CompleteCourseMetadata,
15 feedback::CategoryFeedbackCount,
16 partner_block::PartnersBlock,
17 proposed_page_edits,
18 suspected_cheaters::{CourseModuleThresholdInfo, SuspectedCheaterStatus, SuspectedCheaters},
19};
20use std::sync::Arc;
21use utoipa::{OpenApi, ToSchema};
22
23use headless_lms_utils::{services::sisu::SisuClient, strings::is_ietf_language_code_like};
24use models::{
25 chapters::Chapter,
26 course_instances::{CourseInstance, CourseInstanceForm, NewCourseInstance},
27 course_module_completions::CourseModuleCompletion,
28 course_modules::ModuleUpdates,
29 courses::{
30 Course, CourseBreadcrumbInfo, CourseMetadata, CourseMetadataUpdate, CourseStructure,
31 CourseUpdate, NewCourse,
32 },
33 exercise_slide_submissions::{
34 self, ExerciseAnswersInCourseRequiringAttentionCount, ExerciseSlideSubmissionCount,
35 ExerciseSlideSubmissionCountByExercise, ExerciseSlideSubmissionCountByWeekAndHour,
36 },
37 exercises::{Exercise, ExerciseStatusSummaryForUser},
38 feedback::{self, Feedback},
39 feedback_categories::{self, FeedbackCategory},
40 glossary::{Term, TermUpdate},
41 library,
42 material_references::{MaterialReference, NewMaterialReference},
43 page_visit_datum_summary_by_courses::PageVisitDatumSummaryByCourse,
44 page_visit_datum_summary_by_courses_countries::PageVisitDatumSummaryByCoursesCountries,
45 page_visit_datum_summary_by_courses_device_types::PageVisitDatumSummaryByCourseDeviceTypes,
46 page_visit_datum_summary_by_pages::PageVisitDatumSummaryByPages,
47 pages::Page,
48 peer_or_self_review_configs::PeerOrSelfReviewConfig,
49 peer_or_self_review_questions::PeerOrSelfReviewQuestion,
50 user_course_settings::UserCourseSettings,
51 user_exercise_states::{ExerciseUserCounts, UserCourseProgress},
52};
53
54use crate::{
55 domain::models_requests::{self, JwtKey},
56 prelude::*,
57};
58
59use headless_lms_models::course_language_groups;
60
61use crate::domain::csv_export::course_instance_export::CourseInstancesExportOperation;
62use crate::domain::csv_export::course_research_form_questions_answers_export::CourseResearchFormExportOperation;
63use crate::domain::csv_export::exercise_tasks_export::CourseExerciseTasksExportOperation;
64use crate::domain::csv_export::general_export;
65use crate::domain::csv_export::submissions::CourseSubmissionExportOperation;
66use crate::domain::csv_export::users_export::UsersExportOperation;
67
68#[derive(OpenApi)]
69#[openapi(
70 paths(
71 get_course,
72 get_course_breadcrumb_info,
73 get_all_exercise_statuses_by_course_id,
74 get_all_course_module_completions_for_user_by_course_id,
75 get_user_progress_for_course,
76 get_user_course_settings,
77 post_reprocess_module_completions,
78 post_new_course,
79 update_course,
80 delete_course,
81 get_course_structure,
82 add_media_for_course,
83 get_all_exercises,
84 get_all_exercises_and_count_of_answers_requiring_attention,
85 get_all_course_language_versions,
86 create_course_copy,
87 get_daily_submission_counts,
88 get_daily_user_counts_with_submissions,
89 get_weekday_hour_submission_counts,
90 get_submission_counts_by_exercise,
91 get_course_instances,
92 get_feedback,
93 get_feedback_categories,
94 get_feedback_edit_proposals_count,
95 new_course_instance,
96 glossary,
97 new_glossary_term,
98 get_course_users_counts_by_exercise,
99 post_new_page_ordering,
100 post_new_chapter_ordering,
101 get_material_references_by_course_id,
102 insert_material_references,
103 update_material_reference,
104 delete_material_reference_by_id,
105 update_modules,
106 get_course_default_peer_review,
107 post_update_peer_review_queue_reviews_received,
108 submission_export,
109 user_details_export,
110 exercise_tasks_export,
111 course_instances_export,
112 course_consent_form_answers_export,
113 user_exercise_states_export,
114 get_page_visit_datum_summary,
115 get_page_visit_datum_summary_by_pages,
116 get_page_visit_datum_summary_by_device_types,
117 get_page_visit_datum_summary_by_countries,
118 teacher_reset_course_progress_for_themselves,
119 teacher_reset_course_progress_for_everyone,
120 get_all_suspected_cheaters,
121 get_flagged_suspected_cheaters_count,
122 get_all_thresholds,
123 teacher_dismiss_suspected_cheater,
124 teacher_confirm_suspected_cheater,
125 add_user_to_course_with_join_code,
126 set_join_code_for_course,
127 get_course_with_join_code,
128 post_partners_block,
129 get_partners_block,
130 delete_partners_block,
131 get_sisu_course_llm_descriptions,
132 update_metadata,
133 get_course_prerequisites,
134 get_course_audiences,
135 get_course_metadata,
136 get_all_courses
137 ),
138 nest(
139 (path = "/{course_id}/chatbots", api = chatbots::MainFrontendCourseChatbotsApiDoc),
140 (path = "/{course_id}/stats", api = stats::MainFrontendCourseStatsApiDoc),
141 (path = "/{course_id}/students", api = students::MainFrontendCourseStudentsApiDoc)
142 )
143)]
144pub(crate) struct MainFrontendCoursesApiDoc;
145
146#[utoipa::path(
150 get,
151 path = "/{course_id}",
152 operation_id = "getCourse",
153 tag = "courses",
154 params(
155 ("course_id" = Uuid, Path, description = "Course id")
156 ),
157 responses(
158 (status = 200, description = "Course", body = Course)
159 )
160)]
161#[instrument(skip(pool))]
162async fn get_course(
163 course_id: web::Path<Uuid>,
164 pool: web::Data<PgPool>,
165 user: AuthUser,
166) -> ControllerResult<web::Json<Course>> {
167 let mut conn = pool.acquire().await?;
168 let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
169 let course = models::courses::get_course(&mut conn, *course_id).await?;
170 token.authorized_ok(web::Json(course))
171}
172
173#[utoipa::path(
177 get,
178 path = "/{course_id}/breadcrumb-info",
179 operation_id = "getCourseBreadcrumbInfo",
180 tag = "courses",
181 params(
182 ("course_id" = Uuid, Path, description = "Course id")
183 ),
184 responses(
185 (status = 200, description = "Course breadcrumb information", body = CourseBreadcrumbInfo)
186 )
187)]
188#[instrument(skip(pool))]
189async fn get_course_breadcrumb_info(
190 course_id: web::Path<Uuid>,
191 pool: web::Data<PgPool>,
192 user: AuthUser,
193) -> ControllerResult<web::Json<CourseBreadcrumbInfo>> {
194 let mut conn = pool.acquire().await?;
195 let user_id = Some(user.id);
196 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
197 let info = models::courses::get_course_breadcrumb_info(&mut conn, *course_id).await?;
198 token.authorized_ok(web::Json(info))
199}
200
201#[utoipa::path(
205 get,
206 path = "/{course_id}/status-for-all-exercises/{user_id}",
207 operation_id = "getCourseExerciseStatusesForUser",
208 tag = "courses",
209 params(
210 ("course_id" = Uuid, Path, description = "Course id"),
211 ("user_id" = Uuid, Path, description = "User id")
212 ),
213 responses(
214 (status = 200, description = "Exercise statuses for course user", body = [ExerciseStatusSummaryForUser])
215 )
216)]
217#[instrument(skip(pool))]
218async fn get_all_exercise_statuses_by_course_id(
219 params: web::Path<(Uuid, Uuid)>,
220 pool: web::Data<PgPool>,
221 user: AuthUser,
222) -> ControllerResult<web::Json<Vec<ExerciseStatusSummaryForUser>>> {
223 let (course_id, user_id) = params.into_inner();
224 let mut conn = pool.acquire().await?;
225 let token = authorize(
226 &mut conn,
227 Act::ViewUserProgressOrDetails,
228 Some(user.id),
229 Res::Course(course_id),
230 )
231 .await?;
232 let res = models::exercises::get_all_exercise_statuses_by_user_id_and_course_id(
233 &mut conn, course_id, user_id,
234 )
235 .await?;
236 token.authorized_ok(web::Json(res))
237}
238
239#[utoipa::path(
243 get,
244 path = "/{course_id}/course-module-completions/{user_id}",
245 operation_id = "getCourseModuleCompletionsForUser",
246 tag = "courses",
247 params(
248 ("course_id" = Uuid, Path, description = "Course id"),
249 ("user_id" = Uuid, Path, description = "User id")
250 ),
251 responses(
252 (status = 200, description = "Course module completions for course user", body = [CourseModuleCompletion])
253 )
254)]
255#[instrument(skip(pool))]
256async fn get_all_course_module_completions_for_user_by_course_id(
257 params: web::Path<(Uuid, Uuid)>,
258 pool: web::Data<PgPool>,
259 user: AuthUser,
260) -> ControllerResult<web::Json<Vec<CourseModuleCompletion>>> {
261 let (course_id, user_id) = params.into_inner();
262 let mut conn = pool.acquire().await?;
263 let token = authorize(
264 &mut conn,
265 Act::ViewUserProgressOrDetails,
266 Some(user.id),
267 Res::Course(course_id),
268 )
269 .await?;
270 let res = models::course_module_completions::get_all_by_course_id_and_user_id(
271 &mut conn, course_id, user_id,
272 )
273 .await?;
274 token.authorized_ok(web::Json(res))
275}
276
277#[utoipa::path(
281 get,
282 path = "/{course_id}/progress/{user_id}",
283 operation_id = "getCourseProgressForUser",
284 tag = "courses",
285 params(
286 ("course_id" = Uuid, Path, description = "Course id"),
287 ("user_id" = Uuid, Path, description = "User id")
288 ),
289 responses(
290 (status = 200, description = "User progress for course", body = [UserCourseProgress])
291 )
292)]
293#[instrument(skip(pool))]
294async fn get_user_progress_for_course(
295 path: web::Path<(Uuid, Uuid)>,
296 pool: web::Data<PgPool>,
297 user: AuthUser,
298) -> ControllerResult<web::Json<Vec<UserCourseProgress>>> {
299 let (course_id, target_user_id) = path.into_inner();
300 let mut conn = pool.acquire().await?;
301 let token = authorize(
302 &mut conn,
303 Act::ViewUserProgressOrDetails,
304 Some(user.id),
305 Res::Course(course_id),
306 )
307 .await?;
308 let user_course_progress = models::user_exercise_states::get_user_course_progress(
309 &mut conn,
310 course_id,
311 target_user_id,
312 false,
313 )
314 .await?;
315 token.authorized_ok(web::Json(user_course_progress))
316}
317
318#[utoipa::path(
322 get,
323 path = "/{course_id}/user-settings/{user_id}",
324 operation_id = "getCourseUserSettingsForUser",
325 tag = "courses",
326 params(
327 ("course_id" = Uuid, Path, description = "Course id"),
328 ("user_id" = Uuid, Path, description = "User id")
329 ),
330 responses(
331 (status = 200, description = "User course settings", body = Option<UserCourseSettings>)
332 )
333)]
334#[instrument(skip(pool))]
335async fn get_user_course_settings(
336 path: web::Path<(Uuid, Uuid)>,
337 pool: web::Data<PgPool>,
338 user: AuthUser,
339) -> ControllerResult<web::Json<Option<UserCourseSettings>>> {
340 let (course_id, target_user_id) = path.into_inner();
341 let mut conn = pool.acquire().await?;
342 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
343 let settings = models::user_course_settings::get_user_course_settings_by_course_id(
344 &mut conn,
345 target_user_id,
346 course_id,
347 )
348 .await?;
349 token.authorized_ok(web::Json(settings))
350}
351
352#[utoipa::path(
358 post,
359 path = "/{course_id}/reprocess-completions",
360 operation_id = "reprocessCourseCompletions",
361 tag = "courses",
362 params(
363 ("course_id" = Uuid, Path, description = "Course id")
364 ),
365 responses(
366 (status = 200, description = "Course completions reprocessed", body = bool)
367 )
368)]
369#[instrument(skip(pool, user))]
370async fn post_reprocess_module_completions(
371 pool: web::Data<PgPool>,
372 user: AuthUser,
373 course_id: web::Path<Uuid>,
374) -> ControllerResult<web::Json<bool>> {
375 let mut conn = pool.acquire().await?;
376 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
377 models::library::progressing::process_all_course_completions(&mut conn, *course_id).await?;
378 token.authorized_ok(web::Json(true))
379}
380
381#[utoipa::path(
398 post,
399 path = "",
400 operation_id = "createCourse",
401 tag = "courses",
402 request_body = NewCourse,
403 responses(
404 (status = 200, description = "Created course", body = Course)
405 )
406)]
407#[instrument(skip(pool, app_conf))]
408async fn post_new_course(
409 request_id: RequestId,
410 pool: web::Data<PgPool>,
411 payload: web::Json<NewCourse>,
412 user: AuthUser,
413 app_conf: web::Data<ApplicationConfiguration>,
414 jwt_key: web::Data<JwtKey>,
415) -> ControllerResult<web::Json<Course>> {
416 let mut conn = pool.acquire().await?;
417 let new_course = payload.0;
418 if !is_ietf_language_code_like(&new_course.language_code) {
419 return Err(ControllerError::new(
420 ControllerErrorType::BadRequest,
421 "Malformed language code.".to_string(),
422 None,
423 ));
424 }
425 let token = authorize(
426 &mut conn,
427 Act::CreateCoursesOrExams,
428 Some(user.id),
429 Res::Organization(new_course.organization_id),
430 )
431 .await?;
432
433 let mut tx = conn.begin().await?;
434 let (course, ..) = library::content_management::create_new_course(
435 &mut tx,
436 app_conf.as_ref(),
437 PKeyPolicy::Generate,
438 new_course,
439 user.id,
440 models_requests::make_spec_fetcher(
441 app_conf.base_url.clone(),
442 request_id.0,
443 Arc::clone(&jwt_key),
444 ),
445 models_requests::fetch_service_info,
446 )
447 .await?;
448 models::roles::insert(
449 &mut tx,
450 user.id,
451 models::roles::UserRole::Teacher,
452 models::roles::RoleDomain::Course(course.id),
453 )
454 .await?;
455 tx.commit().await?;
456
457 token.authorized_ok(web::Json(course))
458}
459
460#[utoipa::path(
476 put,
477 path = "/{course_id}",
478 operation_id = "updateCourse",
479 tag = "courses",
480 params(
481 ("course_id" = Uuid, Path, description = "Course id")
482 ),
483 request_body = CourseUpdate,
484 responses(
485 (status = 200, description = "Updated course", body = Course)
486 )
487)]
488#[instrument(skip(pool, app_conf))]
489async fn update_course(
490 payload: web::Json<CourseUpdate>,
491 app_conf: web::Data<ApplicationConfiguration>,
492 course_id: web::Path<Uuid>,
493 pool: web::Data<PgPool>,
494 user: AuthUser,
495) -> ControllerResult<web::Json<Course>> {
496 let mut conn = pool.acquire().await?;
497 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
498 let course_before_update = models::courses::get_course(&mut conn, *course_id).await?;
499 if payload.can_add_chatbot != course_before_update.can_add_chatbot {
500 authorize(&mut conn, Act::Teach, Some(user.id), Res::GlobalPermissions).await?;
502 }
503
504 let course = models::courses::update_course(
505 &mut conn,
506 app_conf.as_ref(),
507 *course_id,
508 payload.0,
509 user.id,
510 true,
511 )
512 .await?;
513
514 token.authorized_ok(web::Json(course))
515}
516
517#[utoipa::path(
521 delete,
522 path = "/{course_id}",
523 operation_id = "deleteCourse",
524 tag = "courses",
525 params(
526 ("course_id" = Uuid, Path, description = "Course id")
527 ),
528 responses(
529 (status = 200, description = "Deleted course", body = serde_json::Value)
530 )
531)]
532#[instrument(skip(pool))]
533async fn delete_course(
534 course_id: web::Path<Uuid>,
535 pool: web::Data<PgPool>,
536 user: AuthUser,
537) -> ControllerResult<web::Json<Course>> {
538 let mut conn = pool.acquire().await?;
539 let token = authorize(
540 &mut conn,
541 Act::UsuallyUnacceptableDeletion,
542 Some(user.id),
543 Res::Course(*course_id),
544 )
545 .await?;
546 let course = models::courses::delete_course(&mut conn, *course_id).await?;
547
548 token.authorized_ok(web::Json(course))
549}
550
551#[utoipa::path(
598 get,
599 path = "/{course_id}/structure",
600 operation_id = "getCourseStructure",
601 tag = "courses",
602 params(
603 ("course_id" = Uuid, Path, description = "Course id")
604 ),
605 responses(
606 (status = 200, description = "Course structure", body = CourseStructure)
607 )
608)]
609#[instrument(skip(pool, file_store, app_conf))]
610async fn get_course_structure(
611 course_id: web::Path<Uuid>,
612 pool: web::Data<PgPool>,
613 user: AuthUser,
614 file_store: web::Data<dyn FileStore>,
615 app_conf: web::Data<ApplicationConfiguration>,
616) -> ControllerResult<web::Json<CourseStructure>> {
617 let mut conn = pool.acquire().await?;
618 let token = authorize(
619 &mut conn,
620 Act::ViewInternalCourseStructure,
621 Some(user.id),
622 Res::Course(*course_id),
623 )
624 .await?;
625 let course_structure = models::courses::get_course_structure(
626 &mut conn,
627 *course_id,
628 file_store.as_ref(),
629 app_conf.as_ref(),
630 )
631 .await?;
632
633 token.authorized_ok(web::Json(course_structure))
634}
635
636#[utoipa::path(
651 post,
652 path = "/{course_id}/upload",
653 operation_id = "uploadCourseMedia",
654 tag = "courses",
655 params(
656 ("course_id" = Uuid, Path, description = "Course id")
657 ),
658 request_body(
659 content = String,
660 content_type = "multipart/form-data"
661 ),
662 responses(
663 (status = 200, description = "Uploaded media result", body = UploadResult)
664 )
665)]
666#[instrument(skip(payload, request, pool, file_store, app_conf))]
667async fn add_media_for_course(
668 course_id: web::Path<Uuid>,
669 payload: Multipart,
670 request: HttpRequest,
671 pool: web::Data<PgPool>,
672 user: AuthUser,
673 file_store: web::Data<dyn FileStore>,
674 app_conf: web::Data<ApplicationConfiguration>,
675) -> ControllerResult<web::Json<UploadResult>> {
676 let mut conn = pool.acquire().await?;
677 let course = models::courses::get_course(&mut conn, *course_id).await?;
678 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
679 let media_path = upload_file_from_cms(
680 request.headers(),
681 payload,
682 MediaOwner::Course(course.id),
683 file_store.as_ref(),
684 &mut conn,
685 user,
686 )
687 .await?;
688 let download_url = file_store.get_download_url(media_path.as_path(), app_conf.as_ref());
689
690 token.authorized_ok(web::Json(UploadResult { url: download_url }))
691}
692
693#[utoipa::path(
697 get,
698 path = "/{course_id}/exercises",
699 operation_id = "getCourseExercises",
700 tag = "courses",
701 params(
702 ("course_id" = Uuid, Path, description = "Course id")
703 ),
704 responses(
705 (status = 200, description = "Exercises for course", body = [Exercise])
706 )
707)]
708#[instrument(skip(pool))]
709async fn get_all_exercises(
710 pool: web::Data<PgPool>,
711 course_id: web::Path<Uuid>,
712 user: AuthUser,
713) -> ControllerResult<web::Json<Vec<Exercise>>> {
714 let mut conn = pool.acquire().await?;
715 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
716 let exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
717
718 token.authorized_ok(web::Json(exercises))
719}
720
721#[utoipa::path(
725 get,
726 path = "/{course_id}/exercises-and-count-of-answers-requiring-attention",
727 operation_id = "getCourseExercisesAndAnswersRequiringAttentionCounts",
728 tag = "courses",
729 params(
730 ("course_id" = Uuid, Path, description = "Course id")
731 ),
732 responses(
733 (
734 status = 200,
735 description = "Exercises and answer attention counts",
736 body = [ExerciseAnswersInCourseRequiringAttentionCount]
737 )
738 )
739)]
740#[instrument(skip(pool))]
741async fn get_all_exercises_and_count_of_answers_requiring_attention(
742 pool: web::Data<PgPool>,
743 course_id: web::Path<Uuid>,
744 user: AuthUser,
745) -> ControllerResult<web::Json<Vec<ExerciseAnswersInCourseRequiringAttentionCount>>> {
746 let mut conn = pool.acquire().await?;
747 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
748 let _exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
749 let count_of_answers_requiring_attention = models::exercise_slide_submissions::get_count_of_answers_requiring_attention_in_exercise_by_course_id(&mut conn, *course_id).await?;
750 token.authorized_ok(web::Json(count_of_answers_requiring_attention))
751}
752
753#[utoipa::path(
765 get,
766 path = "/{course_id}/language-versions",
767 operation_id = "getCourseLanguageVersions",
768 tag = "courses",
769 params(
770 ("course_id" = Uuid, Path, description = "Course id")
771 ),
772 responses(
773 (status = 200, description = "Course language versions", body = [Course])
774 )
775)]
776#[instrument(skip(pool))]
777async fn get_all_course_language_versions(
778 pool: web::Data<PgPool>,
779 course_id: web::Path<Uuid>,
780 user: AuthUser,
781) -> ControllerResult<web::Json<Vec<Course>>> {
782 let mut conn = pool.acquire().await?;
783 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
784 let course = models::courses::get_course(&mut conn, *course_id).await?;
785 let language_versions =
786 models::courses::get_all_language_versions_of_course(&mut conn, &course).await?;
787
788 token.authorized_ok(web::Json(language_versions))
789}
790
791#[derive(Deserialize, Debug, utoipa::ToSchema)]
792#[serde(tag = "mode", rename_all = "snake_case")]
793pub enum CopyCourseMode {
794 Duplicate,
796 SameLanguageGroup,
798 ExistingLanguageGroup { target_course_id: Uuid },
800 NewLanguageGroup,
802}
803
804#[derive(Deserialize, Debug, utoipa::ToSchema)]
805
806pub struct CopyCourseRequest {
807 #[serde(flatten)]
808 pub new_course: NewCourse,
809 pub mode: CopyCourseMode,
810}
811
812#[utoipa::path(
856 post,
857 path = "/{course_id}/create-copy",
858 operation_id = "createCourseCopy",
859 tag = "courses",
860 params(
861 ("course_id" = Uuid, Path, description = "Course id")
862 ),
863 request_body = CopyCourseRequest,
864 responses(
865 (status = 200, description = "Created course copy", body = Course)
866 )
867)]
868#[instrument(skip(pool))]
869pub async fn create_course_copy(
870 pool: web::Data<PgPool>,
871 course_id: web::Path<Uuid>,
872 payload: web::Json<CopyCourseRequest>,
873 user: AuthUser,
874) -> ControllerResult<web::Json<Course>> {
875 let mut conn = pool.acquire().await?;
876 let token = authorize(
877 &mut conn,
878 Act::Duplicate,
879 Some(user.id),
880 Res::Course(*course_id),
881 )
882 .await?;
883
884 let new_course = payload.new_course.clone();
885 authorize(
886 &mut conn,
887 Act::CreateCoursesOrExams,
888 Some(user.id),
889 Res::Organization(new_course.organization_id),
890 )
891 .await?;
892
893 let mut tx = conn.begin().await?;
894
895 let copied_course = match &payload.mode {
896 CopyCourseMode::Duplicate => {
897 models::library::copying::copy_course(&mut tx, *course_id, &new_course, false, user.id)
898 .await?
899 }
900 CopyCourseMode::SameLanguageGroup => {
901 models::library::copying::copy_course(&mut tx, *course_id, &new_course, true, user.id)
902 .await?
903 }
904 CopyCourseMode::ExistingLanguageGroup { target_course_id } => {
905 let target_course = models::courses::get_course(&mut tx, *target_course_id).await?;
906 authorize(
908 &mut tx,
909 Act::Duplicate,
910 Some(user.id),
911 Res::Course(*target_course_id),
912 )
913 .await?;
914 models::library::copying::copy_course_with_language_group(
915 &mut tx,
916 *course_id,
917 target_course.course_language_group_id,
918 &new_course,
919 user.id,
920 )
921 .await?
922 }
923 CopyCourseMode::NewLanguageGroup => {
924 let new_clg_id = course_language_groups::insert(
925 &mut tx,
926 PKeyPolicy::Generate,
927 new_course.slug.as_str(),
928 )
929 .await?;
930 models::library::copying::copy_course_with_language_group(
931 &mut tx,
932 *course_id,
933 new_clg_id,
934 &new_course,
935 user.id,
936 )
937 .await?
938 }
939 };
940
941 models::roles::insert(
942 &mut tx,
943 user.id,
944 models::roles::UserRole::Teacher,
945 models::roles::RoleDomain::Course(copied_course.id),
946 )
947 .await?;
948
949 tx.commit().await?;
950
951 token.authorized_ok(web::Json(copied_course))
952}
953
954#[utoipa::path(
958 get,
959 path = "/{course_id}/daily-submission-counts",
960 operation_id = "getCourseDailySubmissionCounts",
961 tag = "courses",
962 params(
963 ("course_id" = Uuid, Path, description = "Course id")
964 ),
965 responses(
966 (status = 200, description = "Course daily submission counts", body = [ExerciseSlideSubmissionCount])
967 )
968)]
969#[instrument(skip(pool))]
970async fn get_daily_submission_counts(
971 pool: web::Data<PgPool>,
972 course_id: web::Path<Uuid>,
973 user: AuthUser,
974) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
975 let mut conn = pool.acquire().await?;
976 let token = authorize(
977 &mut conn,
978 Act::ViewStats,
979 Some(user.id),
980 Res::Course(*course_id),
981 )
982 .await?;
983 let course = models::courses::get_course(&mut conn, *course_id).await?;
984 let res =
985 exercise_slide_submissions::get_course_daily_slide_submission_counts(&mut conn, &course)
986 .await?;
987
988 token.authorized_ok(web::Json(res))
989}
990
991#[utoipa::path(
995 get,
996 path = "/{course_id}/daily-users-who-have-submitted-something",
997 operation_id = "getCourseDailyUsersWhoSubmittedSomething",
998 tag = "courses",
999 params(
1000 ("course_id" = Uuid, Path, description = "Course id")
1001 ),
1002 responses(
1003 (status = 200, description = "Course daily user submission counts", body = [ExerciseSlideSubmissionCount])
1004 )
1005)]
1006#[instrument(skip(pool))]
1007async fn get_daily_user_counts_with_submissions(
1008 pool: web::Data<PgPool>,
1009 course_id: web::Path<Uuid>,
1010 user: AuthUser,
1011) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
1012 let mut conn = pool.acquire().await?;
1013 let token = authorize(
1014 &mut conn,
1015 Act::ViewStats,
1016 Some(user.id),
1017 Res::Course(*course_id),
1018 )
1019 .await?;
1020 let course = models::courses::get_course(&mut conn, *course_id).await?;
1021 let res = exercise_slide_submissions::get_course_daily_user_counts_with_submissions(
1022 &mut conn, &course,
1023 )
1024 .await?;
1025
1026 token.authorized_ok(web::Json(res))
1027}
1028
1029#[utoipa::path(
1033 get,
1034 path = "/{course_id}/weekday-hour-submission-counts",
1035 operation_id = "getCourseWeekdayHourSubmissionCounts",
1036 tag = "courses",
1037 params(
1038 ("course_id" = Uuid, Path, description = "Course id")
1039 ),
1040 responses(
1041 (status = 200, description = "Course weekday and hour submission counts", body = [ExerciseSlideSubmissionCountByWeekAndHour])
1042 )
1043)]
1044#[instrument(skip(pool))]
1045async fn get_weekday_hour_submission_counts(
1046 pool: web::Data<PgPool>,
1047 course_id: web::Path<Uuid>,
1048 user: AuthUser,
1049) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByWeekAndHour>>> {
1050 let mut conn = pool.acquire().await?;
1051 let token = authorize(
1052 &mut conn,
1053 Act::ViewStats,
1054 Some(user.id),
1055 Res::Course(*course_id),
1056 )
1057 .await?;
1058 let course = models::courses::get_course(&mut conn, *course_id).await?;
1059 let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_weekday_and_hour(
1060 &mut conn, &course,
1061 )
1062 .await?;
1063
1064 token.authorized_ok(web::Json(res))
1065}
1066
1067#[utoipa::path(
1071 get,
1072 path = "/{course_id}/submission-counts-by-exercise",
1073 operation_id = "getCourseSubmissionCountsByExercise",
1074 tag = "courses",
1075 params(
1076 ("course_id" = Uuid, Path, description = "Course id")
1077 ),
1078 responses(
1079 (status = 200, description = "Course submission counts by exercise", body = [ExerciseSlideSubmissionCountByExercise])
1080 )
1081)]
1082#[instrument(skip(pool))]
1083async fn get_submission_counts_by_exercise(
1084 pool: web::Data<PgPool>,
1085 course_id: web::Path<Uuid>,
1086 user: AuthUser,
1087) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByExercise>>> {
1088 let mut conn = pool.acquire().await?;
1089 let token = authorize(
1090 &mut conn,
1091 Act::ViewStats,
1092 Some(user.id),
1093 Res::Course(*course_id),
1094 )
1095 .await?;
1096 let course = models::courses::get_course(&mut conn, *course_id).await?;
1097 let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_exercise(
1098 &mut conn, &course,
1099 )
1100 .await?;
1101
1102 token.authorized_ok(web::Json(res))
1103}
1104
1105#[utoipa::path(
1109 get,
1110 path = "/{course_id}/course-instances",
1111 operation_id = "getCourseInstances",
1112 tag = "courses",
1113 params(
1114 ("course_id" = Uuid, Path, description = "Course id")
1115 ),
1116 responses(
1117 (status = 200, description = "Course instances", body = [CourseInstance])
1118 )
1119)]
1120#[instrument(skip(pool))]
1121async fn get_course_instances(
1122 pool: web::Data<PgPool>,
1123 course_id: web::Path<Uuid>,
1124 user: AuthUser,
1125) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
1126 let mut conn = pool.acquire().await?;
1127 let token = authorize(
1128 &mut conn,
1129 Act::Teach,
1130 Some(user.id),
1131 Res::Course(*course_id),
1132 )
1133 .await?;
1134 let course_instances =
1135 models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
1136
1137 token.authorized_ok(web::Json(course_instances))
1138}
1139
1140#[derive(Debug, Deserialize)]
1141
1142pub struct GetFeedbackQuery {
1143 read: bool,
1144 #[serde(flatten)]
1145 pagination: Pagination,
1146 category_filter: Option<Uuid>,
1147}
1148
1149#[derive(Debug, Deserialize)]
1150pub struct GetFeedbackCategoriesQuery {
1151 read: bool,
1152}
1153
1154#[utoipa::path(
1158 get,
1159 path = "/{course_id}/feedback",
1160 operation_id = "getCourseFeedback",
1161 tag = "courses",
1162 params(
1163 ("course_id" = String, Path, description = "Course id"),
1164 ("read" = bool, Query, description = "Whether to fetch read feedback"),
1165 ("page" = Option<i64>, Query, description = "Page number"),
1166 ("limit" = Option<i64>, Query, description = "Page size"),
1167 ("category_filter" = Option<Uuid>, Query, description = "Selected category")
1168 ),
1169 responses(
1170 (status = 200, description = "Feedback for the course", body = [Feedback])
1171 )
1172)]
1173#[instrument(skip(pool))]
1174pub async fn get_feedback(
1175 course_id: web::Path<Uuid>,
1176 pool: web::Data<PgPool>,
1177 read: web::Query<GetFeedbackQuery>,
1178 user: AuthUser,
1179) -> ControllerResult<web::Json<Vec<Feedback>>> {
1180 let mut conn = pool.acquire().await?;
1181 let token = authorize(
1182 &mut conn,
1183 Act::Teach,
1184 Some(user.id),
1185 Res::Course(*course_id),
1186 )
1187 .await?;
1188 let feedback = feedback::get_feedback_for_course(
1189 &mut conn,
1190 *course_id,
1191 read.category_filter,
1192 read.read,
1193 read.pagination,
1194 )
1195 .await?;
1196
1197 token.authorized_ok(web::Json(feedback))
1198}
1199
1200#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, ToSchema)]
1201pub struct FeedbackEditProposalCounts {
1202 pub read_feedback: u32,
1203 pub unread_feedback: u32,
1204 pub feedback_categories_counts: Vec<CategoryFeedbackCount>,
1205 pub pending_edits: u32,
1206 pub handled_edits: u32,
1207 pub total_waiting: u32,
1209}
1210
1211#[utoipa::path(
1215 get,
1216 path = "/{course_id}/feedback-count",
1217 operation_id = "getCourseFeedbackCount",
1218 tag = "courses",
1219 params(
1220 ("course_id" = Uuid, Path, description = "Course id")
1221 ),
1222 responses(
1223 (status = 200, description = "Feedback and edit proposal counts for the course", body = FeedbackEditProposalCounts)
1224 )
1225)]
1226#[instrument(skip(pool))]
1227pub async fn get_feedback_edit_proposals_count(
1228 course_id: web::Path<Uuid>,
1229 pool: web::Data<PgPool>,
1230 user: AuthUser,
1231) -> ControllerResult<web::Json<FeedbackEditProposalCounts>> {
1232 let mut conn = pool.acquire().await?;
1233 let token = authorize(
1234 &mut conn,
1235 Act::Teach,
1236 Some(user.id),
1237 Res::Course(*course_id),
1238 )
1239 .await?;
1240
1241 let feedback_count = feedback::get_feedback_count_for_course(&mut conn, *course_id).await?;
1242 let edit_proposal_count =
1243 proposed_page_edits::get_proposal_count_for_course(&mut conn, *course_id).await?;
1244
1245 let res = FeedbackEditProposalCounts {
1246 read_feedback: feedback_count.read,
1247 unread_feedback: feedback_count.unread,
1248 pending_edits: edit_proposal_count.pending,
1249 handled_edits: edit_proposal_count.handled,
1250 total_waiting: feedback_count.unread + edit_proposal_count.pending,
1251 feedback_categories_counts: feedback_count.categories,
1252 };
1253
1254 token.authorized_ok(web::Json(res))
1255}
1256
1257#[utoipa::path(
1261 get,
1262 path = "/{course_id}/feedback-categories",
1263 operation_id = "getCourseFeedbackCategories",
1264 tag = "courses",
1265 params(
1266 ("course_id" = Uuid, Path, description = "Course id"),
1267 ("read" = bool, Query, description = "Categories for read/unread status feedback")
1268 ),
1269 responses(
1270 (status = 200, description = "All feedback categories used in feedback for the course", body = Vec<FeedbackCategory>)
1271 )
1272)]
1273#[instrument(skip(pool))]
1274pub async fn get_feedback_categories(
1275 course_id: web::Path<Uuid>,
1276 read: web::Query<GetFeedbackCategoriesQuery>,
1277 pool: web::Data<PgPool>,
1278 user: AuthUser,
1279) -> ControllerResult<web::Json<Vec<FeedbackCategory>>> {
1280 let mut conn = pool.acquire().await?;
1281 let token = authorize(
1282 &mut conn,
1283 Act::Teach,
1284 Some(user.id),
1285 Res::Course(*course_id),
1286 )
1287 .await?;
1288 let res = feedback_categories::get_all_by_read_status(&mut conn, *course_id, read.read).await?;
1289
1290 token.authorized_ok(web::Json(res))
1291}
1292
1293#[utoipa::path(
1297 post,
1298 path = "/{course_id}/new-course-instance",
1299 operation_id = "createCourseInstance",
1300 tag = "courses",
1301 params(
1302 ("course_id" = Uuid, Path, description = "Course id")
1303 ),
1304 request_body = CourseInstanceForm,
1305 responses(
1306 (status = 200, description = "Created course instance id", body = Uuid)
1307 )
1308)]
1309#[instrument(skip(pool))]
1310async fn new_course_instance(
1311 form: web::Json<CourseInstanceForm>,
1312 course_id: web::Path<Uuid>,
1313 pool: web::Data<PgPool>,
1314 user: AuthUser,
1315) -> ControllerResult<web::Json<Uuid>> {
1316 let mut conn = pool.acquire().await?;
1317 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1318 let form = form.into_inner();
1319 let new = NewCourseInstance {
1320 course_id: *course_id,
1321 name: form.name.as_deref(),
1322 description: form.description.as_deref(),
1323 support_email: form.support_email.as_deref(),
1324 teacher_in_charge_name: &form.teacher_in_charge_name,
1325 teacher_in_charge_email: &form.teacher_in_charge_email,
1326 opening_time: form.opening_time,
1327 closing_time: form.closing_time,
1328 };
1329 let ci = models::course_instances::insert(&mut conn, PKeyPolicy::Generate, new).await?;
1330
1331 token.authorized_ok(web::Json(ci.id))
1332}
1333
1334#[instrument(skip(pool))]
1335#[utoipa::path(
1336 get,
1337 path = "/{course_id}/glossary",
1338 operation_id = "getCourseGlossary",
1339 tag = "glossary",
1340 params(
1341 ("course_id" = Uuid, Path, description = "Course id")
1342 ),
1343 responses(
1344 (status = 200, description = "Glossary terms for the course", body = [Term]),
1345 (status = 401, description = "Authentication required"),
1346 (status = 403, description = "User is not allowed to manage the course glossary")
1347 )
1348)]
1349pub(crate) async fn glossary(
1350 pool: web::Data<PgPool>,
1351 course_id: web::Path<Uuid>,
1352 user: AuthUser,
1353) -> ControllerResult<web::Json<Vec<Term>>> {
1354 let mut conn = pool.acquire().await?;
1355 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1356 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
1357
1358 token.authorized_ok(web::Json(glossary))
1359}
1360
1361#[instrument(skip(pool))]
1364async fn _new_term(
1365 pool: web::Data<PgPool>,
1366 course_id: web::Path<Uuid>,
1367 user: AuthUser,
1368) -> ControllerResult<web::Json<Vec<Term>>> {
1369 let mut conn = pool.acquire().await?;
1370 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1371 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
1372
1373 token.authorized_ok(web::Json(glossary))
1374}
1375
1376#[instrument(skip(pool))]
1377#[utoipa::path(
1378 post,
1379 path = "/{course_id}/glossary",
1380 operation_id = "createCourseGlossaryTerm",
1381 tag = "glossary",
1382 params(
1383 ("course_id" = Uuid, Path, description = "Course id")
1384 ),
1385 request_body = TermUpdate,
1386 responses(
1387 (status = 200, description = "Created glossary term id", body = Uuid),
1388 (status = 401, description = "Authentication required"),
1389 (status = 403, description = "User is not allowed to manage the course glossary")
1390 )
1391)]
1392pub(crate) async fn new_glossary_term(
1393 pool: web::Data<PgPool>,
1394 course_id: web::Path<Uuid>,
1395 new_term: web::Json<TermUpdate>,
1396 user: AuthUser,
1397) -> ControllerResult<web::Json<Uuid>> {
1398 let mut conn = pool.acquire().await?;
1399 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1400 let TermUpdate { term, definition } = new_term.into_inner();
1401 let term = models::glossary::insert(&mut conn, &term, &definition, *course_id).await?;
1402
1403 token.authorized_ok(web::Json(term))
1404}
1405
1406#[utoipa::path(
1410 get,
1411 path = "/{course_id}/course-users-counts-by-exercise",
1412 operation_id = "getCourseUsersCountsByExercise",
1413 tag = "courses",
1414 params(
1415 ("course_id" = Uuid, Path, description = "Course id")
1416 ),
1417 responses(
1418 (status = 200, description = "Course users counts by exercise", body = [ExerciseUserCounts])
1419 )
1420)]
1421#[instrument(skip(pool))]
1422pub async fn get_course_users_counts_by_exercise(
1423 course_id: web::Path<Uuid>,
1424 pool: web::Data<PgPool>,
1425 user: AuthUser,
1426) -> ControllerResult<web::Json<Vec<ExerciseUserCounts>>> {
1427 let mut conn = pool.acquire().await?;
1428 let course_id = course_id.into_inner();
1429 let token = authorize(
1430 &mut conn,
1431 Act::ViewStats,
1432 Some(user.id),
1433 Res::Course(course_id),
1434 )
1435 .await?;
1436
1437 let res =
1438 models::user_exercise_states::get_course_users_counts_by_exercise(&mut conn, course_id)
1439 .await?;
1440
1441 token.authorized_ok(web::Json(res))
1442}
1443
1444#[utoipa::path(
1452 post,
1453 path = "/{course_id}/new-page-ordering",
1454 operation_id = "updateCoursePageOrdering",
1455 tag = "courses",
1456 params(
1457 ("course_id" = Uuid, Path, description = "Course id")
1458 ),
1459 request_body = Vec<Page>,
1460 responses(
1461 (status = 200, description = "Course page ordering updated")
1462 )
1463)]
1464#[instrument(skip(pool))]
1465pub async fn post_new_page_ordering(
1466 course_id: web::Path<Uuid>,
1467 pool: web::Data<PgPool>,
1468 user: AuthUser,
1469 payload: web::Json<Vec<Page>>,
1470) -> ControllerResult<web::Json<()>> {
1471 let mut conn = pool.acquire().await?;
1472 let course_id = course_id.into_inner();
1473 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1474
1475 models::pages::reorder_pages(&mut conn, &payload, course_id).await?;
1476
1477 token.authorized_ok(web::Json(()))
1478}
1479
1480#[utoipa::path(
1486 post,
1487 path = "/{course_id}/new-chapter-ordering",
1488 operation_id = "updateCourseChapterOrdering",
1489 tag = "courses",
1490 params(
1491 ("course_id" = Uuid, Path, description = "Course id")
1492 ),
1493 request_body = Vec<Chapter>,
1494 responses(
1495 (status = 200, description = "Course chapter ordering updated")
1496 )
1497)]
1498#[instrument(skip(pool))]
1499pub async fn post_new_chapter_ordering(
1500 course_id: web::Path<Uuid>,
1501 pool: web::Data<PgPool>,
1502 user: AuthUser,
1503 payload: web::Json<Vec<Chapter>>,
1504) -> ControllerResult<web::Json<()>> {
1505 let mut conn = pool.acquire().await?;
1506 let course_id = course_id.into_inner();
1507 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1508
1509 models::pages::reorder_chapters(&mut conn, &payload, course_id, user.id).await?;
1510
1511 token.authorized_ok(web::Json(()))
1512}
1513
1514#[utoipa::path(
1515 get,
1516 path = "/{course_id}/references",
1517 operation_id = "getCourseReferences",
1518 tag = "courses",
1519 params(
1520 ("course_id" = Uuid, Path, description = "Course id")
1521 ),
1522 responses(
1523 (status = 200, description = "Course references", body = [MaterialReference])
1524 )
1525)]
1526#[instrument(skip(pool))]
1527async fn get_material_references_by_course_id(
1528 course_id: web::Path<Uuid>,
1529 pool: web::Data<PgPool>,
1530 user: AuthUser,
1531) -> ControllerResult<web::Json<Vec<MaterialReference>>> {
1532 let mut conn = pool.acquire().await?;
1533 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1534
1535 let res =
1536 models::material_references::get_references_by_course_id(&mut conn, *course_id).await?;
1537 token.authorized_ok(web::Json(res))
1538}
1539
1540#[utoipa::path(
1541 post,
1542 path = "/{course_id}/references",
1543 operation_id = "createCourseReferences",
1544 tag = "courses",
1545 params(
1546 ("course_id" = Uuid, Path, description = "Course id")
1547 ),
1548 request_body = [NewMaterialReference],
1549 responses(
1550 (status = 200, description = "Course references created")
1551 )
1552)]
1553#[instrument(skip(pool))]
1554async fn insert_material_references(
1555 course_id: web::Path<Uuid>,
1556 payload: web::Json<Vec<NewMaterialReference>>,
1557 pool: web::Data<PgPool>,
1558 user: AuthUser,
1559) -> ControllerResult<web::Json<()>> {
1560 let mut conn = pool.acquire().await?;
1561 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1562
1563 models::material_references::insert_reference(&mut conn, *course_id, payload.0).await?;
1564
1565 token.authorized_ok(web::Json(()))
1566}
1567
1568#[utoipa::path(
1569 post,
1570 path = "/{course_id}/references/{reference_id}",
1571 operation_id = "updateCourseReference",
1572 tag = "courses",
1573 params(
1574 ("course_id" = Uuid, Path, description = "Course id"),
1575 ("reference_id" = Uuid, Path, description = "Reference id")
1576 ),
1577 request_body = NewMaterialReference,
1578 responses(
1579 (status = 200, description = "Course reference updated")
1580 )
1581)]
1582#[instrument(skip(pool))]
1583async fn update_material_reference(
1584 path: web::Path<(Uuid, Uuid)>,
1585 pool: web::Data<PgPool>,
1586 user: AuthUser,
1587 payload: web::Json<NewMaterialReference>,
1588) -> ControllerResult<web::Json<()>> {
1589 let (course_id, reference_id) = path.into_inner();
1590 let mut conn = pool.acquire().await?;
1591 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1592
1593 models::material_references::update_by_id_and_course_id(
1594 &mut conn,
1595 reference_id,
1596 course_id,
1597 payload.0,
1598 )
1599 .await?;
1600 token.authorized_ok(web::Json(()))
1601}
1602
1603#[utoipa::path(
1604 delete,
1605 path = "/{course_id}/references/{reference_id}",
1606 operation_id = "deleteCourseReference",
1607 tag = "courses",
1608 params(
1609 ("course_id" = Uuid, Path, description = "Course id"),
1610 ("reference_id" = Uuid, Path, description = "Reference id")
1611 ),
1612 responses(
1613 (status = 200, description = "Course reference deleted")
1614 )
1615)]
1616#[instrument(skip(pool))]
1617async fn delete_material_reference_by_id(
1618 path: web::Path<(Uuid, Uuid)>,
1619 pool: web::Data<PgPool>,
1620 user: AuthUser,
1621) -> ControllerResult<web::Json<()>> {
1622 let (course_id, reference_id) = path.into_inner();
1623 let mut conn = pool.acquire().await?;
1624 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1625
1626 models::material_references::delete_by_id_and_course_id(&mut conn, reference_id, course_id)
1627 .await?;
1628 token.authorized_ok(web::Json(()))
1629}
1630
1631#[utoipa::path(
1632 post,
1633 path = "/{course_id}/course-modules",
1634 operation_id = "updateCourseModules",
1635 tag = "courses",
1636 params(
1637 ("course_id" = Uuid, Path, description = "Course id")
1638 ),
1639 request_body = ModuleUpdates,
1640 responses(
1641 (status = 200, description = "Course modules updated"),
1642 (status = 400, description = "A module or chapter is not in the course, or a completion registration link is not https")
1643 )
1644)]
1645#[instrument(skip(pool))]
1646pub async fn update_modules(
1647 course_id: web::Path<Uuid>,
1648 pool: web::Data<PgPool>,
1649 user: AuthUser,
1650 payload: web::Json<ModuleUpdates>,
1651) -> ControllerResult<web::Json<()>> {
1652 let mut conn = pool.acquire().await?;
1653 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1654
1655 models::course_modules::update_modules(&mut conn, *course_id, payload.into_inner(), user.id)
1656 .await?;
1657 token.authorized_ok(web::Json(()))
1658}
1659
1660#[utoipa::path(
1661 get,
1662 path = "/{course_id}/default-peer-review",
1663 operation_id = "getCourseDefaultPeerReview",
1664 tag = "courses",
1665 params(
1666 ("course_id" = Uuid, Path, description = "Course id")
1667 ),
1668 responses(
1669 (status = 200, description = "Default peer review configuration", body = serde_json::Value)
1670 )
1671)]
1672async fn get_course_default_peer_review(
1673 course_id: web::Path<Uuid>,
1674 pool: web::Data<PgPool>,
1675 user: AuthUser,
1676) -> ControllerResult<web::Json<(PeerOrSelfReviewConfig, Vec<PeerOrSelfReviewQuestion>)>> {
1677 let mut conn = pool.acquire().await?;
1678 let token = authorize(
1679 &mut conn,
1680 Act::Teach,
1681 Some(user.id),
1682 Res::Course(*course_id),
1683 )
1684 .await?;
1685
1686 let peer_review = models::peer_or_self_review_configs::get_default_for_course_by_course_id(
1687 &mut conn, *course_id,
1688 )
1689 .await?;
1690 let peer_or_self_review_questions =
1691 models::peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
1692 &mut conn,
1693 peer_review.id,
1694 )
1695 .await?;
1696 token.authorized_ok(web::Json((peer_review, peer_or_self_review_questions)))
1697}
1698
1699#[utoipa::path(
1705 post,
1706 path = "/{course_id}/update-peer-review-queue-reviews-received",
1707 operation_id = "updateCoursePeerReviewQueueReviewsReceived",
1708 tag = "courses",
1709 params(
1710 ("course_id" = Uuid, Path, description = "Course id")
1711 ),
1712 responses(
1713 (status = 200, description = "Peer review queue updated", body = bool)
1714 )
1715)]
1716#[instrument(skip(pool, user))]
1717async fn post_update_peer_review_queue_reviews_received(
1718 pool: web::Data<PgPool>,
1719 user: AuthUser,
1720 course_id: web::Path<Uuid>,
1721) -> ControllerResult<web::Json<bool>> {
1722 let mut conn = pool.acquire().await?;
1723 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
1724 models::library::peer_or_self_reviewing::update_peer_review_queue_reviews_received(
1725 &mut conn, *course_id,
1726 )
1727 .await?;
1728 token.authorized_ok(web::Json(true))
1729}
1730
1731#[utoipa::path(
1737 get,
1738 path = "/{course_id}/export-submissions",
1739 operation_id = "exportCourseSubmissionsCsv",
1740 tag = "courses",
1741 params(
1742 ("course_id" = Uuid, Path, description = "Course id")
1743 ),
1744 responses(
1745 (status = 200, description = "Course submissions CSV", body = String, content_type = "text/csv")
1746 )
1747)]
1748#[instrument(skip(pool, file_store, app_conf))]
1749pub async fn submission_export(
1750 course_id: web::Path<Uuid>,
1751 pool: web::Data<PgPool>,
1752 user: AuthUser,
1753 file_store: web::Data<dyn FileStore>,
1754 app_conf: web::Data<ApplicationConfiguration>,
1755) -> ControllerResult<HttpResponse> {
1756 let mut conn = pool.acquire().await?;
1757
1758 let token = authorize(
1759 &mut conn,
1760 Act::Teach,
1761 Some(user.id),
1762 Res::Course(*course_id),
1763 )
1764 .await?;
1765
1766 let course = models::courses::get_course(&mut conn, *course_id).await?;
1767
1768 general_export(
1769 pool,
1770 &format!(
1771 "attachment; filename=\"Course: {} - Submissions (exercise tasks) {}.csv\"",
1772 course.name,
1773 Utc::now().format("%Y-%m-%d")
1774 ),
1775 CourseSubmissionExportOperation {
1776 course_id: *course_id,
1777 file_store,
1778 app_conf,
1779 },
1780 token,
1781 )
1782 .await
1783}
1784
1785#[utoipa::path(
1791 get,
1792 path = "/{course_id}/export-user-details",
1793 operation_id = "exportCourseUserDetailsCsv",
1794 tag = "courses",
1795 params(
1796 ("course_id" = Uuid, Path, description = "Course id")
1797 ),
1798 responses(
1799 (status = 200, description = "Course user details CSV", body = String, content_type = "text/csv")
1800 )
1801)]
1802#[instrument(skip(pool))]
1803pub async fn user_details_export(
1804 course_id: web::Path<Uuid>,
1805 pool: web::Data<PgPool>,
1806 user: AuthUser,
1807) -> ControllerResult<HttpResponse> {
1808 let mut conn = pool.acquire().await?;
1809
1810 let token = authorize(
1811 &mut conn,
1812 Act::Teach,
1813 Some(user.id),
1814 Res::Course(*course_id),
1815 )
1816 .await?;
1817
1818 let course = models::courses::get_course(&mut conn, *course_id).await?;
1819
1820 general_export(
1821 pool,
1822 &format!(
1823 "attachment; filename=\"Course: {} - User Details {}.csv\"",
1824 course.name,
1825 Utc::now().format("%Y-%m-%d")
1826 ),
1827 UsersExportOperation {
1828 course_id: *course_id,
1829 },
1830 token,
1831 )
1832 .await
1833}
1834
1835#[utoipa::path(
1841 get,
1842 path = "/{course_id}/export-exercise-tasks",
1843 operation_id = "exportCourseExerciseTasksCsv",
1844 tag = "courses",
1845 params(
1846 ("course_id" = Uuid, Path, description = "Course id")
1847 ),
1848 responses(
1849 (status = 200, description = "Course exercise tasks CSV", body = String, content_type = "text/csv")
1850 )
1851)]
1852#[instrument(skip(pool))]
1853pub async fn exercise_tasks_export(
1854 course_id: web::Path<Uuid>,
1855 pool: web::Data<PgPool>,
1856 user: AuthUser,
1857) -> ControllerResult<HttpResponse> {
1858 let mut conn = pool.acquire().await?;
1859
1860 let token = authorize(
1861 &mut conn,
1862 Act::Teach,
1863 Some(user.id),
1864 Res::Course(*course_id),
1865 )
1866 .await?;
1867
1868 let course = models::courses::get_course(&mut conn, *course_id).await?;
1869
1870 general_export(
1871 pool,
1872 &format!(
1873 "attachment; filename=\"Course: {} - Exercise tasks {}.csv\"",
1874 course.name,
1875 Utc::now().format("%Y-%m-%d")
1876 ),
1877 CourseExerciseTasksExportOperation {
1878 course_id: *course_id,
1879 },
1880 token,
1881 )
1882 .await
1883}
1884
1885#[utoipa::path(
1891 get,
1892 path = "/{course_id}/export-course-instances",
1893 operation_id = "exportCourseInstancesCsv",
1894 tag = "courses",
1895 params(
1896 ("course_id" = Uuid, Path, description = "Course id")
1897 ),
1898 responses(
1899 (status = 200, description = "Course instances CSV", body = String, content_type = "text/csv")
1900 )
1901)]
1902#[instrument(skip(pool))]
1903pub async fn course_instances_export(
1904 course_id: web::Path<Uuid>,
1905 pool: web::Data<PgPool>,
1906 user: AuthUser,
1907) -> ControllerResult<HttpResponse> {
1908 let mut conn = pool.acquire().await?;
1909
1910 let token = authorize(
1911 &mut conn,
1912 Act::Teach,
1913 Some(user.id),
1914 Res::Course(*course_id),
1915 )
1916 .await?;
1917
1918 let course = models::courses::get_course(&mut conn, *course_id).await?;
1919
1920 general_export(
1921 pool,
1922 &format!(
1923 "attachment; filename=\"Course: {} - Instances {}.csv\"",
1924 course.name,
1925 Utc::now().format("%Y-%m-%d")
1926 ),
1927 CourseInstancesExportOperation {
1928 course_id: *course_id,
1929 },
1930 token,
1931 )
1932 .await
1933}
1934
1935#[utoipa::path(
1941 get,
1942 path = "/{course_id}/export-course-user-consents",
1943 operation_id = "exportCourseUserConsentsCsv",
1944 tag = "courses",
1945 params(
1946 ("course_id" = Uuid, Path, description = "Course id")
1947 ),
1948 responses(
1949 (status = 200, description = "Course user consents CSV", body = String, content_type = "text/csv")
1950 )
1951)]
1952#[instrument(skip(pool))]
1953pub async fn course_consent_form_answers_export(
1954 course_id: web::Path<Uuid>,
1955 pool: web::Data<PgPool>,
1956 user: AuthUser,
1957) -> ControllerResult<HttpResponse> {
1958 let mut conn = pool.acquire().await?;
1959
1960 let token = authorize(
1961 &mut conn,
1962 Act::Teach,
1963 Some(user.id),
1964 Res::Course(*course_id),
1965 )
1966 .await?;
1967
1968 let course = models::courses::get_course(&mut conn, *course_id).await?;
1969
1970 general_export(
1971 pool,
1972 &format!(
1973 "attachment; filename=\"Course: {} - User Consents {}.csv\"",
1974 course.name,
1975 Utc::now().format("%Y-%m-%d")
1976 ),
1977 CourseResearchFormExportOperation {
1978 course_id: *course_id,
1979 },
1980 token,
1981 )
1982 .await
1983}
1984
1985#[utoipa::path(
1991 get,
1992 path = "/{course_id}/export-user-exercise-states",
1993 operation_id = "exportCourseUserExerciseStatesCsv",
1994 tag = "courses",
1995 params(
1996 ("course_id" = Uuid, Path, description = "Course id")
1997 ),
1998 responses(
1999 (status = 200, description = "Course user exercise states CSV", body = String, content_type = "text/csv")
2000 )
2001)]
2002#[instrument(skip(pool))]
2003pub async fn user_exercise_states_export(
2004 course_id: web::Path<Uuid>,
2005 pool: web::Data<PgPool>,
2006 user: AuthUser,
2007) -> ControllerResult<HttpResponse> {
2008 let mut conn = pool.acquire().await?;
2009
2010 let token = authorize(
2011 &mut conn,
2012 Act::Teach,
2013 Some(user.id),
2014 Res::Course(*course_id),
2015 )
2016 .await?;
2017
2018 let course = models::courses::get_course(&mut conn, *course_id).await?;
2019
2020 general_export(
2021 pool,
2022 &format!(
2023 "attachment; filename=\"Course: {} - User exercise states {}.csv\"",
2024 course.name,
2025 Utc::now().format("%Y-%m-%d")
2026 ),
2027 UserExerciseStatesExportOperation {
2028 course_id: *course_id,
2029 },
2030 token,
2031 )
2032 .await
2033}
2034
2035#[utoipa::path(
2039 get,
2040 path = "/{course_id}/page-visit-datum-summary",
2041 operation_id = "getCoursePageVisitDatumSummary",
2042 tag = "courses",
2043 params(
2044 ("course_id" = Uuid, Path, description = "Course id")
2045 ),
2046 responses(
2047 (status = 200, description = "Course page visit summary", body = [PageVisitDatumSummaryByCourse])
2048 )
2049)]
2050pub async fn get_page_visit_datum_summary(
2051 course_id: web::Path<Uuid>,
2052 pool: web::Data<PgPool>,
2053 user: AuthUser,
2054) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourse>>> {
2055 let mut conn = pool.acquire().await?;
2056 let course_id = course_id.into_inner();
2057 let token = authorize(
2058 &mut conn,
2059 Act::ViewStats,
2060 Some(user.id),
2061 Res::Course(course_id),
2062 )
2063 .await?;
2064
2065 let res = models::page_visit_datum_summary_by_courses::get_all_for_course(&mut conn, course_id)
2066 .await?;
2067
2068 token.authorized_ok(web::Json(res))
2069}
2070
2071#[utoipa::path(
2075 get,
2076 path = "/{course_id}/page-visit-datum-summary-by-pages",
2077 operation_id = "getCoursePageVisitDatumSummaryByPages",
2078 tag = "courses",
2079 params(
2080 ("course_id" = Uuid, Path, description = "Course id")
2081 ),
2082 responses(
2083 (status = 200, description = "Course page visit summary by pages", body = [PageVisitDatumSummaryByPages])
2084 )
2085)]
2086pub async fn get_page_visit_datum_summary_by_pages(
2087 course_id: web::Path<Uuid>,
2088 pool: web::Data<PgPool>,
2089 user: AuthUser,
2090) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByPages>>> {
2091 let mut conn = pool.acquire().await?;
2092 let course_id = course_id.into_inner();
2093 let token = authorize(
2094 &mut conn,
2095 Act::ViewStats,
2096 Some(user.id),
2097 Res::Course(course_id),
2098 )
2099 .await?;
2100
2101 let res =
2102 models::page_visit_datum_summary_by_pages::get_all_for_course(&mut conn, course_id).await?;
2103
2104 token.authorized_ok(web::Json(res))
2105}
2106
2107#[utoipa::path(
2111 get,
2112 path = "/{course_id}/page-visit-datum-summary-by-device-types",
2113 operation_id = "getCoursePageVisitDatumSummaryByDeviceTypes",
2114 tag = "courses",
2115 params(
2116 ("course_id" = Uuid, Path, description = "Course id")
2117 ),
2118 responses(
2119 (status = 200, description = "Course page visit summary by device types", body = [PageVisitDatumSummaryByCourseDeviceTypes])
2120 )
2121)]
2122pub async fn get_page_visit_datum_summary_by_device_types(
2123 course_id: web::Path<Uuid>,
2124 pool: web::Data<PgPool>,
2125 user: AuthUser,
2126) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourseDeviceTypes>>> {
2127 let mut conn = pool.acquire().await?;
2128 let course_id = course_id.into_inner();
2129 let token = authorize(
2130 &mut conn,
2131 Act::ViewStats,
2132 Some(user.id),
2133 Res::Course(course_id),
2134 )
2135 .await?;
2136
2137 let res = models::page_visit_datum_summary_by_courses_device_types::get_all_for_course(
2138 &mut conn, course_id,
2139 )
2140 .await?;
2141
2142 token.authorized_ok(web::Json(res))
2143}
2144
2145#[utoipa::path(
2149 get,
2150 path = "/{course_id}/page-visit-datum-summary-by-countries",
2151 operation_id = "getCoursePageVisitDatumSummaryByCountries",
2152 tag = "courses",
2153 params(
2154 ("course_id" = Uuid, Path, description = "Course id")
2155 ),
2156 responses(
2157 (status = 200, description = "Course page visit summary by countries", body = [PageVisitDatumSummaryByCoursesCountries])
2158 )
2159)]
2160pub async fn get_page_visit_datum_summary_by_countries(
2161 course_id: web::Path<Uuid>,
2162 pool: web::Data<PgPool>,
2163 user: AuthUser,
2164) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCoursesCountries>>> {
2165 let mut conn = pool.acquire().await?;
2166 let course_id = course_id.into_inner();
2167 let token = authorize(
2168 &mut conn,
2169 Act::ViewStats,
2170 Some(user.id),
2171 Res::Course(course_id),
2172 )
2173 .await?;
2174
2175 let res = models::page_visit_datum_summary_by_courses_countries::get_all_for_course(
2176 &mut conn, course_id,
2177 )
2178 .await?;
2179
2180 token.authorized_ok(web::Json(res))
2181}
2182
2183#[utoipa::path(
2189 delete,
2190 path = "/{course_id}/teacher-reset-course-progress-for-themselves",
2191 operation_id = "resetCourseProgressForTeacherThemselves",
2192 tag = "courses",
2193 params(
2194 ("course_id" = Uuid, Path, description = "Course id")
2195 ),
2196 responses(
2197 (status = 200, description = "Teacher course progress reset", body = bool)
2198 )
2199)]
2200pub async fn teacher_reset_course_progress_for_themselves(
2201 course_id: web::Path<Uuid>,
2202 pool: web::Data<PgPool>,
2203 user: AuthUser,
2204) -> ControllerResult<web::Json<bool>> {
2205 let mut conn = pool.acquire().await?;
2206 let course_id = course_id.into_inner();
2207 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2208
2209 let mut tx = conn.begin().await?;
2210 let course_instances =
2211 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2212 for course_instance in course_instances {
2213 models::course_instances::reset_progress_on_course_instance_for_user(
2214 &mut tx,
2215 user.id,
2216 course_instance.course_id,
2217 )
2218 .await?;
2219 }
2220
2221 tx.commit().await?;
2222 token.authorized_ok(web::Json(true))
2223}
2224
2225#[utoipa::path(
2231 delete,
2232 path = "/{course_id}/teacher-reset-course-progress-for-everyone",
2233 operation_id = "resetCourseProgressForEveryone",
2234 tag = "courses",
2235 params(
2236 ("course_id" = Uuid, Path, description = "Course id")
2237 ),
2238 responses(
2239 (status = 200, description = "Course progress reset for everyone", body = bool)
2240 )
2241)]
2242pub async fn teacher_reset_course_progress_for_everyone(
2243 course_id: web::Path<Uuid>,
2244 pool: web::Data<PgPool>,
2245 user: AuthUser,
2246) -> ControllerResult<web::Json<bool>> {
2247 let mut conn = pool.acquire().await?;
2248 let course_id = course_id.into_inner();
2249 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2250 let course = models::courses::get_course(&mut conn, course_id).await?;
2251 if !course.is_draft {
2252 return Err(ControllerError::new(
2253 ControllerErrorType::BadRequest,
2254 "Can only reset progress for a draft course.".to_string(),
2255 None,
2256 ));
2257 }
2258 let n_course_module_completions =
2260 models::course_module_completions::get_count_of_distinct_completors_by_course_id(
2261 &mut conn, course_id,
2262 )
2263 .await?;
2264 let n_completions_registered_to_study_registry = models::course_module_completion_registered_to_study_registries::get_count_of_distinct_users_with_registrations_by_course_id(
2265 &mut conn, course_id,
2266 ).await?;
2267 if n_course_module_completions > 200 {
2268 return Err(ControllerError::new(
2269 ControllerErrorType::BadRequest,
2270 "Too many students have completed the course.".to_string(),
2271 None,
2272 ));
2273 }
2274 if n_completions_registered_to_study_registry > 2 {
2275 return Err(ControllerError::new(
2276 ControllerErrorType::BadRequest,
2277 "Too many students have registered their completion to a study registry".to_string(),
2278 None,
2279 ));
2280 }
2281
2282 let mut tx = conn.begin().await?;
2283 let course_instances =
2284 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2285
2286 for course_instance in course_instances {
2288 let users_in_course_instance =
2289 models::users::get_users_by_course_instance_enrollment(&mut tx, course_instance.id)
2290 .await?;
2291 for user_in_course_instance in users_in_course_instance {
2292 models::course_instances::reset_progress_on_course_instance_for_user(
2293 &mut tx,
2294 user_in_course_instance.id,
2295 course_instance.course_id,
2296 )
2297 .await?;
2298 }
2299 }
2300
2301 tx.commit().await?;
2302 token.authorized_ok(web::Json(true))
2303}
2304
2305#[derive(Debug, Deserialize)]
2306
2307pub struct GetSuspectedCheatersQuery {
2308 status: SuspectedCheaterStatus,
2309}
2310
2311#[utoipa::path(
2315 get,
2316 path = "/{course_id}/suspected-cheaters",
2317 operation_id = "getCourseSuspectedCheaters",
2318 tag = "courses",
2319 params(
2320 ("course_id" = Uuid, Path, description = "Course id"),
2321 ("status" = SuspectedCheaterStatus, Query, description = "Which review state of suspected cheaters to fetch")
2322 ),
2323 responses(
2324 (status = 200, description = "Suspected cheaters for course", body = [SuspectedCheaters])
2325 )
2326)]
2327#[instrument(skip(pool))]
2328async fn get_all_suspected_cheaters(
2329 user: AuthUser,
2330 params: web::Path<Uuid>,
2331 query: web::Query<GetSuspectedCheatersQuery>,
2332 pool: web::Data<PgPool>,
2333) -> ControllerResult<web::Json<Vec<SuspectedCheaters>>> {
2334 let course_id = params.into_inner();
2335
2336 let mut conn = pool.acquire().await?;
2337 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2338
2339 let course_cheaters = models::suspected_cheaters::get_all_suspected_cheaters_in_course(
2340 &mut conn,
2341 course_id,
2342 query.status,
2343 )
2344 .await?;
2345
2346 token.authorized_ok(web::Json(course_cheaters))
2347}
2348
2349#[utoipa::path(
2353 get,
2354 path = "/{course_id}/suspected-cheaters/flagged-count",
2355 operation_id = "getCourseFlaggedSuspectedCheatersCount",
2356 tag = "courses",
2357 params(
2358 ("course_id" = Uuid, Path, description = "Course id")
2359 ),
2360 responses(
2361 (status = 200, description = "Number of suspected cheaters awaiting review", body = i64, content_type = "application/json")
2362 )
2363)]
2364#[instrument(skip(pool))]
2365async fn get_flagged_suspected_cheaters_count(
2366 user: AuthUser,
2367 params: web::Path<Uuid>,
2368 pool: web::Data<PgPool>,
2369) -> ControllerResult<web::Json<i64>> {
2370 let course_id = params.into_inner();
2371
2372 let mut conn = pool.acquire().await?;
2373 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2374
2375 let count = models::suspected_cheaters::get_count_in_course_by_status(
2376 &mut conn,
2377 course_id,
2378 SuspectedCheaterStatus::Flagged,
2379 )
2380 .await?;
2381
2382 token.authorized_ok(web::Json(count))
2383}
2384
2385#[utoipa::path(
2389 get,
2390 path = "/{course_id}/thresholds",
2391 operation_id = "getCourseThresholds",
2392 tag = "courses",
2393 params(
2394 ("course_id" = Uuid, Path, description = "Course id")
2395 ),
2396 responses(
2397 (status = 200, description = "Course thresholds", body = Vec<CourseModuleThresholdInfo>)
2398 )
2399)]
2400#[instrument(skip(pool))]
2401async fn get_all_thresholds(
2402 user: AuthUser,
2403 params: web::Path<Uuid>,
2404 pool: web::Data<PgPool>,
2405) -> ControllerResult<web::Json<Vec<CourseModuleThresholdInfo>>> {
2406 let mut conn = pool.acquire().await?;
2407 let course_id = params.into_inner();
2408
2409 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2410
2411 let thresholds =
2412 models::suspected_cheaters::get_threshold_info_for_course(&mut conn, course_id).await?;
2413
2414 token.authorized_ok(web::Json(thresholds))
2415}
2416
2417#[utoipa::path(
2421 post,
2422 path = "/{course_id}/suspected-cheaters/dismiss/{user_id}",
2423 operation_id = "dismissCourseSuspectedCheater",
2424 tag = "courses",
2425 params(
2426 ("course_id" = Uuid, Path, description = "Course id"),
2427 ("user_id" = Uuid, Path, description = "Suspected cheater's user id")
2428 ),
2429 responses(
2430 (status = 200, description = "Suspicion dismissed")
2431 )
2432)]
2433#[instrument(skip(pool))]
2434async fn teacher_dismiss_suspected_cheater(
2435 user: AuthUser,
2436 path: web::Path<(Uuid, Uuid)>,
2437 pool: web::Data<PgPool>,
2438) -> ControllerResult<web::Json<()>> {
2439 let (course_id, user_id) = path.into_inner();
2440
2441 let mut conn = pool.acquire().await?;
2442 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2443
2444 models::suspected_cheaters::dismiss_by_user_id_and_course_id(&mut conn, user_id, course_id)
2445 .await?;
2446
2447 token.authorized_ok(web::Json(()))
2448}
2449
2450#[utoipa::path(
2454 post,
2455 path = "/{course_id}/suspected-cheaters/confirm/{user_id}",
2456 operation_id = "confirmCourseSuspectedCheater",
2457 tag = "courses",
2458 params(
2459 ("course_id" = Uuid, Path, description = "Course id"),
2460 ("user_id" = Uuid, Path, description = "Suspected cheater's user id")
2461 ),
2462 responses(
2463 (status = 200, description = "Cheating confirmed")
2464 )
2465)]
2466#[instrument(skip(pool))]
2467async fn teacher_confirm_suspected_cheater(
2468 user: AuthUser,
2469 path: web::Path<(Uuid, Uuid)>,
2470 pool: web::Data<PgPool>,
2471) -> ControllerResult<web::Json<()>> {
2472 let (course_id, user_id) = path.into_inner();
2473
2474 let mut conn = pool.acquire().await?;
2475 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2476
2477 models::suspected_cheaters::confirm_cheater_by_user_id_and_course_id(
2480 &mut conn, user_id, course_id,
2481 )
2482 .await?;
2483
2484 token.authorized_ok(web::Json(()))
2485}
2486
2487#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
2488pub struct JoinCourseWithJoinCodePayload {
2489 join_code: String,
2490}
2491
2492#[utoipa::path(
2496 post,
2497 path = "/{course_id}/join-course-with-join-code",
2498 operation_id = "joinCourseWithJoinCode",
2499 tag = "courses",
2500 params(
2501 ("course_id" = Uuid, Path, description = "Course id")
2502 ),
2503 request_body = JoinCourseWithJoinCodePayload,
2504 responses(
2505 (status = 200, description = "Joined course id", body = Uuid)
2506 )
2507)]
2508#[instrument(skip(pool))]
2509async fn add_user_to_course_with_join_code(
2510 course_id: web::Path<Uuid>,
2511 payload: web::Json<JoinCourseWithJoinCodePayload>,
2512 user: AuthUser,
2513 pool: web::Data<PgPool>,
2514) -> ControllerResult<web::Json<Uuid>> {
2515 let mut conn = pool.acquire().await?;
2516 let token = skip_authorize();
2517
2518 models::courses::get_by_id_and_join_code(&mut conn, *course_id, &payload.join_code).await?;
2519 let joined =
2520 models::join_code_uses::insert(&mut conn, PKeyPolicy::Generate, user.id, *course_id)
2521 .await?;
2522 token.authorized_ok(web::Json(joined))
2523}
2524
2525#[utoipa::path(
2529 post,
2530 path = "/{course_id}/set-join-code",
2531 operation_id = "setCourseJoinCode",
2532 tag = "courses",
2533 params(
2534 ("course_id" = Uuid, Path, description = "Course id")
2535 ),
2536 responses(
2537 (status = 200, description = "Course join code set")
2538 )
2539)]
2540#[instrument(skip(pool))]
2541async fn set_join_code_for_course(
2542 id: web::Path<Uuid>,
2543 pool: web::Data<PgPool>,
2544 user: AuthUser,
2545) -> ControllerResult<HttpResponse> {
2546 let mut conn = pool.acquire().await?;
2547 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*id)).await?;
2548
2549 const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
2550 abcdefghjkmnpqrstuvwxyz";
2551 const PASSWORD_LEN: usize = 64;
2552 let mut rng = rand::rng();
2553
2554 let code: String = (0..PASSWORD_LEN)
2555 .map(|_| {
2556 let idx = rng.random_range(0..CHARSET.len());
2557 CHARSET[idx] as char
2558 })
2559 .collect();
2560
2561 models::courses::set_join_code_for_course(&mut conn, *id, code).await?;
2562 token.authorized_ok(HttpResponse::Ok().finish())
2563}
2564
2565#[utoipa::path(
2569 get,
2570 path = "/join/{join_code}",
2571 operation_id = "getCourseByJoinCode",
2572 tag = "courses",
2573 params(
2574 ("join_code" = String, Path, description = "Course join code")
2575 ),
2576 responses(
2577 (status = 200, description = "Course for join code", body = Course)
2578 )
2579)]
2580#[instrument(skip(pool))]
2581async fn get_course_with_join_code(
2582 join_code: web::Path<String>,
2583 user: AuthUser,
2584 pool: web::Data<PgPool>,
2585) -> ControllerResult<web::Json<Course>> {
2586 let mut conn = pool.acquire().await?;
2587 let token = skip_authorize();
2588 let course =
2589 models::courses::get_course_with_join_code(&mut conn, join_code.to_string()).await?;
2590
2591 token.authorized_ok(web::Json(course))
2592}
2593
2594#[utoipa::path(
2598 post,
2599 path = "/{course_id}/partners-block",
2600 operation_id = "upsertCoursePartnersBlock",
2601 tag = "courses",
2602 params(
2603 ("course_id" = Uuid, Path, description = "Course id")
2604 ),
2605 request_body = Option<serde_json::Value>,
2606 responses(
2607 (status = 200, description = "Partners block", body = serde_json::Value)
2608 )
2609)]
2610#[instrument(skip(payload, pool))]
2611async fn post_partners_block(
2612 path: web::Path<Uuid>,
2613 payload: web::Json<Option<serde_json::Value>>,
2614 pool: web::Data<PgPool>,
2615 user: AuthUser,
2616) -> ControllerResult<web::Json<PartnersBlock>> {
2617 let course_id = path.into_inner();
2618
2619 let content = payload.into_inner();
2620 let mut conn = pool.acquire().await?;
2621 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2622
2623 let upserted_partner_block =
2624 models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
2625
2626 token.authorized_ok(web::Json(upserted_partner_block))
2627}
2628
2629#[utoipa::path(
2633 get,
2634 path = "/{course_id}/partners-block",
2635 operation_id = "getCoursePartnersBlock",
2636 tag = "courses",
2637 params(
2638 ("course_id" = Uuid, Path, description = "Course id")
2639 ),
2640 responses(
2641 (status = 200, description = "Partners block", body = serde_json::Value)
2642 )
2643)]
2644#[instrument(skip(pool))]
2645async fn get_partners_block(
2646 path: web::Path<Uuid>,
2647 user: AuthUser,
2648 pool: web::Data<PgPool>,
2649) -> ControllerResult<web::Json<PartnersBlock>> {
2650 let course_id = path.into_inner();
2651 let mut conn = pool.acquire().await?;
2652 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2653
2654 let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
2656
2657 let partner_block = if course_exists {
2658 models::partner_block::get_partner_block(&mut conn, course_id).await?
2660 } else {
2661 let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
2663
2664 models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
2666 };
2667
2668 token.authorized_ok(web::Json(partner_block))
2669}
2670
2671#[utoipa::path(
2675 delete,
2676 path = "/{course_id}/partners-block",
2677 operation_id = "deleteCoursePartnersBlock",
2678 tag = "courses",
2679 params(
2680 ("course_id" = Uuid, Path, description = "Course id")
2681 ),
2682 responses(
2683 (status = 200, description = "Deleted partners block", body = serde_json::Value)
2684 )
2685)]
2686#[instrument(skip(pool))]
2687async fn delete_partners_block(
2688 path: web::Path<Uuid>,
2689 pool: web::Data<PgPool>,
2690 user: AuthUser,
2691) -> ControllerResult<web::Json<PartnersBlock>> {
2692 let course_id = path.into_inner();
2693 let mut conn = pool.acquire().await?;
2694 let token = authorize(
2695 &mut conn,
2696 Act::UsuallyUnacceptableDeletion,
2697 Some(user.id),
2698 Res::Course(course_id),
2699 )
2700 .await?;
2701 let deleted_partners_block =
2702 models::partner_block::delete_partner_block(&mut conn, course_id).await?;
2703
2704 token.authorized_ok(web::Json(deleted_partners_block))
2705}
2706
2707#[utoipa::path(
2713 get,
2714 path = "/{course_id}/sisu-course-llm-descriptions",
2715 operation_id = "getSisuCourseLlmDescriptions",
2716 tag = "courses",
2717 params(
2718 ("course_id" = Uuid, Path, description = "Course id")
2719 ),
2720 responses(
2721 (status = 200, description = "Sisu course LLM descriptions", body = SisuDescriptionResponse)
2722 )
2723)]
2724#[instrument(skip(pool, app_conf))]
2725async fn get_sisu_course_llm_descriptions(
2726 course_id: web::Path<Uuid>,
2727 pool: web::Data<PgPool>,
2728 app_conf: web::Data<ApplicationConfiguration>,
2729 user: AuthUser,
2730 sisu_client: web::Data<SisuClient>,
2731) -> ControllerResult<web::Json<SisuDescriptionResponse>> {
2732 let (course_modules, course_lang, message_suggest_llm, token) = {
2733 let mut conn = pool.acquire().await?;
2734
2735 let token =
2736 authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
2737 let course_modules =
2738 models::course_modules::get_by_course_id(&mut conn, *course_id).await?;
2739 let course_lang = models::courses::get_course(&mut conn, *course_id)
2740 .await?
2741 .language_code;
2742 let message_suggest_llm = models::application_task_default_language_models::get_for_task(
2743 &mut conn,
2744 ApplicationTask::SisuDescriptionSummary,
2745 )
2746 .await?;
2747 (course_modules, course_lang, message_suggest_llm, token)
2748 };
2749
2750 let uh_course_codes: Vec<String> = course_modules
2751 .into_iter()
2752 .filter_map(|course_module| course_module.uh_course_code)
2753 .collect::<Vec<String>>();
2754 let course_ids = SisuClient::get_course_ids(&sisu_client, uh_course_codes).await?;
2755 let course_info = SisuClient::get_course_info(&sisu_client, course_ids).await?;
2756
2757 let parsed_course_info = SisuClient::parse_course_info(course_info, course_lang);
2758
2759 let llm_descriptions = headless_lms_chatbot::course_description_summary::generate_description(
2760 &app_conf,
2761 message_suggest_llm,
2762 parsed_course_info,
2763 )
2764 .await?;
2765 token.authorized_ok(web::Json(llm_descriptions))
2766}
2767
2768#[utoipa::path(
2773 post,
2774 path = "/{course_id}/update-metadata",
2775 operation_id = "updateMetadata",
2776 tag = "courses",
2777 params(
2778 ("course_id" = Uuid, Path, description = "Course id")
2779 ),
2780 request_body = CourseMetadataUpdate,
2781 responses(
2782 (status = 200, description = "Updated metadata", body = CourseMetadata)
2783 )
2784)]
2785#[instrument(skip(pool, app_conf))]
2786async fn update_metadata(
2787 payload: web::Json<CourseMetadataUpdate>,
2788 app_conf: web::Data<ApplicationConfiguration>,
2789 course_id: web::Path<Uuid>,
2790 pool: web::Data<PgPool>,
2791 user: AuthUser,
2792) -> ControllerResult<web::Json<CourseMetadata>> {
2793 let mut conn = pool.acquire().await?;
2794 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2795 let metadata_update = payload.0;
2796 let course = models::courses::set_metadata(
2797 &mut conn,
2798 app_conf.as_ref(),
2799 *course_id,
2800 metadata_update,
2801 user.id,
2802 )
2803 .await?;
2804
2805 token.authorized_ok(web::Json(course))
2806}
2807
2808#[utoipa::path(
2813 get,
2814 path = "/{course_id}/get-course-prerequisites",
2815 operation_id= "getCoursePrerequisites",
2816 tag = "courses",
2817 params(
2818 ("course_id" = Uuid, Path, description = "Course id")
2819 ),
2820 responses(
2821 (status = 200, description = "Course prerequisites", body = Vec<CoursePrerequisite>)
2822 )
2823)]
2824#[instrument(skip(pool))]
2825async fn get_course_prerequisites(
2826 course_id: web::Path<Uuid>,
2827 pool: web::Data<PgPool>,
2828 user: AuthUser,
2829) -> ControllerResult<web::Json<Vec<CoursePrerequisite>>> {
2830 let mut conn = pool.acquire().await?;
2831 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2832 let prerequisites =
2833 models::course_prerequisites::get_by_course_id(&mut conn, *course_id).await?;
2834
2835 token.authorized_ok(web::Json(prerequisites))
2836}
2837
2838#[utoipa::path(
2843 get,
2844 path = "/{course_id}/get-course-audiences",
2845 operation_id= "getCourseAudiences",
2846 tag = "courses",
2847 params(
2848 ("course_id" = Uuid, Path, description = "Course id")
2849 ),
2850 responses(
2851 (status = 200, description = "Course audiences", body = Vec<CourseAudience>)
2852 )
2853)]
2854#[instrument(skip(pool))]
2855async fn get_course_audiences(
2856 course_id: web::Path<Uuid>,
2857 pool: web::Data<PgPool>,
2858 user: AuthUser,
2859) -> ControllerResult<web::Json<Vec<CourseAudience>>> {
2860 let mut conn = pool.acquire().await?;
2861 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2862 let audiences = models::course_audiences::get_by_course_id(&mut conn, *course_id).await?;
2863
2864 token.authorized_ok(web::Json(audiences))
2865}
2866
2867#[utoipa::path(
2872 get,
2873 path = "/{course_id}/get-course-metadata",
2874 operation_id= "getCourseMetadata",
2875 tag = "courses",
2876 params(
2877 ("course_id" = Uuid, Path, description = "Course id")
2878 ),
2879 responses(
2880 (status = 200, description = "Course metadata", body = CompleteCourseMetadata)
2881 )
2882)]
2883#[instrument(skip(pool))]
2884async fn get_course_metadata(
2885 course_id: web::Path<Uuid>,
2886 pool: web::Data<PgPool>,
2887 user: AuthUser,
2888) -> ControllerResult<web::Json<CompleteCourseMetadata>> {
2889 let mut conn: sqlx::pool::PoolConnection<Postgres> = pool.acquire().await?;
2890 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2891 let metadata = models::courses::get_metadata(&mut conn, *course_id).await?;
2892
2893 token.authorized_ok(web::Json(metadata))
2894}
2895#[utoipa::path(
2901 get,
2902 path = "/",
2903 operation_id = "getAllCourses",
2904 tag = "courses",
2905 responses(
2906 (status = 200, description = "All courses", body = Vec<Course>)
2907 )
2908)]
2909#[instrument(skip(pool))]
2910async fn get_all_courses(
2911 pool: web::Data<PgPool>,
2912 user: AuthUser,
2913) -> ControllerResult<web::Json<Vec<Course>>> {
2914 let mut conn = pool.acquire().await?;
2915 let all_courses = models::courses::all_courses(&mut conn).await?;
2916 let token = authorize(&mut conn, Act::View, Some(user.id), Res::GlobalPermissions).await?;
2917 token.authorized_ok(web::Json(all_courses))
2918}
2919
2920pub fn _add_routes(cfg: &mut ServiceConfig) {
2928 cfg.service(web::scope("/{course_id}/stats").configure(stats::_add_routes))
2929 .service(web::scope("/{course_id}/chatbots").configure(chatbots::_add_routes))
2930 .service(web::scope("/{course_id}/students").configure(students::_add_routes))
2931 .route("/{course_id}", web::get().to(get_course))
2932 .route("", web::post().to(post_new_course))
2933 .route("/{course_id}", web::put().to(update_course))
2934 .route("/{course_id}", web::delete().to(delete_course))
2935 .route(
2936 "/{course_id}/status-for-all-exercises/{user_id}",
2937 web::get().to(get_all_exercise_statuses_by_course_id),
2938 )
2939 .route(
2940 "/{course_id}/course-module-completions/{user_id}",
2941 web::get().to(get_all_course_module_completions_for_user_by_course_id),
2942 )
2943 .route(
2944 "/{course_id}/daily-submission-counts",
2945 web::get().to(get_daily_submission_counts),
2946 )
2947 .route(
2948 "/{course_id}/daily-users-who-have-submitted-something",
2949 web::get().to(get_daily_user_counts_with_submissions),
2950 )
2951 .route("/{course_id}/exercises", web::get().to(get_all_exercises))
2952 .route(
2953 "/{course_id}/exercises-and-count-of-answers-requiring-attention",
2954 web::get().to(get_all_exercises_and_count_of_answers_requiring_attention),
2955 )
2956 .route(
2957 "/{course_id}/structure",
2958 web::get().to(get_course_structure),
2959 )
2960 .route(
2961 "/{course_id}/language-versions",
2962 web::get().to(get_all_course_language_versions),
2963 )
2964 .route(
2965 "/{course_id}/create-copy",
2966 web::post().to(create_course_copy),
2967 )
2968 .route("/{course_id}/upload", web::post().to(add_media_for_course))
2969 .route(
2970 "/{course_id}/weekday-hour-submission-counts",
2971 web::get().to(get_weekday_hour_submission_counts),
2972 )
2973 .route(
2974 "/{course_id}/submission-counts-by-exercise",
2975 web::get().to(get_submission_counts_by_exercise),
2976 )
2977 .route(
2978 "/{course_id}/course-instances",
2979 web::get().to(get_course_instances),
2980 )
2981 .route("/{course_id}/feedback", web::get().to(get_feedback))
2982 .route(
2983 "/{course_id}/feedback-count",
2984 web::get().to(get_feedback_edit_proposals_count),
2985 )
2986 .route(
2987 "/{course_id}/feedback-categories",
2988 web::get().to(get_feedback_categories),
2989 )
2990 .route(
2991 "/{course_id}/new-course-instance",
2992 web::post().to(new_course_instance),
2993 )
2994 .route("/{course_id}/glossary", web::get().to(glossary))
2995 .route("/{course_id}/glossary", web::post().to(new_glossary_term))
2996 .route(
2997 "/{course_id}/course-users-counts-by-exercise",
2998 web::get().to(get_course_users_counts_by_exercise),
2999 )
3000 .route(
3001 "/{course_id}/new-page-ordering",
3002 web::post().to(post_new_page_ordering),
3003 )
3004 .route(
3005 "/{course_id}/new-chapter-ordering",
3006 web::post().to(post_new_chapter_ordering),
3007 )
3008 .route(
3009 "/{course_id}/references",
3010 web::get().to(get_material_references_by_course_id),
3011 )
3012 .route(
3013 "/{course_id}/references",
3014 web::post().to(insert_material_references),
3015 )
3016 .route(
3017 "/{course_id}/references/{reference_id}",
3018 web::post().to(update_material_reference),
3019 )
3020 .route(
3021 "/{course_id}/references/{reference_id}",
3022 web::delete().to(delete_material_reference_by_id),
3023 )
3024 .route(
3025 "/{course_id}/course-modules",
3026 web::post().to(update_modules),
3027 )
3028 .route(
3029 "/{course_id}/default-peer-review",
3030 web::get().to(get_course_default_peer_review),
3031 )
3032 .route(
3033 "/{course_id}/update-peer-review-queue-reviews-received",
3034 web::post().to(post_update_peer_review_queue_reviews_received),
3035 )
3036 .route(
3037 "/{course_id}/breadcrumb-info",
3038 web::get().to(get_course_breadcrumb_info),
3039 )
3040 .route(
3041 "/{course_id}/progress/{user_id}",
3042 web::get().to(get_user_progress_for_course),
3043 )
3044 .route(
3045 "/{course_id}/user-settings/{user_id}",
3046 web::get().to(get_user_course_settings),
3047 )
3048 .route(
3049 "/{course_id}/export-submissions",
3050 web::get().to(submission_export),
3051 )
3052 .route(
3053 "/{course_id}/export-user-details",
3054 web::get().to(user_details_export),
3055 )
3056 .route(
3057 "/{course_id}/export-exercise-tasks",
3058 web::get().to(exercise_tasks_export),
3059 )
3060 .route(
3061 "/{course_id}/export-course-instances",
3062 web::get().to(course_instances_export),
3063 )
3064 .route(
3065 "/{course_id}/export-course-user-consents",
3066 web::get().to(course_consent_form_answers_export),
3067 )
3068 .route(
3069 "/{course_id}/export-user-exercise-states",
3070 web::get().to(user_exercise_states_export),
3071 )
3072 .route(
3073 "/{course_id}/page-visit-datum-summary",
3074 web::get().to(get_page_visit_datum_summary),
3075 )
3076 .route(
3077 "/{course_id}/page-visit-datum-summary-by-pages",
3078 web::get().to(get_page_visit_datum_summary_by_pages),
3079 )
3080 .route(
3081 "/{course_id}/page-visit-datum-summary-by-device-types",
3082 web::get().to(get_page_visit_datum_summary_by_device_types),
3083 )
3084 .route(
3085 "/{course_id}/page-visit-datum-summary-by-countries",
3086 web::get().to(get_page_visit_datum_summary_by_countries),
3087 )
3088 .route(
3089 "/{course_id}/teacher-reset-course-progress-for-themselves",
3090 web::delete().to(teacher_reset_course_progress_for_themselves),
3091 )
3092 .route("/{course_id}/thresholds", web::get().to(get_all_thresholds))
3093 .route(
3094 "/{course_id}/suspected-cheaters",
3095 web::get().to(get_all_suspected_cheaters),
3096 )
3097 .route(
3098 "/{course_id}/suspected-cheaters/flagged-count",
3099 web::get().to(get_flagged_suspected_cheaters_count),
3100 )
3101 .route(
3102 "/{course_id}/suspected-cheaters/dismiss/{user_id}",
3103 web::post().to(teacher_dismiss_suspected_cheater),
3104 )
3105 .route(
3106 "/{course_id}/suspected-cheaters/confirm/{user_id}",
3107 web::post().to(teacher_confirm_suspected_cheater),
3108 )
3109 .route(
3110 "/{course_id}/teacher-reset-course-progress-for-everyone",
3111 web::delete().to(teacher_reset_course_progress_for_everyone),
3112 )
3113 .route(
3114 "/{course_id}/join-course-with-join-code",
3115 web::post().to(add_user_to_course_with_join_code),
3116 )
3117 .route(
3118 "/{course_id}/partners-block",
3119 web::post().to(post_partners_block),
3120 )
3121 .route(
3122 "/{course_id}/partners-block",
3123 web::get().to(get_partners_block),
3124 )
3125 .route(
3126 "/{course_id}/partners-block",
3127 web::delete().to(delete_partners_block),
3128 )
3129 .route(
3130 "/{course_id}/set-join-code",
3131 web::post().to(set_join_code_for_course),
3132 )
3133 .route(
3134 "/{course_id}/reprocess-completions",
3135 web::post().to(post_reprocess_module_completions),
3136 )
3137 .route(
3138 "/join/{join_code}",
3139 web::get().to(get_course_with_join_code),
3140 )
3141 .route(
3142 "/{course_id}/sisu-course-llm-descriptions",
3143 web::get().to(get_sisu_course_llm_descriptions),
3144 )
3145 .route(
3146 "/{course_id}/update-metadata",
3147 web::post().to(update_metadata),
3148 )
3149 .route(
3150 "/{course_id}/get-course-prerequisites",
3151 web::get().to(get_course_prerequisites),
3152 )
3153 .route(
3154 "/{course_id}/get-course-audiences",
3155 web::get().to(get_course_audiences),
3156 )
3157 .route(
3158 "/{course_id}/get-course-metadata",
3159 web::get().to(get_course_metadata),
3160 )
3161 .route("/", web::get().to(get_all_courses));
3162}