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 mut tx = conn.begin().await?;
877
878 let new_course = payload.new_course.clone();
879
880 let copied_course = match &payload.mode {
881 CopyCourseMode::Duplicate => {
882 models::library::copying::copy_course(&mut tx, *course_id, &new_course, false, user.id)
883 .await?
884 }
885 CopyCourseMode::SameLanguageGroup => {
886 models::library::copying::copy_course(&mut tx, *course_id, &new_course, true, user.id)
887 .await?
888 }
889 CopyCourseMode::ExistingLanguageGroup { target_course_id } => {
890 let target_course = models::courses::get_course(&mut tx, *target_course_id).await?;
891 authorize(
893 &mut tx,
894 Act::Duplicate,
895 Some(user.id),
896 Res::Course(*target_course_id),
897 )
898 .await?;
899 models::library::copying::copy_course_with_language_group(
900 &mut tx,
901 *course_id,
902 target_course.course_language_group_id,
903 &new_course,
904 user.id,
905 )
906 .await?
907 }
908 CopyCourseMode::NewLanguageGroup => {
909 let new_clg_id = course_language_groups::insert(
910 &mut tx,
911 PKeyPolicy::Generate,
912 new_course.slug.as_str(),
913 )
914 .await?;
915 models::library::copying::copy_course_with_language_group(
916 &mut tx,
917 *course_id,
918 new_clg_id,
919 &new_course,
920 user.id,
921 )
922 .await?
923 }
924 };
925
926 models::roles::insert(
927 &mut tx,
928 user.id,
929 models::roles::UserRole::Teacher,
930 models::roles::RoleDomain::Course(copied_course.id),
931 )
932 .await?;
933
934 tx.commit().await?;
935
936 token.authorized_ok(web::Json(copied_course))
937}
938
939#[utoipa::path(
943 get,
944 path = "/{course_id}/daily-submission-counts",
945 operation_id = "getCourseDailySubmissionCounts",
946 tag = "courses",
947 params(
948 ("course_id" = Uuid, Path, description = "Course id")
949 ),
950 responses(
951 (status = 200, description = "Course daily submission counts", body = [ExerciseSlideSubmissionCount])
952 )
953)]
954#[instrument(skip(pool))]
955async fn get_daily_submission_counts(
956 pool: web::Data<PgPool>,
957 course_id: web::Path<Uuid>,
958 user: AuthUser,
959) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
960 let mut conn = pool.acquire().await?;
961 let token = authorize(
962 &mut conn,
963 Act::ViewStats,
964 Some(user.id),
965 Res::Course(*course_id),
966 )
967 .await?;
968 let course = models::courses::get_course(&mut conn, *course_id).await?;
969 let res =
970 exercise_slide_submissions::get_course_daily_slide_submission_counts(&mut conn, &course)
971 .await?;
972
973 token.authorized_ok(web::Json(res))
974}
975
976#[utoipa::path(
980 get,
981 path = "/{course_id}/daily-users-who-have-submitted-something",
982 operation_id = "getCourseDailyUsersWhoSubmittedSomething",
983 tag = "courses",
984 params(
985 ("course_id" = Uuid, Path, description = "Course id")
986 ),
987 responses(
988 (status = 200, description = "Course daily user submission counts", body = [ExerciseSlideSubmissionCount])
989 )
990)]
991#[instrument(skip(pool))]
992async fn get_daily_user_counts_with_submissions(
993 pool: web::Data<PgPool>,
994 course_id: web::Path<Uuid>,
995 user: AuthUser,
996) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
997 let mut conn = pool.acquire().await?;
998 let token = authorize(
999 &mut conn,
1000 Act::ViewStats,
1001 Some(user.id),
1002 Res::Course(*course_id),
1003 )
1004 .await?;
1005 let course = models::courses::get_course(&mut conn, *course_id).await?;
1006 let res = exercise_slide_submissions::get_course_daily_user_counts_with_submissions(
1007 &mut conn, &course,
1008 )
1009 .await?;
1010
1011 token.authorized_ok(web::Json(res))
1012}
1013
1014#[utoipa::path(
1018 get,
1019 path = "/{course_id}/weekday-hour-submission-counts",
1020 operation_id = "getCourseWeekdayHourSubmissionCounts",
1021 tag = "courses",
1022 params(
1023 ("course_id" = Uuid, Path, description = "Course id")
1024 ),
1025 responses(
1026 (status = 200, description = "Course weekday and hour submission counts", body = [ExerciseSlideSubmissionCountByWeekAndHour])
1027 )
1028)]
1029#[instrument(skip(pool))]
1030async fn get_weekday_hour_submission_counts(
1031 pool: web::Data<PgPool>,
1032 course_id: web::Path<Uuid>,
1033 user: AuthUser,
1034) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByWeekAndHour>>> {
1035 let mut conn = pool.acquire().await?;
1036 let token = authorize(
1037 &mut conn,
1038 Act::ViewStats,
1039 Some(user.id),
1040 Res::Course(*course_id),
1041 )
1042 .await?;
1043 let course = models::courses::get_course(&mut conn, *course_id).await?;
1044 let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_weekday_and_hour(
1045 &mut conn, &course,
1046 )
1047 .await?;
1048
1049 token.authorized_ok(web::Json(res))
1050}
1051
1052#[utoipa::path(
1056 get,
1057 path = "/{course_id}/submission-counts-by-exercise",
1058 operation_id = "getCourseSubmissionCountsByExercise",
1059 tag = "courses",
1060 params(
1061 ("course_id" = Uuid, Path, description = "Course id")
1062 ),
1063 responses(
1064 (status = 200, description = "Course submission counts by exercise", body = [ExerciseSlideSubmissionCountByExercise])
1065 )
1066)]
1067#[instrument(skip(pool))]
1068async fn get_submission_counts_by_exercise(
1069 pool: web::Data<PgPool>,
1070 course_id: web::Path<Uuid>,
1071 user: AuthUser,
1072) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByExercise>>> {
1073 let mut conn = pool.acquire().await?;
1074 let token = authorize(
1075 &mut conn,
1076 Act::ViewStats,
1077 Some(user.id),
1078 Res::Course(*course_id),
1079 )
1080 .await?;
1081 let course = models::courses::get_course(&mut conn, *course_id).await?;
1082 let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_exercise(
1083 &mut conn, &course,
1084 )
1085 .await?;
1086
1087 token.authorized_ok(web::Json(res))
1088}
1089
1090#[utoipa::path(
1094 get,
1095 path = "/{course_id}/course-instances",
1096 operation_id = "getCourseInstances",
1097 tag = "courses",
1098 params(
1099 ("course_id" = Uuid, Path, description = "Course id")
1100 ),
1101 responses(
1102 (status = 200, description = "Course instances", body = [CourseInstance])
1103 )
1104)]
1105#[instrument(skip(pool))]
1106async fn get_course_instances(
1107 pool: web::Data<PgPool>,
1108 course_id: web::Path<Uuid>,
1109 user: AuthUser,
1110) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
1111 let mut conn = pool.acquire().await?;
1112 let token = authorize(
1113 &mut conn,
1114 Act::Teach,
1115 Some(user.id),
1116 Res::Course(*course_id),
1117 )
1118 .await?;
1119 let course_instances =
1120 models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
1121
1122 token.authorized_ok(web::Json(course_instances))
1123}
1124
1125#[derive(Debug, Deserialize)]
1126
1127pub struct GetFeedbackQuery {
1128 read: bool,
1129 #[serde(flatten)]
1130 pagination: Pagination,
1131}
1132
1133#[utoipa::path(
1137 get,
1138 path = "/{course_id}/feedback",
1139 operation_id = "getCourseFeedback",
1140 tag = "courses",
1141 params(
1142 ("course_id" = String, Path, description = "Course id"),
1143 ("read" = bool, Query, description = "Whether to fetch read feedback"),
1144 ("page" = Option<i64>, Query, description = "Page number"),
1145 ("limit" = Option<i64>, Query, description = "Page size")
1146 ),
1147 responses(
1148 (status = 200, description = "Feedback for the course", body = [Feedback])
1149 )
1150)]
1151#[instrument(skip(pool))]
1152pub async fn get_feedback(
1153 course_id: web::Path<Uuid>,
1154 pool: web::Data<PgPool>,
1155 read: web::Query<GetFeedbackQuery>,
1156 user: AuthUser,
1157) -> ControllerResult<web::Json<Vec<Feedback>>> {
1158 let mut conn = pool.acquire().await?;
1159 let token = authorize(
1160 &mut conn,
1161 Act::Teach,
1162 Some(user.id),
1163 Res::Course(*course_id),
1164 )
1165 .await?;
1166 let feedback =
1167 feedback::get_feedback_for_course(&mut conn, *course_id, read.read, read.pagination)
1168 .await?;
1169
1170 token.authorized_ok(web::Json(feedback))
1171}
1172
1173#[utoipa::path(
1177 get,
1178 path = "/{course_id}/feedback-count",
1179 operation_id = "getCourseFeedbackCount",
1180 tag = "courses",
1181 params(
1182 ("course_id" = Uuid, Path, description = "Course id")
1183 ),
1184 responses(
1185 (status = 200, description = "Feedback counts for the course", body = FeedbackCount)
1186 )
1187)]
1188#[instrument(skip(pool))]
1189pub async fn get_feedback_count(
1190 course_id: web::Path<Uuid>,
1191 pool: web::Data<PgPool>,
1192 user: AuthUser,
1193) -> ControllerResult<web::Json<FeedbackCount>> {
1194 let mut conn = pool.acquire().await?;
1195 let token = authorize(
1196 &mut conn,
1197 Act::Teach,
1198 Some(user.id),
1199 Res::Course(*course_id),
1200 )
1201 .await?;
1202
1203 let feedback_count = feedback::get_feedback_count_for_course(&mut conn, *course_id).await?;
1204
1205 token.authorized_ok(web::Json(feedback_count))
1206}
1207
1208#[utoipa::path(
1212 post,
1213 path = "/{course_id}/new-course-instance",
1214 operation_id = "createCourseInstance",
1215 tag = "courses",
1216 params(
1217 ("course_id" = Uuid, Path, description = "Course id")
1218 ),
1219 request_body = CourseInstanceForm,
1220 responses(
1221 (status = 200, description = "Created course instance id", body = Uuid)
1222 )
1223)]
1224#[instrument(skip(pool))]
1225async fn new_course_instance(
1226 form: web::Json<CourseInstanceForm>,
1227 course_id: web::Path<Uuid>,
1228 pool: web::Data<PgPool>,
1229 user: AuthUser,
1230) -> ControllerResult<web::Json<Uuid>> {
1231 let mut conn = pool.acquire().await?;
1232 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1233 let form = form.into_inner();
1234 let new = NewCourseInstance {
1235 course_id: *course_id,
1236 name: form.name.as_deref(),
1237 description: form.description.as_deref(),
1238 support_email: form.support_email.as_deref(),
1239 teacher_in_charge_name: &form.teacher_in_charge_name,
1240 teacher_in_charge_email: &form.teacher_in_charge_email,
1241 opening_time: form.opening_time,
1242 closing_time: form.closing_time,
1243 };
1244 let ci = models::course_instances::insert(&mut conn, PKeyPolicy::Generate, new).await?;
1245
1246 token.authorized_ok(web::Json(ci.id))
1247}
1248
1249#[instrument(skip(pool))]
1250#[utoipa::path(
1251 get,
1252 path = "/{course_id}/glossary",
1253 operation_id = "getCourseGlossary",
1254 tag = "glossary",
1255 params(
1256 ("course_id" = Uuid, Path, description = "Course id")
1257 ),
1258 responses(
1259 (status = 200, description = "Glossary terms for the course", body = [Term]),
1260 (status = 401, description = "Authentication required"),
1261 (status = 403, description = "User is not allowed to manage the course glossary")
1262 )
1263)]
1264pub(crate) async fn glossary(
1265 pool: web::Data<PgPool>,
1266 course_id: web::Path<Uuid>,
1267 user: AuthUser,
1268) -> ControllerResult<web::Json<Vec<Term>>> {
1269 let mut conn = pool.acquire().await?;
1270 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1271 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
1272
1273 token.authorized_ok(web::Json(glossary))
1274}
1275
1276#[instrument(skip(pool))]
1279async fn _new_term(
1280 pool: web::Data<PgPool>,
1281 course_id: web::Path<Uuid>,
1282 user: AuthUser,
1283) -> ControllerResult<web::Json<Vec<Term>>> {
1284 let mut conn = pool.acquire().await?;
1285 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1286 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
1287
1288 token.authorized_ok(web::Json(glossary))
1289}
1290
1291#[instrument(skip(pool))]
1292#[utoipa::path(
1293 post,
1294 path = "/{course_id}/glossary",
1295 operation_id = "createCourseGlossaryTerm",
1296 tag = "glossary",
1297 params(
1298 ("course_id" = Uuid, Path, description = "Course id")
1299 ),
1300 request_body = TermUpdate,
1301 responses(
1302 (status = 200, description = "Created glossary term id", body = Uuid),
1303 (status = 401, description = "Authentication required"),
1304 (status = 403, description = "User is not allowed to manage the course glossary")
1305 )
1306)]
1307pub(crate) async fn new_glossary_term(
1308 pool: web::Data<PgPool>,
1309 course_id: web::Path<Uuid>,
1310 new_term: web::Json<TermUpdate>,
1311 user: AuthUser,
1312) -> ControllerResult<web::Json<Uuid>> {
1313 let mut conn = pool.acquire().await?;
1314 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1315 let TermUpdate { term, definition } = new_term.into_inner();
1316 let term = models::glossary::insert(&mut conn, &term, &definition, *course_id).await?;
1317
1318 token.authorized_ok(web::Json(term))
1319}
1320
1321#[utoipa::path(
1325 get,
1326 path = "/{course_id}/course-users-counts-by-exercise",
1327 operation_id = "getCourseUsersCountsByExercise",
1328 tag = "courses",
1329 params(
1330 ("course_id" = Uuid, Path, description = "Course id")
1331 ),
1332 responses(
1333 (status = 200, description = "Course users counts by exercise", body = [ExerciseUserCounts])
1334 )
1335)]
1336#[instrument(skip(pool))]
1337pub async fn get_course_users_counts_by_exercise(
1338 course_id: web::Path<Uuid>,
1339 pool: web::Data<PgPool>,
1340 user: AuthUser,
1341) -> ControllerResult<web::Json<Vec<ExerciseUserCounts>>> {
1342 let mut conn = pool.acquire().await?;
1343 let course_id = course_id.into_inner();
1344 let token = authorize(
1345 &mut conn,
1346 Act::ViewStats,
1347 Some(user.id),
1348 Res::Course(course_id),
1349 )
1350 .await?;
1351
1352 let res =
1353 models::user_exercise_states::get_course_users_counts_by_exercise(&mut conn, course_id)
1354 .await?;
1355
1356 token.authorized_ok(web::Json(res))
1357}
1358
1359#[utoipa::path(
1367 post,
1368 path = "/{course_id}/new-page-ordering",
1369 operation_id = "updateCoursePageOrdering",
1370 tag = "courses",
1371 params(
1372 ("course_id" = Uuid, Path, description = "Course id")
1373 ),
1374 request_body = Vec<Page>,
1375 responses(
1376 (status = 200, description = "Course page ordering updated")
1377 )
1378)]
1379#[instrument(skip(pool))]
1380pub async fn post_new_page_ordering(
1381 course_id: web::Path<Uuid>,
1382 pool: web::Data<PgPool>,
1383 user: AuthUser,
1384 payload: web::Json<Vec<Page>>,
1385) -> ControllerResult<web::Json<()>> {
1386 let mut conn = pool.acquire().await?;
1387 let course_id = course_id.into_inner();
1388 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1389
1390 models::pages::reorder_pages(&mut conn, &payload, course_id).await?;
1391
1392 token.authorized_ok(web::Json(()))
1393}
1394
1395#[utoipa::path(
1401 post,
1402 path = "/{course_id}/new-chapter-ordering",
1403 operation_id = "updateCourseChapterOrdering",
1404 tag = "courses",
1405 params(
1406 ("course_id" = Uuid, Path, description = "Course id")
1407 ),
1408 request_body = Vec<Chapter>,
1409 responses(
1410 (status = 200, description = "Course chapter ordering updated")
1411 )
1412)]
1413#[instrument(skip(pool))]
1414pub async fn post_new_chapter_ordering(
1415 course_id: web::Path<Uuid>,
1416 pool: web::Data<PgPool>,
1417 user: AuthUser,
1418 payload: web::Json<Vec<Chapter>>,
1419) -> ControllerResult<web::Json<()>> {
1420 let mut conn = pool.acquire().await?;
1421 let course_id = course_id.into_inner();
1422 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1423
1424 models::pages::reorder_chapters(&mut conn, &payload, course_id).await?;
1425
1426 token.authorized_ok(web::Json(()))
1427}
1428
1429#[utoipa::path(
1430 get,
1431 path = "/{course_id}/references",
1432 operation_id = "getCourseReferences",
1433 tag = "courses",
1434 params(
1435 ("course_id" = Uuid, Path, description = "Course id")
1436 ),
1437 responses(
1438 (status = 200, description = "Course references", body = [MaterialReference])
1439 )
1440)]
1441#[instrument(skip(pool))]
1442async fn get_material_references_by_course_id(
1443 course_id: web::Path<Uuid>,
1444 pool: web::Data<PgPool>,
1445 user: AuthUser,
1446) -> ControllerResult<web::Json<Vec<MaterialReference>>> {
1447 let mut conn = pool.acquire().await?;
1448 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1449
1450 let res =
1451 models::material_references::get_references_by_course_id(&mut conn, *course_id).await?;
1452 token.authorized_ok(web::Json(res))
1453}
1454
1455#[utoipa::path(
1456 post,
1457 path = "/{course_id}/references",
1458 operation_id = "createCourseReferences",
1459 tag = "courses",
1460 params(
1461 ("course_id" = Uuid, Path, description = "Course id")
1462 ),
1463 request_body = [NewMaterialReference],
1464 responses(
1465 (status = 200, description = "Course references created")
1466 )
1467)]
1468#[instrument(skip(pool))]
1469async fn insert_material_references(
1470 course_id: web::Path<Uuid>,
1471 payload: web::Json<Vec<NewMaterialReference>>,
1472 pool: web::Data<PgPool>,
1473 user: AuthUser,
1474) -> ControllerResult<web::Json<()>> {
1475 let mut conn = pool.acquire().await?;
1476 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1477
1478 models::material_references::insert_reference(&mut conn, *course_id, payload.0).await?;
1479
1480 token.authorized_ok(web::Json(()))
1481}
1482
1483#[utoipa::path(
1484 post,
1485 path = "/{course_id}/references/{reference_id}",
1486 operation_id = "updateCourseReference",
1487 tag = "courses",
1488 params(
1489 ("course_id" = Uuid, Path, description = "Course id"),
1490 ("reference_id" = Uuid, Path, description = "Reference id")
1491 ),
1492 request_body = NewMaterialReference,
1493 responses(
1494 (status = 200, description = "Course reference updated")
1495 )
1496)]
1497#[instrument(skip(pool))]
1498async fn update_material_reference(
1499 path: web::Path<(Uuid, Uuid)>,
1500 pool: web::Data<PgPool>,
1501 user: AuthUser,
1502 payload: web::Json<NewMaterialReference>,
1503) -> ControllerResult<web::Json<()>> {
1504 let (course_id, reference_id) = path.into_inner();
1505 let mut conn = pool.acquire().await?;
1506 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1507
1508 models::material_references::update_material_reference_by_id(
1509 &mut conn,
1510 reference_id,
1511 payload.0,
1512 )
1513 .await?;
1514 token.authorized_ok(web::Json(()))
1515}
1516
1517#[utoipa::path(
1518 delete,
1519 path = "/{course_id}/references/{reference_id}",
1520 operation_id = "deleteCourseReference",
1521 tag = "courses",
1522 params(
1523 ("course_id" = Uuid, Path, description = "Course id"),
1524 ("reference_id" = Uuid, Path, description = "Reference id")
1525 ),
1526 responses(
1527 (status = 200, description = "Course reference deleted")
1528 )
1529)]
1530#[instrument(skip(pool))]
1531async fn delete_material_reference_by_id(
1532 path: web::Path<(Uuid, Uuid)>,
1533 pool: web::Data<PgPool>,
1534 user: AuthUser,
1535) -> ControllerResult<web::Json<()>> {
1536 let (course_id, reference_id) = path.into_inner();
1537 let mut conn = pool.acquire().await?;
1538 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1539
1540 models::material_references::delete_reference(&mut conn, reference_id).await?;
1541 token.authorized_ok(web::Json(()))
1542}
1543
1544#[utoipa::path(
1545 post,
1546 path = "/{course_id}/course-modules",
1547 operation_id = "updateCourseModules",
1548 tag = "courses",
1549 params(
1550 ("course_id" = Uuid, Path, description = "Course id")
1551 ),
1552 request_body = ModuleUpdates,
1553 responses(
1554 (status = 200, description = "Course modules updated")
1555 )
1556)]
1557#[instrument(skip(pool))]
1558pub async fn update_modules(
1559 course_id: web::Path<Uuid>,
1560 pool: web::Data<PgPool>,
1561 user: AuthUser,
1562 payload: web::Json<ModuleUpdates>,
1563) -> ControllerResult<web::Json<()>> {
1564 let mut conn = pool.acquire().await?;
1565 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1566
1567 models::course_modules::update_modules(&mut conn, *course_id, payload.into_inner()).await?;
1568 token.authorized_ok(web::Json(()))
1569}
1570
1571#[utoipa::path(
1572 get,
1573 path = "/{course_id}/default-peer-review",
1574 operation_id = "getCourseDefaultPeerReview",
1575 tag = "courses",
1576 params(
1577 ("course_id" = Uuid, Path, description = "Course id")
1578 ),
1579 responses(
1580 (status = 200, description = "Default peer review configuration", body = serde_json::Value)
1581 )
1582)]
1583async fn get_course_default_peer_review(
1584 course_id: web::Path<Uuid>,
1585 pool: web::Data<PgPool>,
1586 user: AuthUser,
1587) -> ControllerResult<web::Json<(PeerOrSelfReviewConfig, Vec<PeerOrSelfReviewQuestion>)>> {
1588 let mut conn = pool.acquire().await?;
1589 let token = authorize(
1590 &mut conn,
1591 Act::Teach,
1592 Some(user.id),
1593 Res::Course(*course_id),
1594 )
1595 .await?;
1596
1597 let peer_review = models::peer_or_self_review_configs::get_default_for_course_by_course_id(
1598 &mut conn, *course_id,
1599 )
1600 .await?;
1601 let peer_or_self_review_questions =
1602 models::peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
1603 &mut conn,
1604 peer_review.id,
1605 )
1606 .await?;
1607 token.authorized_ok(web::Json((peer_review, peer_or_self_review_questions)))
1608}
1609
1610#[utoipa::path(
1616 post,
1617 path = "/{course_id}/update-peer-review-queue-reviews-received",
1618 operation_id = "updateCoursePeerReviewQueueReviewsReceived",
1619 tag = "courses",
1620 params(
1621 ("course_id" = Uuid, Path, description = "Course id")
1622 ),
1623 responses(
1624 (status = 200, description = "Peer review queue updated", body = bool)
1625 )
1626)]
1627#[instrument(skip(pool, user))]
1628async fn post_update_peer_review_queue_reviews_received(
1629 pool: web::Data<PgPool>,
1630 user: AuthUser,
1631 course_id: web::Path<Uuid>,
1632) -> ControllerResult<web::Json<bool>> {
1633 let mut conn = pool.acquire().await?;
1634 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
1635 models::library::peer_or_self_reviewing::update_peer_review_queue_reviews_received(
1636 &mut conn, *course_id,
1637 )
1638 .await?;
1639 token.authorized_ok(web::Json(true))
1640}
1641
1642#[utoipa::path(
1648 get,
1649 path = "/{course_id}/export-submissions",
1650 operation_id = "exportCourseSubmissionsCsv",
1651 tag = "courses",
1652 params(
1653 ("course_id" = Uuid, Path, description = "Course id")
1654 ),
1655 responses(
1656 (status = 200, description = "Course submissions CSV", body = String, content_type = "text/csv")
1657 )
1658)]
1659#[instrument(skip(pool))]
1660pub async fn submission_export(
1661 course_id: web::Path<Uuid>,
1662 pool: web::Data<PgPool>,
1663 user: AuthUser,
1664) -> ControllerResult<HttpResponse> {
1665 let mut conn = pool.acquire().await?;
1666
1667 let token = authorize(
1668 &mut conn,
1669 Act::Teach,
1670 Some(user.id),
1671 Res::Course(*course_id),
1672 )
1673 .await?;
1674
1675 let course = models::courses::get_course(&mut conn, *course_id).await?;
1676
1677 general_export(
1678 pool,
1679 &format!(
1680 "attachment; filename=\"Course: {} - Submissions (exercise tasks) {}.csv\"",
1681 course.name,
1682 Utc::now().format("%Y-%m-%d")
1683 ),
1684 CourseSubmissionExportOperation {
1685 course_id: *course_id,
1686 },
1687 token,
1688 )
1689 .await
1690}
1691
1692#[utoipa::path(
1698 get,
1699 path = "/{course_id}/export-user-details",
1700 operation_id = "exportCourseUserDetailsCsv",
1701 tag = "courses",
1702 params(
1703 ("course_id" = Uuid, Path, description = "Course id")
1704 ),
1705 responses(
1706 (status = 200, description = "Course user details CSV", body = String, content_type = "text/csv")
1707 )
1708)]
1709#[instrument(skip(pool))]
1710pub async fn user_details_export(
1711 course_id: web::Path<Uuid>,
1712 pool: web::Data<PgPool>,
1713 user: AuthUser,
1714) -> ControllerResult<HttpResponse> {
1715 let mut conn = pool.acquire().await?;
1716
1717 let token = authorize(
1718 &mut conn,
1719 Act::Teach,
1720 Some(user.id),
1721 Res::Course(*course_id),
1722 )
1723 .await?;
1724
1725 let course = models::courses::get_course(&mut conn, *course_id).await?;
1726
1727 general_export(
1728 pool,
1729 &format!(
1730 "attachment; filename=\"Course: {} - User Details {}.csv\"",
1731 course.name,
1732 Utc::now().format("%Y-%m-%d")
1733 ),
1734 UsersExportOperation {
1735 course_id: *course_id,
1736 },
1737 token,
1738 )
1739 .await
1740}
1741
1742#[utoipa::path(
1748 get,
1749 path = "/{course_id}/export-exercise-tasks",
1750 operation_id = "exportCourseExerciseTasksCsv",
1751 tag = "courses",
1752 params(
1753 ("course_id" = Uuid, Path, description = "Course id")
1754 ),
1755 responses(
1756 (status = 200, description = "Course exercise tasks CSV", body = String, content_type = "text/csv")
1757 )
1758)]
1759#[instrument(skip(pool))]
1760pub async fn exercise_tasks_export(
1761 course_id: web::Path<Uuid>,
1762 pool: web::Data<PgPool>,
1763 user: AuthUser,
1764) -> ControllerResult<HttpResponse> {
1765 let mut conn = pool.acquire().await?;
1766
1767 let token = authorize(
1768 &mut conn,
1769 Act::Teach,
1770 Some(user.id),
1771 Res::Course(*course_id),
1772 )
1773 .await?;
1774
1775 let course = models::courses::get_course(&mut conn, *course_id).await?;
1776
1777 general_export(
1778 pool,
1779 &format!(
1780 "attachment; filename=\"Course: {} - Exercise tasks {}.csv\"",
1781 course.name,
1782 Utc::now().format("%Y-%m-%d")
1783 ),
1784 CourseExerciseTasksExportOperation {
1785 course_id: *course_id,
1786 },
1787 token,
1788 )
1789 .await
1790}
1791
1792#[utoipa::path(
1798 get,
1799 path = "/{course_id}/export-course-instances",
1800 operation_id = "exportCourseInstancesCsv",
1801 tag = "courses",
1802 params(
1803 ("course_id" = Uuid, Path, description = "Course id")
1804 ),
1805 responses(
1806 (status = 200, description = "Course instances CSV", body = String, content_type = "text/csv")
1807 )
1808)]
1809#[instrument(skip(pool))]
1810pub async fn course_instances_export(
1811 course_id: web::Path<Uuid>,
1812 pool: web::Data<PgPool>,
1813 user: AuthUser,
1814) -> ControllerResult<HttpResponse> {
1815 let mut conn = pool.acquire().await?;
1816
1817 let token = authorize(
1818 &mut conn,
1819 Act::Teach,
1820 Some(user.id),
1821 Res::Course(*course_id),
1822 )
1823 .await?;
1824
1825 let course = models::courses::get_course(&mut conn, *course_id).await?;
1826
1827 general_export(
1828 pool,
1829 &format!(
1830 "attachment; filename=\"Course: {} - Instances {}.csv\"",
1831 course.name,
1832 Utc::now().format("%Y-%m-%d")
1833 ),
1834 CourseInstancesExportOperation {
1835 course_id: *course_id,
1836 },
1837 token,
1838 )
1839 .await
1840}
1841
1842#[utoipa::path(
1848 get,
1849 path = "/{course_id}/export-course-user-consents",
1850 operation_id = "exportCourseUserConsentsCsv",
1851 tag = "courses",
1852 params(
1853 ("course_id" = Uuid, Path, description = "Course id")
1854 ),
1855 responses(
1856 (status = 200, description = "Course user consents CSV", body = String, content_type = "text/csv")
1857 )
1858)]
1859#[instrument(skip(pool))]
1860pub async fn course_consent_form_answers_export(
1861 course_id: web::Path<Uuid>,
1862 pool: web::Data<PgPool>,
1863 user: AuthUser,
1864) -> ControllerResult<HttpResponse> {
1865 let mut conn = pool.acquire().await?;
1866
1867 let token = authorize(
1868 &mut conn,
1869 Act::Teach,
1870 Some(user.id),
1871 Res::Course(*course_id),
1872 )
1873 .await?;
1874
1875 let course = models::courses::get_course(&mut conn, *course_id).await?;
1876
1877 general_export(
1878 pool,
1879 &format!(
1880 "attachment; filename=\"Course: {} - User Consents {}.csv\"",
1881 course.name,
1882 Utc::now().format("%Y-%m-%d")
1883 ),
1884 CourseResearchFormExportOperation {
1885 course_id: *course_id,
1886 },
1887 token,
1888 )
1889 .await
1890}
1891
1892#[utoipa::path(
1898 get,
1899 path = "/{course_id}/export-user-exercise-states",
1900 operation_id = "exportCourseUserExerciseStatesCsv",
1901 tag = "courses",
1902 params(
1903 ("course_id" = Uuid, Path, description = "Course id")
1904 ),
1905 responses(
1906 (status = 200, description = "Course user exercise states CSV", body = String, content_type = "text/csv")
1907 )
1908)]
1909#[instrument(skip(pool))]
1910pub async fn user_exercise_states_export(
1911 course_id: web::Path<Uuid>,
1912 pool: web::Data<PgPool>,
1913 user: AuthUser,
1914) -> ControllerResult<HttpResponse> {
1915 let mut conn = pool.acquire().await?;
1916
1917 let token = authorize(
1918 &mut conn,
1919 Act::Teach,
1920 Some(user.id),
1921 Res::Course(*course_id),
1922 )
1923 .await?;
1924
1925 let course = models::courses::get_course(&mut conn, *course_id).await?;
1926
1927 general_export(
1928 pool,
1929 &format!(
1930 "attachment; filename=\"Course: {} - User exercise states {}.csv\"",
1931 course.name,
1932 Utc::now().format("%Y-%m-%d")
1933 ),
1934 UserExerciseStatesExportOperation {
1935 course_id: *course_id,
1936 },
1937 token,
1938 )
1939 .await
1940}
1941
1942#[utoipa::path(
1946 get,
1947 path = "/{course_id}/page-visit-datum-summary",
1948 operation_id = "getCoursePageVisitDatumSummary",
1949 tag = "courses",
1950 params(
1951 ("course_id" = Uuid, Path, description = "Course id")
1952 ),
1953 responses(
1954 (status = 200, description = "Course page visit summary", body = [PageVisitDatumSummaryByCourse])
1955 )
1956)]
1957pub async fn get_page_visit_datum_summary(
1958 course_id: web::Path<Uuid>,
1959 pool: web::Data<PgPool>,
1960 user: AuthUser,
1961) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourse>>> {
1962 let mut conn = pool.acquire().await?;
1963 let course_id = course_id.into_inner();
1964 let token = authorize(
1965 &mut conn,
1966 Act::ViewStats,
1967 Some(user.id),
1968 Res::Course(course_id),
1969 )
1970 .await?;
1971
1972 let res = models::page_visit_datum_summary_by_courses::get_all_for_course(&mut conn, course_id)
1973 .await?;
1974
1975 token.authorized_ok(web::Json(res))
1976}
1977
1978#[utoipa::path(
1982 get,
1983 path = "/{course_id}/page-visit-datum-summary-by-pages",
1984 operation_id = "getCoursePageVisitDatumSummaryByPages",
1985 tag = "courses",
1986 params(
1987 ("course_id" = Uuid, Path, description = "Course id")
1988 ),
1989 responses(
1990 (status = 200, description = "Course page visit summary by pages", body = [PageVisitDatumSummaryByPages])
1991 )
1992)]
1993pub async fn get_page_visit_datum_summary_by_pages(
1994 course_id: web::Path<Uuid>,
1995 pool: web::Data<PgPool>,
1996 user: AuthUser,
1997) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByPages>>> {
1998 let mut conn = pool.acquire().await?;
1999 let course_id = course_id.into_inner();
2000 let token = authorize(
2001 &mut conn,
2002 Act::ViewStats,
2003 Some(user.id),
2004 Res::Course(course_id),
2005 )
2006 .await?;
2007
2008 let res =
2009 models::page_visit_datum_summary_by_pages::get_all_for_course(&mut conn, course_id).await?;
2010
2011 token.authorized_ok(web::Json(res))
2012}
2013
2014#[utoipa::path(
2018 get,
2019 path = "/{course_id}/page-visit-datum-summary-by-device-types",
2020 operation_id = "getCoursePageVisitDatumSummaryByDeviceTypes",
2021 tag = "courses",
2022 params(
2023 ("course_id" = Uuid, Path, description = "Course id")
2024 ),
2025 responses(
2026 (status = 200, description = "Course page visit summary by device types", body = [PageVisitDatumSummaryByCourseDeviceTypes])
2027 )
2028)]
2029pub async fn get_page_visit_datum_summary_by_device_types(
2030 course_id: web::Path<Uuid>,
2031 pool: web::Data<PgPool>,
2032 user: AuthUser,
2033) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourseDeviceTypes>>> {
2034 let mut conn = pool.acquire().await?;
2035 let course_id = course_id.into_inner();
2036 let token = authorize(
2037 &mut conn,
2038 Act::ViewStats,
2039 Some(user.id),
2040 Res::Course(course_id),
2041 )
2042 .await?;
2043
2044 let res = models::page_visit_datum_summary_by_courses_device_types::get_all_for_course(
2045 &mut conn, course_id,
2046 )
2047 .await?;
2048
2049 token.authorized_ok(web::Json(res))
2050}
2051
2052#[utoipa::path(
2056 get,
2057 path = "/{course_id}/page-visit-datum-summary-by-countries",
2058 operation_id = "getCoursePageVisitDatumSummaryByCountries",
2059 tag = "courses",
2060 params(
2061 ("course_id" = Uuid, Path, description = "Course id")
2062 ),
2063 responses(
2064 (status = 200, description = "Course page visit summary by countries", body = [PageVisitDatumSummaryByCoursesCountries])
2065 )
2066)]
2067pub async fn get_page_visit_datum_summary_by_countries(
2068 course_id: web::Path<Uuid>,
2069 pool: web::Data<PgPool>,
2070 user: AuthUser,
2071) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCoursesCountries>>> {
2072 let mut conn = pool.acquire().await?;
2073 let course_id = course_id.into_inner();
2074 let token = authorize(
2075 &mut conn,
2076 Act::ViewStats,
2077 Some(user.id),
2078 Res::Course(course_id),
2079 )
2080 .await?;
2081
2082 let res = models::page_visit_datum_summary_by_courses_countries::get_all_for_course(
2083 &mut conn, course_id,
2084 )
2085 .await?;
2086
2087 token.authorized_ok(web::Json(res))
2088}
2089
2090#[utoipa::path(
2096 delete,
2097 path = "/{course_id}/teacher-reset-course-progress-for-themselves",
2098 operation_id = "resetCourseProgressForTeacherThemselves",
2099 tag = "courses",
2100 params(
2101 ("course_id" = Uuid, Path, description = "Course id")
2102 ),
2103 responses(
2104 (status = 200, description = "Teacher course progress reset", body = bool)
2105 )
2106)]
2107pub async fn teacher_reset_course_progress_for_themselves(
2108 course_id: web::Path<Uuid>,
2109 pool: web::Data<PgPool>,
2110 user: AuthUser,
2111) -> ControllerResult<web::Json<bool>> {
2112 let mut conn = pool.acquire().await?;
2113 let course_id = course_id.into_inner();
2114 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2115
2116 let mut tx = conn.begin().await?;
2117 let course_instances =
2118 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2119 for course_instance in course_instances {
2120 models::course_instances::reset_progress_on_course_instance_for_user(
2121 &mut tx,
2122 user.id,
2123 course_instance.course_id,
2124 )
2125 .await?;
2126 }
2127
2128 tx.commit().await?;
2129 token.authorized_ok(web::Json(true))
2130}
2131
2132#[utoipa::path(
2138 delete,
2139 path = "/{course_id}/teacher-reset-course-progress-for-everyone",
2140 operation_id = "resetCourseProgressForEveryone",
2141 tag = "courses",
2142 params(
2143 ("course_id" = Uuid, Path, description = "Course id")
2144 ),
2145 responses(
2146 (status = 200, description = "Course progress reset for everyone", body = bool)
2147 )
2148)]
2149pub async fn teacher_reset_course_progress_for_everyone(
2150 course_id: web::Path<Uuid>,
2151 pool: web::Data<PgPool>,
2152 user: AuthUser,
2153) -> ControllerResult<web::Json<bool>> {
2154 let mut conn = pool.acquire().await?;
2155 let course_id = course_id.into_inner();
2156 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2157 let course = models::courses::get_course(&mut conn, course_id).await?;
2158 if !course.is_draft {
2159 return Err(ControllerError::new(
2160 ControllerErrorType::BadRequest,
2161 "Can only reset progress for a draft course.".to_string(),
2162 None,
2163 ));
2164 }
2165 let n_course_module_completions =
2167 models::course_module_completions::get_count_of_distinct_completors_by_course_id(
2168 &mut conn, course_id,
2169 )
2170 .await?;
2171 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(
2172 &mut conn, course_id,
2173 ).await?;
2174 if n_course_module_completions > 200 {
2175 return Err(ControllerError::new(
2176 ControllerErrorType::BadRequest,
2177 "Too many students have completed the course.".to_string(),
2178 None,
2179 ));
2180 }
2181 if n_completions_registered_to_study_registry > 2 {
2182 return Err(ControllerError::new(
2183 ControllerErrorType::BadRequest,
2184 "Too many students have registered their completion to a study registry".to_string(),
2185 None,
2186 ));
2187 }
2188
2189 let mut tx = conn.begin().await?;
2190 let course_instances =
2191 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2192
2193 for course_instance in course_instances {
2195 let users_in_course_instance =
2196 models::users::get_users_by_course_instance_enrollment(&mut tx, course_instance.id)
2197 .await?;
2198 for user_in_course_instance in users_in_course_instance {
2199 models::course_instances::reset_progress_on_course_instance_for_user(
2200 &mut tx,
2201 user_in_course_instance.id,
2202 course_instance.course_id,
2203 )
2204 .await?;
2205 }
2206 }
2207
2208 tx.commit().await?;
2209 token.authorized_ok(web::Json(true))
2210}
2211
2212#[derive(Debug, Deserialize)]
2213
2214pub struct GetSuspectedCheatersQuery {
2215 archive: bool,
2216}
2217
2218#[utoipa::path(
2222 get,
2223 path = "/{course_id}/suspected-cheaters",
2224 operation_id = "getCourseSuspectedCheaters",
2225 tag = "courses",
2226 params(
2227 ("course_id" = Uuid, Path, description = "Course id"),
2228 ("archive" = bool, Query, description = "Whether to fetch archived suspected cheaters")
2229 ),
2230 responses(
2231 (status = 200, description = "Suspected cheaters for course", body = [SuspectedCheaters])
2232 )
2233)]
2234#[instrument(skip(pool))]
2235async fn get_all_suspected_cheaters(
2236 user: AuthUser,
2237 params: web::Path<Uuid>,
2238 query: web::Query<GetSuspectedCheatersQuery>,
2239 pool: web::Data<PgPool>,
2240) -> ControllerResult<web::Json<Vec<SuspectedCheaters>>> {
2241 let course_id = params.into_inner();
2242
2243 let mut conn = pool.acquire().await?;
2244 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2245
2246 let course_cheaters = models::suspected_cheaters::get_all_suspected_cheaters_in_course(
2247 &mut conn,
2248 course_id,
2249 query.archive,
2250 )
2251 .await?;
2252
2253 token.authorized_ok(web::Json(course_cheaters))
2254}
2255
2256#[utoipa::path(
2260 get,
2261 path = "/{course_id}/thresholds",
2262 operation_id = "getCourseThresholds",
2263 tag = "courses",
2264 params(
2265 ("course_id" = Uuid, Path, description = "Course id")
2266 ),
2267 responses(
2268 (status = 200, description = "Course thresholds", body = serde_json::Value)
2269 )
2270)]
2271#[instrument(skip(pool))]
2272async fn get_all_thresholds(
2273 user: AuthUser,
2274 params: web::Path<Uuid>,
2275 pool: web::Data<PgPool>,
2276) -> ControllerResult<web::Json<Vec<Threshold>>> {
2277 let mut conn = pool.acquire().await?;
2278 let course_id = params.into_inner();
2279
2280 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2281
2282 let thresholds =
2283 models::suspected_cheaters::get_all_thresholds_for_course(&mut conn, course_id).await?;
2284
2285 token.authorized_ok(web::Json(thresholds))
2286}
2287
2288#[utoipa::path(
2292 post,
2293 path = "/{course_id}/suspected-cheaters/archive/{id}",
2294 operation_id = "archiveCourseSuspectedCheater",
2295 tag = "courses",
2296 params(
2297 ("course_id" = Uuid, Path, description = "Course id"),
2298 ("id" = Uuid, Path, description = "Suspected cheater user id")
2299 ),
2300 responses(
2301 (status = 200, description = "Suspected cheater archived")
2302 )
2303)]
2304#[instrument(skip(pool))]
2305async fn teacher_archive_suspected_cheater(
2306 user: AuthUser,
2307 path: web::Path<(Uuid, Uuid)>,
2308 pool: web::Data<PgPool>,
2309) -> ControllerResult<web::Json<()>> {
2310 let (course_id, user_id) = path.into_inner();
2311
2312 let mut conn = pool.acquire().await?;
2313 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2314
2315 models::suspected_cheaters::archive_suspected_cheater(&mut conn, user_id).await?;
2316
2317 token.authorized_ok(web::Json(()))
2318}
2319
2320#[utoipa::path(
2324 post,
2325 path = "/{course_id}/suspected-cheaters/approve/{id}",
2326 operation_id = "approveCourseSuspectedCheater",
2327 tag = "courses",
2328 params(
2329 ("course_id" = Uuid, Path, description = "Course id"),
2330 ("id" = Uuid, Path, description = "Suspected cheater user id")
2331 ),
2332 responses(
2333 (status = 200, description = "Suspected cheater approved")
2334 )
2335)]
2336#[instrument(skip(pool))]
2337async fn teacher_approve_suspected_cheater(
2338 user: AuthUser,
2339 path: web::Path<(Uuid, Uuid)>,
2340 pool: web::Data<PgPool>,
2341) -> ControllerResult<web::Json<()>> {
2342 let (course_id, user_id) = path.into_inner();
2343
2344 let mut conn = pool.acquire().await?;
2345 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2346
2347 models::suspected_cheaters::approve_suspected_cheater(&mut conn, user_id).await?;
2348
2349 models::course_module_completions::update_passed_and_grade_status(
2352 &mut conn, course_id, user_id, false, 0,
2353 )
2354 .await?;
2355
2356 token.authorized_ok(web::Json(()))
2357}
2358
2359#[utoipa::path(
2363 post,
2364 path = "/{course_id}/join-course-with-join-code",
2365 operation_id = "joinCourseWithJoinCode",
2366 tag = "courses",
2367 params(
2368 ("course_id" = Uuid, Path, description = "Course id")
2369 ),
2370 responses(
2371 (status = 200, description = "Joined course id", body = Uuid)
2372 )
2373)]
2374#[instrument(skip(pool))]
2375async fn add_user_to_course_with_join_code(
2376 course_id: web::Path<Uuid>,
2377 user: AuthUser,
2378 pool: web::Data<PgPool>,
2379) -> ControllerResult<web::Json<Uuid>> {
2380 let mut conn = pool.acquire().await?;
2381 let token = skip_authorize();
2382
2383 let joined =
2384 models::join_code_uses::insert(&mut conn, PKeyPolicy::Generate, user.id, *course_id)
2385 .await?;
2386 token.authorized_ok(web::Json(joined))
2387}
2388
2389#[utoipa::path(
2393 post,
2394 path = "/{course_id}/set-join-code",
2395 operation_id = "setCourseJoinCode",
2396 tag = "courses",
2397 params(
2398 ("course_id" = Uuid, Path, description = "Course id")
2399 ),
2400 responses(
2401 (status = 200, description = "Course join code set")
2402 )
2403)]
2404#[instrument(skip(pool))]
2405async fn set_join_code_for_course(
2406 id: web::Path<Uuid>,
2407 pool: web::Data<PgPool>,
2408 user: AuthUser,
2409) -> ControllerResult<HttpResponse> {
2410 let mut conn = pool.acquire().await?;
2411 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*id)).await?;
2412
2413 const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
2414 abcdefghjkmnpqrstuvwxyz";
2415 const PASSWORD_LEN: usize = 64;
2416 let mut rng = rand::rng();
2417
2418 let code: String = (0..PASSWORD_LEN)
2419 .map(|_| {
2420 let idx = rng.random_range(0..CHARSET.len());
2421 CHARSET[idx] as char
2422 })
2423 .collect();
2424
2425 models::courses::set_join_code_for_course(&mut conn, *id, code).await?;
2426 token.authorized_ok(HttpResponse::Ok().finish())
2427}
2428
2429#[utoipa::path(
2433 get,
2434 path = "/join/{join_code}",
2435 operation_id = "getCourseByJoinCode",
2436 tag = "courses",
2437 params(
2438 ("join_code" = String, Path, description = "Course join code")
2439 ),
2440 responses(
2441 (status = 200, description = "Course for join code", body = Course)
2442 )
2443)]
2444#[instrument(skip(pool))]
2445async fn get_course_with_join_code(
2446 join_code: web::Path<String>,
2447 user: AuthUser,
2448 pool: web::Data<PgPool>,
2449) -> ControllerResult<web::Json<Course>> {
2450 let mut conn = pool.acquire().await?;
2451 let token = skip_authorize();
2452 let course =
2453 models::courses::get_course_with_join_code(&mut conn, join_code.to_string()).await?;
2454
2455 token.authorized_ok(web::Json(course))
2456}
2457
2458#[utoipa::path(
2462 post,
2463 path = "/{course_id}/partners-block",
2464 operation_id = "upsertCoursePartnersBlock",
2465 tag = "courses",
2466 params(
2467 ("course_id" = Uuid, Path, description = "Course id")
2468 ),
2469 request_body = Option<serde_json::Value>,
2470 responses(
2471 (status = 200, description = "Partners block", body = serde_json::Value)
2472 )
2473)]
2474#[instrument(skip(payload, pool))]
2475async fn post_partners_block(
2476 path: web::Path<Uuid>,
2477 payload: web::Json<Option<serde_json::Value>>,
2478 pool: web::Data<PgPool>,
2479 user: AuthUser,
2480) -> ControllerResult<web::Json<PartnersBlock>> {
2481 let course_id = path.into_inner();
2482
2483 let content = payload.into_inner();
2484 let mut conn = pool.acquire().await?;
2485 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2486
2487 let upserted_partner_block =
2488 models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
2489
2490 token.authorized_ok(web::Json(upserted_partner_block))
2491}
2492
2493#[utoipa::path(
2497 get,
2498 path = "/{course_id}/partners-block",
2499 operation_id = "getCoursePartnersBlock",
2500 tag = "courses",
2501 params(
2502 ("course_id" = Uuid, Path, description = "Course id")
2503 ),
2504 responses(
2505 (status = 200, description = "Partners block", body = serde_json::Value)
2506 )
2507)]
2508#[instrument(skip(pool))]
2509async fn get_partners_block(
2510 path: web::Path<Uuid>,
2511 user: AuthUser,
2512 pool: web::Data<PgPool>,
2513) -> ControllerResult<web::Json<PartnersBlock>> {
2514 let course_id = path.into_inner();
2515 let mut conn = pool.acquire().await?;
2516 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2517
2518 let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
2520
2521 let partner_block = if course_exists {
2522 models::partner_block::get_partner_block(&mut conn, course_id).await?
2524 } else {
2525 let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
2527
2528 models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
2530 };
2531
2532 token.authorized_ok(web::Json(partner_block))
2533}
2534
2535#[utoipa::path(
2539 delete,
2540 path = "/{course_id}/partners-block",
2541 operation_id = "deleteCoursePartnersBlock",
2542 tag = "courses",
2543 params(
2544 ("course_id" = Uuid, Path, description = "Course id")
2545 ),
2546 responses(
2547 (status = 200, description = "Deleted partners block", body = serde_json::Value)
2548 )
2549)]
2550#[instrument(skip(pool))]
2551async fn delete_partners_block(
2552 path: web::Path<Uuid>,
2553 pool: web::Data<PgPool>,
2554 user: AuthUser,
2555) -> ControllerResult<web::Json<PartnersBlock>> {
2556 let course_id = path.into_inner();
2557 let mut conn = pool.acquire().await?;
2558 let token = authorize(
2559 &mut conn,
2560 Act::UsuallyUnacceptableDeletion,
2561 Some(user.id),
2562 Res::Course(course_id),
2563 )
2564 .await?;
2565 let deleted_partners_block =
2566 models::partner_block::delete_partner_block(&mut conn, course_id).await?;
2567
2568 token.authorized_ok(web::Json(deleted_partners_block))
2569}
2570
2571pub fn _add_routes(cfg: &mut ServiceConfig) {
2579 cfg.service(web::scope("/{course_id}/stats").configure(stats::_add_routes))
2580 .service(web::scope("/{course_id}/chatbots").configure(chatbots::_add_routes))
2581 .service(web::scope("/{course_id}/students").configure(students::_add_routes))
2582 .route("/{course_id}", web::get().to(get_course))
2583 .route("", web::post().to(post_new_course))
2584 .route("/{course_id}", web::put().to(update_course))
2585 .route("/{course_id}", web::delete().to(delete_course))
2586 .route(
2587 "/{course_id}/status-for-all-exercises/{user_id}",
2588 web::get().to(get_all_exercise_statuses_by_course_id),
2589 )
2590 .route(
2591 "/{course_id}/course-module-completions/{user_id}",
2592 web::get().to(get_all_course_module_completions_for_user_by_course_id),
2593 )
2594 .route(
2595 "/{course_id}/daily-submission-counts",
2596 web::get().to(get_daily_submission_counts),
2597 )
2598 .route(
2599 "/{course_id}/daily-users-who-have-submitted-something",
2600 web::get().to(get_daily_user_counts_with_submissions),
2601 )
2602 .route("/{course_id}/exercises", web::get().to(get_all_exercises))
2603 .route(
2604 "/{course_id}/exercises-and-count-of-answers-requiring-attention",
2605 web::get().to(get_all_exercises_and_count_of_answers_requiring_attention),
2606 )
2607 .route(
2608 "/{course_id}/structure",
2609 web::get().to(get_course_structure),
2610 )
2611 .route(
2612 "/{course_id}/language-versions",
2613 web::get().to(get_all_course_language_versions),
2614 )
2615 .route(
2616 "/{course_id}/create-copy",
2617 web::post().to(create_course_copy),
2618 )
2619 .route("/{course_id}/upload", web::post().to(add_media_for_course))
2620 .route(
2621 "/{course_id}/weekday-hour-submission-counts",
2622 web::get().to(get_weekday_hour_submission_counts),
2623 )
2624 .route(
2625 "/{course_id}/submission-counts-by-exercise",
2626 web::get().to(get_submission_counts_by_exercise),
2627 )
2628 .route(
2629 "/{course_id}/course-instances",
2630 web::get().to(get_course_instances),
2631 )
2632 .route("/{course_id}/feedback", web::get().to(get_feedback))
2633 .route(
2634 "/{course_id}/feedback-count",
2635 web::get().to(get_feedback_count),
2636 )
2637 .route(
2638 "/{course_id}/new-course-instance",
2639 web::post().to(new_course_instance),
2640 )
2641 .route("/{course_id}/glossary", web::get().to(glossary))
2642 .route("/{course_id}/glossary", web::post().to(new_glossary_term))
2643 .route(
2644 "/{course_id}/course-users-counts-by-exercise",
2645 web::get().to(get_course_users_counts_by_exercise),
2646 )
2647 .route(
2648 "/{course_id}/new-page-ordering",
2649 web::post().to(post_new_page_ordering),
2650 )
2651 .route(
2652 "/{course_id}/new-chapter-ordering",
2653 web::post().to(post_new_chapter_ordering),
2654 )
2655 .route(
2656 "/{course_id}/references",
2657 web::get().to(get_material_references_by_course_id),
2658 )
2659 .route(
2660 "/{course_id}/references",
2661 web::post().to(insert_material_references),
2662 )
2663 .route(
2664 "/{course_id}/references/{reference_id}",
2665 web::post().to(update_material_reference),
2666 )
2667 .route(
2668 "/{course_id}/references/{reference_id}",
2669 web::delete().to(delete_material_reference_by_id),
2670 )
2671 .route(
2672 "/{course_id}/course-modules",
2673 web::post().to(update_modules),
2674 )
2675 .route(
2676 "/{course_id}/default-peer-review",
2677 web::get().to(get_course_default_peer_review),
2678 )
2679 .route(
2680 "/{course_id}/update-peer-review-queue-reviews-received",
2681 web::post().to(post_update_peer_review_queue_reviews_received),
2682 )
2683 .route(
2684 "/{course_id}/breadcrumb-info",
2685 web::get().to(get_course_breadcrumb_info),
2686 )
2687 .route(
2688 "/{course_id}/progress/{user_id}",
2689 web::get().to(get_user_progress_for_course),
2690 )
2691 .route(
2692 "/{course_id}/user-settings/{user_id}",
2693 web::get().to(get_user_course_settings),
2694 )
2695 .route(
2696 "/{course_id}/export-submissions",
2697 web::get().to(submission_export),
2698 )
2699 .route(
2700 "/{course_id}/export-user-details",
2701 web::get().to(user_details_export),
2702 )
2703 .route(
2704 "/{course_id}/export-exercise-tasks",
2705 web::get().to(exercise_tasks_export),
2706 )
2707 .route(
2708 "/{course_id}/export-course-instances",
2709 web::get().to(course_instances_export),
2710 )
2711 .route(
2712 "/{course_id}/export-course-user-consents",
2713 web::get().to(course_consent_form_answers_export),
2714 )
2715 .route(
2716 "/{course_id}/export-user-exercise-states",
2717 web::get().to(user_exercise_states_export),
2718 )
2719 .route(
2720 "/{course_id}/page-visit-datum-summary",
2721 web::get().to(get_page_visit_datum_summary),
2722 )
2723 .route(
2724 "/{course_id}/page-visit-datum-summary-by-pages",
2725 web::get().to(get_page_visit_datum_summary_by_pages),
2726 )
2727 .route(
2728 "/{course_id}/page-visit-datum-summary-by-device-types",
2729 web::get().to(get_page_visit_datum_summary_by_device_types),
2730 )
2731 .route(
2732 "/{course_id}/page-visit-datum-summary-by-countries",
2733 web::get().to(get_page_visit_datum_summary_by_countries),
2734 )
2735 .route(
2736 "/{course_id}/teacher-reset-course-progress-for-themselves",
2737 web::delete().to(teacher_reset_course_progress_for_themselves),
2738 )
2739 .route("/{course_id}/thresholds", web::get().to(get_all_thresholds))
2740 .route(
2741 "/{course_id}/suspected-cheaters",
2742 web::get().to(get_all_suspected_cheaters),
2743 )
2744 .route(
2745 "/{course_id}/suspected-cheaters/archive/{id}",
2746 web::post().to(teacher_archive_suspected_cheater),
2747 )
2748 .route(
2749 "/{course_id}/suspected-cheaters/approve/{id}",
2750 web::post().to(teacher_approve_suspected_cheater),
2751 )
2752 .route(
2753 "/{course_id}/teacher-reset-course-progress-for-everyone",
2754 web::delete().to(teacher_reset_course_progress_for_everyone),
2755 )
2756 .route(
2757 "/{course_id}/join-course-with-join-code",
2758 web::post().to(add_user_to_course_with_join_code),
2759 )
2760 .route(
2761 "/{course_id}/partners-block",
2762 web::post().to(post_partners_block),
2763 )
2764 .route(
2765 "/{course_id}/partners-block",
2766 web::get().to(get_partners_block),
2767 )
2768 .route(
2769 "/{course_id}/partners-block",
2770 web::delete().to(delete_partners_block),
2771 )
2772 .route(
2773 "/{course_id}/set-join-code",
2774 web::post().to(set_join_code_for_course),
2775 )
2776 .route(
2777 "/{course_id}/reprocess-completions",
2778 web::post().to(post_reprocess_module_completions),
2779 )
2780 .route(
2781 "/join/{join_code}",
2782 web::get().to(get_course_with_join_code),
2783 );
2784}