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 (status = 400, description = "A module or chapter is not in the course, or a completion registration link is not https")
1584 )
1585)]
1586#[instrument(skip(pool))]
1587pub async fn update_modules(
1588 course_id: web::Path<Uuid>,
1589 pool: web::Data<PgPool>,
1590 user: AuthUser,
1591 payload: web::Json<ModuleUpdates>,
1592) -> ControllerResult<web::Json<()>> {
1593 let mut conn = pool.acquire().await?;
1594 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1595
1596 let updates = payload.into_inner();
1597 if models::course_modules::would_change_credit_registration_via_suotar(
1598 &mut conn, *course_id, &updates,
1599 )
1600 .await?
1601 {
1602 authorize(
1604 &mut conn,
1605 Act::Administrate,
1606 Some(user.id),
1607 Res::GlobalPermissions,
1608 )
1609 .await?;
1610 }
1611
1612 models::course_modules::update_modules(&mut conn, *course_id, updates).await?;
1613 token.authorized_ok(web::Json(()))
1614}
1615
1616#[utoipa::path(
1617 get,
1618 path = "/{course_id}/default-peer-review",
1619 operation_id = "getCourseDefaultPeerReview",
1620 tag = "courses",
1621 params(
1622 ("course_id" = Uuid, Path, description = "Course id")
1623 ),
1624 responses(
1625 (status = 200, description = "Default peer review configuration", body = serde_json::Value)
1626 )
1627)]
1628async fn get_course_default_peer_review(
1629 course_id: web::Path<Uuid>,
1630 pool: web::Data<PgPool>,
1631 user: AuthUser,
1632) -> ControllerResult<web::Json<(PeerOrSelfReviewConfig, Vec<PeerOrSelfReviewQuestion>)>> {
1633 let mut conn = pool.acquire().await?;
1634 let token = authorize(
1635 &mut conn,
1636 Act::Teach,
1637 Some(user.id),
1638 Res::Course(*course_id),
1639 )
1640 .await?;
1641
1642 let peer_review = models::peer_or_self_review_configs::get_default_for_course_by_course_id(
1643 &mut conn, *course_id,
1644 )
1645 .await?;
1646 let peer_or_self_review_questions =
1647 models::peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
1648 &mut conn,
1649 peer_review.id,
1650 )
1651 .await?;
1652 token.authorized_ok(web::Json((peer_review, peer_or_self_review_questions)))
1653}
1654
1655#[utoipa::path(
1661 post,
1662 path = "/{course_id}/update-peer-review-queue-reviews-received",
1663 operation_id = "updateCoursePeerReviewQueueReviewsReceived",
1664 tag = "courses",
1665 params(
1666 ("course_id" = Uuid, Path, description = "Course id")
1667 ),
1668 responses(
1669 (status = 200, description = "Peer review queue updated", body = bool)
1670 )
1671)]
1672#[instrument(skip(pool, user))]
1673async fn post_update_peer_review_queue_reviews_received(
1674 pool: web::Data<PgPool>,
1675 user: AuthUser,
1676 course_id: web::Path<Uuid>,
1677) -> ControllerResult<web::Json<bool>> {
1678 let mut conn = pool.acquire().await?;
1679 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
1680 models::library::peer_or_self_reviewing::update_peer_review_queue_reviews_received(
1681 &mut conn, *course_id,
1682 )
1683 .await?;
1684 token.authorized_ok(web::Json(true))
1685}
1686
1687#[utoipa::path(
1693 get,
1694 path = "/{course_id}/export-submissions",
1695 operation_id = "exportCourseSubmissionsCsv",
1696 tag = "courses",
1697 params(
1698 ("course_id" = Uuid, Path, description = "Course id")
1699 ),
1700 responses(
1701 (status = 200, description = "Course submissions CSV", body = String, content_type = "text/csv")
1702 )
1703)]
1704#[instrument(skip(pool, file_store, app_conf))]
1705pub async fn submission_export(
1706 course_id: web::Path<Uuid>,
1707 pool: web::Data<PgPool>,
1708 user: AuthUser,
1709 file_store: web::Data<dyn FileStore>,
1710 app_conf: web::Data<ApplicationConfiguration>,
1711) -> ControllerResult<HttpResponse> {
1712 let mut conn = pool.acquire().await?;
1713
1714 let token = authorize(
1715 &mut conn,
1716 Act::Teach,
1717 Some(user.id),
1718 Res::Course(*course_id),
1719 )
1720 .await?;
1721
1722 let course = models::courses::get_course(&mut conn, *course_id).await?;
1723
1724 general_export(
1725 pool,
1726 &format!(
1727 "attachment; filename=\"Course: {} - Submissions (exercise tasks) {}.csv\"",
1728 course.name,
1729 Utc::now().format("%Y-%m-%d")
1730 ),
1731 CourseSubmissionExportOperation {
1732 course_id: *course_id,
1733 file_store,
1734 app_conf,
1735 },
1736 token,
1737 )
1738 .await
1739}
1740
1741#[utoipa::path(
1747 get,
1748 path = "/{course_id}/export-user-details",
1749 operation_id = "exportCourseUserDetailsCsv",
1750 tag = "courses",
1751 params(
1752 ("course_id" = Uuid, Path, description = "Course id")
1753 ),
1754 responses(
1755 (status = 200, description = "Course user details CSV", body = String, content_type = "text/csv")
1756 )
1757)]
1758#[instrument(skip(pool))]
1759pub async fn user_details_export(
1760 course_id: web::Path<Uuid>,
1761 pool: web::Data<PgPool>,
1762 user: AuthUser,
1763) -> ControllerResult<HttpResponse> {
1764 let mut conn = pool.acquire().await?;
1765
1766 let token = authorize(
1767 &mut conn,
1768 Act::Teach,
1769 Some(user.id),
1770 Res::Course(*course_id),
1771 )
1772 .await?;
1773
1774 let course = models::courses::get_course(&mut conn, *course_id).await?;
1775
1776 general_export(
1777 pool,
1778 &format!(
1779 "attachment; filename=\"Course: {} - User Details {}.csv\"",
1780 course.name,
1781 Utc::now().format("%Y-%m-%d")
1782 ),
1783 UsersExportOperation {
1784 course_id: *course_id,
1785 },
1786 token,
1787 )
1788 .await
1789}
1790
1791#[utoipa::path(
1797 get,
1798 path = "/{course_id}/export-exercise-tasks",
1799 operation_id = "exportCourseExerciseTasksCsv",
1800 tag = "courses",
1801 params(
1802 ("course_id" = Uuid, Path, description = "Course id")
1803 ),
1804 responses(
1805 (status = 200, description = "Course exercise tasks CSV", body = String, content_type = "text/csv")
1806 )
1807)]
1808#[instrument(skip(pool))]
1809pub async fn exercise_tasks_export(
1810 course_id: web::Path<Uuid>,
1811 pool: web::Data<PgPool>,
1812 user: AuthUser,
1813) -> ControllerResult<HttpResponse> {
1814 let mut conn = pool.acquire().await?;
1815
1816 let token = authorize(
1817 &mut conn,
1818 Act::Teach,
1819 Some(user.id),
1820 Res::Course(*course_id),
1821 )
1822 .await?;
1823
1824 let course = models::courses::get_course(&mut conn, *course_id).await?;
1825
1826 general_export(
1827 pool,
1828 &format!(
1829 "attachment; filename=\"Course: {} - Exercise tasks {}.csv\"",
1830 course.name,
1831 Utc::now().format("%Y-%m-%d")
1832 ),
1833 CourseExerciseTasksExportOperation {
1834 course_id: *course_id,
1835 },
1836 token,
1837 )
1838 .await
1839}
1840
1841#[utoipa::path(
1847 get,
1848 path = "/{course_id}/export-course-instances",
1849 operation_id = "exportCourseInstancesCsv",
1850 tag = "courses",
1851 params(
1852 ("course_id" = Uuid, Path, description = "Course id")
1853 ),
1854 responses(
1855 (status = 200, description = "Course instances CSV", body = String, content_type = "text/csv")
1856 )
1857)]
1858#[instrument(skip(pool))]
1859pub async fn course_instances_export(
1860 course_id: web::Path<Uuid>,
1861 pool: web::Data<PgPool>,
1862 user: AuthUser,
1863) -> ControllerResult<HttpResponse> {
1864 let mut conn = pool.acquire().await?;
1865
1866 let token = authorize(
1867 &mut conn,
1868 Act::Teach,
1869 Some(user.id),
1870 Res::Course(*course_id),
1871 )
1872 .await?;
1873
1874 let course = models::courses::get_course(&mut conn, *course_id).await?;
1875
1876 general_export(
1877 pool,
1878 &format!(
1879 "attachment; filename=\"Course: {} - Instances {}.csv\"",
1880 course.name,
1881 Utc::now().format("%Y-%m-%d")
1882 ),
1883 CourseInstancesExportOperation {
1884 course_id: *course_id,
1885 },
1886 token,
1887 )
1888 .await
1889}
1890
1891#[utoipa::path(
1897 get,
1898 path = "/{course_id}/export-course-user-consents",
1899 operation_id = "exportCourseUserConsentsCsv",
1900 tag = "courses",
1901 params(
1902 ("course_id" = Uuid, Path, description = "Course id")
1903 ),
1904 responses(
1905 (status = 200, description = "Course user consents CSV", body = String, content_type = "text/csv")
1906 )
1907)]
1908#[instrument(skip(pool))]
1909pub async fn course_consent_form_answers_export(
1910 course_id: web::Path<Uuid>,
1911 pool: web::Data<PgPool>,
1912 user: AuthUser,
1913) -> ControllerResult<HttpResponse> {
1914 let mut conn = pool.acquire().await?;
1915
1916 let token = authorize(
1917 &mut conn,
1918 Act::Teach,
1919 Some(user.id),
1920 Res::Course(*course_id),
1921 )
1922 .await?;
1923
1924 let course = models::courses::get_course(&mut conn, *course_id).await?;
1925
1926 general_export(
1927 pool,
1928 &format!(
1929 "attachment; filename=\"Course: {} - User Consents {}.csv\"",
1930 course.name,
1931 Utc::now().format("%Y-%m-%d")
1932 ),
1933 CourseResearchFormExportOperation {
1934 course_id: *course_id,
1935 },
1936 token,
1937 )
1938 .await
1939}
1940
1941#[utoipa::path(
1947 get,
1948 path = "/{course_id}/export-user-exercise-states",
1949 operation_id = "exportCourseUserExerciseStatesCsv",
1950 tag = "courses",
1951 params(
1952 ("course_id" = Uuid, Path, description = "Course id")
1953 ),
1954 responses(
1955 (status = 200, description = "Course user exercise states CSV", body = String, content_type = "text/csv")
1956 )
1957)]
1958#[instrument(skip(pool))]
1959pub async fn user_exercise_states_export(
1960 course_id: web::Path<Uuid>,
1961 pool: web::Data<PgPool>,
1962 user: AuthUser,
1963) -> ControllerResult<HttpResponse> {
1964 let mut conn = pool.acquire().await?;
1965
1966 let token = authorize(
1967 &mut conn,
1968 Act::Teach,
1969 Some(user.id),
1970 Res::Course(*course_id),
1971 )
1972 .await?;
1973
1974 let course = models::courses::get_course(&mut conn, *course_id).await?;
1975
1976 general_export(
1977 pool,
1978 &format!(
1979 "attachment; filename=\"Course: {} - User exercise states {}.csv\"",
1980 course.name,
1981 Utc::now().format("%Y-%m-%d")
1982 ),
1983 UserExerciseStatesExportOperation {
1984 course_id: *course_id,
1985 },
1986 token,
1987 )
1988 .await
1989}
1990
1991#[utoipa::path(
1995 get,
1996 path = "/{course_id}/page-visit-datum-summary",
1997 operation_id = "getCoursePageVisitDatumSummary",
1998 tag = "courses",
1999 params(
2000 ("course_id" = Uuid, Path, description = "Course id")
2001 ),
2002 responses(
2003 (status = 200, description = "Course page visit summary", body = [PageVisitDatumSummaryByCourse])
2004 )
2005)]
2006pub async fn get_page_visit_datum_summary(
2007 course_id: web::Path<Uuid>,
2008 pool: web::Data<PgPool>,
2009 user: AuthUser,
2010) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourse>>> {
2011 let mut conn = pool.acquire().await?;
2012 let course_id = course_id.into_inner();
2013 let token = authorize(
2014 &mut conn,
2015 Act::ViewStats,
2016 Some(user.id),
2017 Res::Course(course_id),
2018 )
2019 .await?;
2020
2021 let res = models::page_visit_datum_summary_by_courses::get_all_for_course(&mut conn, course_id)
2022 .await?;
2023
2024 token.authorized_ok(web::Json(res))
2025}
2026
2027#[utoipa::path(
2031 get,
2032 path = "/{course_id}/page-visit-datum-summary-by-pages",
2033 operation_id = "getCoursePageVisitDatumSummaryByPages",
2034 tag = "courses",
2035 params(
2036 ("course_id" = Uuid, Path, description = "Course id")
2037 ),
2038 responses(
2039 (status = 200, description = "Course page visit summary by pages", body = [PageVisitDatumSummaryByPages])
2040 )
2041)]
2042pub async fn get_page_visit_datum_summary_by_pages(
2043 course_id: web::Path<Uuid>,
2044 pool: web::Data<PgPool>,
2045 user: AuthUser,
2046) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByPages>>> {
2047 let mut conn = pool.acquire().await?;
2048 let course_id = course_id.into_inner();
2049 let token = authorize(
2050 &mut conn,
2051 Act::ViewStats,
2052 Some(user.id),
2053 Res::Course(course_id),
2054 )
2055 .await?;
2056
2057 let res =
2058 models::page_visit_datum_summary_by_pages::get_all_for_course(&mut conn, course_id).await?;
2059
2060 token.authorized_ok(web::Json(res))
2061}
2062
2063#[utoipa::path(
2067 get,
2068 path = "/{course_id}/page-visit-datum-summary-by-device-types",
2069 operation_id = "getCoursePageVisitDatumSummaryByDeviceTypes",
2070 tag = "courses",
2071 params(
2072 ("course_id" = Uuid, Path, description = "Course id")
2073 ),
2074 responses(
2075 (status = 200, description = "Course page visit summary by device types", body = [PageVisitDatumSummaryByCourseDeviceTypes])
2076 )
2077)]
2078pub async fn get_page_visit_datum_summary_by_device_types(
2079 course_id: web::Path<Uuid>,
2080 pool: web::Data<PgPool>,
2081 user: AuthUser,
2082) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourseDeviceTypes>>> {
2083 let mut conn = pool.acquire().await?;
2084 let course_id = course_id.into_inner();
2085 let token = authorize(
2086 &mut conn,
2087 Act::ViewStats,
2088 Some(user.id),
2089 Res::Course(course_id),
2090 )
2091 .await?;
2092
2093 let res = models::page_visit_datum_summary_by_courses_device_types::get_all_for_course(
2094 &mut conn, course_id,
2095 )
2096 .await?;
2097
2098 token.authorized_ok(web::Json(res))
2099}
2100
2101#[utoipa::path(
2105 get,
2106 path = "/{course_id}/page-visit-datum-summary-by-countries",
2107 operation_id = "getCoursePageVisitDatumSummaryByCountries",
2108 tag = "courses",
2109 params(
2110 ("course_id" = Uuid, Path, description = "Course id")
2111 ),
2112 responses(
2113 (status = 200, description = "Course page visit summary by countries", body = [PageVisitDatumSummaryByCoursesCountries])
2114 )
2115)]
2116pub async fn get_page_visit_datum_summary_by_countries(
2117 course_id: web::Path<Uuid>,
2118 pool: web::Data<PgPool>,
2119 user: AuthUser,
2120) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCoursesCountries>>> {
2121 let mut conn = pool.acquire().await?;
2122 let course_id = course_id.into_inner();
2123 let token = authorize(
2124 &mut conn,
2125 Act::ViewStats,
2126 Some(user.id),
2127 Res::Course(course_id),
2128 )
2129 .await?;
2130
2131 let res = models::page_visit_datum_summary_by_courses_countries::get_all_for_course(
2132 &mut conn, course_id,
2133 )
2134 .await?;
2135
2136 token.authorized_ok(web::Json(res))
2137}
2138
2139#[utoipa::path(
2145 delete,
2146 path = "/{course_id}/teacher-reset-course-progress-for-themselves",
2147 operation_id = "resetCourseProgressForTeacherThemselves",
2148 tag = "courses",
2149 params(
2150 ("course_id" = Uuid, Path, description = "Course id")
2151 ),
2152 responses(
2153 (status = 200, description = "Teacher course progress reset", body = bool)
2154 )
2155)]
2156pub async fn teacher_reset_course_progress_for_themselves(
2157 course_id: web::Path<Uuid>,
2158 pool: web::Data<PgPool>,
2159 user: AuthUser,
2160) -> ControllerResult<web::Json<bool>> {
2161 let mut conn = pool.acquire().await?;
2162 let course_id = course_id.into_inner();
2163 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2164
2165 let mut tx = conn.begin().await?;
2166 let course_instances =
2167 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2168 for course_instance in course_instances {
2169 models::course_instances::reset_progress_on_course_instance_for_user(
2170 &mut tx,
2171 user.id,
2172 course_instance.course_id,
2173 )
2174 .await?;
2175 }
2176
2177 tx.commit().await?;
2178 token.authorized_ok(web::Json(true))
2179}
2180
2181#[utoipa::path(
2187 delete,
2188 path = "/{course_id}/teacher-reset-course-progress-for-everyone",
2189 operation_id = "resetCourseProgressForEveryone",
2190 tag = "courses",
2191 params(
2192 ("course_id" = Uuid, Path, description = "Course id")
2193 ),
2194 responses(
2195 (status = 200, description = "Course progress reset for everyone", body = bool)
2196 )
2197)]
2198pub async fn teacher_reset_course_progress_for_everyone(
2199 course_id: web::Path<Uuid>,
2200 pool: web::Data<PgPool>,
2201 user: AuthUser,
2202) -> ControllerResult<web::Json<bool>> {
2203 let mut conn = pool.acquire().await?;
2204 let course_id = course_id.into_inner();
2205 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2206 let course = models::courses::get_course(&mut conn, course_id).await?;
2207 if !course.is_draft {
2208 return Err(ControllerError::new(
2209 ControllerErrorType::BadRequest,
2210 "Can only reset progress for a draft course.".to_string(),
2211 None,
2212 ));
2213 }
2214 let n_course_module_completions =
2216 models::course_module_completions::get_count_of_distinct_completors_by_course_id(
2217 &mut conn, course_id,
2218 )
2219 .await?;
2220 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(
2221 &mut conn, course_id,
2222 ).await?;
2223 if n_course_module_completions > 200 {
2224 return Err(ControllerError::new(
2225 ControllerErrorType::BadRequest,
2226 "Too many students have completed the course.".to_string(),
2227 None,
2228 ));
2229 }
2230 if n_completions_registered_to_study_registry > 2 {
2231 return Err(ControllerError::new(
2232 ControllerErrorType::BadRequest,
2233 "Too many students have registered their completion to a study registry".to_string(),
2234 None,
2235 ));
2236 }
2237
2238 let mut tx = conn.begin().await?;
2239 let course_instances =
2240 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2241
2242 for course_instance in course_instances {
2244 let users_in_course_instance =
2245 models::users::get_users_by_course_instance_enrollment(&mut tx, course_instance.id)
2246 .await?;
2247 for user_in_course_instance in users_in_course_instance {
2248 models::course_instances::reset_progress_on_course_instance_for_user(
2249 &mut tx,
2250 user_in_course_instance.id,
2251 course_instance.course_id,
2252 )
2253 .await?;
2254 }
2255 }
2256
2257 tx.commit().await?;
2258 token.authorized_ok(web::Json(true))
2259}
2260
2261#[derive(Debug, Deserialize)]
2262
2263pub struct GetSuspectedCheatersQuery {
2264 status: SuspectedCheaterStatus,
2265}
2266
2267#[utoipa::path(
2271 get,
2272 path = "/{course_id}/suspected-cheaters",
2273 operation_id = "getCourseSuspectedCheaters",
2274 tag = "courses",
2275 params(
2276 ("course_id" = Uuid, Path, description = "Course id"),
2277 ("status" = SuspectedCheaterStatus, Query, description = "Which review state of suspected cheaters to fetch")
2278 ),
2279 responses(
2280 (status = 200, description = "Suspected cheaters for course", body = [SuspectedCheaters])
2281 )
2282)]
2283#[instrument(skip(pool))]
2284async fn get_all_suspected_cheaters(
2285 user: AuthUser,
2286 params: web::Path<Uuid>,
2287 query: web::Query<GetSuspectedCheatersQuery>,
2288 pool: web::Data<PgPool>,
2289) -> ControllerResult<web::Json<Vec<SuspectedCheaters>>> {
2290 let course_id = params.into_inner();
2291
2292 let mut conn = pool.acquire().await?;
2293 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2294
2295 let course_cheaters = models::suspected_cheaters::get_all_suspected_cheaters_in_course(
2296 &mut conn,
2297 course_id,
2298 query.status,
2299 )
2300 .await?;
2301
2302 token.authorized_ok(web::Json(course_cheaters))
2303}
2304
2305#[utoipa::path(
2309 get,
2310 path = "/{course_id}/suspected-cheaters/flagged-count",
2311 operation_id = "getCourseFlaggedSuspectedCheatersCount",
2312 tag = "courses",
2313 params(
2314 ("course_id" = Uuid, Path, description = "Course id")
2315 ),
2316 responses(
2317 (status = 200, description = "Number of suspected cheaters awaiting review", body = i64, content_type = "application/json")
2318 )
2319)]
2320#[instrument(skip(pool))]
2321async fn get_flagged_suspected_cheaters_count(
2322 user: AuthUser,
2323 params: web::Path<Uuid>,
2324 pool: web::Data<PgPool>,
2325) -> ControllerResult<web::Json<i64>> {
2326 let course_id = params.into_inner();
2327
2328 let mut conn = pool.acquire().await?;
2329 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2330
2331 let count = models::suspected_cheaters::get_count_in_course_by_status(
2332 &mut conn,
2333 course_id,
2334 SuspectedCheaterStatus::Flagged,
2335 )
2336 .await?;
2337
2338 token.authorized_ok(web::Json(count))
2339}
2340
2341#[utoipa::path(
2345 get,
2346 path = "/{course_id}/thresholds",
2347 operation_id = "getCourseThresholds",
2348 tag = "courses",
2349 params(
2350 ("course_id" = Uuid, Path, description = "Course id")
2351 ),
2352 responses(
2353 (status = 200, description = "Course thresholds", body = Vec<CourseModuleThresholdInfo>)
2354 )
2355)]
2356#[instrument(skip(pool))]
2357async fn get_all_thresholds(
2358 user: AuthUser,
2359 params: web::Path<Uuid>,
2360 pool: web::Data<PgPool>,
2361) -> ControllerResult<web::Json<Vec<CourseModuleThresholdInfo>>> {
2362 let mut conn = pool.acquire().await?;
2363 let course_id = params.into_inner();
2364
2365 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2366
2367 let thresholds =
2368 models::suspected_cheaters::get_threshold_info_for_course(&mut conn, course_id).await?;
2369
2370 token.authorized_ok(web::Json(thresholds))
2371}
2372
2373#[utoipa::path(
2377 post,
2378 path = "/{course_id}/suspected-cheaters/dismiss/{user_id}",
2379 operation_id = "dismissCourseSuspectedCheater",
2380 tag = "courses",
2381 params(
2382 ("course_id" = Uuid, Path, description = "Course id"),
2383 ("user_id" = Uuid, Path, description = "Suspected cheater's user id")
2384 ),
2385 responses(
2386 (status = 200, description = "Suspicion dismissed")
2387 )
2388)]
2389#[instrument(skip(pool))]
2390async fn teacher_dismiss_suspected_cheater(
2391 user: AuthUser,
2392 path: web::Path<(Uuid, Uuid)>,
2393 pool: web::Data<PgPool>,
2394) -> ControllerResult<web::Json<()>> {
2395 let (course_id, user_id) = path.into_inner();
2396
2397 let mut conn = pool.acquire().await?;
2398 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2399
2400 models::suspected_cheaters::dismiss_by_user_id_and_course_id(&mut conn, user_id, course_id)
2401 .await?;
2402
2403 token.authorized_ok(web::Json(()))
2404}
2405
2406#[utoipa::path(
2410 post,
2411 path = "/{course_id}/suspected-cheaters/confirm/{user_id}",
2412 operation_id = "confirmCourseSuspectedCheater",
2413 tag = "courses",
2414 params(
2415 ("course_id" = Uuid, Path, description = "Course id"),
2416 ("user_id" = Uuid, Path, description = "Suspected cheater's user id")
2417 ),
2418 responses(
2419 (status = 200, description = "Cheating confirmed")
2420 )
2421)]
2422#[instrument(skip(pool))]
2423async fn teacher_confirm_suspected_cheater(
2424 user: AuthUser,
2425 path: web::Path<(Uuid, Uuid)>,
2426 pool: web::Data<PgPool>,
2427) -> ControllerResult<web::Json<()>> {
2428 let (course_id, user_id) = path.into_inner();
2429
2430 let mut conn = pool.acquire().await?;
2431 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2432
2433 models::suspected_cheaters::confirm_cheater_by_user_id_and_course_id(
2436 &mut conn, user_id, course_id,
2437 )
2438 .await?;
2439
2440 token.authorized_ok(web::Json(()))
2441}
2442
2443#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
2444pub struct JoinCourseWithJoinCodePayload {
2445 join_code: String,
2446}
2447
2448#[utoipa::path(
2452 post,
2453 path = "/{course_id}/join-course-with-join-code",
2454 operation_id = "joinCourseWithJoinCode",
2455 tag = "courses",
2456 params(
2457 ("course_id" = Uuid, Path, description = "Course id")
2458 ),
2459 request_body = JoinCourseWithJoinCodePayload,
2460 responses(
2461 (status = 200, description = "Joined course id", body = Uuid)
2462 )
2463)]
2464#[instrument(skip(pool))]
2465async fn add_user_to_course_with_join_code(
2466 course_id: web::Path<Uuid>,
2467 payload: web::Json<JoinCourseWithJoinCodePayload>,
2468 user: AuthUser,
2469 pool: web::Data<PgPool>,
2470) -> ControllerResult<web::Json<Uuid>> {
2471 let mut conn = pool.acquire().await?;
2472 let token = skip_authorize();
2473
2474 models::courses::get_by_id_and_join_code(&mut conn, *course_id, &payload.join_code).await?;
2475 let joined =
2476 models::join_code_uses::insert(&mut conn, PKeyPolicy::Generate, user.id, *course_id)
2477 .await?;
2478 token.authorized_ok(web::Json(joined))
2479}
2480
2481#[utoipa::path(
2485 post,
2486 path = "/{course_id}/set-join-code",
2487 operation_id = "setCourseJoinCode",
2488 tag = "courses",
2489 params(
2490 ("course_id" = Uuid, Path, description = "Course id")
2491 ),
2492 responses(
2493 (status = 200, description = "Course join code set")
2494 )
2495)]
2496#[instrument(skip(pool))]
2497async fn set_join_code_for_course(
2498 id: web::Path<Uuid>,
2499 pool: web::Data<PgPool>,
2500 user: AuthUser,
2501) -> ControllerResult<HttpResponse> {
2502 let mut conn = pool.acquire().await?;
2503 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*id)).await?;
2504
2505 const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
2506 abcdefghjkmnpqrstuvwxyz";
2507 const PASSWORD_LEN: usize = 64;
2508 let mut rng = rand::rng();
2509
2510 let code: String = (0..PASSWORD_LEN)
2511 .map(|_| {
2512 let idx = rng.random_range(0..CHARSET.len());
2513 CHARSET[idx] as char
2514 })
2515 .collect();
2516
2517 models::courses::set_join_code_for_course(&mut conn, *id, code).await?;
2518 token.authorized_ok(HttpResponse::Ok().finish())
2519}
2520
2521#[utoipa::path(
2525 get,
2526 path = "/join/{join_code}",
2527 operation_id = "getCourseByJoinCode",
2528 tag = "courses",
2529 params(
2530 ("join_code" = String, Path, description = "Course join code")
2531 ),
2532 responses(
2533 (status = 200, description = "Course for join code", body = Course)
2534 )
2535)]
2536#[instrument(skip(pool))]
2537async fn get_course_with_join_code(
2538 join_code: web::Path<String>,
2539 user: AuthUser,
2540 pool: web::Data<PgPool>,
2541) -> ControllerResult<web::Json<Course>> {
2542 let mut conn = pool.acquire().await?;
2543 let token = skip_authorize();
2544 let course =
2545 models::courses::get_course_with_join_code(&mut conn, join_code.to_string()).await?;
2546
2547 token.authorized_ok(web::Json(course))
2548}
2549
2550#[utoipa::path(
2554 post,
2555 path = "/{course_id}/partners-block",
2556 operation_id = "upsertCoursePartnersBlock",
2557 tag = "courses",
2558 params(
2559 ("course_id" = Uuid, Path, description = "Course id")
2560 ),
2561 request_body = Option<serde_json::Value>,
2562 responses(
2563 (status = 200, description = "Partners block", body = serde_json::Value)
2564 )
2565)]
2566#[instrument(skip(payload, pool))]
2567async fn post_partners_block(
2568 path: web::Path<Uuid>,
2569 payload: web::Json<Option<serde_json::Value>>,
2570 pool: web::Data<PgPool>,
2571 user: AuthUser,
2572) -> ControllerResult<web::Json<PartnersBlock>> {
2573 let course_id = path.into_inner();
2574
2575 let content = payload.into_inner();
2576 let mut conn = pool.acquire().await?;
2577 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2578
2579 let upserted_partner_block =
2580 models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
2581
2582 token.authorized_ok(web::Json(upserted_partner_block))
2583}
2584
2585#[utoipa::path(
2589 get,
2590 path = "/{course_id}/partners-block",
2591 operation_id = "getCoursePartnersBlock",
2592 tag = "courses",
2593 params(
2594 ("course_id" = Uuid, Path, description = "Course id")
2595 ),
2596 responses(
2597 (status = 200, description = "Partners block", body = serde_json::Value)
2598 )
2599)]
2600#[instrument(skip(pool))]
2601async fn get_partners_block(
2602 path: web::Path<Uuid>,
2603 user: AuthUser,
2604 pool: web::Data<PgPool>,
2605) -> ControllerResult<web::Json<PartnersBlock>> {
2606 let course_id = path.into_inner();
2607 let mut conn = pool.acquire().await?;
2608 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2609
2610 let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
2612
2613 let partner_block = if course_exists {
2614 models::partner_block::get_partner_block(&mut conn, course_id).await?
2616 } else {
2617 let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
2619
2620 models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
2622 };
2623
2624 token.authorized_ok(web::Json(partner_block))
2625}
2626
2627#[utoipa::path(
2631 delete,
2632 path = "/{course_id}/partners-block",
2633 operation_id = "deleteCoursePartnersBlock",
2634 tag = "courses",
2635 params(
2636 ("course_id" = Uuid, Path, description = "Course id")
2637 ),
2638 responses(
2639 (status = 200, description = "Deleted partners block", body = serde_json::Value)
2640 )
2641)]
2642#[instrument(skip(pool))]
2643async fn delete_partners_block(
2644 path: web::Path<Uuid>,
2645 pool: web::Data<PgPool>,
2646 user: AuthUser,
2647) -> ControllerResult<web::Json<PartnersBlock>> {
2648 let course_id = path.into_inner();
2649 let mut conn = pool.acquire().await?;
2650 let token = authorize(
2651 &mut conn,
2652 Act::UsuallyUnacceptableDeletion,
2653 Some(user.id),
2654 Res::Course(course_id),
2655 )
2656 .await?;
2657 let deleted_partners_block =
2658 models::partner_block::delete_partner_block(&mut conn, course_id).await?;
2659
2660 token.authorized_ok(web::Json(deleted_partners_block))
2661}
2662
2663#[utoipa::path(
2669 get,
2670 path = "/{course_id}/sisu-course-llm-descriptions",
2671 operation_id = "getSisuCourseLlmDescriptions",
2672 tag = "courses",
2673 params(
2674 ("course_id" = Uuid, Path, description = "Course id")
2675 ),
2676 responses(
2677 (status = 200, description = "Sisu course LLM descriptions", body = SisuDescriptionResponse)
2678 )
2679)]
2680#[instrument(skip(pool, app_conf))]
2681async fn get_sisu_course_llm_descriptions(
2682 course_id: web::Path<Uuid>,
2683 pool: web::Data<PgPool>,
2684 app_conf: web::Data<ApplicationConfiguration>,
2685 user: AuthUser,
2686 sisu_client: web::Data<SisuClient>,
2687) -> ControllerResult<web::Json<SisuDescriptionResponse>> {
2688 let (course_modules, course_lang, message_suggest_llm, token) = {
2689 let mut conn = pool.acquire().await?;
2690
2691 let token =
2692 authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
2693 let course_modules =
2694 models::course_modules::get_by_course_id(&mut conn, *course_id).await?;
2695 let course_lang = models::courses::get_course(&mut conn, *course_id)
2696 .await?
2697 .language_code;
2698 let message_suggest_llm = models::application_task_default_language_models::get_for_task(
2699 &mut conn,
2700 ApplicationTask::SisuDescriptionSummary,
2701 )
2702 .await?;
2703 (course_modules, course_lang, message_suggest_llm, token)
2704 };
2705
2706 let uh_course_codes: Vec<String> = course_modules
2707 .into_iter()
2708 .filter_map(|course_module| course_module.uh_course_code)
2709 .collect::<Vec<String>>();
2710 let course_ids = SisuClient::get_course_ids(&sisu_client, uh_course_codes).await?;
2711 let course_info = SisuClient::get_course_info(&sisu_client, course_ids).await?;
2712
2713 let parsed_course_info = SisuClient::parse_course_info(course_info, course_lang);
2714
2715 let llm_descriptions = headless_lms_chatbot::course_description_summary::generate_description(
2716 &app_conf,
2717 message_suggest_llm,
2718 parsed_course_info,
2719 )
2720 .await?;
2721 token.authorized_ok(web::Json(llm_descriptions))
2722}
2723
2724#[utoipa::path(
2729 post,
2730 path = "/{course_id}/update-metadata",
2731 operation_id = "updateMetadata",
2732 tag = "courses",
2733 params(
2734 ("course_id" = Uuid, Path, description = "Course id")
2735 ),
2736 request_body = CourseMetadataUpdate,
2737 responses(
2738 (status = 200, description = "Updated metadata", body = CourseMetadata)
2739 )
2740)]
2741#[instrument(skip(pool, app_conf))]
2742async fn update_metadata(
2743 payload: web::Json<CourseMetadataUpdate>,
2744 app_conf: web::Data<ApplicationConfiguration>,
2745 course_id: web::Path<Uuid>,
2746 pool: web::Data<PgPool>,
2747 user: AuthUser,
2748) -> ControllerResult<web::Json<CourseMetadata>> {
2749 let mut conn = pool.acquire().await?;
2750 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2751 let metadata_update = payload.0;
2752 let course =
2753 models::courses::set_metadata(&mut conn, app_conf.as_ref(), *course_id, metadata_update)
2754 .await?;
2755
2756 token.authorized_ok(web::Json(course))
2757}
2758
2759#[utoipa::path(
2764 get,
2765 path = "/{course_id}/get-course-prerequisites",
2766 operation_id= "getCoursePrerequisites",
2767 tag = "courses",
2768 params(
2769 ("course_id" = Uuid, Path, description = "Course id")
2770 ),
2771 responses(
2772 (status = 200, description = "Course prerequisites", body = Vec<CoursePrerequisite>)
2773 )
2774)]
2775#[instrument(skip(pool))]
2776async fn get_course_prerequisites(
2777 course_id: web::Path<Uuid>,
2778 pool: web::Data<PgPool>,
2779 user: AuthUser,
2780) -> ControllerResult<web::Json<Vec<CoursePrerequisite>>> {
2781 let mut conn = pool.acquire().await?;
2782 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2783 let prerequisites =
2784 models::course_prerequisites::get_by_course_id(&mut conn, *course_id).await?;
2785
2786 token.authorized_ok(web::Json(prerequisites))
2787}
2788
2789#[utoipa::path(
2794 get,
2795 path = "/{course_id}/get-course-audiences",
2796 operation_id= "getCourseAudiences",
2797 tag = "courses",
2798 params(
2799 ("course_id" = Uuid, Path, description = "Course id")
2800 ),
2801 responses(
2802 (status = 200, description = "Course audiences", body = Vec<CourseAudience>)
2803 )
2804)]
2805#[instrument(skip(pool))]
2806async fn get_course_audiences(
2807 course_id: web::Path<Uuid>,
2808 pool: web::Data<PgPool>,
2809 user: AuthUser,
2810) -> ControllerResult<web::Json<Vec<CourseAudience>>> {
2811 let mut conn = pool.acquire().await?;
2812 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2813 let audiences = models::course_audiences::get_by_course_id(&mut conn, *course_id).await?;
2814
2815 token.authorized_ok(web::Json(audiences))
2816}
2817
2818#[utoipa::path(
2823 get,
2824 path = "/{course_id}/get-course-metadata",
2825 operation_id= "getCourseMetadata",
2826 tag = "courses",
2827 params(
2828 ("course_id" = Uuid, Path, description = "Course id")
2829 ),
2830 responses(
2831 (status = 200, description = "Course metadata", body = CompleteCourseMetadata)
2832 )
2833)]
2834#[instrument(skip(pool))]
2835async fn get_course_metadata(
2836 course_id: web::Path<Uuid>,
2837 pool: web::Data<PgPool>,
2838 user: AuthUser,
2839) -> ControllerResult<web::Json<CompleteCourseMetadata>> {
2840 let mut conn: sqlx::pool::PoolConnection<Postgres> = pool.acquire().await?;
2841 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2842 let metadata = models::courses::get_metadata(&mut conn, *course_id).await?;
2843
2844 token.authorized_ok(web::Json(metadata))
2845}
2846#[utoipa::path(
2852 get,
2853 path = "/",
2854 operation_id = "getAllCourses",
2855 tag = "courses",
2856 responses(
2857 (status = 200, description = "All courses", body = Vec<Course>)
2858 )
2859)]
2860#[instrument(skip(pool))]
2861async fn get_all_courses(
2862 pool: web::Data<PgPool>,
2863 user: AuthUser,
2864) -> ControllerResult<web::Json<Vec<Course>>> {
2865 let mut conn = pool.acquire().await?;
2866 let all_courses = models::courses::all_courses(&mut conn).await?;
2867 let token = authorize(&mut conn, Act::View, Some(user.id), Res::GlobalPermissions).await?;
2868 token.authorized_ok(web::Json(all_courses))
2869}
2870
2871pub fn _add_routes(cfg: &mut ServiceConfig) {
2879 cfg.service(web::scope("/{course_id}/stats").configure(stats::_add_routes))
2880 .service(web::scope("/{course_id}/chatbots").configure(chatbots::_add_routes))
2881 .service(web::scope("/{course_id}/students").configure(students::_add_routes))
2882 .route("/{course_id}", web::get().to(get_course))
2883 .route("", web::post().to(post_new_course))
2884 .route("/{course_id}", web::put().to(update_course))
2885 .route("/{course_id}", web::delete().to(delete_course))
2886 .route(
2887 "/{course_id}/status-for-all-exercises/{user_id}",
2888 web::get().to(get_all_exercise_statuses_by_course_id),
2889 )
2890 .route(
2891 "/{course_id}/course-module-completions/{user_id}",
2892 web::get().to(get_all_course_module_completions_for_user_by_course_id),
2893 )
2894 .route(
2895 "/{course_id}/daily-submission-counts",
2896 web::get().to(get_daily_submission_counts),
2897 )
2898 .route(
2899 "/{course_id}/daily-users-who-have-submitted-something",
2900 web::get().to(get_daily_user_counts_with_submissions),
2901 )
2902 .route("/{course_id}/exercises", web::get().to(get_all_exercises))
2903 .route(
2904 "/{course_id}/exercises-and-count-of-answers-requiring-attention",
2905 web::get().to(get_all_exercises_and_count_of_answers_requiring_attention),
2906 )
2907 .route(
2908 "/{course_id}/structure",
2909 web::get().to(get_course_structure),
2910 )
2911 .route(
2912 "/{course_id}/language-versions",
2913 web::get().to(get_all_course_language_versions),
2914 )
2915 .route(
2916 "/{course_id}/create-copy",
2917 web::post().to(create_course_copy),
2918 )
2919 .route("/{course_id}/upload", web::post().to(add_media_for_course))
2920 .route(
2921 "/{course_id}/weekday-hour-submission-counts",
2922 web::get().to(get_weekday_hour_submission_counts),
2923 )
2924 .route(
2925 "/{course_id}/submission-counts-by-exercise",
2926 web::get().to(get_submission_counts_by_exercise),
2927 )
2928 .route(
2929 "/{course_id}/course-instances",
2930 web::get().to(get_course_instances),
2931 )
2932 .route("/{course_id}/feedback", web::get().to(get_feedback))
2933 .route(
2934 "/{course_id}/feedback-count",
2935 web::get().to(get_feedback_count),
2936 )
2937 .route(
2938 "/{course_id}/new-course-instance",
2939 web::post().to(new_course_instance),
2940 )
2941 .route("/{course_id}/glossary", web::get().to(glossary))
2942 .route("/{course_id}/glossary", web::post().to(new_glossary_term))
2943 .route(
2944 "/{course_id}/course-users-counts-by-exercise",
2945 web::get().to(get_course_users_counts_by_exercise),
2946 )
2947 .route(
2948 "/{course_id}/new-page-ordering",
2949 web::post().to(post_new_page_ordering),
2950 )
2951 .route(
2952 "/{course_id}/new-chapter-ordering",
2953 web::post().to(post_new_chapter_ordering),
2954 )
2955 .route(
2956 "/{course_id}/references",
2957 web::get().to(get_material_references_by_course_id),
2958 )
2959 .route(
2960 "/{course_id}/references",
2961 web::post().to(insert_material_references),
2962 )
2963 .route(
2964 "/{course_id}/references/{reference_id}",
2965 web::post().to(update_material_reference),
2966 )
2967 .route(
2968 "/{course_id}/references/{reference_id}",
2969 web::delete().to(delete_material_reference_by_id),
2970 )
2971 .route(
2972 "/{course_id}/course-modules",
2973 web::post().to(update_modules),
2974 )
2975 .route(
2976 "/{course_id}/default-peer-review",
2977 web::get().to(get_course_default_peer_review),
2978 )
2979 .route(
2980 "/{course_id}/update-peer-review-queue-reviews-received",
2981 web::post().to(post_update_peer_review_queue_reviews_received),
2982 )
2983 .route(
2984 "/{course_id}/breadcrumb-info",
2985 web::get().to(get_course_breadcrumb_info),
2986 )
2987 .route(
2988 "/{course_id}/progress/{user_id}",
2989 web::get().to(get_user_progress_for_course),
2990 )
2991 .route(
2992 "/{course_id}/user-settings/{user_id}",
2993 web::get().to(get_user_course_settings),
2994 )
2995 .route(
2996 "/{course_id}/export-submissions",
2997 web::get().to(submission_export),
2998 )
2999 .route(
3000 "/{course_id}/export-user-details",
3001 web::get().to(user_details_export),
3002 )
3003 .route(
3004 "/{course_id}/export-exercise-tasks",
3005 web::get().to(exercise_tasks_export),
3006 )
3007 .route(
3008 "/{course_id}/export-course-instances",
3009 web::get().to(course_instances_export),
3010 )
3011 .route(
3012 "/{course_id}/export-course-user-consents",
3013 web::get().to(course_consent_form_answers_export),
3014 )
3015 .route(
3016 "/{course_id}/export-user-exercise-states",
3017 web::get().to(user_exercise_states_export),
3018 )
3019 .route(
3020 "/{course_id}/page-visit-datum-summary",
3021 web::get().to(get_page_visit_datum_summary),
3022 )
3023 .route(
3024 "/{course_id}/page-visit-datum-summary-by-pages",
3025 web::get().to(get_page_visit_datum_summary_by_pages),
3026 )
3027 .route(
3028 "/{course_id}/page-visit-datum-summary-by-device-types",
3029 web::get().to(get_page_visit_datum_summary_by_device_types),
3030 )
3031 .route(
3032 "/{course_id}/page-visit-datum-summary-by-countries",
3033 web::get().to(get_page_visit_datum_summary_by_countries),
3034 )
3035 .route(
3036 "/{course_id}/teacher-reset-course-progress-for-themselves",
3037 web::delete().to(teacher_reset_course_progress_for_themselves),
3038 )
3039 .route("/{course_id}/thresholds", web::get().to(get_all_thresholds))
3040 .route(
3041 "/{course_id}/suspected-cheaters",
3042 web::get().to(get_all_suspected_cheaters),
3043 )
3044 .route(
3045 "/{course_id}/suspected-cheaters/flagged-count",
3046 web::get().to(get_flagged_suspected_cheaters_count),
3047 )
3048 .route(
3049 "/{course_id}/suspected-cheaters/dismiss/{user_id}",
3050 web::post().to(teacher_dismiss_suspected_cheater),
3051 )
3052 .route(
3053 "/{course_id}/suspected-cheaters/confirm/{user_id}",
3054 web::post().to(teacher_confirm_suspected_cheater),
3055 )
3056 .route(
3057 "/{course_id}/teacher-reset-course-progress-for-everyone",
3058 web::delete().to(teacher_reset_course_progress_for_everyone),
3059 )
3060 .route(
3061 "/{course_id}/join-course-with-join-code",
3062 web::post().to(add_user_to_course_with_join_code),
3063 )
3064 .route(
3065 "/{course_id}/partners-block",
3066 web::post().to(post_partners_block),
3067 )
3068 .route(
3069 "/{course_id}/partners-block",
3070 web::get().to(get_partners_block),
3071 )
3072 .route(
3073 "/{course_id}/partners-block",
3074 web::delete().to(delete_partners_block),
3075 )
3076 .route(
3077 "/{course_id}/set-join-code",
3078 web::post().to(set_join_code_for_course),
3079 )
3080 .route(
3081 "/{course_id}/reprocess-completions",
3082 web::post().to(post_reprocess_module_completions),
3083 )
3084 .route(
3085 "/join/{join_code}",
3086 web::get().to(get_course_with_join_code),
3087 )
3088 .route(
3089 "/{course_id}/sisu-course-llm-descriptions",
3090 web::get().to(get_sisu_course_llm_descriptions),
3091 )
3092 .route(
3093 "/{course_id}/update-metadata",
3094 web::post().to(update_metadata),
3095 )
3096 .route(
3097 "/{course_id}/get-course-prerequisites",
3098 web::get().to(get_course_prerequisites),
3099 )
3100 .route(
3101 "/{course_id}/get-course-audiences",
3102 web::get().to(get_course_audiences),
3103 )
3104 .route(
3105 "/{course_id}/get-course-metadata",
3106 web::get().to(get_course_metadata),
3107 )
3108 .route("/", web::get().to(get_all_courses));
3109}