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 PKeyPolicy::Generate,
433 new_course,
434 user.id,
435 models_requests::make_spec_fetcher(
436 app_conf.base_url.clone(),
437 request_id.0,
438 Arc::clone(&jwt_key),
439 ),
440 models_requests::fetch_service_info,
441 )
442 .await?;
443 models::roles::insert(
444 &mut tx,
445 user.id,
446 models::roles::UserRole::Teacher,
447 models::roles::RoleDomain::Course(course.id),
448 )
449 .await?;
450 tx.commit().await?;
451
452 token.authorized_ok(web::Json(course))
453}
454
455#[utoipa::path(
471 put,
472 path = "/{course_id}",
473 operation_id = "updateCourse",
474 tag = "courses",
475 params(
476 ("course_id" = Uuid, Path, description = "Course id")
477 ),
478 request_body = CourseUpdate,
479 responses(
480 (status = 200, description = "Updated course", body = Course)
481 )
482)]
483#[instrument(skip(pool))]
484async fn update_course(
485 payload: web::Json<CourseUpdate>,
486 course_id: web::Path<Uuid>,
487 pool: web::Data<PgPool>,
488 user: AuthUser,
489) -> ControllerResult<web::Json<Course>> {
490 let mut conn = pool.acquire().await?;
491 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
492 let course_update = payload.0;
493 let course_before_update = models::courses::get_course(&mut conn, *course_id).await?;
494 if course_update.can_add_chatbot != course_before_update.can_add_chatbot {
495 let _token2 =
497 authorize(&mut conn, Act::Teach, Some(user.id), Res::GlobalPermissions).await?;
498 }
499
500 let locking_just_enabled =
501 !course_before_update.chapter_locking_enabled && course_update.chapter_locking_enabled;
502
503 let course = models::courses::update_course(&mut conn, *course_id, course_update).await?;
504
505 if locking_just_enabled {
506 use models::{user_chapter_locking_statuses, user_course_settings};
507
508 let all_user_settings =
509 user_course_settings::get_all_by_course_id(&mut conn, *course_id).await?;
510
511 for settings in all_user_settings {
512 let _ = user_chapter_locking_statuses::get_or_init_all_for_course(
513 &mut conn,
514 settings.user_id,
515 *course_id,
516 )
517 .await?;
518 }
519 }
520
521 token.authorized_ok(web::Json(course))
522}
523
524#[utoipa::path(
528 delete,
529 path = "/{course_id}",
530 operation_id = "deleteCourse",
531 tag = "courses",
532 params(
533 ("course_id" = Uuid, Path, description = "Course id")
534 ),
535 responses(
536 (status = 200, description = "Deleted course", body = serde_json::Value)
537 )
538)]
539#[instrument(skip(pool))]
540async fn delete_course(
541 course_id: web::Path<Uuid>,
542 pool: web::Data<PgPool>,
543 user: AuthUser,
544) -> ControllerResult<web::Json<Course>> {
545 let mut conn = pool.acquire().await?;
546 let token = authorize(
547 &mut conn,
548 Act::UsuallyUnacceptableDeletion,
549 Some(user.id),
550 Res::Course(*course_id),
551 )
552 .await?;
553 let course = models::courses::delete_course(&mut conn, *course_id).await?;
554
555 token.authorized_ok(web::Json(course))
556}
557
558#[utoipa::path(
605 get,
606 path = "/{course_id}/structure",
607 operation_id = "getCourseStructure",
608 tag = "courses",
609 params(
610 ("course_id" = Uuid, Path, description = "Course id")
611 ),
612 responses(
613 (status = 200, description = "Course structure", body = CourseStructure)
614 )
615)]
616#[instrument(skip(pool, file_store, app_conf))]
617async fn get_course_structure(
618 course_id: web::Path<Uuid>,
619 pool: web::Data<PgPool>,
620 user: AuthUser,
621 file_store: web::Data<dyn FileStore>,
622 app_conf: web::Data<ApplicationConfiguration>,
623) -> ControllerResult<web::Json<CourseStructure>> {
624 let mut conn = pool.acquire().await?;
625 let token = authorize(
626 &mut conn,
627 Act::ViewInternalCourseStructure,
628 Some(user.id),
629 Res::Course(*course_id),
630 )
631 .await?;
632 let course_structure = models::courses::get_course_structure(
633 &mut conn,
634 *course_id,
635 file_store.as_ref(),
636 app_conf.as_ref(),
637 )
638 .await?;
639
640 token.authorized_ok(web::Json(course_structure))
641}
642
643#[utoipa::path(
658 post,
659 path = "/{course_id}/upload",
660 operation_id = "uploadCourseMedia",
661 tag = "courses",
662 params(
663 ("course_id" = Uuid, Path, description = "Course id")
664 ),
665 request_body(
666 content = String,
667 content_type = "multipart/form-data"
668 ),
669 responses(
670 (status = 200, description = "Uploaded media result", body = UploadResult)
671 )
672)]
673#[instrument(skip(payload, request, pool, file_store, app_conf))]
674async fn add_media_for_course(
675 course_id: web::Path<Uuid>,
676 payload: Multipart,
677 request: HttpRequest,
678 pool: web::Data<PgPool>,
679 user: AuthUser,
680 file_store: web::Data<dyn FileStore>,
681 app_conf: web::Data<ApplicationConfiguration>,
682) -> ControllerResult<web::Json<UploadResult>> {
683 let mut conn = pool.acquire().await?;
684 let course = models::courses::get_course(&mut conn, *course_id).await?;
685 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
686 let media_path = upload_file_from_cms(
687 request.headers(),
688 payload,
689 StoreKind::Course(course.id),
690 file_store.as_ref(),
691 &mut conn,
692 user,
693 )
694 .await?;
695 let download_url = file_store.get_download_url(media_path.as_path(), app_conf.as_ref());
696
697 token.authorized_ok(web::Json(UploadResult { url: download_url }))
698}
699
700#[utoipa::path(
704 get,
705 path = "/{course_id}/exercises",
706 operation_id = "getCourseExercises",
707 tag = "courses",
708 params(
709 ("course_id" = Uuid, Path, description = "Course id")
710 ),
711 responses(
712 (status = 200, description = "Exercises for course", body = [Exercise])
713 )
714)]
715#[instrument(skip(pool))]
716async fn get_all_exercises(
717 pool: web::Data<PgPool>,
718 course_id: web::Path<Uuid>,
719 user: AuthUser,
720) -> ControllerResult<web::Json<Vec<Exercise>>> {
721 let mut conn = pool.acquire().await?;
722 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
723 let exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
724
725 token.authorized_ok(web::Json(exercises))
726}
727
728#[utoipa::path(
732 get,
733 path = "/{course_id}/exercises-and-count-of-answers-requiring-attention",
734 operation_id = "getCourseExercisesAndAnswersRequiringAttentionCounts",
735 tag = "courses",
736 params(
737 ("course_id" = Uuid, Path, description = "Course id")
738 ),
739 responses(
740 (
741 status = 200,
742 description = "Exercises and answer attention counts",
743 body = [ExerciseAnswersInCourseRequiringAttentionCount]
744 )
745 )
746)]
747#[instrument(skip(pool))]
748async fn get_all_exercises_and_count_of_answers_requiring_attention(
749 pool: web::Data<PgPool>,
750 course_id: web::Path<Uuid>,
751 user: AuthUser,
752) -> ControllerResult<web::Json<Vec<ExerciseAnswersInCourseRequiringAttentionCount>>> {
753 let mut conn = pool.acquire().await?;
754 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
755 let _exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
756 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?;
757 token.authorized_ok(web::Json(count_of_answers_requiring_attention))
758}
759
760#[utoipa::path(
772 get,
773 path = "/{course_id}/language-versions",
774 operation_id = "getCourseLanguageVersions",
775 tag = "courses",
776 params(
777 ("course_id" = Uuid, Path, description = "Course id")
778 ),
779 responses(
780 (status = 200, description = "Course language versions", body = [Course])
781 )
782)]
783#[instrument(skip(pool))]
784async fn get_all_course_language_versions(
785 pool: web::Data<PgPool>,
786 course_id: web::Path<Uuid>,
787 user: AuthUser,
788) -> ControllerResult<web::Json<Vec<Course>>> {
789 let mut conn = pool.acquire().await?;
790 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
791 let course = models::courses::get_course(&mut conn, *course_id).await?;
792 let language_versions =
793 models::courses::get_all_language_versions_of_course(&mut conn, &course).await?;
794
795 token.authorized_ok(web::Json(language_versions))
796}
797
798#[derive(Deserialize, Debug, utoipa::ToSchema)]
799#[serde(tag = "mode", rename_all = "snake_case")]
800pub enum CopyCourseMode {
801 Duplicate,
803 SameLanguageGroup,
805 ExistingLanguageGroup { target_course_id: Uuid },
807 NewLanguageGroup,
809}
810
811#[derive(Deserialize, Debug, utoipa::ToSchema)]
812
813pub struct CopyCourseRequest {
814 #[serde(flatten)]
815 pub new_course: NewCourse,
816 pub mode: CopyCourseMode,
817}
818
819#[utoipa::path(
863 post,
864 path = "/{course_id}/create-copy",
865 operation_id = "createCourseCopy",
866 tag = "courses",
867 params(
868 ("course_id" = Uuid, Path, description = "Course id")
869 ),
870 request_body = CopyCourseRequest,
871 responses(
872 (status = 200, description = "Created course copy", body = Course)
873 )
874)]
875#[instrument(skip(pool))]
876pub async fn create_course_copy(
877 pool: web::Data<PgPool>,
878 course_id: web::Path<Uuid>,
879 payload: web::Json<CopyCourseRequest>,
880 user: AuthUser,
881) -> ControllerResult<web::Json<Course>> {
882 let mut conn = pool.acquire().await?;
883 let token = authorize(
884 &mut conn,
885 Act::Duplicate,
886 Some(user.id),
887 Res::Course(*course_id),
888 )
889 .await?;
890
891 let new_course = payload.new_course.clone();
892 authorize(
893 &mut conn,
894 Act::CreateCoursesOrExams,
895 Some(user.id),
896 Res::Organization(new_course.organization_id),
897 )
898 .await?;
899
900 let mut tx = conn.begin().await?;
901
902 let copied_course = match &payload.mode {
903 CopyCourseMode::Duplicate => {
904 models::library::copying::copy_course(&mut tx, *course_id, &new_course, false, user.id)
905 .await?
906 }
907 CopyCourseMode::SameLanguageGroup => {
908 models::library::copying::copy_course(&mut tx, *course_id, &new_course, true, user.id)
909 .await?
910 }
911 CopyCourseMode::ExistingLanguageGroup { target_course_id } => {
912 let target_course = models::courses::get_course(&mut tx, *target_course_id).await?;
913 authorize(
915 &mut tx,
916 Act::Duplicate,
917 Some(user.id),
918 Res::Course(*target_course_id),
919 )
920 .await?;
921 models::library::copying::copy_course_with_language_group(
922 &mut tx,
923 *course_id,
924 target_course.course_language_group_id,
925 &new_course,
926 user.id,
927 )
928 .await?
929 }
930 CopyCourseMode::NewLanguageGroup => {
931 let new_clg_id = course_language_groups::insert(
932 &mut tx,
933 PKeyPolicy::Generate,
934 new_course.slug.as_str(),
935 )
936 .await?;
937 models::library::copying::copy_course_with_language_group(
938 &mut tx,
939 *course_id,
940 new_clg_id,
941 &new_course,
942 user.id,
943 )
944 .await?
945 }
946 };
947
948 models::roles::insert(
949 &mut tx,
950 user.id,
951 models::roles::UserRole::Teacher,
952 models::roles::RoleDomain::Course(copied_course.id),
953 )
954 .await?;
955
956 tx.commit().await?;
957
958 token.authorized_ok(web::Json(copied_course))
959}
960
961#[utoipa::path(
965 get,
966 path = "/{course_id}/daily-submission-counts",
967 operation_id = "getCourseDailySubmissionCounts",
968 tag = "courses",
969 params(
970 ("course_id" = Uuid, Path, description = "Course id")
971 ),
972 responses(
973 (status = 200, description = "Course daily submission counts", body = [ExerciseSlideSubmissionCount])
974 )
975)]
976#[instrument(skip(pool))]
977async fn get_daily_submission_counts(
978 pool: web::Data<PgPool>,
979 course_id: web::Path<Uuid>,
980 user: AuthUser,
981) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
982 let mut conn = pool.acquire().await?;
983 let token = authorize(
984 &mut conn,
985 Act::ViewStats,
986 Some(user.id),
987 Res::Course(*course_id),
988 )
989 .await?;
990 let course = models::courses::get_course(&mut conn, *course_id).await?;
991 let res =
992 exercise_slide_submissions::get_course_daily_slide_submission_counts(&mut conn, &course)
993 .await?;
994
995 token.authorized_ok(web::Json(res))
996}
997
998#[utoipa::path(
1002 get,
1003 path = "/{course_id}/daily-users-who-have-submitted-something",
1004 operation_id = "getCourseDailyUsersWhoSubmittedSomething",
1005 tag = "courses",
1006 params(
1007 ("course_id" = Uuid, Path, description = "Course id")
1008 ),
1009 responses(
1010 (status = 200, description = "Course daily user submission counts", body = [ExerciseSlideSubmissionCount])
1011 )
1012)]
1013#[instrument(skip(pool))]
1014async fn get_daily_user_counts_with_submissions(
1015 pool: web::Data<PgPool>,
1016 course_id: web::Path<Uuid>,
1017 user: AuthUser,
1018) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
1019 let mut conn = pool.acquire().await?;
1020 let token = authorize(
1021 &mut conn,
1022 Act::ViewStats,
1023 Some(user.id),
1024 Res::Course(*course_id),
1025 )
1026 .await?;
1027 let course = models::courses::get_course(&mut conn, *course_id).await?;
1028 let res = exercise_slide_submissions::get_course_daily_user_counts_with_submissions(
1029 &mut conn, &course,
1030 )
1031 .await?;
1032
1033 token.authorized_ok(web::Json(res))
1034}
1035
1036#[utoipa::path(
1040 get,
1041 path = "/{course_id}/weekday-hour-submission-counts",
1042 operation_id = "getCourseWeekdayHourSubmissionCounts",
1043 tag = "courses",
1044 params(
1045 ("course_id" = Uuid, Path, description = "Course id")
1046 ),
1047 responses(
1048 (status = 200, description = "Course weekday and hour submission counts", body = [ExerciseSlideSubmissionCountByWeekAndHour])
1049 )
1050)]
1051#[instrument(skip(pool))]
1052async fn get_weekday_hour_submission_counts(
1053 pool: web::Data<PgPool>,
1054 course_id: web::Path<Uuid>,
1055 user: AuthUser,
1056) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByWeekAndHour>>> {
1057 let mut conn = pool.acquire().await?;
1058 let token = authorize(
1059 &mut conn,
1060 Act::ViewStats,
1061 Some(user.id),
1062 Res::Course(*course_id),
1063 )
1064 .await?;
1065 let course = models::courses::get_course(&mut conn, *course_id).await?;
1066 let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_weekday_and_hour(
1067 &mut conn, &course,
1068 )
1069 .await?;
1070
1071 token.authorized_ok(web::Json(res))
1072}
1073
1074#[utoipa::path(
1078 get,
1079 path = "/{course_id}/submission-counts-by-exercise",
1080 operation_id = "getCourseSubmissionCountsByExercise",
1081 tag = "courses",
1082 params(
1083 ("course_id" = Uuid, Path, description = "Course id")
1084 ),
1085 responses(
1086 (status = 200, description = "Course submission counts by exercise", body = [ExerciseSlideSubmissionCountByExercise])
1087 )
1088)]
1089#[instrument(skip(pool))]
1090async fn get_submission_counts_by_exercise(
1091 pool: web::Data<PgPool>,
1092 course_id: web::Path<Uuid>,
1093 user: AuthUser,
1094) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByExercise>>> {
1095 let mut conn = pool.acquire().await?;
1096 let token = authorize(
1097 &mut conn,
1098 Act::ViewStats,
1099 Some(user.id),
1100 Res::Course(*course_id),
1101 )
1102 .await?;
1103 let course = models::courses::get_course(&mut conn, *course_id).await?;
1104 let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_exercise(
1105 &mut conn, &course,
1106 )
1107 .await?;
1108
1109 token.authorized_ok(web::Json(res))
1110}
1111
1112#[utoipa::path(
1116 get,
1117 path = "/{course_id}/course-instances",
1118 operation_id = "getCourseInstances",
1119 tag = "courses",
1120 params(
1121 ("course_id" = Uuid, Path, description = "Course id")
1122 ),
1123 responses(
1124 (status = 200, description = "Course instances", body = [CourseInstance])
1125 )
1126)]
1127#[instrument(skip(pool))]
1128async fn get_course_instances(
1129 pool: web::Data<PgPool>,
1130 course_id: web::Path<Uuid>,
1131 user: AuthUser,
1132) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
1133 let mut conn = pool.acquire().await?;
1134 let token = authorize(
1135 &mut conn,
1136 Act::Teach,
1137 Some(user.id),
1138 Res::Course(*course_id),
1139 )
1140 .await?;
1141 let course_instances =
1142 models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
1143
1144 token.authorized_ok(web::Json(course_instances))
1145}
1146
1147#[derive(Debug, Deserialize)]
1148
1149pub struct GetFeedbackQuery {
1150 read: bool,
1151 #[serde(flatten)]
1152 pagination: Pagination,
1153}
1154
1155#[utoipa::path(
1159 get,
1160 path = "/{course_id}/feedback",
1161 operation_id = "getCourseFeedback",
1162 tag = "courses",
1163 params(
1164 ("course_id" = String, Path, description = "Course id"),
1165 ("read" = bool, Query, description = "Whether to fetch read feedback"),
1166 ("page" = Option<i64>, Query, description = "Page number"),
1167 ("limit" = Option<i64>, Query, description = "Page size")
1168 ),
1169 responses(
1170 (status = 200, description = "Feedback for the course", body = [Feedback])
1171 )
1172)]
1173#[instrument(skip(pool))]
1174pub async fn get_feedback(
1175 course_id: web::Path<Uuid>,
1176 pool: web::Data<PgPool>,
1177 read: web::Query<GetFeedbackQuery>,
1178 user: AuthUser,
1179) -> ControllerResult<web::Json<Vec<Feedback>>> {
1180 let mut conn = pool.acquire().await?;
1181 let token = authorize(
1182 &mut conn,
1183 Act::Teach,
1184 Some(user.id),
1185 Res::Course(*course_id),
1186 )
1187 .await?;
1188 let feedback =
1189 feedback::get_feedback_for_course(&mut conn, *course_id, read.read, read.pagination)
1190 .await?;
1191
1192 token.authorized_ok(web::Json(feedback))
1193}
1194
1195#[utoipa::path(
1199 get,
1200 path = "/{course_id}/feedback-count",
1201 operation_id = "getCourseFeedbackCount",
1202 tag = "courses",
1203 params(
1204 ("course_id" = Uuid, Path, description = "Course id")
1205 ),
1206 responses(
1207 (status = 200, description = "Feedback counts for the course", body = FeedbackCount)
1208 )
1209)]
1210#[instrument(skip(pool))]
1211pub async fn get_feedback_count(
1212 course_id: web::Path<Uuid>,
1213 pool: web::Data<PgPool>,
1214 user: AuthUser,
1215) -> ControllerResult<web::Json<FeedbackCount>> {
1216 let mut conn = pool.acquire().await?;
1217 let token = authorize(
1218 &mut conn,
1219 Act::Teach,
1220 Some(user.id),
1221 Res::Course(*course_id),
1222 )
1223 .await?;
1224
1225 let feedback_count = feedback::get_feedback_count_for_course(&mut conn, *course_id).await?;
1226
1227 token.authorized_ok(web::Json(feedback_count))
1228}
1229
1230#[utoipa::path(
1234 post,
1235 path = "/{course_id}/new-course-instance",
1236 operation_id = "createCourseInstance",
1237 tag = "courses",
1238 params(
1239 ("course_id" = Uuid, Path, description = "Course id")
1240 ),
1241 request_body = CourseInstanceForm,
1242 responses(
1243 (status = 200, description = "Created course instance id", body = Uuid)
1244 )
1245)]
1246#[instrument(skip(pool))]
1247async fn new_course_instance(
1248 form: web::Json<CourseInstanceForm>,
1249 course_id: web::Path<Uuid>,
1250 pool: web::Data<PgPool>,
1251 user: AuthUser,
1252) -> ControllerResult<web::Json<Uuid>> {
1253 let mut conn = pool.acquire().await?;
1254 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1255 let form = form.into_inner();
1256 let new = NewCourseInstance {
1257 course_id: *course_id,
1258 name: form.name.as_deref(),
1259 description: form.description.as_deref(),
1260 support_email: form.support_email.as_deref(),
1261 teacher_in_charge_name: &form.teacher_in_charge_name,
1262 teacher_in_charge_email: &form.teacher_in_charge_email,
1263 opening_time: form.opening_time,
1264 closing_time: form.closing_time,
1265 };
1266 let ci = models::course_instances::insert(&mut conn, PKeyPolicy::Generate, new).await?;
1267
1268 token.authorized_ok(web::Json(ci.id))
1269}
1270
1271#[instrument(skip(pool))]
1272#[utoipa::path(
1273 get,
1274 path = "/{course_id}/glossary",
1275 operation_id = "getCourseGlossary",
1276 tag = "glossary",
1277 params(
1278 ("course_id" = Uuid, Path, description = "Course id")
1279 ),
1280 responses(
1281 (status = 200, description = "Glossary terms for the course", body = [Term]),
1282 (status = 401, description = "Authentication required"),
1283 (status = 403, description = "User is not allowed to manage the course glossary")
1284 )
1285)]
1286pub(crate) async fn glossary(
1287 pool: web::Data<PgPool>,
1288 course_id: web::Path<Uuid>,
1289 user: AuthUser,
1290) -> ControllerResult<web::Json<Vec<Term>>> {
1291 let mut conn = pool.acquire().await?;
1292 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1293 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
1294
1295 token.authorized_ok(web::Json(glossary))
1296}
1297
1298#[instrument(skip(pool))]
1301async fn _new_term(
1302 pool: web::Data<PgPool>,
1303 course_id: web::Path<Uuid>,
1304 user: AuthUser,
1305) -> ControllerResult<web::Json<Vec<Term>>> {
1306 let mut conn = pool.acquire().await?;
1307 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1308 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
1309
1310 token.authorized_ok(web::Json(glossary))
1311}
1312
1313#[instrument(skip(pool))]
1314#[utoipa::path(
1315 post,
1316 path = "/{course_id}/glossary",
1317 operation_id = "createCourseGlossaryTerm",
1318 tag = "glossary",
1319 params(
1320 ("course_id" = Uuid, Path, description = "Course id")
1321 ),
1322 request_body = TermUpdate,
1323 responses(
1324 (status = 200, description = "Created glossary term id", body = Uuid),
1325 (status = 401, description = "Authentication required"),
1326 (status = 403, description = "User is not allowed to manage the course glossary")
1327 )
1328)]
1329pub(crate) async fn new_glossary_term(
1330 pool: web::Data<PgPool>,
1331 course_id: web::Path<Uuid>,
1332 new_term: web::Json<TermUpdate>,
1333 user: AuthUser,
1334) -> ControllerResult<web::Json<Uuid>> {
1335 let mut conn = pool.acquire().await?;
1336 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1337 let TermUpdate { term, definition } = new_term.into_inner();
1338 let term = models::glossary::insert(&mut conn, &term, &definition, *course_id).await?;
1339
1340 token.authorized_ok(web::Json(term))
1341}
1342
1343#[utoipa::path(
1347 get,
1348 path = "/{course_id}/course-users-counts-by-exercise",
1349 operation_id = "getCourseUsersCountsByExercise",
1350 tag = "courses",
1351 params(
1352 ("course_id" = Uuid, Path, description = "Course id")
1353 ),
1354 responses(
1355 (status = 200, description = "Course users counts by exercise", body = [ExerciseUserCounts])
1356 )
1357)]
1358#[instrument(skip(pool))]
1359pub async fn get_course_users_counts_by_exercise(
1360 course_id: web::Path<Uuid>,
1361 pool: web::Data<PgPool>,
1362 user: AuthUser,
1363) -> ControllerResult<web::Json<Vec<ExerciseUserCounts>>> {
1364 let mut conn = pool.acquire().await?;
1365 let course_id = course_id.into_inner();
1366 let token = authorize(
1367 &mut conn,
1368 Act::ViewStats,
1369 Some(user.id),
1370 Res::Course(course_id),
1371 )
1372 .await?;
1373
1374 let res =
1375 models::user_exercise_states::get_course_users_counts_by_exercise(&mut conn, course_id)
1376 .await?;
1377
1378 token.authorized_ok(web::Json(res))
1379}
1380
1381#[utoipa::path(
1389 post,
1390 path = "/{course_id}/new-page-ordering",
1391 operation_id = "updateCoursePageOrdering",
1392 tag = "courses",
1393 params(
1394 ("course_id" = Uuid, Path, description = "Course id")
1395 ),
1396 request_body = Vec<Page>,
1397 responses(
1398 (status = 200, description = "Course page ordering updated")
1399 )
1400)]
1401#[instrument(skip(pool))]
1402pub async fn post_new_page_ordering(
1403 course_id: web::Path<Uuid>,
1404 pool: web::Data<PgPool>,
1405 user: AuthUser,
1406 payload: web::Json<Vec<Page>>,
1407) -> ControllerResult<web::Json<()>> {
1408 let mut conn = pool.acquire().await?;
1409 let course_id = course_id.into_inner();
1410 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1411
1412 models::pages::reorder_pages(&mut conn, &payload, course_id).await?;
1413
1414 token.authorized_ok(web::Json(()))
1415}
1416
1417#[utoipa::path(
1423 post,
1424 path = "/{course_id}/new-chapter-ordering",
1425 operation_id = "updateCourseChapterOrdering",
1426 tag = "courses",
1427 params(
1428 ("course_id" = Uuid, Path, description = "Course id")
1429 ),
1430 request_body = Vec<Chapter>,
1431 responses(
1432 (status = 200, description = "Course chapter ordering updated")
1433 )
1434)]
1435#[instrument(skip(pool))]
1436pub async fn post_new_chapter_ordering(
1437 course_id: web::Path<Uuid>,
1438 pool: web::Data<PgPool>,
1439 user: AuthUser,
1440 payload: web::Json<Vec<Chapter>>,
1441) -> ControllerResult<web::Json<()>> {
1442 let mut conn = pool.acquire().await?;
1443 let course_id = course_id.into_inner();
1444 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1445
1446 models::pages::reorder_chapters(&mut conn, &payload, course_id).await?;
1447
1448 token.authorized_ok(web::Json(()))
1449}
1450
1451#[utoipa::path(
1452 get,
1453 path = "/{course_id}/references",
1454 operation_id = "getCourseReferences",
1455 tag = "courses",
1456 params(
1457 ("course_id" = Uuid, Path, description = "Course id")
1458 ),
1459 responses(
1460 (status = 200, description = "Course references", body = [MaterialReference])
1461 )
1462)]
1463#[instrument(skip(pool))]
1464async fn get_material_references_by_course_id(
1465 course_id: web::Path<Uuid>,
1466 pool: web::Data<PgPool>,
1467 user: AuthUser,
1468) -> ControllerResult<web::Json<Vec<MaterialReference>>> {
1469 let mut conn = pool.acquire().await?;
1470 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1471
1472 let res =
1473 models::material_references::get_references_by_course_id(&mut conn, *course_id).await?;
1474 token.authorized_ok(web::Json(res))
1475}
1476
1477#[utoipa::path(
1478 post,
1479 path = "/{course_id}/references",
1480 operation_id = "createCourseReferences",
1481 tag = "courses",
1482 params(
1483 ("course_id" = Uuid, Path, description = "Course id")
1484 ),
1485 request_body = [NewMaterialReference],
1486 responses(
1487 (status = 200, description = "Course references created")
1488 )
1489)]
1490#[instrument(skip(pool))]
1491async fn insert_material_references(
1492 course_id: web::Path<Uuid>,
1493 payload: web::Json<Vec<NewMaterialReference>>,
1494 pool: web::Data<PgPool>,
1495 user: AuthUser,
1496) -> ControllerResult<web::Json<()>> {
1497 let mut conn = pool.acquire().await?;
1498 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1499
1500 models::material_references::insert_reference(&mut conn, *course_id, payload.0).await?;
1501
1502 token.authorized_ok(web::Json(()))
1503}
1504
1505#[utoipa::path(
1506 post,
1507 path = "/{course_id}/references/{reference_id}",
1508 operation_id = "updateCourseReference",
1509 tag = "courses",
1510 params(
1511 ("course_id" = Uuid, Path, description = "Course id"),
1512 ("reference_id" = Uuid, Path, description = "Reference id")
1513 ),
1514 request_body = NewMaterialReference,
1515 responses(
1516 (status = 200, description = "Course reference updated")
1517 )
1518)]
1519#[instrument(skip(pool))]
1520async fn update_material_reference(
1521 path: web::Path<(Uuid, Uuid)>,
1522 pool: web::Data<PgPool>,
1523 user: AuthUser,
1524 payload: web::Json<NewMaterialReference>,
1525) -> ControllerResult<web::Json<()>> {
1526 let (course_id, reference_id) = path.into_inner();
1527 let mut conn = pool.acquire().await?;
1528 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1529
1530 models::material_references::update_by_id_and_course_id(
1531 &mut conn,
1532 reference_id,
1533 course_id,
1534 payload.0,
1535 )
1536 .await?;
1537 token.authorized_ok(web::Json(()))
1538}
1539
1540#[utoipa::path(
1541 delete,
1542 path = "/{course_id}/references/{reference_id}",
1543 operation_id = "deleteCourseReference",
1544 tag = "courses",
1545 params(
1546 ("course_id" = Uuid, Path, description = "Course id"),
1547 ("reference_id" = Uuid, Path, description = "Reference id")
1548 ),
1549 responses(
1550 (status = 200, description = "Course reference deleted")
1551 )
1552)]
1553#[instrument(skip(pool))]
1554async fn delete_material_reference_by_id(
1555 path: web::Path<(Uuid, Uuid)>,
1556 pool: web::Data<PgPool>,
1557 user: AuthUser,
1558) -> ControllerResult<web::Json<()>> {
1559 let (course_id, reference_id) = path.into_inner();
1560 let mut conn = pool.acquire().await?;
1561 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1562
1563 models::material_references::delete_by_id_and_course_id(&mut conn, reference_id, course_id)
1564 .await?;
1565 token.authorized_ok(web::Json(()))
1566}
1567
1568#[utoipa::path(
1569 post,
1570 path = "/{course_id}/course-modules",
1571 operation_id = "updateCourseModules",
1572 tag = "courses",
1573 params(
1574 ("course_id" = Uuid, Path, description = "Course id")
1575 ),
1576 request_body = ModuleUpdates,
1577 responses(
1578 (status = 200, description = "Course modules updated")
1579 )
1580)]
1581#[instrument(skip(pool))]
1582pub async fn update_modules(
1583 course_id: web::Path<Uuid>,
1584 pool: web::Data<PgPool>,
1585 user: AuthUser,
1586 payload: web::Json<ModuleUpdates>,
1587) -> ControllerResult<web::Json<()>> {
1588 let mut conn = pool.acquire().await?;
1589 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1590
1591 models::course_modules::update_modules(&mut conn, *course_id, payload.into_inner()).await?;
1592 token.authorized_ok(web::Json(()))
1593}
1594
1595#[utoipa::path(
1596 get,
1597 path = "/{course_id}/default-peer-review",
1598 operation_id = "getCourseDefaultPeerReview",
1599 tag = "courses",
1600 params(
1601 ("course_id" = Uuid, Path, description = "Course id")
1602 ),
1603 responses(
1604 (status = 200, description = "Default peer review configuration", body = serde_json::Value)
1605 )
1606)]
1607async fn get_course_default_peer_review(
1608 course_id: web::Path<Uuid>,
1609 pool: web::Data<PgPool>,
1610 user: AuthUser,
1611) -> ControllerResult<web::Json<(PeerOrSelfReviewConfig, Vec<PeerOrSelfReviewQuestion>)>> {
1612 let mut conn = pool.acquire().await?;
1613 let token = authorize(
1614 &mut conn,
1615 Act::Teach,
1616 Some(user.id),
1617 Res::Course(*course_id),
1618 )
1619 .await?;
1620
1621 let peer_review = models::peer_or_self_review_configs::get_default_for_course_by_course_id(
1622 &mut conn, *course_id,
1623 )
1624 .await?;
1625 let peer_or_self_review_questions =
1626 models::peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
1627 &mut conn,
1628 peer_review.id,
1629 )
1630 .await?;
1631 token.authorized_ok(web::Json((peer_review, peer_or_self_review_questions)))
1632}
1633
1634#[utoipa::path(
1640 post,
1641 path = "/{course_id}/update-peer-review-queue-reviews-received",
1642 operation_id = "updateCoursePeerReviewQueueReviewsReceived",
1643 tag = "courses",
1644 params(
1645 ("course_id" = Uuid, Path, description = "Course id")
1646 ),
1647 responses(
1648 (status = 200, description = "Peer review queue updated", body = bool)
1649 )
1650)]
1651#[instrument(skip(pool, user))]
1652async fn post_update_peer_review_queue_reviews_received(
1653 pool: web::Data<PgPool>,
1654 user: AuthUser,
1655 course_id: web::Path<Uuid>,
1656) -> ControllerResult<web::Json<bool>> {
1657 let mut conn = pool.acquire().await?;
1658 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
1659 models::library::peer_or_self_reviewing::update_peer_review_queue_reviews_received(
1660 &mut conn, *course_id,
1661 )
1662 .await?;
1663 token.authorized_ok(web::Json(true))
1664}
1665
1666#[utoipa::path(
1672 get,
1673 path = "/{course_id}/export-submissions",
1674 operation_id = "exportCourseSubmissionsCsv",
1675 tag = "courses",
1676 params(
1677 ("course_id" = Uuid, Path, description = "Course id")
1678 ),
1679 responses(
1680 (status = 200, description = "Course submissions CSV", body = String, content_type = "text/csv")
1681 )
1682)]
1683#[instrument(skip(pool))]
1684pub async fn submission_export(
1685 course_id: web::Path<Uuid>,
1686 pool: web::Data<PgPool>,
1687 user: AuthUser,
1688) -> ControllerResult<HttpResponse> {
1689 let mut conn = pool.acquire().await?;
1690
1691 let token = authorize(
1692 &mut conn,
1693 Act::Teach,
1694 Some(user.id),
1695 Res::Course(*course_id),
1696 )
1697 .await?;
1698
1699 let course = models::courses::get_course(&mut conn, *course_id).await?;
1700
1701 general_export(
1702 pool,
1703 &format!(
1704 "attachment; filename=\"Course: {} - Submissions (exercise tasks) {}.csv\"",
1705 course.name,
1706 Utc::now().format("%Y-%m-%d")
1707 ),
1708 CourseSubmissionExportOperation {
1709 course_id: *course_id,
1710 },
1711 token,
1712 )
1713 .await
1714}
1715
1716#[utoipa::path(
1722 get,
1723 path = "/{course_id}/export-user-details",
1724 operation_id = "exportCourseUserDetailsCsv",
1725 tag = "courses",
1726 params(
1727 ("course_id" = Uuid, Path, description = "Course id")
1728 ),
1729 responses(
1730 (status = 200, description = "Course user details CSV", body = String, content_type = "text/csv")
1731 )
1732)]
1733#[instrument(skip(pool))]
1734pub async fn user_details_export(
1735 course_id: web::Path<Uuid>,
1736 pool: web::Data<PgPool>,
1737 user: AuthUser,
1738) -> ControllerResult<HttpResponse> {
1739 let mut conn = pool.acquire().await?;
1740
1741 let token = authorize(
1742 &mut conn,
1743 Act::Teach,
1744 Some(user.id),
1745 Res::Course(*course_id),
1746 )
1747 .await?;
1748
1749 let course = models::courses::get_course(&mut conn, *course_id).await?;
1750
1751 general_export(
1752 pool,
1753 &format!(
1754 "attachment; filename=\"Course: {} - User Details {}.csv\"",
1755 course.name,
1756 Utc::now().format("%Y-%m-%d")
1757 ),
1758 UsersExportOperation {
1759 course_id: *course_id,
1760 },
1761 token,
1762 )
1763 .await
1764}
1765
1766#[utoipa::path(
1772 get,
1773 path = "/{course_id}/export-exercise-tasks",
1774 operation_id = "exportCourseExerciseTasksCsv",
1775 tag = "courses",
1776 params(
1777 ("course_id" = Uuid, Path, description = "Course id")
1778 ),
1779 responses(
1780 (status = 200, description = "Course exercise tasks CSV", body = String, content_type = "text/csv")
1781 )
1782)]
1783#[instrument(skip(pool))]
1784pub async fn exercise_tasks_export(
1785 course_id: web::Path<Uuid>,
1786 pool: web::Data<PgPool>,
1787 user: AuthUser,
1788) -> ControllerResult<HttpResponse> {
1789 let mut conn = pool.acquire().await?;
1790
1791 let token = authorize(
1792 &mut conn,
1793 Act::Teach,
1794 Some(user.id),
1795 Res::Course(*course_id),
1796 )
1797 .await?;
1798
1799 let course = models::courses::get_course(&mut conn, *course_id).await?;
1800
1801 general_export(
1802 pool,
1803 &format!(
1804 "attachment; filename=\"Course: {} - Exercise tasks {}.csv\"",
1805 course.name,
1806 Utc::now().format("%Y-%m-%d")
1807 ),
1808 CourseExerciseTasksExportOperation {
1809 course_id: *course_id,
1810 },
1811 token,
1812 )
1813 .await
1814}
1815
1816#[utoipa::path(
1822 get,
1823 path = "/{course_id}/export-course-instances",
1824 operation_id = "exportCourseInstancesCsv",
1825 tag = "courses",
1826 params(
1827 ("course_id" = Uuid, Path, description = "Course id")
1828 ),
1829 responses(
1830 (status = 200, description = "Course instances CSV", body = String, content_type = "text/csv")
1831 )
1832)]
1833#[instrument(skip(pool))]
1834pub async fn course_instances_export(
1835 course_id: web::Path<Uuid>,
1836 pool: web::Data<PgPool>,
1837 user: AuthUser,
1838) -> ControllerResult<HttpResponse> {
1839 let mut conn = pool.acquire().await?;
1840
1841 let token = authorize(
1842 &mut conn,
1843 Act::Teach,
1844 Some(user.id),
1845 Res::Course(*course_id),
1846 )
1847 .await?;
1848
1849 let course = models::courses::get_course(&mut conn, *course_id).await?;
1850
1851 general_export(
1852 pool,
1853 &format!(
1854 "attachment; filename=\"Course: {} - Instances {}.csv\"",
1855 course.name,
1856 Utc::now().format("%Y-%m-%d")
1857 ),
1858 CourseInstancesExportOperation {
1859 course_id: *course_id,
1860 },
1861 token,
1862 )
1863 .await
1864}
1865
1866#[utoipa::path(
1872 get,
1873 path = "/{course_id}/export-course-user-consents",
1874 operation_id = "exportCourseUserConsentsCsv",
1875 tag = "courses",
1876 params(
1877 ("course_id" = Uuid, Path, description = "Course id")
1878 ),
1879 responses(
1880 (status = 200, description = "Course user consents CSV", body = String, content_type = "text/csv")
1881 )
1882)]
1883#[instrument(skip(pool))]
1884pub async fn course_consent_form_answers_export(
1885 course_id: web::Path<Uuid>,
1886 pool: web::Data<PgPool>,
1887 user: AuthUser,
1888) -> ControllerResult<HttpResponse> {
1889 let mut conn = pool.acquire().await?;
1890
1891 let token = authorize(
1892 &mut conn,
1893 Act::Teach,
1894 Some(user.id),
1895 Res::Course(*course_id),
1896 )
1897 .await?;
1898
1899 let course = models::courses::get_course(&mut conn, *course_id).await?;
1900
1901 general_export(
1902 pool,
1903 &format!(
1904 "attachment; filename=\"Course: {} - User Consents {}.csv\"",
1905 course.name,
1906 Utc::now().format("%Y-%m-%d")
1907 ),
1908 CourseResearchFormExportOperation {
1909 course_id: *course_id,
1910 },
1911 token,
1912 )
1913 .await
1914}
1915
1916#[utoipa::path(
1922 get,
1923 path = "/{course_id}/export-user-exercise-states",
1924 operation_id = "exportCourseUserExerciseStatesCsv",
1925 tag = "courses",
1926 params(
1927 ("course_id" = Uuid, Path, description = "Course id")
1928 ),
1929 responses(
1930 (status = 200, description = "Course user exercise states CSV", body = String, content_type = "text/csv")
1931 )
1932)]
1933#[instrument(skip(pool))]
1934pub async fn user_exercise_states_export(
1935 course_id: web::Path<Uuid>,
1936 pool: web::Data<PgPool>,
1937 user: AuthUser,
1938) -> ControllerResult<HttpResponse> {
1939 let mut conn = pool.acquire().await?;
1940
1941 let token = authorize(
1942 &mut conn,
1943 Act::Teach,
1944 Some(user.id),
1945 Res::Course(*course_id),
1946 )
1947 .await?;
1948
1949 let course = models::courses::get_course(&mut conn, *course_id).await?;
1950
1951 general_export(
1952 pool,
1953 &format!(
1954 "attachment; filename=\"Course: {} - User exercise states {}.csv\"",
1955 course.name,
1956 Utc::now().format("%Y-%m-%d")
1957 ),
1958 UserExerciseStatesExportOperation {
1959 course_id: *course_id,
1960 },
1961 token,
1962 )
1963 .await
1964}
1965
1966#[utoipa::path(
1970 get,
1971 path = "/{course_id}/page-visit-datum-summary",
1972 operation_id = "getCoursePageVisitDatumSummary",
1973 tag = "courses",
1974 params(
1975 ("course_id" = Uuid, Path, description = "Course id")
1976 ),
1977 responses(
1978 (status = 200, description = "Course page visit summary", body = [PageVisitDatumSummaryByCourse])
1979 )
1980)]
1981pub async fn get_page_visit_datum_summary(
1982 course_id: web::Path<Uuid>,
1983 pool: web::Data<PgPool>,
1984 user: AuthUser,
1985) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourse>>> {
1986 let mut conn = pool.acquire().await?;
1987 let course_id = course_id.into_inner();
1988 let token = authorize(
1989 &mut conn,
1990 Act::ViewStats,
1991 Some(user.id),
1992 Res::Course(course_id),
1993 )
1994 .await?;
1995
1996 let res = models::page_visit_datum_summary_by_courses::get_all_for_course(&mut conn, course_id)
1997 .await?;
1998
1999 token.authorized_ok(web::Json(res))
2000}
2001
2002#[utoipa::path(
2006 get,
2007 path = "/{course_id}/page-visit-datum-summary-by-pages",
2008 operation_id = "getCoursePageVisitDatumSummaryByPages",
2009 tag = "courses",
2010 params(
2011 ("course_id" = Uuid, Path, description = "Course id")
2012 ),
2013 responses(
2014 (status = 200, description = "Course page visit summary by pages", body = [PageVisitDatumSummaryByPages])
2015 )
2016)]
2017pub async fn get_page_visit_datum_summary_by_pages(
2018 course_id: web::Path<Uuid>,
2019 pool: web::Data<PgPool>,
2020 user: AuthUser,
2021) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByPages>>> {
2022 let mut conn = pool.acquire().await?;
2023 let course_id = course_id.into_inner();
2024 let token = authorize(
2025 &mut conn,
2026 Act::ViewStats,
2027 Some(user.id),
2028 Res::Course(course_id),
2029 )
2030 .await?;
2031
2032 let res =
2033 models::page_visit_datum_summary_by_pages::get_all_for_course(&mut conn, course_id).await?;
2034
2035 token.authorized_ok(web::Json(res))
2036}
2037
2038#[utoipa::path(
2042 get,
2043 path = "/{course_id}/page-visit-datum-summary-by-device-types",
2044 operation_id = "getCoursePageVisitDatumSummaryByDeviceTypes",
2045 tag = "courses",
2046 params(
2047 ("course_id" = Uuid, Path, description = "Course id")
2048 ),
2049 responses(
2050 (status = 200, description = "Course page visit summary by device types", body = [PageVisitDatumSummaryByCourseDeviceTypes])
2051 )
2052)]
2053pub async fn get_page_visit_datum_summary_by_device_types(
2054 course_id: web::Path<Uuid>,
2055 pool: web::Data<PgPool>,
2056 user: AuthUser,
2057) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourseDeviceTypes>>> {
2058 let mut conn = pool.acquire().await?;
2059 let course_id = course_id.into_inner();
2060 let token = authorize(
2061 &mut conn,
2062 Act::ViewStats,
2063 Some(user.id),
2064 Res::Course(course_id),
2065 )
2066 .await?;
2067
2068 let res = models::page_visit_datum_summary_by_courses_device_types::get_all_for_course(
2069 &mut conn, course_id,
2070 )
2071 .await?;
2072
2073 token.authorized_ok(web::Json(res))
2074}
2075
2076#[utoipa::path(
2080 get,
2081 path = "/{course_id}/page-visit-datum-summary-by-countries",
2082 operation_id = "getCoursePageVisitDatumSummaryByCountries",
2083 tag = "courses",
2084 params(
2085 ("course_id" = Uuid, Path, description = "Course id")
2086 ),
2087 responses(
2088 (status = 200, description = "Course page visit summary by countries", body = [PageVisitDatumSummaryByCoursesCountries])
2089 )
2090)]
2091pub async fn get_page_visit_datum_summary_by_countries(
2092 course_id: web::Path<Uuid>,
2093 pool: web::Data<PgPool>,
2094 user: AuthUser,
2095) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCoursesCountries>>> {
2096 let mut conn = pool.acquire().await?;
2097 let course_id = course_id.into_inner();
2098 let token = authorize(
2099 &mut conn,
2100 Act::ViewStats,
2101 Some(user.id),
2102 Res::Course(course_id),
2103 )
2104 .await?;
2105
2106 let res = models::page_visit_datum_summary_by_courses_countries::get_all_for_course(
2107 &mut conn, course_id,
2108 )
2109 .await?;
2110
2111 token.authorized_ok(web::Json(res))
2112}
2113
2114#[utoipa::path(
2120 delete,
2121 path = "/{course_id}/teacher-reset-course-progress-for-themselves",
2122 operation_id = "resetCourseProgressForTeacherThemselves",
2123 tag = "courses",
2124 params(
2125 ("course_id" = Uuid, Path, description = "Course id")
2126 ),
2127 responses(
2128 (status = 200, description = "Teacher course progress reset", body = bool)
2129 )
2130)]
2131pub async fn teacher_reset_course_progress_for_themselves(
2132 course_id: web::Path<Uuid>,
2133 pool: web::Data<PgPool>,
2134 user: AuthUser,
2135) -> ControllerResult<web::Json<bool>> {
2136 let mut conn = pool.acquire().await?;
2137 let course_id = course_id.into_inner();
2138 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2139
2140 let mut tx = conn.begin().await?;
2141 let course_instances =
2142 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2143 for course_instance in course_instances {
2144 models::course_instances::reset_progress_on_course_instance_for_user(
2145 &mut tx,
2146 user.id,
2147 course_instance.course_id,
2148 )
2149 .await?;
2150 }
2151
2152 tx.commit().await?;
2153 token.authorized_ok(web::Json(true))
2154}
2155
2156#[utoipa::path(
2162 delete,
2163 path = "/{course_id}/teacher-reset-course-progress-for-everyone",
2164 operation_id = "resetCourseProgressForEveryone",
2165 tag = "courses",
2166 params(
2167 ("course_id" = Uuid, Path, description = "Course id")
2168 ),
2169 responses(
2170 (status = 200, description = "Course progress reset for everyone", body = bool)
2171 )
2172)]
2173pub async fn teacher_reset_course_progress_for_everyone(
2174 course_id: web::Path<Uuid>,
2175 pool: web::Data<PgPool>,
2176 user: AuthUser,
2177) -> ControllerResult<web::Json<bool>> {
2178 let mut conn = pool.acquire().await?;
2179 let course_id = course_id.into_inner();
2180 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2181 let course = models::courses::get_course(&mut conn, course_id).await?;
2182 if !course.is_draft {
2183 return Err(ControllerError::new(
2184 ControllerErrorType::BadRequest,
2185 "Can only reset progress for a draft course.".to_string(),
2186 None,
2187 ));
2188 }
2189 let n_course_module_completions =
2191 models::course_module_completions::get_count_of_distinct_completors_by_course_id(
2192 &mut conn, course_id,
2193 )
2194 .await?;
2195 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(
2196 &mut conn, course_id,
2197 ).await?;
2198 if n_course_module_completions > 200 {
2199 return Err(ControllerError::new(
2200 ControllerErrorType::BadRequest,
2201 "Too many students have completed the course.".to_string(),
2202 None,
2203 ));
2204 }
2205 if n_completions_registered_to_study_registry > 2 {
2206 return Err(ControllerError::new(
2207 ControllerErrorType::BadRequest,
2208 "Too many students have registered their completion to a study registry".to_string(),
2209 None,
2210 ));
2211 }
2212
2213 let mut tx = conn.begin().await?;
2214 let course_instances =
2215 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2216
2217 for course_instance in course_instances {
2219 let users_in_course_instance =
2220 models::users::get_users_by_course_instance_enrollment(&mut tx, course_instance.id)
2221 .await?;
2222 for user_in_course_instance in users_in_course_instance {
2223 models::course_instances::reset_progress_on_course_instance_for_user(
2224 &mut tx,
2225 user_in_course_instance.id,
2226 course_instance.course_id,
2227 )
2228 .await?;
2229 }
2230 }
2231
2232 tx.commit().await?;
2233 token.authorized_ok(web::Json(true))
2234}
2235
2236#[derive(Debug, Deserialize)]
2237
2238pub struct GetSuspectedCheatersQuery {
2239 status: SuspectedCheaterStatus,
2240}
2241
2242#[utoipa::path(
2246 get,
2247 path = "/{course_id}/suspected-cheaters",
2248 operation_id = "getCourseSuspectedCheaters",
2249 tag = "courses",
2250 params(
2251 ("course_id" = Uuid, Path, description = "Course id"),
2252 ("status" = SuspectedCheaterStatus, Query, description = "Which review state of suspected cheaters to fetch")
2253 ),
2254 responses(
2255 (status = 200, description = "Suspected cheaters for course", body = [SuspectedCheaters])
2256 )
2257)]
2258#[instrument(skip(pool))]
2259async fn get_all_suspected_cheaters(
2260 user: AuthUser,
2261 params: web::Path<Uuid>,
2262 query: web::Query<GetSuspectedCheatersQuery>,
2263 pool: web::Data<PgPool>,
2264) -> ControllerResult<web::Json<Vec<SuspectedCheaters>>> {
2265 let course_id = params.into_inner();
2266
2267 let mut conn = pool.acquire().await?;
2268 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2269
2270 let course_cheaters = models::suspected_cheaters::get_all_suspected_cheaters_in_course(
2271 &mut conn,
2272 course_id,
2273 query.status,
2274 )
2275 .await?;
2276
2277 token.authorized_ok(web::Json(course_cheaters))
2278}
2279
2280#[utoipa::path(
2284 get,
2285 path = "/{course_id}/suspected-cheaters/flagged-count",
2286 operation_id = "getCourseFlaggedSuspectedCheatersCount",
2287 tag = "courses",
2288 params(
2289 ("course_id" = Uuid, Path, description = "Course id")
2290 ),
2291 responses(
2292 (status = 200, description = "Number of suspected cheaters awaiting review", body = i64, content_type = "application/json")
2293 )
2294)]
2295#[instrument(skip(pool))]
2296async fn get_flagged_suspected_cheaters_count(
2297 user: AuthUser,
2298 params: web::Path<Uuid>,
2299 pool: web::Data<PgPool>,
2300) -> ControllerResult<web::Json<i64>> {
2301 let course_id = params.into_inner();
2302
2303 let mut conn = pool.acquire().await?;
2304 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2305
2306 let count = models::suspected_cheaters::get_count_in_course_by_status(
2307 &mut conn,
2308 course_id,
2309 SuspectedCheaterStatus::Flagged,
2310 )
2311 .await?;
2312
2313 token.authorized_ok(web::Json(count))
2314}
2315
2316#[utoipa::path(
2320 get,
2321 path = "/{course_id}/thresholds",
2322 operation_id = "getCourseThresholds",
2323 tag = "courses",
2324 params(
2325 ("course_id" = Uuid, Path, description = "Course id")
2326 ),
2327 responses(
2328 (status = 200, description = "Course thresholds", body = Vec<CourseModuleThresholdInfo>)
2329 )
2330)]
2331#[instrument(skip(pool))]
2332async fn get_all_thresholds(
2333 user: AuthUser,
2334 params: web::Path<Uuid>,
2335 pool: web::Data<PgPool>,
2336) -> ControllerResult<web::Json<Vec<CourseModuleThresholdInfo>>> {
2337 let mut conn = pool.acquire().await?;
2338 let course_id = params.into_inner();
2339
2340 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2341
2342 let thresholds =
2343 models::suspected_cheaters::get_threshold_info_for_course(&mut conn, course_id).await?;
2344
2345 token.authorized_ok(web::Json(thresholds))
2346}
2347
2348#[utoipa::path(
2352 post,
2353 path = "/{course_id}/suspected-cheaters/dismiss/{user_id}",
2354 operation_id = "dismissCourseSuspectedCheater",
2355 tag = "courses",
2356 params(
2357 ("course_id" = Uuid, Path, description = "Course id"),
2358 ("user_id" = Uuid, Path, description = "Suspected cheater's user id")
2359 ),
2360 responses(
2361 (status = 200, description = "Suspicion dismissed")
2362 )
2363)]
2364#[instrument(skip(pool))]
2365async fn teacher_dismiss_suspected_cheater(
2366 user: AuthUser,
2367 path: web::Path<(Uuid, Uuid)>,
2368 pool: web::Data<PgPool>,
2369) -> ControllerResult<web::Json<()>> {
2370 let (course_id, user_id) = path.into_inner();
2371
2372 let mut conn = pool.acquire().await?;
2373 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2374
2375 models::suspected_cheaters::dismiss_by_user_id_and_course_id(&mut conn, user_id, course_id)
2376 .await?;
2377
2378 token.authorized_ok(web::Json(()))
2379}
2380
2381#[utoipa::path(
2385 post,
2386 path = "/{course_id}/suspected-cheaters/confirm/{user_id}",
2387 operation_id = "confirmCourseSuspectedCheater",
2388 tag = "courses",
2389 params(
2390 ("course_id" = Uuid, Path, description = "Course id"),
2391 ("user_id" = Uuid, Path, description = "Suspected cheater's user id")
2392 ),
2393 responses(
2394 (status = 200, description = "Cheating confirmed")
2395 )
2396)]
2397#[instrument(skip(pool))]
2398async fn teacher_confirm_suspected_cheater(
2399 user: AuthUser,
2400 path: web::Path<(Uuid, Uuid)>,
2401 pool: web::Data<PgPool>,
2402) -> ControllerResult<web::Json<()>> {
2403 let (course_id, user_id) = path.into_inner();
2404
2405 let mut conn = pool.acquire().await?;
2406 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2407
2408 models::suspected_cheaters::confirm_cheater_by_user_id_and_course_id(
2411 &mut conn, user_id, course_id,
2412 )
2413 .await?;
2414
2415 token.authorized_ok(web::Json(()))
2416}
2417
2418#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
2419pub struct JoinCourseWithJoinCodePayload {
2420 join_code: String,
2421}
2422
2423#[utoipa::path(
2427 post,
2428 path = "/{course_id}/join-course-with-join-code",
2429 operation_id = "joinCourseWithJoinCode",
2430 tag = "courses",
2431 params(
2432 ("course_id" = Uuid, Path, description = "Course id")
2433 ),
2434 request_body = JoinCourseWithJoinCodePayload,
2435 responses(
2436 (status = 200, description = "Joined course id", body = Uuid)
2437 )
2438)]
2439#[instrument(skip(pool))]
2440async fn add_user_to_course_with_join_code(
2441 course_id: web::Path<Uuid>,
2442 payload: web::Json<JoinCourseWithJoinCodePayload>,
2443 user: AuthUser,
2444 pool: web::Data<PgPool>,
2445) -> ControllerResult<web::Json<Uuid>> {
2446 let mut conn = pool.acquire().await?;
2447 let token = skip_authorize();
2448
2449 models::courses::get_by_id_and_join_code(&mut conn, *course_id, &payload.join_code).await?;
2450 let joined =
2451 models::join_code_uses::insert(&mut conn, PKeyPolicy::Generate, user.id, *course_id)
2452 .await?;
2453 token.authorized_ok(web::Json(joined))
2454}
2455
2456#[utoipa::path(
2460 post,
2461 path = "/{course_id}/set-join-code",
2462 operation_id = "setCourseJoinCode",
2463 tag = "courses",
2464 params(
2465 ("course_id" = Uuid, Path, description = "Course id")
2466 ),
2467 responses(
2468 (status = 200, description = "Course join code set")
2469 )
2470)]
2471#[instrument(skip(pool))]
2472async fn set_join_code_for_course(
2473 id: web::Path<Uuid>,
2474 pool: web::Data<PgPool>,
2475 user: AuthUser,
2476) -> ControllerResult<HttpResponse> {
2477 let mut conn = pool.acquire().await?;
2478 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*id)).await?;
2479
2480 const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
2481 abcdefghjkmnpqrstuvwxyz";
2482 const PASSWORD_LEN: usize = 64;
2483 let mut rng = rand::rng();
2484
2485 let code: String = (0..PASSWORD_LEN)
2486 .map(|_| {
2487 let idx = rng.random_range(0..CHARSET.len());
2488 CHARSET[idx] as char
2489 })
2490 .collect();
2491
2492 models::courses::set_join_code_for_course(&mut conn, *id, code).await?;
2493 token.authorized_ok(HttpResponse::Ok().finish())
2494}
2495
2496#[utoipa::path(
2500 get,
2501 path = "/join/{join_code}",
2502 operation_id = "getCourseByJoinCode",
2503 tag = "courses",
2504 params(
2505 ("join_code" = String, Path, description = "Course join code")
2506 ),
2507 responses(
2508 (status = 200, description = "Course for join code", body = Course)
2509 )
2510)]
2511#[instrument(skip(pool))]
2512async fn get_course_with_join_code(
2513 join_code: web::Path<String>,
2514 user: AuthUser,
2515 pool: web::Data<PgPool>,
2516) -> ControllerResult<web::Json<Course>> {
2517 let mut conn = pool.acquire().await?;
2518 let token = skip_authorize();
2519 let course =
2520 models::courses::get_course_with_join_code(&mut conn, join_code.to_string()).await?;
2521
2522 token.authorized_ok(web::Json(course))
2523}
2524
2525#[utoipa::path(
2529 post,
2530 path = "/{course_id}/partners-block",
2531 operation_id = "upsertCoursePartnersBlock",
2532 tag = "courses",
2533 params(
2534 ("course_id" = Uuid, Path, description = "Course id")
2535 ),
2536 request_body = Option<serde_json::Value>,
2537 responses(
2538 (status = 200, description = "Partners block", body = serde_json::Value)
2539 )
2540)]
2541#[instrument(skip(payload, pool))]
2542async fn post_partners_block(
2543 path: web::Path<Uuid>,
2544 payload: web::Json<Option<serde_json::Value>>,
2545 pool: web::Data<PgPool>,
2546 user: AuthUser,
2547) -> ControllerResult<web::Json<PartnersBlock>> {
2548 let course_id = path.into_inner();
2549
2550 let content = payload.into_inner();
2551 let mut conn = pool.acquire().await?;
2552 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2553
2554 let upserted_partner_block =
2555 models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
2556
2557 token.authorized_ok(web::Json(upserted_partner_block))
2558}
2559
2560#[utoipa::path(
2564 get,
2565 path = "/{course_id}/partners-block",
2566 operation_id = "getCoursePartnersBlock",
2567 tag = "courses",
2568 params(
2569 ("course_id" = Uuid, Path, description = "Course id")
2570 ),
2571 responses(
2572 (status = 200, description = "Partners block", body = serde_json::Value)
2573 )
2574)]
2575#[instrument(skip(pool))]
2576async fn get_partners_block(
2577 path: web::Path<Uuid>,
2578 user: AuthUser,
2579 pool: web::Data<PgPool>,
2580) -> ControllerResult<web::Json<PartnersBlock>> {
2581 let course_id = path.into_inner();
2582 let mut conn = pool.acquire().await?;
2583 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2584
2585 let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
2587
2588 let partner_block = if course_exists {
2589 models::partner_block::get_partner_block(&mut conn, course_id).await?
2591 } else {
2592 let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
2594
2595 models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
2597 };
2598
2599 token.authorized_ok(web::Json(partner_block))
2600}
2601
2602#[utoipa::path(
2606 delete,
2607 path = "/{course_id}/partners-block",
2608 operation_id = "deleteCoursePartnersBlock",
2609 tag = "courses",
2610 params(
2611 ("course_id" = Uuid, Path, description = "Course id")
2612 ),
2613 responses(
2614 (status = 200, description = "Deleted partners block", body = serde_json::Value)
2615 )
2616)]
2617#[instrument(skip(pool))]
2618async fn delete_partners_block(
2619 path: web::Path<Uuid>,
2620 pool: web::Data<PgPool>,
2621 user: AuthUser,
2622) -> ControllerResult<web::Json<PartnersBlock>> {
2623 let course_id = path.into_inner();
2624 let mut conn = pool.acquire().await?;
2625 let token = authorize(
2626 &mut conn,
2627 Act::UsuallyUnacceptableDeletion,
2628 Some(user.id),
2629 Res::Course(course_id),
2630 )
2631 .await?;
2632 let deleted_partners_block =
2633 models::partner_block::delete_partner_block(&mut conn, course_id).await?;
2634
2635 token.authorized_ok(web::Json(deleted_partners_block))
2636}
2637
2638#[utoipa::path(
2644 get,
2645 path = "/{course_id}/sisu-course-llm-descriptions",
2646 operation_id = "getSisuCourseLlmDescriptions",
2647 tag = "courses",
2648 params(
2649 ("course_id" = Uuid, Path, description = "Course id")
2650 ),
2651 responses(
2652 (status = 200, description = "Sisu course LLM descriptions", body = SisuDescriptionResponse)
2653 )
2654)]
2655#[instrument(skip(pool, app_conf))]
2656async fn get_sisu_course_llm_descriptions(
2657 course_id: web::Path<Uuid>,
2658 pool: web::Data<PgPool>,
2659 app_conf: web::Data<ApplicationConfiguration>,
2660 user: AuthUser,
2661 sisu_client: web::Data<SisuClient>,
2662) -> ControllerResult<web::Json<SisuDescriptionResponse>> {
2663 let (course_modules, course_lang, message_suggest_llm, token) = {
2664 let mut conn = pool.acquire().await?;
2665
2666 let token =
2667 authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
2668 let course_modules =
2669 models::course_modules::get_by_course_id(&mut conn, *course_id).await?;
2670 let course_lang = models::courses::get_course(&mut conn, *course_id)
2671 .await?
2672 .language_code;
2673 let message_suggest_llm = models::application_task_default_language_models::get_for_task(
2674 &mut conn,
2675 ApplicationTask::SisuDescriptionSummary,
2676 )
2677 .await?;
2678 (course_modules, course_lang, message_suggest_llm, token)
2679 };
2680
2681 let uh_course_codes: Vec<String> = course_modules
2682 .into_iter()
2683 .filter_map(|course_module| course_module.uh_course_code)
2684 .collect::<Vec<String>>();
2685 let course_ids = SisuClient::get_course_ids(&sisu_client, uh_course_codes).await?;
2686 let course_info = SisuClient::get_course_info(&sisu_client, course_ids).await?;
2687
2688 let parsed_course_info = SisuClient::parse_course_info(course_info, course_lang);
2689
2690 let llm_descriptions = headless_lms_chatbot::course_description_summary::generate_description(
2691 &app_conf,
2692 message_suggest_llm,
2693 parsed_course_info,
2694 )
2695 .await?;
2696 token.authorized_ok(web::Json(llm_descriptions))
2697}
2698
2699#[utoipa::path(
2704 post,
2705 path = "/{course_id}/update-metadata",
2706 operation_id = "updateMetadata",
2707 tag = "courses",
2708 params(
2709 ("course_id" = Uuid, Path, description = "Course id")
2710 ),
2711 request_body = CourseMetadataUpdate,
2712 responses(
2713 (status = 200, description = "Updated metadata", body = CourseMetadata)
2714 )
2715)]
2716#[instrument(skip(pool))]
2717async fn update_metadata(
2718 payload: web::Json<CourseMetadataUpdate>,
2719 course_id: web::Path<Uuid>,
2720 pool: web::Data<PgPool>,
2721 user: AuthUser,
2722) -> ControllerResult<web::Json<CourseMetadata>> {
2723 let mut conn = pool.acquire().await?;
2724 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2725 let metadata_update = payload.0;
2726 let course = models::courses::set_metadata(&mut conn, *course_id, metadata_update).await?;
2727
2728 token.authorized_ok(web::Json(course))
2729}
2730
2731#[utoipa::path(
2736 get,
2737 path = "/{course_id}/get-course-prerequisites",
2738 operation_id= "getCoursePrerequisites",
2739 tag = "courses",
2740 params(
2741 ("course_id" = Uuid, Path, description = "Course id")
2742 ),
2743 responses(
2744 (status = 200, description = "Course prerequisites", body = Vec<CoursePrerequisite>)
2745 )
2746)]
2747#[instrument(skip(pool))]
2748async fn get_course_prerequisites(
2749 course_id: web::Path<Uuid>,
2750 pool: web::Data<PgPool>,
2751 user: AuthUser,
2752) -> ControllerResult<web::Json<Vec<CoursePrerequisite>>> {
2753 let mut conn = pool.acquire().await?;
2754 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2755 let prerequisites =
2756 models::course_prerequisites::get_by_course_id(&mut conn, *course_id).await?;
2757
2758 token.authorized_ok(web::Json(prerequisites))
2759}
2760
2761#[utoipa::path(
2766 get,
2767 path = "/{course_id}/get-course-audiences",
2768 operation_id= "getCourseAudiences",
2769 tag = "courses",
2770 params(
2771 ("course_id" = Uuid, Path, description = "Course id")
2772 ),
2773 responses(
2774 (status = 200, description = "Course audiences", body = Vec<CourseAudience>)
2775 )
2776)]
2777#[instrument(skip(pool))]
2778async fn get_course_audiences(
2779 course_id: web::Path<Uuid>,
2780 pool: web::Data<PgPool>,
2781 user: AuthUser,
2782) -> ControllerResult<web::Json<Vec<CourseAudience>>> {
2783 let mut conn = pool.acquire().await?;
2784 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2785 let audiences = models::course_audiences::get_by_course_id(&mut conn, *course_id).await?;
2786
2787 token.authorized_ok(web::Json(audiences))
2788}
2789
2790#[utoipa::path(
2795 get,
2796 path = "/{course_id}/get-course-metadata",
2797 operation_id= "getCourseMetadata",
2798 tag = "courses",
2799 params(
2800 ("course_id" = Uuid, Path, description = "Course id")
2801 ),
2802 responses(
2803 (status = 200, description = "Course metadata", body = CompleteCourseMetadata)
2804 )
2805)]
2806#[instrument(skip(pool))]
2807async fn get_course_metadata(
2808 course_id: web::Path<Uuid>,
2809 pool: web::Data<PgPool>,
2810 user: AuthUser,
2811) -> ControllerResult<web::Json<CompleteCourseMetadata>> {
2812 let mut conn: sqlx::pool::PoolConnection<Postgres> = pool.acquire().await?;
2813 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2814 let metadata = models::courses::get_metadata(&mut conn, *course_id).await?;
2815
2816 token.authorized_ok(web::Json(metadata))
2817}
2818#[utoipa::path(
2824 get,
2825 path = "/",
2826 operation_id = "getAllCourses",
2827 tag = "courses",
2828 responses(
2829 (status = 200, description = "All courses", body = Vec<Course>)
2830 )
2831)]
2832#[instrument(skip(pool))]
2833async fn get_all_courses(
2834 pool: web::Data<PgPool>,
2835 user: AuthUser,
2836) -> ControllerResult<web::Json<Vec<Course>>> {
2837 let mut conn = pool.acquire().await?;
2838 let all_courses = models::courses::all_courses(&mut conn).await?;
2839 let token = authorize(&mut conn, Act::View, Some(user.id), Res::GlobalPermissions).await?;
2840 token.authorized_ok(web::Json(all_courses))
2841}
2842
2843pub fn _add_routes(cfg: &mut ServiceConfig) {
2851 cfg.service(web::scope("/{course_id}/stats").configure(stats::_add_routes))
2852 .service(web::scope("/{course_id}/chatbots").configure(chatbots::_add_routes))
2853 .service(web::scope("/{course_id}/students").configure(students::_add_routes))
2854 .route("/{course_id}", web::get().to(get_course))
2855 .route("", web::post().to(post_new_course))
2856 .route("/{course_id}", web::put().to(update_course))
2857 .route("/{course_id}", web::delete().to(delete_course))
2858 .route(
2859 "/{course_id}/status-for-all-exercises/{user_id}",
2860 web::get().to(get_all_exercise_statuses_by_course_id),
2861 )
2862 .route(
2863 "/{course_id}/course-module-completions/{user_id}",
2864 web::get().to(get_all_course_module_completions_for_user_by_course_id),
2865 )
2866 .route(
2867 "/{course_id}/daily-submission-counts",
2868 web::get().to(get_daily_submission_counts),
2869 )
2870 .route(
2871 "/{course_id}/daily-users-who-have-submitted-something",
2872 web::get().to(get_daily_user_counts_with_submissions),
2873 )
2874 .route("/{course_id}/exercises", web::get().to(get_all_exercises))
2875 .route(
2876 "/{course_id}/exercises-and-count-of-answers-requiring-attention",
2877 web::get().to(get_all_exercises_and_count_of_answers_requiring_attention),
2878 )
2879 .route(
2880 "/{course_id}/structure",
2881 web::get().to(get_course_structure),
2882 )
2883 .route(
2884 "/{course_id}/language-versions",
2885 web::get().to(get_all_course_language_versions),
2886 )
2887 .route(
2888 "/{course_id}/create-copy",
2889 web::post().to(create_course_copy),
2890 )
2891 .route("/{course_id}/upload", web::post().to(add_media_for_course))
2892 .route(
2893 "/{course_id}/weekday-hour-submission-counts",
2894 web::get().to(get_weekday_hour_submission_counts),
2895 )
2896 .route(
2897 "/{course_id}/submission-counts-by-exercise",
2898 web::get().to(get_submission_counts_by_exercise),
2899 )
2900 .route(
2901 "/{course_id}/course-instances",
2902 web::get().to(get_course_instances),
2903 )
2904 .route("/{course_id}/feedback", web::get().to(get_feedback))
2905 .route(
2906 "/{course_id}/feedback-count",
2907 web::get().to(get_feedback_count),
2908 )
2909 .route(
2910 "/{course_id}/new-course-instance",
2911 web::post().to(new_course_instance),
2912 )
2913 .route("/{course_id}/glossary", web::get().to(glossary))
2914 .route("/{course_id}/glossary", web::post().to(new_glossary_term))
2915 .route(
2916 "/{course_id}/course-users-counts-by-exercise",
2917 web::get().to(get_course_users_counts_by_exercise),
2918 )
2919 .route(
2920 "/{course_id}/new-page-ordering",
2921 web::post().to(post_new_page_ordering),
2922 )
2923 .route(
2924 "/{course_id}/new-chapter-ordering",
2925 web::post().to(post_new_chapter_ordering),
2926 )
2927 .route(
2928 "/{course_id}/references",
2929 web::get().to(get_material_references_by_course_id),
2930 )
2931 .route(
2932 "/{course_id}/references",
2933 web::post().to(insert_material_references),
2934 )
2935 .route(
2936 "/{course_id}/references/{reference_id}",
2937 web::post().to(update_material_reference),
2938 )
2939 .route(
2940 "/{course_id}/references/{reference_id}",
2941 web::delete().to(delete_material_reference_by_id),
2942 )
2943 .route(
2944 "/{course_id}/course-modules",
2945 web::post().to(update_modules),
2946 )
2947 .route(
2948 "/{course_id}/default-peer-review",
2949 web::get().to(get_course_default_peer_review),
2950 )
2951 .route(
2952 "/{course_id}/update-peer-review-queue-reviews-received",
2953 web::post().to(post_update_peer_review_queue_reviews_received),
2954 )
2955 .route(
2956 "/{course_id}/breadcrumb-info",
2957 web::get().to(get_course_breadcrumb_info),
2958 )
2959 .route(
2960 "/{course_id}/progress/{user_id}",
2961 web::get().to(get_user_progress_for_course),
2962 )
2963 .route(
2964 "/{course_id}/user-settings/{user_id}",
2965 web::get().to(get_user_course_settings),
2966 )
2967 .route(
2968 "/{course_id}/export-submissions",
2969 web::get().to(submission_export),
2970 )
2971 .route(
2972 "/{course_id}/export-user-details",
2973 web::get().to(user_details_export),
2974 )
2975 .route(
2976 "/{course_id}/export-exercise-tasks",
2977 web::get().to(exercise_tasks_export),
2978 )
2979 .route(
2980 "/{course_id}/export-course-instances",
2981 web::get().to(course_instances_export),
2982 )
2983 .route(
2984 "/{course_id}/export-course-user-consents",
2985 web::get().to(course_consent_form_answers_export),
2986 )
2987 .route(
2988 "/{course_id}/export-user-exercise-states",
2989 web::get().to(user_exercise_states_export),
2990 )
2991 .route(
2992 "/{course_id}/page-visit-datum-summary",
2993 web::get().to(get_page_visit_datum_summary),
2994 )
2995 .route(
2996 "/{course_id}/page-visit-datum-summary-by-pages",
2997 web::get().to(get_page_visit_datum_summary_by_pages),
2998 )
2999 .route(
3000 "/{course_id}/page-visit-datum-summary-by-device-types",
3001 web::get().to(get_page_visit_datum_summary_by_device_types),
3002 )
3003 .route(
3004 "/{course_id}/page-visit-datum-summary-by-countries",
3005 web::get().to(get_page_visit_datum_summary_by_countries),
3006 )
3007 .route(
3008 "/{course_id}/teacher-reset-course-progress-for-themselves",
3009 web::delete().to(teacher_reset_course_progress_for_themselves),
3010 )
3011 .route("/{course_id}/thresholds", web::get().to(get_all_thresholds))
3012 .route(
3013 "/{course_id}/suspected-cheaters",
3014 web::get().to(get_all_suspected_cheaters),
3015 )
3016 .route(
3017 "/{course_id}/suspected-cheaters/flagged-count",
3018 web::get().to(get_flagged_suspected_cheaters_count),
3019 )
3020 .route(
3021 "/{course_id}/suspected-cheaters/dismiss/{user_id}",
3022 web::post().to(teacher_dismiss_suspected_cheater),
3023 )
3024 .route(
3025 "/{course_id}/suspected-cheaters/confirm/{user_id}",
3026 web::post().to(teacher_confirm_suspected_cheater),
3027 )
3028 .route(
3029 "/{course_id}/teacher-reset-course-progress-for-everyone",
3030 web::delete().to(teacher_reset_course_progress_for_everyone),
3031 )
3032 .route(
3033 "/{course_id}/join-course-with-join-code",
3034 web::post().to(add_user_to_course_with_join_code),
3035 )
3036 .route(
3037 "/{course_id}/partners-block",
3038 web::post().to(post_partners_block),
3039 )
3040 .route(
3041 "/{course_id}/partners-block",
3042 web::get().to(get_partners_block),
3043 )
3044 .route(
3045 "/{course_id}/partners-block",
3046 web::delete().to(delete_partners_block),
3047 )
3048 .route(
3049 "/{course_id}/set-join-code",
3050 web::post().to(set_join_code_for_course),
3051 )
3052 .route(
3053 "/{course_id}/reprocess-completions",
3054 web::post().to(post_reprocess_module_completions),
3055 )
3056 .route(
3057 "/join/{join_code}",
3058 web::get().to(get_course_with_join_code),
3059 )
3060 .route(
3061 "/{course_id}/sisu-course-llm-descriptions",
3062 web::get().to(get_sisu_course_llm_descriptions),
3063 )
3064 .route(
3065 "/{course_id}/update-metadata",
3066 web::post().to(update_metadata),
3067 )
3068 .route(
3069 "/{course_id}/get-course-prerequisites",
3070 web::get().to(get_course_prerequisites),
3071 )
3072 .route(
3073 "/{course_id}/get-course-audiences",
3074 web::get().to(get_course_audiences),
3075 )
3076 .route(
3077 "/{course_id}/get-course-metadata",
3078 web::get().to(get_course_metadata),
3079 )
3080 .route("/", web::get().to(get_all_courses));
3081}