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_models::{
10 partner_block::PartnersBlock,
11 suspected_cheaters::{CourseModuleThresholdInfo, SuspectedCheaterStatus, SuspectedCheaters},
12};
13use std::sync::Arc;
14use utoipa::OpenApi;
15
16use headless_lms_utils::strings::is_ietf_language_code_like;
17use models::{
18 chapters::Chapter,
19 course_instances::{CourseInstance, CourseInstanceForm, NewCourseInstance},
20 course_module_completions::CourseModuleCompletion,
21 course_modules::ModuleUpdates,
22 courses::{Course, CourseBreadcrumbInfo, CourseStructure, CourseUpdate, NewCourse},
23 exercise_slide_submissions::{
24 self, ExerciseAnswersInCourseRequiringAttentionCount, ExerciseSlideSubmissionCount,
25 ExerciseSlideSubmissionCountByExercise, ExerciseSlideSubmissionCountByWeekAndHour,
26 },
27 exercises::{Exercise, ExerciseStatusSummaryForUser},
28 feedback::{self, Feedback, FeedbackCount},
29 glossary::{Term, TermUpdate},
30 library,
31 material_references::{MaterialReference, NewMaterialReference},
32 page_visit_datum_summary_by_courses::PageVisitDatumSummaryByCourse,
33 page_visit_datum_summary_by_courses_countries::PageVisitDatumSummaryByCoursesCountries,
34 page_visit_datum_summary_by_courses_device_types::PageVisitDatumSummaryByCourseDeviceTypes,
35 page_visit_datum_summary_by_pages::PageVisitDatumSummaryByPages,
36 pages::Page,
37 peer_or_self_review_configs::PeerOrSelfReviewConfig,
38 peer_or_self_review_questions::PeerOrSelfReviewQuestion,
39 user_course_settings::UserCourseSettings,
40 user_exercise_states::{ExerciseUserCounts, UserCourseProgress},
41};
42
43use crate::{
44 domain::models_requests::{self, JwtKey},
45 prelude::*,
46};
47
48use headless_lms_models::course_language_groups;
49
50use crate::domain::csv_export::course_instance_export::CourseInstancesExportOperation;
51use crate::domain::csv_export::course_research_form_questions_answers_export::CourseResearchFormExportOperation;
52use crate::domain::csv_export::exercise_tasks_export::CourseExerciseTasksExportOperation;
53use crate::domain::csv_export::general_export;
54use crate::domain::csv_export::submissions::CourseSubmissionExportOperation;
55use crate::domain::csv_export::users_export::UsersExportOperation;
56
57#[derive(OpenApi)]
58#[openapi(
59 paths(
60 get_course,
61 get_course_breadcrumb_info,
62 get_all_exercise_statuses_by_course_id,
63 get_all_course_module_completions_for_user_by_course_id,
64 get_user_progress_for_course,
65 get_user_course_settings,
66 post_reprocess_module_completions,
67 post_new_course,
68 update_course,
69 delete_course,
70 get_course_structure,
71 add_media_for_course,
72 get_all_exercises,
73 get_all_exercises_and_count_of_answers_requiring_attention,
74 get_all_course_language_versions,
75 create_course_copy,
76 get_daily_submission_counts,
77 get_daily_user_counts_with_submissions,
78 get_weekday_hour_submission_counts,
79 get_submission_counts_by_exercise,
80 get_course_instances,
81 get_feedback,
82 get_feedback_count,
83 new_course_instance,
84 glossary,
85 new_glossary_term,
86 get_course_users_counts_by_exercise,
87 post_new_page_ordering,
88 post_new_chapter_ordering,
89 get_material_references_by_course_id,
90 insert_material_references,
91 update_material_reference,
92 delete_material_reference_by_id,
93 update_modules,
94 get_course_default_peer_review,
95 post_update_peer_review_queue_reviews_received,
96 submission_export,
97 user_details_export,
98 exercise_tasks_export,
99 course_instances_export,
100 course_consent_form_answers_export,
101 user_exercise_states_export,
102 get_page_visit_datum_summary,
103 get_page_visit_datum_summary_by_pages,
104 get_page_visit_datum_summary_by_device_types,
105 get_page_visit_datum_summary_by_countries,
106 teacher_reset_course_progress_for_themselves,
107 teacher_reset_course_progress_for_everyone,
108 get_all_suspected_cheaters,
109 get_flagged_suspected_cheaters_count,
110 get_all_thresholds,
111 teacher_dismiss_suspected_cheater,
112 teacher_confirm_suspected_cheater,
113 add_user_to_course_with_join_code,
114 set_join_code_for_course,
115 get_course_with_join_code,
116 post_partners_block,
117 get_partners_block,
118 delete_partners_block
119 ),
120 nest(
121 (path = "/{course_id}/chatbots", api = chatbots::MainFrontendCourseChatbotsApiDoc),
122 (path = "/{course_id}/stats", api = stats::MainFrontendCourseStatsApiDoc),
123 (path = "/{course_id}/students", api = students::MainFrontendCourseStudentsApiDoc)
124 )
125)]
126pub(crate) struct MainFrontendCoursesApiDoc;
127
128#[utoipa::path(
132 get,
133 path = "/{course_id}",
134 operation_id = "getCourse",
135 tag = "courses",
136 params(
137 ("course_id" = Uuid, Path, description = "Course id")
138 ),
139 responses(
140 (status = 200, description = "Course", body = Course)
141 )
142)]
143#[instrument(skip(pool))]
144async fn get_course(
145 course_id: web::Path<Uuid>,
146 pool: web::Data<PgPool>,
147 user: AuthUser,
148) -> ControllerResult<web::Json<Course>> {
149 let mut conn = pool.acquire().await?;
150 let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
151 let course = models::courses::get_course(&mut conn, *course_id).await?;
152 token.authorized_ok(web::Json(course))
153}
154
155#[utoipa::path(
159 get,
160 path = "/{course_id}/breadcrumb-info",
161 operation_id = "getCourseBreadcrumbInfo",
162 tag = "courses",
163 params(
164 ("course_id" = Uuid, Path, description = "Course id")
165 ),
166 responses(
167 (status = 200, description = "Course breadcrumb information", body = CourseBreadcrumbInfo)
168 )
169)]
170#[instrument(skip(pool))]
171async fn get_course_breadcrumb_info(
172 course_id: web::Path<Uuid>,
173 pool: web::Data<PgPool>,
174 user: AuthUser,
175) -> ControllerResult<web::Json<CourseBreadcrumbInfo>> {
176 let mut conn = pool.acquire().await?;
177 let user_id = Some(user.id);
178 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
179 let info = models::courses::get_course_breadcrumb_info(&mut conn, *course_id).await?;
180 token.authorized_ok(web::Json(info))
181}
182
183#[utoipa::path(
187 get,
188 path = "/{course_id}/status-for-all-exercises/{user_id}",
189 operation_id = "getCourseExerciseStatusesForUser",
190 tag = "courses",
191 params(
192 ("course_id" = Uuid, Path, description = "Course id"),
193 ("user_id" = Uuid, Path, description = "User id")
194 ),
195 responses(
196 (status = 200, description = "Exercise statuses for course user", body = [ExerciseStatusSummaryForUser])
197 )
198)]
199#[instrument(skip(pool))]
200async fn get_all_exercise_statuses_by_course_id(
201 params: web::Path<(Uuid, Uuid)>,
202 pool: web::Data<PgPool>,
203 user: AuthUser,
204) -> ControllerResult<web::Json<Vec<ExerciseStatusSummaryForUser>>> {
205 let (course_id, user_id) = params.into_inner();
206 let mut conn = pool.acquire().await?;
207 let token = authorize(
208 &mut conn,
209 Act::ViewUserProgressOrDetails,
210 Some(user.id),
211 Res::Course(course_id),
212 )
213 .await?;
214 let res = models::exercises::get_all_exercise_statuses_by_user_id_and_course_id(
215 &mut conn, course_id, user_id,
216 )
217 .await?;
218 token.authorized_ok(web::Json(res))
219}
220
221#[utoipa::path(
225 get,
226 path = "/{course_id}/course-module-completions/{user_id}",
227 operation_id = "getCourseModuleCompletionsForUser",
228 tag = "courses",
229 params(
230 ("course_id" = Uuid, Path, description = "Course id"),
231 ("user_id" = Uuid, Path, description = "User id")
232 ),
233 responses(
234 (status = 200, description = "Course module completions for course user", body = [CourseModuleCompletion])
235 )
236)]
237#[instrument(skip(pool))]
238async fn get_all_course_module_completions_for_user_by_course_id(
239 params: web::Path<(Uuid, Uuid)>,
240 pool: web::Data<PgPool>,
241 user: AuthUser,
242) -> ControllerResult<web::Json<Vec<CourseModuleCompletion>>> {
243 let (course_id, user_id) = params.into_inner();
244 let mut conn = pool.acquire().await?;
245 let token = authorize(
246 &mut conn,
247 Act::ViewUserProgressOrDetails,
248 Some(user.id),
249 Res::Course(course_id),
250 )
251 .await?;
252 let res = models::course_module_completions::get_all_by_course_id_and_user_id(
253 &mut conn, course_id, user_id,
254 )
255 .await?;
256 token.authorized_ok(web::Json(res))
257}
258
259#[utoipa::path(
263 get,
264 path = "/{course_id}/progress/{user_id}",
265 operation_id = "getCourseProgressForUser",
266 tag = "courses",
267 params(
268 ("course_id" = Uuid, Path, description = "Course id"),
269 ("user_id" = Uuid, Path, description = "User id")
270 ),
271 responses(
272 (status = 200, description = "User progress for course", body = [UserCourseProgress])
273 )
274)]
275#[instrument(skip(pool))]
276async fn get_user_progress_for_course(
277 path: web::Path<(Uuid, Uuid)>,
278 pool: web::Data<PgPool>,
279 user: AuthUser,
280) -> ControllerResult<web::Json<Vec<UserCourseProgress>>> {
281 let (course_id, target_user_id) = path.into_inner();
282 let mut conn = pool.acquire().await?;
283 let token = authorize(
284 &mut conn,
285 Act::ViewUserProgressOrDetails,
286 Some(user.id),
287 Res::Course(course_id),
288 )
289 .await?;
290 let user_course_progress = models::user_exercise_states::get_user_course_progress(
291 &mut conn,
292 course_id,
293 target_user_id,
294 false,
295 )
296 .await?;
297 token.authorized_ok(web::Json(user_course_progress))
298}
299
300#[utoipa::path(
304 get,
305 path = "/{course_id}/user-settings/{user_id}",
306 operation_id = "getCourseUserSettingsForUser",
307 tag = "courses",
308 params(
309 ("course_id" = Uuid, Path, description = "Course id"),
310 ("user_id" = Uuid, Path, description = "User id")
311 ),
312 responses(
313 (status = 200, description = "User course settings", body = Option<UserCourseSettings>)
314 )
315)]
316#[instrument(skip(pool))]
317async fn get_user_course_settings(
318 path: web::Path<(Uuid, Uuid)>,
319 pool: web::Data<PgPool>,
320 user: AuthUser,
321) -> ControllerResult<web::Json<Option<UserCourseSettings>>> {
322 let (course_id, target_user_id) = path.into_inner();
323 let mut conn = pool.acquire().await?;
324 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
325 let settings = models::user_course_settings::get_user_course_settings_by_course_id(
326 &mut conn,
327 target_user_id,
328 course_id,
329 )
330 .await?;
331 token.authorized_ok(web::Json(settings))
332}
333
334#[utoipa::path(
340 post,
341 path = "/{course_id}/reprocess-completions",
342 operation_id = "reprocessCourseCompletions",
343 tag = "courses",
344 params(
345 ("course_id" = Uuid, Path, description = "Course id")
346 ),
347 responses(
348 (status = 200, description = "Course completions reprocessed", body = bool)
349 )
350)]
351#[instrument(skip(pool, user))]
352async fn post_reprocess_module_completions(
353 pool: web::Data<PgPool>,
354 user: AuthUser,
355 course_id: web::Path<Uuid>,
356) -> ControllerResult<web::Json<bool>> {
357 let mut conn = pool.acquire().await?;
358 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
359 models::library::progressing::process_all_course_completions(&mut conn, *course_id).await?;
360 token.authorized_ok(web::Json(true))
361}
362
363#[utoipa::path(
380 post,
381 path = "",
382 operation_id = "createCourse",
383 tag = "courses",
384 request_body = NewCourse,
385 responses(
386 (status = 200, description = "Created course", body = Course)
387 )
388)]
389#[instrument(skip(pool, app_conf))]
390async fn post_new_course(
391 request_id: RequestId,
392 pool: web::Data<PgPool>,
393 payload: web::Json<NewCourse>,
394 user: AuthUser,
395 app_conf: web::Data<ApplicationConfiguration>,
396 jwt_key: web::Data<JwtKey>,
397) -> ControllerResult<web::Json<Course>> {
398 let mut conn = pool.acquire().await?;
399 let new_course = payload.0;
400 if !is_ietf_language_code_like(&new_course.language_code) {
401 return Err(ControllerError::new(
402 ControllerErrorType::BadRequest,
403 "Malformed language code.".to_string(),
404 None,
405 ));
406 }
407 let token = authorize(
408 &mut conn,
409 Act::CreateCoursesOrExams,
410 Some(user.id),
411 Res::Organization(new_course.organization_id),
412 )
413 .await?;
414
415 let mut tx = conn.begin().await?;
416 let (course, ..) = library::content_management::create_new_course(
417 &mut tx,
418 PKeyPolicy::Generate,
419 new_course,
420 user.id,
421 models_requests::make_spec_fetcher(
422 app_conf.base_url.clone(),
423 request_id.0,
424 Arc::clone(&jwt_key),
425 ),
426 models_requests::fetch_service_info,
427 )
428 .await?;
429 models::roles::insert(
430 &mut tx,
431 user.id,
432 models::roles::UserRole::Teacher,
433 models::roles::RoleDomain::Course(course.id),
434 )
435 .await?;
436 tx.commit().await?;
437
438 token.authorized_ok(web::Json(course))
439}
440
441#[utoipa::path(
457 put,
458 path = "/{course_id}",
459 operation_id = "updateCourse",
460 tag = "courses",
461 params(
462 ("course_id" = Uuid, Path, description = "Course id")
463 ),
464 request_body = CourseUpdate,
465 responses(
466 (status = 200, description = "Updated course", body = Course)
467 )
468)]
469#[instrument(skip(pool))]
470async fn update_course(
471 payload: web::Json<CourseUpdate>,
472 course_id: web::Path<Uuid>,
473 pool: web::Data<PgPool>,
474 user: AuthUser,
475) -> ControllerResult<web::Json<Course>> {
476 let mut conn = pool.acquire().await?;
477 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
478 let course_update = payload.0;
479 let course_before_update = models::courses::get_course(&mut conn, *course_id).await?;
480 if course_update.can_add_chatbot != course_before_update.can_add_chatbot {
481 let _token2 =
483 authorize(&mut conn, Act::Teach, Some(user.id), Res::GlobalPermissions).await?;
484 }
485
486 let locking_just_enabled =
487 !course_before_update.chapter_locking_enabled && course_update.chapter_locking_enabled;
488
489 let course = models::courses::update_course(&mut conn, *course_id, course_update).await?;
490
491 if locking_just_enabled {
492 use models::{user_chapter_locking_statuses, user_course_settings};
493
494 let all_user_settings =
495 user_course_settings::get_all_by_course_id(&mut conn, *course_id).await?;
496
497 for settings in all_user_settings {
498 let _ = user_chapter_locking_statuses::get_or_init_all_for_course(
499 &mut conn,
500 settings.user_id,
501 *course_id,
502 )
503 .await?;
504 }
505 }
506
507 token.authorized_ok(web::Json(course))
508}
509
510#[utoipa::path(
514 delete,
515 path = "/{course_id}",
516 operation_id = "deleteCourse",
517 tag = "courses",
518 params(
519 ("course_id" = Uuid, Path, description = "Course id")
520 ),
521 responses(
522 (status = 200, description = "Deleted course", body = serde_json::Value)
523 )
524)]
525#[instrument(skip(pool))]
526async fn delete_course(
527 course_id: web::Path<Uuid>,
528 pool: web::Data<PgPool>,
529 user: AuthUser,
530) -> ControllerResult<web::Json<Course>> {
531 let mut conn = pool.acquire().await?;
532 let token = authorize(
533 &mut conn,
534 Act::UsuallyUnacceptableDeletion,
535 Some(user.id),
536 Res::Course(*course_id),
537 )
538 .await?;
539 let course = models::courses::delete_course(&mut conn, *course_id).await?;
540
541 token.authorized_ok(web::Json(course))
542}
543
544#[utoipa::path(
591 get,
592 path = "/{course_id}/structure",
593 operation_id = "getCourseStructure",
594 tag = "courses",
595 params(
596 ("course_id" = Uuid, Path, description = "Course id")
597 ),
598 responses(
599 (status = 200, description = "Course structure", body = CourseStructure)
600 )
601)]
602#[instrument(skip(pool, file_store, app_conf))]
603async fn get_course_structure(
604 course_id: web::Path<Uuid>,
605 pool: web::Data<PgPool>,
606 user: AuthUser,
607 file_store: web::Data<dyn FileStore>,
608 app_conf: web::Data<ApplicationConfiguration>,
609) -> ControllerResult<web::Json<CourseStructure>> {
610 let mut conn = pool.acquire().await?;
611 let token = authorize(
612 &mut conn,
613 Act::ViewInternalCourseStructure,
614 Some(user.id),
615 Res::Course(*course_id),
616 )
617 .await?;
618 let course_structure = models::courses::get_course_structure(
619 &mut conn,
620 *course_id,
621 file_store.as_ref(),
622 app_conf.as_ref(),
623 )
624 .await?;
625
626 token.authorized_ok(web::Json(course_structure))
627}
628
629#[utoipa::path(
644 post,
645 path = "/{course_id}/upload",
646 operation_id = "uploadCourseMedia",
647 tag = "courses",
648 params(
649 ("course_id" = Uuid, Path, description = "Course id")
650 ),
651 request_body(
652 content = String,
653 content_type = "multipart/form-data"
654 ),
655 responses(
656 (status = 200, description = "Uploaded media result", body = UploadResult)
657 )
658)]
659#[instrument(skip(payload, request, pool, file_store, app_conf))]
660async fn add_media_for_course(
661 course_id: web::Path<Uuid>,
662 payload: Multipart,
663 request: HttpRequest,
664 pool: web::Data<PgPool>,
665 user: AuthUser,
666 file_store: web::Data<dyn FileStore>,
667 app_conf: web::Data<ApplicationConfiguration>,
668) -> ControllerResult<web::Json<UploadResult>> {
669 let mut conn = pool.acquire().await?;
670 let course = models::courses::get_course(&mut conn, *course_id).await?;
671 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
672 let media_path = upload_file_from_cms(
673 request.headers(),
674 payload,
675 StoreKind::Course(course.id),
676 file_store.as_ref(),
677 &mut conn,
678 user,
679 )
680 .await?;
681 let download_url = file_store.get_download_url(media_path.as_path(), app_conf.as_ref());
682
683 token.authorized_ok(web::Json(UploadResult { url: download_url }))
684}
685
686#[utoipa::path(
690 get,
691 path = "/{course_id}/exercises",
692 operation_id = "getCourseExercises",
693 tag = "courses",
694 params(
695 ("course_id" = Uuid, Path, description = "Course id")
696 ),
697 responses(
698 (status = 200, description = "Exercises for course", body = [Exercise])
699 )
700)]
701#[instrument(skip(pool))]
702async fn get_all_exercises(
703 pool: web::Data<PgPool>,
704 course_id: web::Path<Uuid>,
705 user: AuthUser,
706) -> ControllerResult<web::Json<Vec<Exercise>>> {
707 let mut conn = pool.acquire().await?;
708 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
709 let exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
710
711 token.authorized_ok(web::Json(exercises))
712}
713
714#[utoipa::path(
718 get,
719 path = "/{course_id}/exercises-and-count-of-answers-requiring-attention",
720 operation_id = "getCourseExercisesAndAnswersRequiringAttentionCounts",
721 tag = "courses",
722 params(
723 ("course_id" = Uuid, Path, description = "Course id")
724 ),
725 responses(
726 (
727 status = 200,
728 description = "Exercises and answer attention counts",
729 body = [ExerciseAnswersInCourseRequiringAttentionCount]
730 )
731 )
732)]
733#[instrument(skip(pool))]
734async fn get_all_exercises_and_count_of_answers_requiring_attention(
735 pool: web::Data<PgPool>,
736 course_id: web::Path<Uuid>,
737 user: AuthUser,
738) -> ControllerResult<web::Json<Vec<ExerciseAnswersInCourseRequiringAttentionCount>>> {
739 let mut conn = pool.acquire().await?;
740 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
741 let _exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
742 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?;
743 token.authorized_ok(web::Json(count_of_answers_requiring_attention))
744}
745
746#[utoipa::path(
758 get,
759 path = "/{course_id}/language-versions",
760 operation_id = "getCourseLanguageVersions",
761 tag = "courses",
762 params(
763 ("course_id" = Uuid, Path, description = "Course id")
764 ),
765 responses(
766 (status = 200, description = "Course language versions", body = [Course])
767 )
768)]
769#[instrument(skip(pool))]
770async fn get_all_course_language_versions(
771 pool: web::Data<PgPool>,
772 course_id: web::Path<Uuid>,
773 user: AuthUser,
774) -> ControllerResult<web::Json<Vec<Course>>> {
775 let mut conn = pool.acquire().await?;
776 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
777 let course = models::courses::get_course(&mut conn, *course_id).await?;
778 let language_versions =
779 models::courses::get_all_language_versions_of_course(&mut conn, &course).await?;
780
781 token.authorized_ok(web::Json(language_versions))
782}
783
784#[derive(Deserialize, Debug, utoipa::ToSchema)]
785#[serde(tag = "mode", rename_all = "snake_case")]
786pub enum CopyCourseMode {
787 Duplicate,
789 SameLanguageGroup,
791 ExistingLanguageGroup { target_course_id: Uuid },
793 NewLanguageGroup,
795}
796
797#[derive(Deserialize, Debug, utoipa::ToSchema)]
798
799pub struct CopyCourseRequest {
800 #[serde(flatten)]
801 pub new_course: NewCourse,
802 pub mode: CopyCourseMode,
803}
804
805#[utoipa::path(
849 post,
850 path = "/{course_id}/create-copy",
851 operation_id = "createCourseCopy",
852 tag = "courses",
853 params(
854 ("course_id" = Uuid, Path, description = "Course id")
855 ),
856 request_body = CopyCourseRequest,
857 responses(
858 (status = 200, description = "Created course copy", body = Course)
859 )
860)]
861#[instrument(skip(pool))]
862pub async fn create_course_copy(
863 pool: web::Data<PgPool>,
864 course_id: web::Path<Uuid>,
865 payload: web::Json<CopyCourseRequest>,
866 user: AuthUser,
867) -> ControllerResult<web::Json<Course>> {
868 let mut conn = pool.acquire().await?;
869 let token = authorize(
870 &mut conn,
871 Act::Duplicate,
872 Some(user.id),
873 Res::Course(*course_id),
874 )
875 .await?;
876
877 let new_course = payload.new_course.clone();
878 authorize(
879 &mut conn,
880 Act::CreateCoursesOrExams,
881 Some(user.id),
882 Res::Organization(new_course.organization_id),
883 )
884 .await?;
885
886 let mut tx = conn.begin().await?;
887
888 let copied_course = match &payload.mode {
889 CopyCourseMode::Duplicate => {
890 models::library::copying::copy_course(&mut tx, *course_id, &new_course, false, user.id)
891 .await?
892 }
893 CopyCourseMode::SameLanguageGroup => {
894 models::library::copying::copy_course(&mut tx, *course_id, &new_course, true, user.id)
895 .await?
896 }
897 CopyCourseMode::ExistingLanguageGroup { target_course_id } => {
898 let target_course = models::courses::get_course(&mut tx, *target_course_id).await?;
899 authorize(
901 &mut tx,
902 Act::Duplicate,
903 Some(user.id),
904 Res::Course(*target_course_id),
905 )
906 .await?;
907 models::library::copying::copy_course_with_language_group(
908 &mut tx,
909 *course_id,
910 target_course.course_language_group_id,
911 &new_course,
912 user.id,
913 )
914 .await?
915 }
916 CopyCourseMode::NewLanguageGroup => {
917 let new_clg_id = course_language_groups::insert(
918 &mut tx,
919 PKeyPolicy::Generate,
920 new_course.slug.as_str(),
921 )
922 .await?;
923 models::library::copying::copy_course_with_language_group(
924 &mut tx,
925 *course_id,
926 new_clg_id,
927 &new_course,
928 user.id,
929 )
930 .await?
931 }
932 };
933
934 models::roles::insert(
935 &mut tx,
936 user.id,
937 models::roles::UserRole::Teacher,
938 models::roles::RoleDomain::Course(copied_course.id),
939 )
940 .await?;
941
942 tx.commit().await?;
943
944 token.authorized_ok(web::Json(copied_course))
945}
946
947#[utoipa::path(
951 get,
952 path = "/{course_id}/daily-submission-counts",
953 operation_id = "getCourseDailySubmissionCounts",
954 tag = "courses",
955 params(
956 ("course_id" = Uuid, Path, description = "Course id")
957 ),
958 responses(
959 (status = 200, description = "Course daily submission counts", body = [ExerciseSlideSubmissionCount])
960 )
961)]
962#[instrument(skip(pool))]
963async fn get_daily_submission_counts(
964 pool: web::Data<PgPool>,
965 course_id: web::Path<Uuid>,
966 user: AuthUser,
967) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
968 let mut conn = pool.acquire().await?;
969 let token = authorize(
970 &mut conn,
971 Act::ViewStats,
972 Some(user.id),
973 Res::Course(*course_id),
974 )
975 .await?;
976 let course = models::courses::get_course(&mut conn, *course_id).await?;
977 let res =
978 exercise_slide_submissions::get_course_daily_slide_submission_counts(&mut conn, &course)
979 .await?;
980
981 token.authorized_ok(web::Json(res))
982}
983
984#[utoipa::path(
988 get,
989 path = "/{course_id}/daily-users-who-have-submitted-something",
990 operation_id = "getCourseDailyUsersWhoSubmittedSomething",
991 tag = "courses",
992 params(
993 ("course_id" = Uuid, Path, description = "Course id")
994 ),
995 responses(
996 (status = 200, description = "Course daily user submission counts", body = [ExerciseSlideSubmissionCount])
997 )
998)]
999#[instrument(skip(pool))]
1000async fn get_daily_user_counts_with_submissions(
1001 pool: web::Data<PgPool>,
1002 course_id: web::Path<Uuid>,
1003 user: AuthUser,
1004) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
1005 let mut conn = pool.acquire().await?;
1006 let token = authorize(
1007 &mut conn,
1008 Act::ViewStats,
1009 Some(user.id),
1010 Res::Course(*course_id),
1011 )
1012 .await?;
1013 let course = models::courses::get_course(&mut conn, *course_id).await?;
1014 let res = exercise_slide_submissions::get_course_daily_user_counts_with_submissions(
1015 &mut conn, &course,
1016 )
1017 .await?;
1018
1019 token.authorized_ok(web::Json(res))
1020}
1021
1022#[utoipa::path(
1026 get,
1027 path = "/{course_id}/weekday-hour-submission-counts",
1028 operation_id = "getCourseWeekdayHourSubmissionCounts",
1029 tag = "courses",
1030 params(
1031 ("course_id" = Uuid, Path, description = "Course id")
1032 ),
1033 responses(
1034 (status = 200, description = "Course weekday and hour submission counts", body = [ExerciseSlideSubmissionCountByWeekAndHour])
1035 )
1036)]
1037#[instrument(skip(pool))]
1038async fn get_weekday_hour_submission_counts(
1039 pool: web::Data<PgPool>,
1040 course_id: web::Path<Uuid>,
1041 user: AuthUser,
1042) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByWeekAndHour>>> {
1043 let mut conn = pool.acquire().await?;
1044 let token = authorize(
1045 &mut conn,
1046 Act::ViewStats,
1047 Some(user.id),
1048 Res::Course(*course_id),
1049 )
1050 .await?;
1051 let course = models::courses::get_course(&mut conn, *course_id).await?;
1052 let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_weekday_and_hour(
1053 &mut conn, &course,
1054 )
1055 .await?;
1056
1057 token.authorized_ok(web::Json(res))
1058}
1059
1060#[utoipa::path(
1064 get,
1065 path = "/{course_id}/submission-counts-by-exercise",
1066 operation_id = "getCourseSubmissionCountsByExercise",
1067 tag = "courses",
1068 params(
1069 ("course_id" = Uuid, Path, description = "Course id")
1070 ),
1071 responses(
1072 (status = 200, description = "Course submission counts by exercise", body = [ExerciseSlideSubmissionCountByExercise])
1073 )
1074)]
1075#[instrument(skip(pool))]
1076async fn get_submission_counts_by_exercise(
1077 pool: web::Data<PgPool>,
1078 course_id: web::Path<Uuid>,
1079 user: AuthUser,
1080) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByExercise>>> {
1081 let mut conn = pool.acquire().await?;
1082 let token = authorize(
1083 &mut conn,
1084 Act::ViewStats,
1085 Some(user.id),
1086 Res::Course(*course_id),
1087 )
1088 .await?;
1089 let course = models::courses::get_course(&mut conn, *course_id).await?;
1090 let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_exercise(
1091 &mut conn, &course,
1092 )
1093 .await?;
1094
1095 token.authorized_ok(web::Json(res))
1096}
1097
1098#[utoipa::path(
1102 get,
1103 path = "/{course_id}/course-instances",
1104 operation_id = "getCourseInstances",
1105 tag = "courses",
1106 params(
1107 ("course_id" = Uuid, Path, description = "Course id")
1108 ),
1109 responses(
1110 (status = 200, description = "Course instances", body = [CourseInstance])
1111 )
1112)]
1113#[instrument(skip(pool))]
1114async fn get_course_instances(
1115 pool: web::Data<PgPool>,
1116 course_id: web::Path<Uuid>,
1117 user: AuthUser,
1118) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
1119 let mut conn = pool.acquire().await?;
1120 let token = authorize(
1121 &mut conn,
1122 Act::Teach,
1123 Some(user.id),
1124 Res::Course(*course_id),
1125 )
1126 .await?;
1127 let course_instances =
1128 models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
1129
1130 token.authorized_ok(web::Json(course_instances))
1131}
1132
1133#[derive(Debug, Deserialize)]
1134
1135pub struct GetFeedbackQuery {
1136 read: bool,
1137 #[serde(flatten)]
1138 pagination: Pagination,
1139}
1140
1141#[utoipa::path(
1145 get,
1146 path = "/{course_id}/feedback",
1147 operation_id = "getCourseFeedback",
1148 tag = "courses",
1149 params(
1150 ("course_id" = String, Path, description = "Course id"),
1151 ("read" = bool, Query, description = "Whether to fetch read feedback"),
1152 ("page" = Option<i64>, Query, description = "Page number"),
1153 ("limit" = Option<i64>, Query, description = "Page size")
1154 ),
1155 responses(
1156 (status = 200, description = "Feedback for the course", body = [Feedback])
1157 )
1158)]
1159#[instrument(skip(pool))]
1160pub async fn get_feedback(
1161 course_id: web::Path<Uuid>,
1162 pool: web::Data<PgPool>,
1163 read: web::Query<GetFeedbackQuery>,
1164 user: AuthUser,
1165) -> ControllerResult<web::Json<Vec<Feedback>>> {
1166 let mut conn = pool.acquire().await?;
1167 let token = authorize(
1168 &mut conn,
1169 Act::Teach,
1170 Some(user.id),
1171 Res::Course(*course_id),
1172 )
1173 .await?;
1174 let feedback =
1175 feedback::get_feedback_for_course(&mut conn, *course_id, read.read, read.pagination)
1176 .await?;
1177
1178 token.authorized_ok(web::Json(feedback))
1179}
1180
1181#[utoipa::path(
1185 get,
1186 path = "/{course_id}/feedback-count",
1187 operation_id = "getCourseFeedbackCount",
1188 tag = "courses",
1189 params(
1190 ("course_id" = Uuid, Path, description = "Course id")
1191 ),
1192 responses(
1193 (status = 200, description = "Feedback counts for the course", body = FeedbackCount)
1194 )
1195)]
1196#[instrument(skip(pool))]
1197pub async fn get_feedback_count(
1198 course_id: web::Path<Uuid>,
1199 pool: web::Data<PgPool>,
1200 user: AuthUser,
1201) -> ControllerResult<web::Json<FeedbackCount>> {
1202 let mut conn = pool.acquire().await?;
1203 let token = authorize(
1204 &mut conn,
1205 Act::Teach,
1206 Some(user.id),
1207 Res::Course(*course_id),
1208 )
1209 .await?;
1210
1211 let feedback_count = feedback::get_feedback_count_for_course(&mut conn, *course_id).await?;
1212
1213 token.authorized_ok(web::Json(feedback_count))
1214}
1215
1216#[utoipa::path(
1220 post,
1221 path = "/{course_id}/new-course-instance",
1222 operation_id = "createCourseInstance",
1223 tag = "courses",
1224 params(
1225 ("course_id" = Uuid, Path, description = "Course id")
1226 ),
1227 request_body = CourseInstanceForm,
1228 responses(
1229 (status = 200, description = "Created course instance id", body = Uuid)
1230 )
1231)]
1232#[instrument(skip(pool))]
1233async fn new_course_instance(
1234 form: web::Json<CourseInstanceForm>,
1235 course_id: web::Path<Uuid>,
1236 pool: web::Data<PgPool>,
1237 user: AuthUser,
1238) -> ControllerResult<web::Json<Uuid>> {
1239 let mut conn = pool.acquire().await?;
1240 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1241 let form = form.into_inner();
1242 let new = NewCourseInstance {
1243 course_id: *course_id,
1244 name: form.name.as_deref(),
1245 description: form.description.as_deref(),
1246 support_email: form.support_email.as_deref(),
1247 teacher_in_charge_name: &form.teacher_in_charge_name,
1248 teacher_in_charge_email: &form.teacher_in_charge_email,
1249 opening_time: form.opening_time,
1250 closing_time: form.closing_time,
1251 };
1252 let ci = models::course_instances::insert(&mut conn, PKeyPolicy::Generate, new).await?;
1253
1254 token.authorized_ok(web::Json(ci.id))
1255}
1256
1257#[instrument(skip(pool))]
1258#[utoipa::path(
1259 get,
1260 path = "/{course_id}/glossary",
1261 operation_id = "getCourseGlossary",
1262 tag = "glossary",
1263 params(
1264 ("course_id" = Uuid, Path, description = "Course id")
1265 ),
1266 responses(
1267 (status = 200, description = "Glossary terms for the course", body = [Term]),
1268 (status = 401, description = "Authentication required"),
1269 (status = 403, description = "User is not allowed to manage the course glossary")
1270 )
1271)]
1272pub(crate) async fn glossary(
1273 pool: web::Data<PgPool>,
1274 course_id: web::Path<Uuid>,
1275 user: AuthUser,
1276) -> ControllerResult<web::Json<Vec<Term>>> {
1277 let mut conn = pool.acquire().await?;
1278 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1279 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
1280
1281 token.authorized_ok(web::Json(glossary))
1282}
1283
1284#[instrument(skip(pool))]
1287async fn _new_term(
1288 pool: web::Data<PgPool>,
1289 course_id: web::Path<Uuid>,
1290 user: AuthUser,
1291) -> ControllerResult<web::Json<Vec<Term>>> {
1292 let mut conn = pool.acquire().await?;
1293 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1294 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
1295
1296 token.authorized_ok(web::Json(glossary))
1297}
1298
1299#[instrument(skip(pool))]
1300#[utoipa::path(
1301 post,
1302 path = "/{course_id}/glossary",
1303 operation_id = "createCourseGlossaryTerm",
1304 tag = "glossary",
1305 params(
1306 ("course_id" = Uuid, Path, description = "Course id")
1307 ),
1308 request_body = TermUpdate,
1309 responses(
1310 (status = 200, description = "Created glossary term id", body = Uuid),
1311 (status = 401, description = "Authentication required"),
1312 (status = 403, description = "User is not allowed to manage the course glossary")
1313 )
1314)]
1315pub(crate) async fn new_glossary_term(
1316 pool: web::Data<PgPool>,
1317 course_id: web::Path<Uuid>,
1318 new_term: web::Json<TermUpdate>,
1319 user: AuthUser,
1320) -> ControllerResult<web::Json<Uuid>> {
1321 let mut conn = pool.acquire().await?;
1322 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1323 let TermUpdate { term, definition } = new_term.into_inner();
1324 let term = models::glossary::insert(&mut conn, &term, &definition, *course_id).await?;
1325
1326 token.authorized_ok(web::Json(term))
1327}
1328
1329#[utoipa::path(
1333 get,
1334 path = "/{course_id}/course-users-counts-by-exercise",
1335 operation_id = "getCourseUsersCountsByExercise",
1336 tag = "courses",
1337 params(
1338 ("course_id" = Uuid, Path, description = "Course id")
1339 ),
1340 responses(
1341 (status = 200, description = "Course users counts by exercise", body = [ExerciseUserCounts])
1342 )
1343)]
1344#[instrument(skip(pool))]
1345pub async fn get_course_users_counts_by_exercise(
1346 course_id: web::Path<Uuid>,
1347 pool: web::Data<PgPool>,
1348 user: AuthUser,
1349) -> ControllerResult<web::Json<Vec<ExerciseUserCounts>>> {
1350 let mut conn = pool.acquire().await?;
1351 let course_id = course_id.into_inner();
1352 let token = authorize(
1353 &mut conn,
1354 Act::ViewStats,
1355 Some(user.id),
1356 Res::Course(course_id),
1357 )
1358 .await?;
1359
1360 let res =
1361 models::user_exercise_states::get_course_users_counts_by_exercise(&mut conn, course_id)
1362 .await?;
1363
1364 token.authorized_ok(web::Json(res))
1365}
1366
1367#[utoipa::path(
1375 post,
1376 path = "/{course_id}/new-page-ordering",
1377 operation_id = "updateCoursePageOrdering",
1378 tag = "courses",
1379 params(
1380 ("course_id" = Uuid, Path, description = "Course id")
1381 ),
1382 request_body = Vec<Page>,
1383 responses(
1384 (status = 200, description = "Course page ordering updated")
1385 )
1386)]
1387#[instrument(skip(pool))]
1388pub async fn post_new_page_ordering(
1389 course_id: web::Path<Uuid>,
1390 pool: web::Data<PgPool>,
1391 user: AuthUser,
1392 payload: web::Json<Vec<Page>>,
1393) -> ControllerResult<web::Json<()>> {
1394 let mut conn = pool.acquire().await?;
1395 let course_id = course_id.into_inner();
1396 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1397
1398 models::pages::reorder_pages(&mut conn, &payload, course_id).await?;
1399
1400 token.authorized_ok(web::Json(()))
1401}
1402
1403#[utoipa::path(
1409 post,
1410 path = "/{course_id}/new-chapter-ordering",
1411 operation_id = "updateCourseChapterOrdering",
1412 tag = "courses",
1413 params(
1414 ("course_id" = Uuid, Path, description = "Course id")
1415 ),
1416 request_body = Vec<Chapter>,
1417 responses(
1418 (status = 200, description = "Course chapter ordering updated")
1419 )
1420)]
1421#[instrument(skip(pool))]
1422pub async fn post_new_chapter_ordering(
1423 course_id: web::Path<Uuid>,
1424 pool: web::Data<PgPool>,
1425 user: AuthUser,
1426 payload: web::Json<Vec<Chapter>>,
1427) -> ControllerResult<web::Json<()>> {
1428 let mut conn = pool.acquire().await?;
1429 let course_id = course_id.into_inner();
1430 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1431
1432 models::pages::reorder_chapters(&mut conn, &payload, course_id).await?;
1433
1434 token.authorized_ok(web::Json(()))
1435}
1436
1437#[utoipa::path(
1438 get,
1439 path = "/{course_id}/references",
1440 operation_id = "getCourseReferences",
1441 tag = "courses",
1442 params(
1443 ("course_id" = Uuid, Path, description = "Course id")
1444 ),
1445 responses(
1446 (status = 200, description = "Course references", body = [MaterialReference])
1447 )
1448)]
1449#[instrument(skip(pool))]
1450async fn get_material_references_by_course_id(
1451 course_id: web::Path<Uuid>,
1452 pool: web::Data<PgPool>,
1453 user: AuthUser,
1454) -> ControllerResult<web::Json<Vec<MaterialReference>>> {
1455 let mut conn = pool.acquire().await?;
1456 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1457
1458 let res =
1459 models::material_references::get_references_by_course_id(&mut conn, *course_id).await?;
1460 token.authorized_ok(web::Json(res))
1461}
1462
1463#[utoipa::path(
1464 post,
1465 path = "/{course_id}/references",
1466 operation_id = "createCourseReferences",
1467 tag = "courses",
1468 params(
1469 ("course_id" = Uuid, Path, description = "Course id")
1470 ),
1471 request_body = [NewMaterialReference],
1472 responses(
1473 (status = 200, description = "Course references created")
1474 )
1475)]
1476#[instrument(skip(pool))]
1477async fn insert_material_references(
1478 course_id: web::Path<Uuid>,
1479 payload: web::Json<Vec<NewMaterialReference>>,
1480 pool: web::Data<PgPool>,
1481 user: AuthUser,
1482) -> ControllerResult<web::Json<()>> {
1483 let mut conn = pool.acquire().await?;
1484 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1485
1486 models::material_references::insert_reference(&mut conn, *course_id, payload.0).await?;
1487
1488 token.authorized_ok(web::Json(()))
1489}
1490
1491#[utoipa::path(
1492 post,
1493 path = "/{course_id}/references/{reference_id}",
1494 operation_id = "updateCourseReference",
1495 tag = "courses",
1496 params(
1497 ("course_id" = Uuid, Path, description = "Course id"),
1498 ("reference_id" = Uuid, Path, description = "Reference id")
1499 ),
1500 request_body = NewMaterialReference,
1501 responses(
1502 (status = 200, description = "Course reference updated")
1503 )
1504)]
1505#[instrument(skip(pool))]
1506async fn update_material_reference(
1507 path: web::Path<(Uuid, Uuid)>,
1508 pool: web::Data<PgPool>,
1509 user: AuthUser,
1510 payload: web::Json<NewMaterialReference>,
1511) -> ControllerResult<web::Json<()>> {
1512 let (course_id, reference_id) = path.into_inner();
1513 let mut conn = pool.acquire().await?;
1514 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1515
1516 models::material_references::update_by_id_and_course_id(
1517 &mut conn,
1518 reference_id,
1519 course_id,
1520 payload.0,
1521 )
1522 .await?;
1523 token.authorized_ok(web::Json(()))
1524}
1525
1526#[utoipa::path(
1527 delete,
1528 path = "/{course_id}/references/{reference_id}",
1529 operation_id = "deleteCourseReference",
1530 tag = "courses",
1531 params(
1532 ("course_id" = Uuid, Path, description = "Course id"),
1533 ("reference_id" = Uuid, Path, description = "Reference id")
1534 ),
1535 responses(
1536 (status = 200, description = "Course reference deleted")
1537 )
1538)]
1539#[instrument(skip(pool))]
1540async fn delete_material_reference_by_id(
1541 path: web::Path<(Uuid, Uuid)>,
1542 pool: web::Data<PgPool>,
1543 user: AuthUser,
1544) -> ControllerResult<web::Json<()>> {
1545 let (course_id, reference_id) = path.into_inner();
1546 let mut conn = pool.acquire().await?;
1547 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1548
1549 models::material_references::delete_by_id_and_course_id(&mut conn, reference_id, course_id)
1550 .await?;
1551 token.authorized_ok(web::Json(()))
1552}
1553
1554#[utoipa::path(
1555 post,
1556 path = "/{course_id}/course-modules",
1557 operation_id = "updateCourseModules",
1558 tag = "courses",
1559 params(
1560 ("course_id" = Uuid, Path, description = "Course id")
1561 ),
1562 request_body = ModuleUpdates,
1563 responses(
1564 (status = 200, description = "Course modules updated")
1565 )
1566)]
1567#[instrument(skip(pool))]
1568pub async fn update_modules(
1569 course_id: web::Path<Uuid>,
1570 pool: web::Data<PgPool>,
1571 user: AuthUser,
1572 payload: web::Json<ModuleUpdates>,
1573) -> ControllerResult<web::Json<()>> {
1574 let mut conn = pool.acquire().await?;
1575 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1576
1577 models::course_modules::update_modules(&mut conn, *course_id, payload.into_inner()).await?;
1578 token.authorized_ok(web::Json(()))
1579}
1580
1581#[utoipa::path(
1582 get,
1583 path = "/{course_id}/default-peer-review",
1584 operation_id = "getCourseDefaultPeerReview",
1585 tag = "courses",
1586 params(
1587 ("course_id" = Uuid, Path, description = "Course id")
1588 ),
1589 responses(
1590 (status = 200, description = "Default peer review configuration", body = serde_json::Value)
1591 )
1592)]
1593async fn get_course_default_peer_review(
1594 course_id: web::Path<Uuid>,
1595 pool: web::Data<PgPool>,
1596 user: AuthUser,
1597) -> ControllerResult<web::Json<(PeerOrSelfReviewConfig, Vec<PeerOrSelfReviewQuestion>)>> {
1598 let mut conn = pool.acquire().await?;
1599 let token = authorize(
1600 &mut conn,
1601 Act::Teach,
1602 Some(user.id),
1603 Res::Course(*course_id),
1604 )
1605 .await?;
1606
1607 let peer_review = models::peer_or_self_review_configs::get_default_for_course_by_course_id(
1608 &mut conn, *course_id,
1609 )
1610 .await?;
1611 let peer_or_self_review_questions =
1612 models::peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
1613 &mut conn,
1614 peer_review.id,
1615 )
1616 .await?;
1617 token.authorized_ok(web::Json((peer_review, peer_or_self_review_questions)))
1618}
1619
1620#[utoipa::path(
1626 post,
1627 path = "/{course_id}/update-peer-review-queue-reviews-received",
1628 operation_id = "updateCoursePeerReviewQueueReviewsReceived",
1629 tag = "courses",
1630 params(
1631 ("course_id" = Uuid, Path, description = "Course id")
1632 ),
1633 responses(
1634 (status = 200, description = "Peer review queue updated", body = bool)
1635 )
1636)]
1637#[instrument(skip(pool, user))]
1638async fn post_update_peer_review_queue_reviews_received(
1639 pool: web::Data<PgPool>,
1640 user: AuthUser,
1641 course_id: web::Path<Uuid>,
1642) -> ControllerResult<web::Json<bool>> {
1643 let mut conn = pool.acquire().await?;
1644 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
1645 models::library::peer_or_self_reviewing::update_peer_review_queue_reviews_received(
1646 &mut conn, *course_id,
1647 )
1648 .await?;
1649 token.authorized_ok(web::Json(true))
1650}
1651
1652#[utoipa::path(
1658 get,
1659 path = "/{course_id}/export-submissions",
1660 operation_id = "exportCourseSubmissionsCsv",
1661 tag = "courses",
1662 params(
1663 ("course_id" = Uuid, Path, description = "Course id")
1664 ),
1665 responses(
1666 (status = 200, description = "Course submissions CSV", body = String, content_type = "text/csv")
1667 )
1668)]
1669#[instrument(skip(pool))]
1670pub async fn submission_export(
1671 course_id: web::Path<Uuid>,
1672 pool: web::Data<PgPool>,
1673 user: AuthUser,
1674) -> ControllerResult<HttpResponse> {
1675 let mut conn = pool.acquire().await?;
1676
1677 let token = authorize(
1678 &mut conn,
1679 Act::Teach,
1680 Some(user.id),
1681 Res::Course(*course_id),
1682 )
1683 .await?;
1684
1685 let course = models::courses::get_course(&mut conn, *course_id).await?;
1686
1687 general_export(
1688 pool,
1689 &format!(
1690 "attachment; filename=\"Course: {} - Submissions (exercise tasks) {}.csv\"",
1691 course.name,
1692 Utc::now().format("%Y-%m-%d")
1693 ),
1694 CourseSubmissionExportOperation {
1695 course_id: *course_id,
1696 },
1697 token,
1698 )
1699 .await
1700}
1701
1702#[utoipa::path(
1708 get,
1709 path = "/{course_id}/export-user-details",
1710 operation_id = "exportCourseUserDetailsCsv",
1711 tag = "courses",
1712 params(
1713 ("course_id" = Uuid, Path, description = "Course id")
1714 ),
1715 responses(
1716 (status = 200, description = "Course user details CSV", body = String, content_type = "text/csv")
1717 )
1718)]
1719#[instrument(skip(pool))]
1720pub async fn user_details_export(
1721 course_id: web::Path<Uuid>,
1722 pool: web::Data<PgPool>,
1723 user: AuthUser,
1724) -> ControllerResult<HttpResponse> {
1725 let mut conn = pool.acquire().await?;
1726
1727 let token = authorize(
1728 &mut conn,
1729 Act::Teach,
1730 Some(user.id),
1731 Res::Course(*course_id),
1732 )
1733 .await?;
1734
1735 let course = models::courses::get_course(&mut conn, *course_id).await?;
1736
1737 general_export(
1738 pool,
1739 &format!(
1740 "attachment; filename=\"Course: {} - User Details {}.csv\"",
1741 course.name,
1742 Utc::now().format("%Y-%m-%d")
1743 ),
1744 UsersExportOperation {
1745 course_id: *course_id,
1746 },
1747 token,
1748 )
1749 .await
1750}
1751
1752#[utoipa::path(
1758 get,
1759 path = "/{course_id}/export-exercise-tasks",
1760 operation_id = "exportCourseExerciseTasksCsv",
1761 tag = "courses",
1762 params(
1763 ("course_id" = Uuid, Path, description = "Course id")
1764 ),
1765 responses(
1766 (status = 200, description = "Course exercise tasks CSV", body = String, content_type = "text/csv")
1767 )
1768)]
1769#[instrument(skip(pool))]
1770pub async fn exercise_tasks_export(
1771 course_id: web::Path<Uuid>,
1772 pool: web::Data<PgPool>,
1773 user: AuthUser,
1774) -> ControllerResult<HttpResponse> {
1775 let mut conn = pool.acquire().await?;
1776
1777 let token = authorize(
1778 &mut conn,
1779 Act::Teach,
1780 Some(user.id),
1781 Res::Course(*course_id),
1782 )
1783 .await?;
1784
1785 let course = models::courses::get_course(&mut conn, *course_id).await?;
1786
1787 general_export(
1788 pool,
1789 &format!(
1790 "attachment; filename=\"Course: {} - Exercise tasks {}.csv\"",
1791 course.name,
1792 Utc::now().format("%Y-%m-%d")
1793 ),
1794 CourseExerciseTasksExportOperation {
1795 course_id: *course_id,
1796 },
1797 token,
1798 )
1799 .await
1800}
1801
1802#[utoipa::path(
1808 get,
1809 path = "/{course_id}/export-course-instances",
1810 operation_id = "exportCourseInstancesCsv",
1811 tag = "courses",
1812 params(
1813 ("course_id" = Uuid, Path, description = "Course id")
1814 ),
1815 responses(
1816 (status = 200, description = "Course instances CSV", body = String, content_type = "text/csv")
1817 )
1818)]
1819#[instrument(skip(pool))]
1820pub async fn course_instances_export(
1821 course_id: web::Path<Uuid>,
1822 pool: web::Data<PgPool>,
1823 user: AuthUser,
1824) -> ControllerResult<HttpResponse> {
1825 let mut conn = pool.acquire().await?;
1826
1827 let token = authorize(
1828 &mut conn,
1829 Act::Teach,
1830 Some(user.id),
1831 Res::Course(*course_id),
1832 )
1833 .await?;
1834
1835 let course = models::courses::get_course(&mut conn, *course_id).await?;
1836
1837 general_export(
1838 pool,
1839 &format!(
1840 "attachment; filename=\"Course: {} - Instances {}.csv\"",
1841 course.name,
1842 Utc::now().format("%Y-%m-%d")
1843 ),
1844 CourseInstancesExportOperation {
1845 course_id: *course_id,
1846 },
1847 token,
1848 )
1849 .await
1850}
1851
1852#[utoipa::path(
1858 get,
1859 path = "/{course_id}/export-course-user-consents",
1860 operation_id = "exportCourseUserConsentsCsv",
1861 tag = "courses",
1862 params(
1863 ("course_id" = Uuid, Path, description = "Course id")
1864 ),
1865 responses(
1866 (status = 200, description = "Course user consents CSV", body = String, content_type = "text/csv")
1867 )
1868)]
1869#[instrument(skip(pool))]
1870pub async fn course_consent_form_answers_export(
1871 course_id: web::Path<Uuid>,
1872 pool: web::Data<PgPool>,
1873 user: AuthUser,
1874) -> ControllerResult<HttpResponse> {
1875 let mut conn = pool.acquire().await?;
1876
1877 let token = authorize(
1878 &mut conn,
1879 Act::Teach,
1880 Some(user.id),
1881 Res::Course(*course_id),
1882 )
1883 .await?;
1884
1885 let course = models::courses::get_course(&mut conn, *course_id).await?;
1886
1887 general_export(
1888 pool,
1889 &format!(
1890 "attachment; filename=\"Course: {} - User Consents {}.csv\"",
1891 course.name,
1892 Utc::now().format("%Y-%m-%d")
1893 ),
1894 CourseResearchFormExportOperation {
1895 course_id: *course_id,
1896 },
1897 token,
1898 )
1899 .await
1900}
1901
1902#[utoipa::path(
1908 get,
1909 path = "/{course_id}/export-user-exercise-states",
1910 operation_id = "exportCourseUserExerciseStatesCsv",
1911 tag = "courses",
1912 params(
1913 ("course_id" = Uuid, Path, description = "Course id")
1914 ),
1915 responses(
1916 (status = 200, description = "Course user exercise states CSV", body = String, content_type = "text/csv")
1917 )
1918)]
1919#[instrument(skip(pool))]
1920pub async fn user_exercise_states_export(
1921 course_id: web::Path<Uuid>,
1922 pool: web::Data<PgPool>,
1923 user: AuthUser,
1924) -> ControllerResult<HttpResponse> {
1925 let mut conn = pool.acquire().await?;
1926
1927 let token = authorize(
1928 &mut conn,
1929 Act::Teach,
1930 Some(user.id),
1931 Res::Course(*course_id),
1932 )
1933 .await?;
1934
1935 let course = models::courses::get_course(&mut conn, *course_id).await?;
1936
1937 general_export(
1938 pool,
1939 &format!(
1940 "attachment; filename=\"Course: {} - User exercise states {}.csv\"",
1941 course.name,
1942 Utc::now().format("%Y-%m-%d")
1943 ),
1944 UserExerciseStatesExportOperation {
1945 course_id: *course_id,
1946 },
1947 token,
1948 )
1949 .await
1950}
1951
1952#[utoipa::path(
1956 get,
1957 path = "/{course_id}/page-visit-datum-summary",
1958 operation_id = "getCoursePageVisitDatumSummary",
1959 tag = "courses",
1960 params(
1961 ("course_id" = Uuid, Path, description = "Course id")
1962 ),
1963 responses(
1964 (status = 200, description = "Course page visit summary", body = [PageVisitDatumSummaryByCourse])
1965 )
1966)]
1967pub async fn get_page_visit_datum_summary(
1968 course_id: web::Path<Uuid>,
1969 pool: web::Data<PgPool>,
1970 user: AuthUser,
1971) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourse>>> {
1972 let mut conn = pool.acquire().await?;
1973 let course_id = course_id.into_inner();
1974 let token = authorize(
1975 &mut conn,
1976 Act::ViewStats,
1977 Some(user.id),
1978 Res::Course(course_id),
1979 )
1980 .await?;
1981
1982 let res = models::page_visit_datum_summary_by_courses::get_all_for_course(&mut conn, course_id)
1983 .await?;
1984
1985 token.authorized_ok(web::Json(res))
1986}
1987
1988#[utoipa::path(
1992 get,
1993 path = "/{course_id}/page-visit-datum-summary-by-pages",
1994 operation_id = "getCoursePageVisitDatumSummaryByPages",
1995 tag = "courses",
1996 params(
1997 ("course_id" = Uuid, Path, description = "Course id")
1998 ),
1999 responses(
2000 (status = 200, description = "Course page visit summary by pages", body = [PageVisitDatumSummaryByPages])
2001 )
2002)]
2003pub async fn get_page_visit_datum_summary_by_pages(
2004 course_id: web::Path<Uuid>,
2005 pool: web::Data<PgPool>,
2006 user: AuthUser,
2007) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByPages>>> {
2008 let mut conn = pool.acquire().await?;
2009 let course_id = course_id.into_inner();
2010 let token = authorize(
2011 &mut conn,
2012 Act::ViewStats,
2013 Some(user.id),
2014 Res::Course(course_id),
2015 )
2016 .await?;
2017
2018 let res =
2019 models::page_visit_datum_summary_by_pages::get_all_for_course(&mut conn, course_id).await?;
2020
2021 token.authorized_ok(web::Json(res))
2022}
2023
2024#[utoipa::path(
2028 get,
2029 path = "/{course_id}/page-visit-datum-summary-by-device-types",
2030 operation_id = "getCoursePageVisitDatumSummaryByDeviceTypes",
2031 tag = "courses",
2032 params(
2033 ("course_id" = Uuid, Path, description = "Course id")
2034 ),
2035 responses(
2036 (status = 200, description = "Course page visit summary by device types", body = [PageVisitDatumSummaryByCourseDeviceTypes])
2037 )
2038)]
2039pub async fn get_page_visit_datum_summary_by_device_types(
2040 course_id: web::Path<Uuid>,
2041 pool: web::Data<PgPool>,
2042 user: AuthUser,
2043) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourseDeviceTypes>>> {
2044 let mut conn = pool.acquire().await?;
2045 let course_id = course_id.into_inner();
2046 let token = authorize(
2047 &mut conn,
2048 Act::ViewStats,
2049 Some(user.id),
2050 Res::Course(course_id),
2051 )
2052 .await?;
2053
2054 let res = models::page_visit_datum_summary_by_courses_device_types::get_all_for_course(
2055 &mut conn, course_id,
2056 )
2057 .await?;
2058
2059 token.authorized_ok(web::Json(res))
2060}
2061
2062#[utoipa::path(
2066 get,
2067 path = "/{course_id}/page-visit-datum-summary-by-countries",
2068 operation_id = "getCoursePageVisitDatumSummaryByCountries",
2069 tag = "courses",
2070 params(
2071 ("course_id" = Uuid, Path, description = "Course id")
2072 ),
2073 responses(
2074 (status = 200, description = "Course page visit summary by countries", body = [PageVisitDatumSummaryByCoursesCountries])
2075 )
2076)]
2077pub async fn get_page_visit_datum_summary_by_countries(
2078 course_id: web::Path<Uuid>,
2079 pool: web::Data<PgPool>,
2080 user: AuthUser,
2081) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCoursesCountries>>> {
2082 let mut conn = pool.acquire().await?;
2083 let course_id = course_id.into_inner();
2084 let token = authorize(
2085 &mut conn,
2086 Act::ViewStats,
2087 Some(user.id),
2088 Res::Course(course_id),
2089 )
2090 .await?;
2091
2092 let res = models::page_visit_datum_summary_by_courses_countries::get_all_for_course(
2093 &mut conn, course_id,
2094 )
2095 .await?;
2096
2097 token.authorized_ok(web::Json(res))
2098}
2099
2100#[utoipa::path(
2106 delete,
2107 path = "/{course_id}/teacher-reset-course-progress-for-themselves",
2108 operation_id = "resetCourseProgressForTeacherThemselves",
2109 tag = "courses",
2110 params(
2111 ("course_id" = Uuid, Path, description = "Course id")
2112 ),
2113 responses(
2114 (status = 200, description = "Teacher course progress reset", body = bool)
2115 )
2116)]
2117pub async fn teacher_reset_course_progress_for_themselves(
2118 course_id: web::Path<Uuid>,
2119 pool: web::Data<PgPool>,
2120 user: AuthUser,
2121) -> ControllerResult<web::Json<bool>> {
2122 let mut conn = pool.acquire().await?;
2123 let course_id = course_id.into_inner();
2124 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2125
2126 let mut tx = conn.begin().await?;
2127 let course_instances =
2128 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2129 for course_instance in course_instances {
2130 models::course_instances::reset_progress_on_course_instance_for_user(
2131 &mut tx,
2132 user.id,
2133 course_instance.course_id,
2134 )
2135 .await?;
2136 }
2137
2138 tx.commit().await?;
2139 token.authorized_ok(web::Json(true))
2140}
2141
2142#[utoipa::path(
2148 delete,
2149 path = "/{course_id}/teacher-reset-course-progress-for-everyone",
2150 operation_id = "resetCourseProgressForEveryone",
2151 tag = "courses",
2152 params(
2153 ("course_id" = Uuid, Path, description = "Course id")
2154 ),
2155 responses(
2156 (status = 200, description = "Course progress reset for everyone", body = bool)
2157 )
2158)]
2159pub async fn teacher_reset_course_progress_for_everyone(
2160 course_id: web::Path<Uuid>,
2161 pool: web::Data<PgPool>,
2162 user: AuthUser,
2163) -> ControllerResult<web::Json<bool>> {
2164 let mut conn = pool.acquire().await?;
2165 let course_id = course_id.into_inner();
2166 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2167 let course = models::courses::get_course(&mut conn, course_id).await?;
2168 if !course.is_draft {
2169 return Err(ControllerError::new(
2170 ControllerErrorType::BadRequest,
2171 "Can only reset progress for a draft course.".to_string(),
2172 None,
2173 ));
2174 }
2175 let n_course_module_completions =
2177 models::course_module_completions::get_count_of_distinct_completors_by_course_id(
2178 &mut conn, course_id,
2179 )
2180 .await?;
2181 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(
2182 &mut conn, course_id,
2183 ).await?;
2184 if n_course_module_completions > 200 {
2185 return Err(ControllerError::new(
2186 ControllerErrorType::BadRequest,
2187 "Too many students have completed the course.".to_string(),
2188 None,
2189 ));
2190 }
2191 if n_completions_registered_to_study_registry > 2 {
2192 return Err(ControllerError::new(
2193 ControllerErrorType::BadRequest,
2194 "Too many students have registered their completion to a study registry".to_string(),
2195 None,
2196 ));
2197 }
2198
2199 let mut tx = conn.begin().await?;
2200 let course_instances =
2201 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2202
2203 for course_instance in course_instances {
2205 let users_in_course_instance =
2206 models::users::get_users_by_course_instance_enrollment(&mut tx, course_instance.id)
2207 .await?;
2208 for user_in_course_instance in users_in_course_instance {
2209 models::course_instances::reset_progress_on_course_instance_for_user(
2210 &mut tx,
2211 user_in_course_instance.id,
2212 course_instance.course_id,
2213 )
2214 .await?;
2215 }
2216 }
2217
2218 tx.commit().await?;
2219 token.authorized_ok(web::Json(true))
2220}
2221
2222#[derive(Debug, Deserialize)]
2223
2224pub struct GetSuspectedCheatersQuery {
2225 status: SuspectedCheaterStatus,
2226}
2227
2228#[utoipa::path(
2232 get,
2233 path = "/{course_id}/suspected-cheaters",
2234 operation_id = "getCourseSuspectedCheaters",
2235 tag = "courses",
2236 params(
2237 ("course_id" = Uuid, Path, description = "Course id"),
2238 ("status" = SuspectedCheaterStatus, Query, description = "Which review state of suspected cheaters to fetch")
2239 ),
2240 responses(
2241 (status = 200, description = "Suspected cheaters for course", body = [SuspectedCheaters])
2242 )
2243)]
2244#[instrument(skip(pool))]
2245async fn get_all_suspected_cheaters(
2246 user: AuthUser,
2247 params: web::Path<Uuid>,
2248 query: web::Query<GetSuspectedCheatersQuery>,
2249 pool: web::Data<PgPool>,
2250) -> ControllerResult<web::Json<Vec<SuspectedCheaters>>> {
2251 let course_id = params.into_inner();
2252
2253 let mut conn = pool.acquire().await?;
2254 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2255
2256 let course_cheaters = models::suspected_cheaters::get_all_suspected_cheaters_in_course(
2257 &mut conn,
2258 course_id,
2259 query.status,
2260 )
2261 .await?;
2262
2263 token.authorized_ok(web::Json(course_cheaters))
2264}
2265
2266#[utoipa::path(
2270 get,
2271 path = "/{course_id}/suspected-cheaters/flagged-count",
2272 operation_id = "getCourseFlaggedSuspectedCheatersCount",
2273 tag = "courses",
2274 params(
2275 ("course_id" = Uuid, Path, description = "Course id")
2276 ),
2277 responses(
2278 (status = 200, description = "Number of suspected cheaters awaiting review", body = i64, content_type = "application/json")
2279 )
2280)]
2281#[instrument(skip(pool))]
2282async fn get_flagged_suspected_cheaters_count(
2283 user: AuthUser,
2284 params: web::Path<Uuid>,
2285 pool: web::Data<PgPool>,
2286) -> ControllerResult<web::Json<i64>> {
2287 let course_id = params.into_inner();
2288
2289 let mut conn = pool.acquire().await?;
2290 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2291
2292 let count = models::suspected_cheaters::get_count_in_course_by_status(
2293 &mut conn,
2294 course_id,
2295 SuspectedCheaterStatus::Flagged,
2296 )
2297 .await?;
2298
2299 token.authorized_ok(web::Json(count))
2300}
2301
2302#[utoipa::path(
2306 get,
2307 path = "/{course_id}/thresholds",
2308 operation_id = "getCourseThresholds",
2309 tag = "courses",
2310 params(
2311 ("course_id" = Uuid, Path, description = "Course id")
2312 ),
2313 responses(
2314 (status = 200, description = "Course thresholds", body = Vec<CourseModuleThresholdInfo>)
2315 )
2316)]
2317#[instrument(skip(pool))]
2318async fn get_all_thresholds(
2319 user: AuthUser,
2320 params: web::Path<Uuid>,
2321 pool: web::Data<PgPool>,
2322) -> ControllerResult<web::Json<Vec<CourseModuleThresholdInfo>>> {
2323 let mut conn = pool.acquire().await?;
2324 let course_id = params.into_inner();
2325
2326 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2327
2328 let thresholds =
2329 models::suspected_cheaters::get_threshold_info_for_course(&mut conn, course_id).await?;
2330
2331 token.authorized_ok(web::Json(thresholds))
2332}
2333
2334#[utoipa::path(
2338 post,
2339 path = "/{course_id}/suspected-cheaters/dismiss/{user_id}",
2340 operation_id = "dismissCourseSuspectedCheater",
2341 tag = "courses",
2342 params(
2343 ("course_id" = Uuid, Path, description = "Course id"),
2344 ("user_id" = Uuid, Path, description = "Suspected cheater's user id")
2345 ),
2346 responses(
2347 (status = 200, description = "Suspicion dismissed")
2348 )
2349)]
2350#[instrument(skip(pool))]
2351async fn teacher_dismiss_suspected_cheater(
2352 user: AuthUser,
2353 path: web::Path<(Uuid, Uuid)>,
2354 pool: web::Data<PgPool>,
2355) -> ControllerResult<web::Json<()>> {
2356 let (course_id, user_id) = path.into_inner();
2357
2358 let mut conn = pool.acquire().await?;
2359 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2360
2361 models::suspected_cheaters::dismiss_by_user_id_and_course_id(&mut conn, user_id, course_id)
2362 .await?;
2363
2364 token.authorized_ok(web::Json(()))
2365}
2366
2367#[utoipa::path(
2371 post,
2372 path = "/{course_id}/suspected-cheaters/confirm/{user_id}",
2373 operation_id = "confirmCourseSuspectedCheater",
2374 tag = "courses",
2375 params(
2376 ("course_id" = Uuid, Path, description = "Course id"),
2377 ("user_id" = Uuid, Path, description = "Suspected cheater's user id")
2378 ),
2379 responses(
2380 (status = 200, description = "Cheating confirmed")
2381 )
2382)]
2383#[instrument(skip(pool))]
2384async fn teacher_confirm_suspected_cheater(
2385 user: AuthUser,
2386 path: web::Path<(Uuid, Uuid)>,
2387 pool: web::Data<PgPool>,
2388) -> ControllerResult<web::Json<()>> {
2389 let (course_id, user_id) = path.into_inner();
2390
2391 let mut conn = pool.acquire().await?;
2392 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2393
2394 models::suspected_cheaters::confirm_cheater_by_user_id_and_course_id(
2397 &mut conn, user_id, course_id,
2398 )
2399 .await?;
2400
2401 token.authorized_ok(web::Json(()))
2402}
2403
2404#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
2405pub struct JoinCourseWithJoinCodePayload {
2406 join_code: String,
2407}
2408
2409#[utoipa::path(
2413 post,
2414 path = "/{course_id}/join-course-with-join-code",
2415 operation_id = "joinCourseWithJoinCode",
2416 tag = "courses",
2417 params(
2418 ("course_id" = Uuid, Path, description = "Course id")
2419 ),
2420 request_body = JoinCourseWithJoinCodePayload,
2421 responses(
2422 (status = 200, description = "Joined course id", body = Uuid)
2423 )
2424)]
2425#[instrument(skip(pool))]
2426async fn add_user_to_course_with_join_code(
2427 course_id: web::Path<Uuid>,
2428 payload: web::Json<JoinCourseWithJoinCodePayload>,
2429 user: AuthUser,
2430 pool: web::Data<PgPool>,
2431) -> ControllerResult<web::Json<Uuid>> {
2432 let mut conn = pool.acquire().await?;
2433 let token = skip_authorize();
2434
2435 models::courses::get_by_id_and_join_code(&mut conn, *course_id, &payload.join_code).await?;
2436 let joined =
2437 models::join_code_uses::insert(&mut conn, PKeyPolicy::Generate, user.id, *course_id)
2438 .await?;
2439 token.authorized_ok(web::Json(joined))
2440}
2441
2442#[utoipa::path(
2446 post,
2447 path = "/{course_id}/set-join-code",
2448 operation_id = "setCourseJoinCode",
2449 tag = "courses",
2450 params(
2451 ("course_id" = Uuid, Path, description = "Course id")
2452 ),
2453 responses(
2454 (status = 200, description = "Course join code set")
2455 )
2456)]
2457#[instrument(skip(pool))]
2458async fn set_join_code_for_course(
2459 id: web::Path<Uuid>,
2460 pool: web::Data<PgPool>,
2461 user: AuthUser,
2462) -> ControllerResult<HttpResponse> {
2463 let mut conn = pool.acquire().await?;
2464 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*id)).await?;
2465
2466 const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
2467 abcdefghjkmnpqrstuvwxyz";
2468 const PASSWORD_LEN: usize = 64;
2469 let mut rng = rand::rng();
2470
2471 let code: String = (0..PASSWORD_LEN)
2472 .map(|_| {
2473 let idx = rng.random_range(0..CHARSET.len());
2474 CHARSET[idx] as char
2475 })
2476 .collect();
2477
2478 models::courses::set_join_code_for_course(&mut conn, *id, code).await?;
2479 token.authorized_ok(HttpResponse::Ok().finish())
2480}
2481
2482#[utoipa::path(
2486 get,
2487 path = "/join/{join_code}",
2488 operation_id = "getCourseByJoinCode",
2489 tag = "courses",
2490 params(
2491 ("join_code" = String, Path, description = "Course join code")
2492 ),
2493 responses(
2494 (status = 200, description = "Course for join code", body = Course)
2495 )
2496)]
2497#[instrument(skip(pool))]
2498async fn get_course_with_join_code(
2499 join_code: web::Path<String>,
2500 user: AuthUser,
2501 pool: web::Data<PgPool>,
2502) -> ControllerResult<web::Json<Course>> {
2503 let mut conn = pool.acquire().await?;
2504 let token = skip_authorize();
2505 let course =
2506 models::courses::get_course_with_join_code(&mut conn, join_code.to_string()).await?;
2507
2508 token.authorized_ok(web::Json(course))
2509}
2510
2511#[utoipa::path(
2515 post,
2516 path = "/{course_id}/partners-block",
2517 operation_id = "upsertCoursePartnersBlock",
2518 tag = "courses",
2519 params(
2520 ("course_id" = Uuid, Path, description = "Course id")
2521 ),
2522 request_body = Option<serde_json::Value>,
2523 responses(
2524 (status = 200, description = "Partners block", body = serde_json::Value)
2525 )
2526)]
2527#[instrument(skip(payload, pool))]
2528async fn post_partners_block(
2529 path: web::Path<Uuid>,
2530 payload: web::Json<Option<serde_json::Value>>,
2531 pool: web::Data<PgPool>,
2532 user: AuthUser,
2533) -> ControllerResult<web::Json<PartnersBlock>> {
2534 let course_id = path.into_inner();
2535
2536 let content = payload.into_inner();
2537 let mut conn = pool.acquire().await?;
2538 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2539
2540 let upserted_partner_block =
2541 models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
2542
2543 token.authorized_ok(web::Json(upserted_partner_block))
2544}
2545
2546#[utoipa::path(
2550 get,
2551 path = "/{course_id}/partners-block",
2552 operation_id = "getCoursePartnersBlock",
2553 tag = "courses",
2554 params(
2555 ("course_id" = Uuid, Path, description = "Course id")
2556 ),
2557 responses(
2558 (status = 200, description = "Partners block", body = serde_json::Value)
2559 )
2560)]
2561#[instrument(skip(pool))]
2562async fn get_partners_block(
2563 path: web::Path<Uuid>,
2564 user: AuthUser,
2565 pool: web::Data<PgPool>,
2566) -> ControllerResult<web::Json<PartnersBlock>> {
2567 let course_id = path.into_inner();
2568 let mut conn = pool.acquire().await?;
2569 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2570
2571 let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
2573
2574 let partner_block = if course_exists {
2575 models::partner_block::get_partner_block(&mut conn, course_id).await?
2577 } else {
2578 let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
2580
2581 models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
2583 };
2584
2585 token.authorized_ok(web::Json(partner_block))
2586}
2587
2588#[utoipa::path(
2592 delete,
2593 path = "/{course_id}/partners-block",
2594 operation_id = "deleteCoursePartnersBlock",
2595 tag = "courses",
2596 params(
2597 ("course_id" = Uuid, Path, description = "Course id")
2598 ),
2599 responses(
2600 (status = 200, description = "Deleted partners block", body = serde_json::Value)
2601 )
2602)]
2603#[instrument(skip(pool))]
2604async fn delete_partners_block(
2605 path: web::Path<Uuid>,
2606 pool: web::Data<PgPool>,
2607 user: AuthUser,
2608) -> ControllerResult<web::Json<PartnersBlock>> {
2609 let course_id = path.into_inner();
2610 let mut conn = pool.acquire().await?;
2611 let token = authorize(
2612 &mut conn,
2613 Act::UsuallyUnacceptableDeletion,
2614 Some(user.id),
2615 Res::Course(course_id),
2616 )
2617 .await?;
2618 let deleted_partners_block =
2619 models::partner_block::delete_partner_block(&mut conn, course_id).await?;
2620
2621 token.authorized_ok(web::Json(deleted_partners_block))
2622}
2623
2624pub fn _add_routes(cfg: &mut ServiceConfig) {
2632 cfg.service(web::scope("/{course_id}/stats").configure(stats::_add_routes))
2633 .service(web::scope("/{course_id}/chatbots").configure(chatbots::_add_routes))
2634 .service(web::scope("/{course_id}/students").configure(students::_add_routes))
2635 .route("/{course_id}", web::get().to(get_course))
2636 .route("", web::post().to(post_new_course))
2637 .route("/{course_id}", web::put().to(update_course))
2638 .route("/{course_id}", web::delete().to(delete_course))
2639 .route(
2640 "/{course_id}/status-for-all-exercises/{user_id}",
2641 web::get().to(get_all_exercise_statuses_by_course_id),
2642 )
2643 .route(
2644 "/{course_id}/course-module-completions/{user_id}",
2645 web::get().to(get_all_course_module_completions_for_user_by_course_id),
2646 )
2647 .route(
2648 "/{course_id}/daily-submission-counts",
2649 web::get().to(get_daily_submission_counts),
2650 )
2651 .route(
2652 "/{course_id}/daily-users-who-have-submitted-something",
2653 web::get().to(get_daily_user_counts_with_submissions),
2654 )
2655 .route("/{course_id}/exercises", web::get().to(get_all_exercises))
2656 .route(
2657 "/{course_id}/exercises-and-count-of-answers-requiring-attention",
2658 web::get().to(get_all_exercises_and_count_of_answers_requiring_attention),
2659 )
2660 .route(
2661 "/{course_id}/structure",
2662 web::get().to(get_course_structure),
2663 )
2664 .route(
2665 "/{course_id}/language-versions",
2666 web::get().to(get_all_course_language_versions),
2667 )
2668 .route(
2669 "/{course_id}/create-copy",
2670 web::post().to(create_course_copy),
2671 )
2672 .route("/{course_id}/upload", web::post().to(add_media_for_course))
2673 .route(
2674 "/{course_id}/weekday-hour-submission-counts",
2675 web::get().to(get_weekday_hour_submission_counts),
2676 )
2677 .route(
2678 "/{course_id}/submission-counts-by-exercise",
2679 web::get().to(get_submission_counts_by_exercise),
2680 )
2681 .route(
2682 "/{course_id}/course-instances",
2683 web::get().to(get_course_instances),
2684 )
2685 .route("/{course_id}/feedback", web::get().to(get_feedback))
2686 .route(
2687 "/{course_id}/feedback-count",
2688 web::get().to(get_feedback_count),
2689 )
2690 .route(
2691 "/{course_id}/new-course-instance",
2692 web::post().to(new_course_instance),
2693 )
2694 .route("/{course_id}/glossary", web::get().to(glossary))
2695 .route("/{course_id}/glossary", web::post().to(new_glossary_term))
2696 .route(
2697 "/{course_id}/course-users-counts-by-exercise",
2698 web::get().to(get_course_users_counts_by_exercise),
2699 )
2700 .route(
2701 "/{course_id}/new-page-ordering",
2702 web::post().to(post_new_page_ordering),
2703 )
2704 .route(
2705 "/{course_id}/new-chapter-ordering",
2706 web::post().to(post_new_chapter_ordering),
2707 )
2708 .route(
2709 "/{course_id}/references",
2710 web::get().to(get_material_references_by_course_id),
2711 )
2712 .route(
2713 "/{course_id}/references",
2714 web::post().to(insert_material_references),
2715 )
2716 .route(
2717 "/{course_id}/references/{reference_id}",
2718 web::post().to(update_material_reference),
2719 )
2720 .route(
2721 "/{course_id}/references/{reference_id}",
2722 web::delete().to(delete_material_reference_by_id),
2723 )
2724 .route(
2725 "/{course_id}/course-modules",
2726 web::post().to(update_modules),
2727 )
2728 .route(
2729 "/{course_id}/default-peer-review",
2730 web::get().to(get_course_default_peer_review),
2731 )
2732 .route(
2733 "/{course_id}/update-peer-review-queue-reviews-received",
2734 web::post().to(post_update_peer_review_queue_reviews_received),
2735 )
2736 .route(
2737 "/{course_id}/breadcrumb-info",
2738 web::get().to(get_course_breadcrumb_info),
2739 )
2740 .route(
2741 "/{course_id}/progress/{user_id}",
2742 web::get().to(get_user_progress_for_course),
2743 )
2744 .route(
2745 "/{course_id}/user-settings/{user_id}",
2746 web::get().to(get_user_course_settings),
2747 )
2748 .route(
2749 "/{course_id}/export-submissions",
2750 web::get().to(submission_export),
2751 )
2752 .route(
2753 "/{course_id}/export-user-details",
2754 web::get().to(user_details_export),
2755 )
2756 .route(
2757 "/{course_id}/export-exercise-tasks",
2758 web::get().to(exercise_tasks_export),
2759 )
2760 .route(
2761 "/{course_id}/export-course-instances",
2762 web::get().to(course_instances_export),
2763 )
2764 .route(
2765 "/{course_id}/export-course-user-consents",
2766 web::get().to(course_consent_form_answers_export),
2767 )
2768 .route(
2769 "/{course_id}/export-user-exercise-states",
2770 web::get().to(user_exercise_states_export),
2771 )
2772 .route(
2773 "/{course_id}/page-visit-datum-summary",
2774 web::get().to(get_page_visit_datum_summary),
2775 )
2776 .route(
2777 "/{course_id}/page-visit-datum-summary-by-pages",
2778 web::get().to(get_page_visit_datum_summary_by_pages),
2779 )
2780 .route(
2781 "/{course_id}/page-visit-datum-summary-by-device-types",
2782 web::get().to(get_page_visit_datum_summary_by_device_types),
2783 )
2784 .route(
2785 "/{course_id}/page-visit-datum-summary-by-countries",
2786 web::get().to(get_page_visit_datum_summary_by_countries),
2787 )
2788 .route(
2789 "/{course_id}/teacher-reset-course-progress-for-themselves",
2790 web::delete().to(teacher_reset_course_progress_for_themselves),
2791 )
2792 .route("/{course_id}/thresholds", web::get().to(get_all_thresholds))
2793 .route(
2794 "/{course_id}/suspected-cheaters",
2795 web::get().to(get_all_suspected_cheaters),
2796 )
2797 .route(
2798 "/{course_id}/suspected-cheaters/flagged-count",
2799 web::get().to(get_flagged_suspected_cheaters_count),
2800 )
2801 .route(
2802 "/{course_id}/suspected-cheaters/dismiss/{user_id}",
2803 web::post().to(teacher_dismiss_suspected_cheater),
2804 )
2805 .route(
2806 "/{course_id}/suspected-cheaters/confirm/{user_id}",
2807 web::post().to(teacher_confirm_suspected_cheater),
2808 )
2809 .route(
2810 "/{course_id}/teacher-reset-course-progress-for-everyone",
2811 web::delete().to(teacher_reset_course_progress_for_everyone),
2812 )
2813 .route(
2814 "/{course_id}/join-course-with-join-code",
2815 web::post().to(add_user_to_course_with_join_code),
2816 )
2817 .route(
2818 "/{course_id}/partners-block",
2819 web::post().to(post_partners_block),
2820 )
2821 .route(
2822 "/{course_id}/partners-block",
2823 web::get().to(get_partners_block),
2824 )
2825 .route(
2826 "/{course_id}/partners-block",
2827 web::delete().to(delete_partners_block),
2828 )
2829 .route(
2830 "/{course_id}/set-join-code",
2831 web::post().to(set_join_code_for_course),
2832 )
2833 .route(
2834 "/{course_id}/reprocess-completions",
2835 web::post().to(post_reprocess_module_completions),
2836 )
2837 .route(
2838 "/join/{join_code}",
2839 web::get().to(get_course_with_join_code),
2840 );
2841}