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