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