1pub mod chatbots;
4pub mod stats;
5
6use chrono::Utc;
7use domain::csv_export::user_exercise_states_export::UserExerciseStatesExportOperation;
8use headless_lms_models::{
9 partner_block::PartnersBlock,
10 suspected_cheaters::{SuspectedCheaters, ThresholdData},
11};
12use rand::Rng;
13use std::sync::Arc;
14
15use headless_lms_utils::strings::is_ietf_language_code_like;
16use models::{
17 chapters::Chapter,
18 course_instances::{CourseInstance, CourseInstanceForm, NewCourseInstance},
19 course_modules::ModuleUpdates,
20 courses::{Course, CourseBreadcrumbInfo, CourseStructure, CourseUpdate, NewCourse},
21 exercise_slide_submissions::{
22 self, ExerciseAnswersInCourseRequiringAttentionCount, ExerciseSlideSubmissionCount,
23 ExerciseSlideSubmissionCountByExercise, ExerciseSlideSubmissionCountByWeekAndHour,
24 },
25 exercises::Exercise,
26 feedback::{self, Feedback, FeedbackCount},
27 glossary::{Term, TermUpdate},
28 library,
29 material_references::{MaterialReference, NewMaterialReference},
30 page_visit_datum_summary_by_courses::PageVisitDatumSummaryByCourse,
31 page_visit_datum_summary_by_courses_countries::PageVisitDatumSummaryByCoursesCountries,
32 page_visit_datum_summary_by_courses_device_types::PageVisitDatumSummaryByCourseDeviceTypes,
33 page_visit_datum_summary_by_pages::PageVisitDatumSummaryByPages,
34 pages::Page,
35 peer_or_self_review_configs::PeerOrSelfReviewConfig,
36 peer_or_self_review_questions::PeerOrSelfReviewQuestion,
37 user_course_settings::UserCourseSettings,
38 user_exercise_states::ExerciseUserCounts,
39};
40
41use crate::{
42 domain::models_requests::{self, JwtKey},
43 prelude::*,
44};
45
46use headless_lms_models::course_language_groups;
47
48use crate::domain::csv_export::course_instance_export::CourseInstancesExportOperation;
49use crate::domain::csv_export::course_research_form_questions_answers_export::CourseResearchFormExportOperation;
50use crate::domain::csv_export::exercise_tasks_export::CourseExerciseTasksExportOperation;
51use crate::domain::csv_export::general_export;
52use crate::domain::csv_export::submissions::CourseSubmissionExportOperation;
53use crate::domain::csv_export::users_export::UsersExportOperation;
54
55#[instrument(skip(pool))]
59async fn get_course(
60 course_id: web::Path<Uuid>,
61 pool: web::Data<PgPool>,
62 user: AuthUser,
63) -> ControllerResult<web::Json<Course>> {
64 let mut conn = pool.acquire().await?;
65 let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
66 let course = models::courses::get_course(&mut conn, *course_id).await?;
67 token.authorized_ok(web::Json(course))
68}
69
70#[instrument(skip(pool))]
74async fn get_course_breadcrumb_info(
75 course_id: web::Path<Uuid>,
76 pool: web::Data<PgPool>,
77 user: AuthUser,
78) -> ControllerResult<web::Json<CourseBreadcrumbInfo>> {
79 let mut conn = pool.acquire().await?;
80 let user_id = Some(user.id);
81 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
82 let info = models::courses::get_course_breadcrumb_info(&mut conn, *course_id).await?;
83 token.authorized_ok(web::Json(info))
84}
85
86#[instrument(skip(pool))]
90async fn get_user_course_settings(
91 path: web::Path<(Uuid, Uuid)>,
92 pool: web::Data<PgPool>,
93 user: AuthUser,
94) -> ControllerResult<web::Json<Option<UserCourseSettings>>> {
95 let (course_id, target_user_id) = path.into_inner();
96 let mut conn = pool.acquire().await?;
97 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
98 let settings = models::user_course_settings::get_user_course_settings_by_course_id(
99 &mut conn,
100 target_user_id,
101 course_id,
102 )
103 .await?;
104 token.authorized_ok(web::Json(settings))
105}
106
107#[instrument(skip(pool, user))]
114async fn post_reprocess_module_completions(
115 pool: web::Data<PgPool>,
116 user: AuthUser,
117 course_id: web::Path<Uuid>,
118) -> ControllerResult<web::Json<bool>> {
119 let mut conn = pool.acquire().await?;
120 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
121 models::library::progressing::process_all_course_completions(&mut conn, *course_id).await?;
122 token.authorized_ok(web::Json(true))
123}
124
125#[instrument(skip(pool, app_conf))]
143async fn post_new_course(
144 request_id: RequestId,
145 pool: web::Data<PgPool>,
146 payload: web::Json<NewCourse>,
147 user: AuthUser,
148 app_conf: web::Data<ApplicationConfiguration>,
149 jwt_key: web::Data<JwtKey>,
150) -> ControllerResult<web::Json<Course>> {
151 let mut conn = pool.acquire().await?;
152 let new_course = payload.0;
153 if !is_ietf_language_code_like(&new_course.language_code) {
154 return Err(ControllerError::new(
155 ControllerErrorType::BadRequest,
156 "Malformed language code.".to_string(),
157 None,
158 ));
159 }
160 let token = authorize(
161 &mut conn,
162 Act::CreateCoursesOrExams,
163 Some(user.id),
164 Res::Organization(new_course.organization_id),
165 )
166 .await?;
167
168 let mut tx = conn.begin().await?;
169 let (course, ..) = library::content_management::create_new_course(
170 &mut tx,
171 PKeyPolicy::Generate,
172 new_course,
173 user.id,
174 models_requests::make_spec_fetcher(
175 app_conf.base_url.clone(),
176 request_id.0,
177 Arc::clone(&jwt_key),
178 ),
179 models_requests::fetch_service_info,
180 )
181 .await?;
182 models::roles::insert(
183 &mut tx,
184 user.id,
185 models::roles::UserRole::Teacher,
186 models::roles::RoleDomain::Course(course.id),
187 )
188 .await?;
189 tx.commit().await?;
190
191 token.authorized_ok(web::Json(course))
192}
193
194#[instrument(skip(pool))]
210async fn update_course(
211 payload: web::Json<CourseUpdate>,
212 course_id: web::Path<Uuid>,
213 pool: web::Data<PgPool>,
214 user: AuthUser,
215) -> ControllerResult<web::Json<Course>> {
216 let mut conn = pool.acquire().await?;
217 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
218 let course_update = payload.0;
219 let course_before_update = models::courses::get_course(&mut conn, *course_id).await?;
220 if course_update.can_add_chatbot != course_before_update.can_add_chatbot {
221 let _token2 =
223 authorize(&mut conn, Act::Teach, Some(user.id), Res::GlobalPermissions).await?;
224 }
225 let course = models::courses::update_course(&mut conn, *course_id, course_update).await?;
226 token.authorized_ok(web::Json(course))
227}
228
229#[instrument(skip(pool))]
233async fn delete_course(
234 course_id: web::Path<Uuid>,
235 pool: web::Data<PgPool>,
236 user: AuthUser,
237) -> ControllerResult<web::Json<Course>> {
238 let mut conn = pool.acquire().await?;
239 let token = authorize(
240 &mut conn,
241 Act::UsuallyUnacceptableDeletion,
242 Some(user.id),
243 Res::Course(*course_id),
244 )
245 .await?;
246 let course = models::courses::delete_course(&mut conn, *course_id).await?;
247
248 token.authorized_ok(web::Json(course))
249}
250
251#[instrument(skip(pool, file_store, app_conf))]
299async fn get_course_structure(
300 course_id: web::Path<Uuid>,
301 pool: web::Data<PgPool>,
302 user: AuthUser,
303 file_store: web::Data<dyn FileStore>,
304 app_conf: web::Data<ApplicationConfiguration>,
305) -> ControllerResult<web::Json<CourseStructure>> {
306 let mut conn = pool.acquire().await?;
307 let token = authorize(
308 &mut conn,
309 Act::ViewInternalCourseStructure,
310 Some(user.id),
311 Res::Course(*course_id),
312 )
313 .await?;
314 let course_structure = models::courses::get_course_structure(
315 &mut conn,
316 *course_id,
317 file_store.as_ref(),
318 app_conf.as_ref(),
319 )
320 .await?;
321
322 token.authorized_ok(web::Json(course_structure))
323}
324
325#[instrument(skip(payload, request, pool, file_store, app_conf))]
341async fn add_media_for_course(
342 course_id: web::Path<Uuid>,
343 payload: Multipart,
344 request: HttpRequest,
345 pool: web::Data<PgPool>,
346 user: AuthUser,
347 file_store: web::Data<dyn FileStore>,
348 app_conf: web::Data<ApplicationConfiguration>,
349) -> ControllerResult<web::Json<UploadResult>> {
350 let mut conn = pool.acquire().await?;
351 let course = models::courses::get_course(&mut conn, *course_id).await?;
352 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
353 let media_path = upload_file_from_cms(
354 request.headers(),
355 payload,
356 StoreKind::Course(course.id),
357 file_store.as_ref(),
358 &mut conn,
359 user,
360 )
361 .await?;
362 let download_url = file_store.get_download_url(media_path.as_path(), app_conf.as_ref());
363
364 token.authorized_ok(web::Json(UploadResult { url: download_url }))
365}
366
367#[instrument(skip(pool))]
371async fn get_all_exercises(
372 pool: web::Data<PgPool>,
373 course_id: web::Path<Uuid>,
374 user: AuthUser,
375) -> ControllerResult<web::Json<Vec<Exercise>>> {
376 let mut conn = pool.acquire().await?;
377 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
378 let exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
379
380 token.authorized_ok(web::Json(exercises))
381}
382
383#[instrument(skip(pool))]
387async fn get_all_exercises_and_count_of_answers_requiring_attention(
388 pool: web::Data<PgPool>,
389 course_id: web::Path<Uuid>,
390 user: AuthUser,
391) -> ControllerResult<web::Json<Vec<ExerciseAnswersInCourseRequiringAttentionCount>>> {
392 let mut conn = pool.acquire().await?;
393 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
394 let _exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
395 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?;
396 token.authorized_ok(web::Json(count_of_answers_requiring_attention))
397}
398
399#[instrument(skip(pool))]
411async fn get_all_course_language_versions(
412 pool: web::Data<PgPool>,
413 course_id: web::Path<Uuid>,
414 user: AuthUser,
415) -> ControllerResult<web::Json<Vec<Course>>> {
416 let mut conn = pool.acquire().await?;
417 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
418 let course = models::courses::get_course(&mut conn, *course_id).await?;
419 let language_versions =
420 models::courses::get_all_language_versions_of_course(&mut conn, &course).await?;
421
422 token.authorized_ok(web::Json(language_versions))
423}
424
425#[derive(Deserialize, Debug)]
426#[serde(tag = "mode", rename_all = "snake_case")]
427#[cfg_attr(feature = "ts_rs", derive(TS))]
428pub enum CopyCourseMode {
429 Duplicate,
431 SameLanguageGroup,
433 ExistingLanguageGroup { target_course_id: Uuid },
435 NewLanguageGroup,
437}
438
439#[derive(Deserialize, Debug)]
440#[cfg_attr(feature = "ts_rs", derive(TS))]
441pub struct CopyCourseRequest {
442 #[serde(flatten)]
443 pub new_course: NewCourse,
444 pub mode: CopyCourseMode,
445}
446
447#[instrument(skip(pool))]
491pub async fn create_course_copy(
492 pool: web::Data<PgPool>,
493 course_id: web::Path<Uuid>,
494 payload: web::Json<CopyCourseRequest>,
495 user: AuthUser,
496) -> ControllerResult<web::Json<Course>> {
497 let mut conn = pool.acquire().await?;
498 let token = authorize(
499 &mut conn,
500 Act::Duplicate,
501 Some(user.id),
502 Res::Course(*course_id),
503 )
504 .await?;
505
506 let mut tx = conn.begin().await?;
507
508 let copied_course = match &payload.mode {
509 CopyCourseMode::Duplicate => {
510 models::library::copying::copy_course(
511 &mut tx,
512 *course_id,
513 &payload.new_course,
514 false,
515 user.id,
516 )
517 .await?
518 }
519 CopyCourseMode::SameLanguageGroup => {
520 models::library::copying::copy_course(
521 &mut tx,
522 *course_id,
523 &payload.new_course,
524 true,
525 user.id,
526 )
527 .await?
528 }
529 CopyCourseMode::ExistingLanguageGroup { target_course_id } => {
530 let target_course = models::courses::get_course(&mut tx, *target_course_id).await?;
531 authorize(
533 &mut tx,
534 Act::Duplicate,
535 Some(user.id),
536 Res::Course(*target_course_id),
537 )
538 .await?;
539 models::library::copying::copy_course_with_language_group(
540 &mut tx,
541 *course_id,
542 target_course.course_language_group_id,
543 &payload.new_course,
544 user.id,
545 )
546 .await?
547 }
548 CopyCourseMode::NewLanguageGroup => {
549 let new_clg_id = course_language_groups::insert(&mut tx, PKeyPolicy::Generate).await?;
550 models::library::copying::copy_course_with_language_group(
551 &mut tx,
552 *course_id,
553 new_clg_id,
554 &payload.new_course,
555 user.id,
556 )
557 .await?
558 }
559 };
560
561 models::roles::insert(
562 &mut tx,
563 user.id,
564 models::roles::UserRole::Teacher,
565 models::roles::RoleDomain::Course(copied_course.id),
566 )
567 .await?;
568
569 tx.commit().await?;
570
571 token.authorized_ok(web::Json(copied_course))
572}
573
574#[instrument(skip(pool))]
578async fn get_daily_submission_counts(
579 pool: web::Data<PgPool>,
580 course_id: web::Path<Uuid>,
581 user: AuthUser,
582) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
583 let mut conn = pool.acquire().await?;
584 let token = authorize(
585 &mut conn,
586 Act::ViewStats,
587 Some(user.id),
588 Res::Course(*course_id),
589 )
590 .await?;
591 let course = models::courses::get_course(&mut conn, *course_id).await?;
592 let res =
593 exercise_slide_submissions::get_course_daily_slide_submission_counts(&mut conn, &course)
594 .await?;
595
596 token.authorized_ok(web::Json(res))
597}
598
599#[instrument(skip(pool))]
603async fn get_daily_user_counts_with_submissions(
604 pool: web::Data<PgPool>,
605 course_id: web::Path<Uuid>,
606 user: AuthUser,
607) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
608 let mut conn = pool.acquire().await?;
609 let token = authorize(
610 &mut conn,
611 Act::ViewStats,
612 Some(user.id),
613 Res::Course(*course_id),
614 )
615 .await?;
616 let course = models::courses::get_course(&mut conn, *course_id).await?;
617 let res = exercise_slide_submissions::get_course_daily_user_counts_with_submissions(
618 &mut conn, &course,
619 )
620 .await?;
621
622 token.authorized_ok(web::Json(res))
623}
624
625#[instrument(skip(pool))]
629async fn get_weekday_hour_submission_counts(
630 pool: web::Data<PgPool>,
631 course_id: web::Path<Uuid>,
632 user: AuthUser,
633) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByWeekAndHour>>> {
634 let mut conn = pool.acquire().await?;
635 let token = authorize(
636 &mut conn,
637 Act::ViewStats,
638 Some(user.id),
639 Res::Course(*course_id),
640 )
641 .await?;
642 let course = models::courses::get_course(&mut conn, *course_id).await?;
643 let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_weekday_and_hour(
644 &mut conn, &course,
645 )
646 .await?;
647
648 token.authorized_ok(web::Json(res))
649}
650
651#[instrument(skip(pool))]
655async fn get_submission_counts_by_exercise(
656 pool: web::Data<PgPool>,
657 course_id: web::Path<Uuid>,
658 user: AuthUser,
659) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByExercise>>> {
660 let mut conn = pool.acquire().await?;
661 let token = authorize(
662 &mut conn,
663 Act::ViewStats,
664 Some(user.id),
665 Res::Course(*course_id),
666 )
667 .await?;
668 let course = models::courses::get_course(&mut conn, *course_id).await?;
669 let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_exercise(
670 &mut conn, &course,
671 )
672 .await?;
673
674 token.authorized_ok(web::Json(res))
675}
676
677#[instrument(skip(pool))]
681async fn get_course_instances(
682 pool: web::Data<PgPool>,
683 course_id: web::Path<Uuid>,
684 user: AuthUser,
685) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
686 let mut conn = pool.acquire().await?;
687 let token = authorize(
688 &mut conn,
689 Act::Teach,
690 Some(user.id),
691 Res::Course(*course_id),
692 )
693 .await?;
694 let course_instances =
695 models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
696
697 token.authorized_ok(web::Json(course_instances))
698}
699
700#[derive(Debug, Deserialize)]
701#[cfg_attr(feature = "ts_rs", derive(TS))]
702pub struct GetFeedbackQuery {
703 read: bool,
704 #[serde(flatten)]
705 pagination: Pagination,
706}
707
708#[instrument(skip(pool))]
712pub async fn get_feedback(
713 course_id: web::Path<Uuid>,
714 pool: web::Data<PgPool>,
715 read: web::Query<GetFeedbackQuery>,
716 user: AuthUser,
717) -> ControllerResult<web::Json<Vec<Feedback>>> {
718 let mut conn = pool.acquire().await?;
719 let token = authorize(
720 &mut conn,
721 Act::Teach,
722 Some(user.id),
723 Res::Course(*course_id),
724 )
725 .await?;
726 let feedback =
727 feedback::get_feedback_for_course(&mut conn, *course_id, read.read, read.pagination)
728 .await?;
729
730 token.authorized_ok(web::Json(feedback))
731}
732
733#[instrument(skip(pool))]
737pub async fn get_feedback_count(
738 course_id: web::Path<Uuid>,
739 pool: web::Data<PgPool>,
740 user: AuthUser,
741) -> ControllerResult<web::Json<FeedbackCount>> {
742 let mut conn = pool.acquire().await?;
743 let token = authorize(
744 &mut conn,
745 Act::Teach,
746 Some(user.id),
747 Res::Course(*course_id),
748 )
749 .await?;
750
751 let feedback_count = feedback::get_feedback_count_for_course(&mut conn, *course_id).await?;
752
753 token.authorized_ok(web::Json(feedback_count))
754}
755
756#[instrument(skip(pool))]
760async fn new_course_instance(
761 form: web::Json<CourseInstanceForm>,
762 course_id: web::Path<Uuid>,
763 pool: web::Data<PgPool>,
764 user: AuthUser,
765) -> ControllerResult<web::Json<Uuid>> {
766 let mut conn = pool.acquire().await?;
767 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
768 let form = form.into_inner();
769 let new = NewCourseInstance {
770 course_id: *course_id,
771 name: form.name.as_deref(),
772 description: form.description.as_deref(),
773 support_email: form.support_email.as_deref(),
774 teacher_in_charge_name: &form.teacher_in_charge_name,
775 teacher_in_charge_email: &form.teacher_in_charge_email,
776 opening_time: form.opening_time,
777 closing_time: form.closing_time,
778 };
779 let ci = models::course_instances::insert(&mut conn, PKeyPolicy::Generate, new).await?;
780
781 token.authorized_ok(web::Json(ci.id))
782}
783
784#[instrument(skip(pool))]
785async fn glossary(
786 pool: web::Data<PgPool>,
787 course_id: web::Path<Uuid>,
788 user: AuthUser,
789) -> ControllerResult<web::Json<Vec<Term>>> {
790 let mut conn = pool.acquire().await?;
791 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
792 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
793
794 token.authorized_ok(web::Json(glossary))
795}
796
797#[instrument(skip(pool))]
800async fn _new_term(
801 pool: web::Data<PgPool>,
802 course_id: web::Path<Uuid>,
803 user: AuthUser,
804) -> ControllerResult<web::Json<Vec<Term>>> {
805 let mut conn = pool.acquire().await?;
806 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
807 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
808
809 token.authorized_ok(web::Json(glossary))
810}
811
812#[instrument(skip(pool))]
813async fn new_glossary_term(
814 pool: web::Data<PgPool>,
815 course_id: web::Path<Uuid>,
816 new_term: web::Json<TermUpdate>,
817 user: AuthUser,
818) -> ControllerResult<web::Json<Uuid>> {
819 let mut conn = pool.acquire().await?;
820 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
821 let TermUpdate { term, definition } = new_term.into_inner();
822 let term = models::glossary::insert(&mut conn, &term, &definition, *course_id).await?;
823
824 token.authorized_ok(web::Json(term))
825}
826
827#[instrument(skip(pool))]
831pub async fn get_course_users_counts_by_exercise(
832 course_id: web::Path<Uuid>,
833 pool: web::Data<PgPool>,
834 user: AuthUser,
835) -> ControllerResult<web::Json<Vec<ExerciseUserCounts>>> {
836 let mut conn = pool.acquire().await?;
837 let course_id = course_id.into_inner();
838 let token = authorize(
839 &mut conn,
840 Act::ViewStats,
841 Some(user.id),
842 Res::Course(course_id),
843 )
844 .await?;
845
846 let res =
847 models::user_exercise_states::get_course_users_counts_by_exercise(&mut conn, course_id)
848 .await?;
849
850 token.authorized_ok(web::Json(res))
851}
852
853#[instrument(skip(pool))]
861pub async fn post_new_page_ordering(
862 course_id: web::Path<Uuid>,
863 pool: web::Data<PgPool>,
864 user: AuthUser,
865 payload: web::Json<Vec<Page>>,
866) -> ControllerResult<web::Json<()>> {
867 let mut conn = pool.acquire().await?;
868 let course_id = course_id.into_inner();
869 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
870
871 models::pages::reorder_pages(&mut conn, &payload, course_id).await?;
872
873 token.authorized_ok(web::Json(()))
874}
875
876#[instrument(skip(pool))]
882pub async fn post_new_chapter_ordering(
883 course_id: web::Path<Uuid>,
884 pool: web::Data<PgPool>,
885 user: AuthUser,
886 payload: web::Json<Vec<Chapter>>,
887) -> ControllerResult<web::Json<()>> {
888 let mut conn = pool.acquire().await?;
889 let course_id = course_id.into_inner();
890 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
891
892 models::pages::reorder_chapters(&mut conn, &payload, course_id).await?;
893
894 token.authorized_ok(web::Json(()))
895}
896
897#[instrument(skip(pool))]
898async fn get_material_references_by_course_id(
899 course_id: web::Path<Uuid>,
900 pool: web::Data<PgPool>,
901 user: AuthUser,
902) -> ControllerResult<web::Json<Vec<MaterialReference>>> {
903 let mut conn = pool.acquire().await?;
904 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
905
906 let res =
907 models::material_references::get_references_by_course_id(&mut conn, *course_id).await?;
908 token.authorized_ok(web::Json(res))
909}
910
911#[instrument(skip(pool))]
912async fn insert_material_references(
913 course_id: web::Path<Uuid>,
914 payload: web::Json<Vec<NewMaterialReference>>,
915 pool: web::Data<PgPool>,
916 user: AuthUser,
917) -> ControllerResult<web::Json<()>> {
918 let mut conn = pool.acquire().await?;
919 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
920
921 models::material_references::insert_reference(&mut conn, *course_id, payload.0).await?;
922
923 token.authorized_ok(web::Json(()))
924}
925
926#[instrument(skip(pool))]
927async fn update_material_reference(
928 path: web::Path<(Uuid, Uuid)>,
929 pool: web::Data<PgPool>,
930 user: AuthUser,
931 payload: web::Json<NewMaterialReference>,
932) -> ControllerResult<web::Json<()>> {
933 let (course_id, reference_id) = path.into_inner();
934 let mut conn = pool.acquire().await?;
935 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
936
937 models::material_references::update_material_reference_by_id(
938 &mut conn,
939 reference_id,
940 payload.0,
941 )
942 .await?;
943 token.authorized_ok(web::Json(()))
944}
945
946#[instrument(skip(pool))]
947async fn delete_material_reference_by_id(
948 path: web::Path<(Uuid, Uuid)>,
949 pool: web::Data<PgPool>,
950 user: AuthUser,
951) -> ControllerResult<web::Json<()>> {
952 let (course_id, reference_id) = path.into_inner();
953 let mut conn = pool.acquire().await?;
954 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
955
956 models::material_references::delete_reference(&mut conn, reference_id).await?;
957 token.authorized_ok(web::Json(()))
958}
959
960#[instrument(skip(pool))]
961pub async fn update_modules(
962 course_id: web::Path<Uuid>,
963 pool: web::Data<PgPool>,
964 user: AuthUser,
965 payload: web::Json<ModuleUpdates>,
966) -> ControllerResult<web::Json<()>> {
967 let mut conn = pool.acquire().await?;
968 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
969
970 models::course_modules::update_modules(&mut conn, *course_id, payload.into_inner()).await?;
971 token.authorized_ok(web::Json(()))
972}
973
974async fn get_course_default_peer_review(
975 course_id: web::Path<Uuid>,
976 pool: web::Data<PgPool>,
977 user: AuthUser,
978) -> ControllerResult<web::Json<(PeerOrSelfReviewConfig, Vec<PeerOrSelfReviewQuestion>)>> {
979 let mut conn = pool.acquire().await?;
980 let token = authorize(
981 &mut conn,
982 Act::Teach,
983 Some(user.id),
984 Res::Course(*course_id),
985 )
986 .await?;
987
988 let peer_review = models::peer_or_self_review_configs::get_default_for_course_by_course_id(
989 &mut conn, *course_id,
990 )
991 .await?;
992 let peer_or_self_review_questions =
993 models::peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
994 &mut conn,
995 peer_review.id,
996 )
997 .await?;
998 token.authorized_ok(web::Json((peer_review, peer_or_self_review_questions)))
999}
1000
1001#[instrument(skip(pool, user))]
1008async fn post_update_peer_review_queue_reviews_received(
1009 pool: web::Data<PgPool>,
1010 user: AuthUser,
1011 course_id: web::Path<Uuid>,
1012) -> ControllerResult<web::Json<bool>> {
1013 let mut conn = pool.acquire().await?;
1014 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
1015 models::library::peer_or_self_reviewing::update_peer_review_queue_reviews_received(
1016 &mut conn, *course_id,
1017 )
1018 .await?;
1019 token.authorized_ok(web::Json(true))
1020}
1021
1022#[instrument(skip(pool))]
1028pub async fn submission_export(
1029 course_id: web::Path<Uuid>,
1030 pool: web::Data<PgPool>,
1031 user: AuthUser,
1032) -> ControllerResult<HttpResponse> {
1033 let mut conn = pool.acquire().await?;
1034
1035 let token = authorize(
1036 &mut conn,
1037 Act::Teach,
1038 Some(user.id),
1039 Res::Course(*course_id),
1040 )
1041 .await?;
1042
1043 let course = models::courses::get_course(&mut conn, *course_id).await?;
1044
1045 general_export(
1046 pool,
1047 &format!(
1048 "attachment; filename=\"Course: {} - Submissions (exercise tasks) {}.csv\"",
1049 course.name,
1050 Utc::now().format("%Y-%m-%d")
1051 ),
1052 CourseSubmissionExportOperation {
1053 course_id: *course_id,
1054 },
1055 token,
1056 )
1057 .await
1058}
1059
1060#[instrument(skip(pool))]
1066pub async fn user_details_export(
1067 course_id: web::Path<Uuid>,
1068 pool: web::Data<PgPool>,
1069 user: AuthUser,
1070) -> ControllerResult<HttpResponse> {
1071 let mut conn = pool.acquire().await?;
1072
1073 let token = authorize(
1074 &mut conn,
1075 Act::Teach,
1076 Some(user.id),
1077 Res::Course(*course_id),
1078 )
1079 .await?;
1080
1081 let course = models::courses::get_course(&mut conn, *course_id).await?;
1082
1083 general_export(
1084 pool,
1085 &format!(
1086 "attachment; filename=\"Course: {} - User Details {}.csv\"",
1087 course.name,
1088 Utc::now().format("%Y-%m-%d")
1089 ),
1090 UsersExportOperation {
1091 course_id: *course_id,
1092 },
1093 token,
1094 )
1095 .await
1096}
1097
1098#[instrument(skip(pool))]
1104pub async fn exercise_tasks_export(
1105 course_id: web::Path<Uuid>,
1106 pool: web::Data<PgPool>,
1107 user: AuthUser,
1108) -> ControllerResult<HttpResponse> {
1109 let mut conn = pool.acquire().await?;
1110
1111 let token = authorize(
1112 &mut conn,
1113 Act::Teach,
1114 Some(user.id),
1115 Res::Course(*course_id),
1116 )
1117 .await?;
1118
1119 let course = models::courses::get_course(&mut conn, *course_id).await?;
1120
1121 general_export(
1122 pool,
1123 &format!(
1124 "attachment; filename=\"Course: {} - Exercise tasks {}.csv\"",
1125 course.name,
1126 Utc::now().format("%Y-%m-%d")
1127 ),
1128 CourseExerciseTasksExportOperation {
1129 course_id: *course_id,
1130 },
1131 token,
1132 )
1133 .await
1134}
1135
1136#[instrument(skip(pool))]
1142pub async fn course_instances_export(
1143 course_id: web::Path<Uuid>,
1144 pool: web::Data<PgPool>,
1145 user: AuthUser,
1146) -> ControllerResult<HttpResponse> {
1147 let mut conn = pool.acquire().await?;
1148
1149 let token = authorize(
1150 &mut conn,
1151 Act::Teach,
1152 Some(user.id),
1153 Res::Course(*course_id),
1154 )
1155 .await?;
1156
1157 let course = models::courses::get_course(&mut conn, *course_id).await?;
1158
1159 general_export(
1160 pool,
1161 &format!(
1162 "attachment; filename=\"Course: {} - Instances {}.csv\"",
1163 course.name,
1164 Utc::now().format("%Y-%m-%d")
1165 ),
1166 CourseInstancesExportOperation {
1167 course_id: *course_id,
1168 },
1169 token,
1170 )
1171 .await
1172}
1173
1174#[instrument(skip(pool))]
1180pub async fn course_consent_form_answers_export(
1181 course_id: web::Path<Uuid>,
1182 pool: web::Data<PgPool>,
1183 user: AuthUser,
1184) -> ControllerResult<HttpResponse> {
1185 let mut conn = pool.acquire().await?;
1186
1187 let token = authorize(
1188 &mut conn,
1189 Act::Teach,
1190 Some(user.id),
1191 Res::Course(*course_id),
1192 )
1193 .await?;
1194
1195 let course = models::courses::get_course(&mut conn, *course_id).await?;
1196
1197 general_export(
1198 pool,
1199 &format!(
1200 "attachment; filename=\"Course: {} - User Consents {}.csv\"",
1201 course.name,
1202 Utc::now().format("%Y-%m-%d")
1203 ),
1204 CourseResearchFormExportOperation {
1205 course_id: *course_id,
1206 },
1207 token,
1208 )
1209 .await
1210}
1211
1212#[instrument(skip(pool))]
1218pub async fn user_exercise_states_export(
1219 course_id: web::Path<Uuid>,
1220 pool: web::Data<PgPool>,
1221 user: AuthUser,
1222) -> ControllerResult<HttpResponse> {
1223 let mut conn = pool.acquire().await?;
1224
1225 let token = authorize(
1226 &mut conn,
1227 Act::Teach,
1228 Some(user.id),
1229 Res::Course(*course_id),
1230 )
1231 .await?;
1232
1233 let course = models::courses::get_course(&mut conn, *course_id).await?;
1234
1235 general_export(
1236 pool,
1237 &format!(
1238 "attachment; filename=\"Course: {} - User exercise states {}.csv\"",
1239 course.name,
1240 Utc::now().format("%Y-%m-%d")
1241 ),
1242 UserExerciseStatesExportOperation {
1243 course_id: *course_id,
1244 },
1245 token,
1246 )
1247 .await
1248}
1249
1250pub async fn get_page_visit_datum_summary(
1254 course_id: web::Path<Uuid>,
1255 pool: web::Data<PgPool>,
1256 user: AuthUser,
1257) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourse>>> {
1258 let mut conn = pool.acquire().await?;
1259 let course_id = course_id.into_inner();
1260 let token = authorize(
1261 &mut conn,
1262 Act::ViewStats,
1263 Some(user.id),
1264 Res::Course(course_id),
1265 )
1266 .await?;
1267
1268 let res = models::page_visit_datum_summary_by_courses::get_all_for_course(&mut conn, course_id)
1269 .await?;
1270
1271 token.authorized_ok(web::Json(res))
1272}
1273
1274pub async fn get_page_visit_datum_summary_by_pages(
1278 course_id: web::Path<Uuid>,
1279 pool: web::Data<PgPool>,
1280 user: AuthUser,
1281) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByPages>>> {
1282 let mut conn = pool.acquire().await?;
1283 let course_id = course_id.into_inner();
1284 let token = authorize(
1285 &mut conn,
1286 Act::ViewStats,
1287 Some(user.id),
1288 Res::Course(course_id),
1289 )
1290 .await?;
1291
1292 let res =
1293 models::page_visit_datum_summary_by_pages::get_all_for_course(&mut conn, course_id).await?;
1294
1295 token.authorized_ok(web::Json(res))
1296}
1297
1298pub async fn get_page_visit_datum_summary_by_device_types(
1302 course_id: web::Path<Uuid>,
1303 pool: web::Data<PgPool>,
1304 user: AuthUser,
1305) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourseDeviceTypes>>> {
1306 let mut conn = pool.acquire().await?;
1307 let course_id = course_id.into_inner();
1308 let token = authorize(
1309 &mut conn,
1310 Act::ViewStats,
1311 Some(user.id),
1312 Res::Course(course_id),
1313 )
1314 .await?;
1315
1316 let res = models::page_visit_datum_summary_by_courses_device_types::get_all_for_course(
1317 &mut conn, course_id,
1318 )
1319 .await?;
1320
1321 token.authorized_ok(web::Json(res))
1322}
1323
1324pub async fn get_page_visit_datum_summary_by_countries(
1328 course_id: web::Path<Uuid>,
1329 pool: web::Data<PgPool>,
1330 user: AuthUser,
1331) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCoursesCountries>>> {
1332 let mut conn = pool.acquire().await?;
1333 let course_id = course_id.into_inner();
1334 let token = authorize(
1335 &mut conn,
1336 Act::ViewStats,
1337 Some(user.id),
1338 Res::Course(course_id),
1339 )
1340 .await?;
1341
1342 let res = models::page_visit_datum_summary_by_courses_countries::get_all_for_course(
1343 &mut conn, course_id,
1344 )
1345 .await?;
1346
1347 token.authorized_ok(web::Json(res))
1348}
1349
1350pub async fn teacher_reset_course_progress_for_themselves(
1356 course_id: web::Path<Uuid>,
1357 pool: web::Data<PgPool>,
1358 user: AuthUser,
1359) -> ControllerResult<web::Json<bool>> {
1360 let mut conn = pool.acquire().await?;
1361 let course_id = course_id.into_inner();
1362 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1363
1364 let mut tx = conn.begin().await?;
1365 let course_instances =
1366 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
1367 for course_instance in course_instances {
1368 models::course_instances::reset_progress_on_course_instance_for_user(
1369 &mut tx,
1370 user.id,
1371 course_instance.course_id,
1372 )
1373 .await?;
1374 }
1375
1376 tx.commit().await?;
1377 token.authorized_ok(web::Json(true))
1378}
1379
1380pub async fn teacher_reset_course_progress_for_everyone(
1386 course_id: web::Path<Uuid>,
1387 pool: web::Data<PgPool>,
1388 user: AuthUser,
1389) -> ControllerResult<web::Json<bool>> {
1390 let mut conn = pool.acquire().await?;
1391 let course_id = course_id.into_inner();
1392 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1393 let course = models::courses::get_course(&mut conn, course_id).await?;
1394 if !course.is_draft {
1395 return Err(ControllerError::new(
1396 ControllerErrorType::BadRequest,
1397 "Can only reset progress for a draft course.".to_string(),
1398 None,
1399 ));
1400 }
1401 let n_course_module_completions =
1403 models::course_module_completions::get_count_of_distinct_completors_by_course_id(
1404 &mut conn, course_id,
1405 )
1406 .await?;
1407 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(
1408 &mut conn, course_id,
1409 ).await?;
1410 if n_course_module_completions > 200 {
1411 return Err(ControllerError::new(
1412 ControllerErrorType::BadRequest,
1413 "Too many students have completed the course.".to_string(),
1414 None,
1415 ));
1416 }
1417 if n_completions_registered_to_study_registry > 2 {
1418 return Err(ControllerError::new(
1419 ControllerErrorType::BadRequest,
1420 "Too many students have registered their completion to a study registry".to_string(),
1421 None,
1422 ));
1423 }
1424
1425 let mut tx = conn.begin().await?;
1426 let course_instances =
1427 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
1428
1429 for course_instance in course_instances {
1431 let users_in_course_instance =
1432 models::users::get_users_by_course_instance_enrollment(&mut tx, course_instance.id)
1433 .await?;
1434 for user_in_course_instance in users_in_course_instance {
1435 models::course_instances::reset_progress_on_course_instance_for_user(
1436 &mut tx,
1437 user_in_course_instance.id,
1438 course_instance.course_id,
1439 )
1440 .await?;
1441 }
1442 }
1443
1444 tx.commit().await?;
1445 token.authorized_ok(web::Json(true))
1446}
1447
1448#[derive(Debug, Deserialize)]
1449#[cfg_attr(feature = "ts_rs", derive(TS))]
1450pub struct GetSuspectedCheatersQuery {
1451 archive: bool,
1452}
1453
1454#[instrument(skip(pool))]
1458async fn get_all_suspected_cheaters(
1459 user: AuthUser,
1460 params: web::Path<Uuid>,
1461 query: web::Query<GetSuspectedCheatersQuery>,
1462 pool: web::Data<PgPool>,
1463) -> ControllerResult<web::Json<Vec<SuspectedCheaters>>> {
1464 let course_id = params.into_inner();
1465
1466 let mut conn = pool.acquire().await?;
1467 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1468
1469 let course_cheaters = models::suspected_cheaters::get_all_suspected_cheaters_in_course(
1470 &mut conn,
1471 course_id,
1472 query.archive,
1473 )
1474 .await?;
1475
1476 token.authorized_ok(web::Json(course_cheaters))
1477}
1478
1479#[instrument(skip(pool))]
1483async fn insert_threshold(
1484 pool: web::Data<PgPool>,
1485 params: web::Path<Uuid>,
1486 payload: web::Json<ThresholdData>,
1487 user: AuthUser,
1488) -> ControllerResult<web::Json<()>> {
1489 let mut conn = pool.acquire().await?;
1490
1491 let course_id = params.into_inner();
1492 let new_threshold = payload.0;
1493 let duration: Option<i32> = new_threshold.duration_seconds;
1494
1495 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1496
1497 models::suspected_cheaters::insert_thresholds(
1498 &mut conn,
1499 course_id,
1500 duration,
1501 new_threshold.points,
1502 )
1503 .await?;
1504
1505 token.authorized_ok(web::Json(()))
1506}
1507
1508#[instrument(skip(pool))]
1512async fn teacher_archive_suspected_cheater(
1513 user: AuthUser,
1514 path: web::Path<(Uuid, Uuid)>,
1515 pool: web::Data<PgPool>,
1516) -> ControllerResult<web::Json<()>> {
1517 let (course_id, user_id) = path.into_inner();
1518
1519 let mut conn = pool.acquire().await?;
1520 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1521
1522 models::suspected_cheaters::archive_suspected_cheater(&mut conn, user_id).await?;
1523
1524 token.authorized_ok(web::Json(()))
1525}
1526
1527#[instrument(skip(pool))]
1531async fn teacher_approve_suspected_cheater(
1532 user: AuthUser,
1533 path: web::Path<(Uuid, Uuid)>,
1534 pool: web::Data<PgPool>,
1535) -> ControllerResult<web::Json<()>> {
1536 let (course_id, user_id) = path.into_inner();
1537
1538 let mut conn = pool.acquire().await?;
1539 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1540
1541 models::suspected_cheaters::approve_suspected_cheater(&mut conn, user_id).await?;
1542
1543 models::course_module_completions::update_passed_and_grade_status(
1546 &mut conn, course_id, user_id, false, 0,
1547 )
1548 .await?;
1549
1550 token.authorized_ok(web::Json(()))
1551}
1552
1553#[instrument(skip(pool))]
1557async fn add_user_to_course_with_join_code(
1558 course_id: web::Path<Uuid>,
1559 user: AuthUser,
1560 pool: web::Data<PgPool>,
1561) -> ControllerResult<web::Json<Uuid>> {
1562 let mut conn = pool.acquire().await?;
1563 let token = skip_authorize();
1564
1565 let joined =
1566 models::join_code_uses::insert(&mut conn, PKeyPolicy::Generate, user.id, *course_id)
1567 .await?;
1568 token.authorized_ok(web::Json(joined))
1569}
1570
1571#[instrument(skip(pool))]
1575async fn set_join_code_for_course(
1576 id: web::Path<Uuid>,
1577 pool: web::Data<PgPool>,
1578 user: AuthUser,
1579) -> ControllerResult<HttpResponse> {
1580 let mut conn = pool.acquire().await?;
1581 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*id)).await?;
1582
1583 const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
1584 abcdefghjkmnpqrstuvwxyz";
1585 const PASSWORD_LEN: usize = 64;
1586 let mut rng = rand::rng();
1587
1588 let code: String = (0..PASSWORD_LEN)
1589 .map(|_| {
1590 let idx = rng.random_range(0..CHARSET.len());
1591 CHARSET[idx] as char
1592 })
1593 .collect();
1594
1595 models::courses::set_join_code_for_course(&mut conn, *id, code).await?;
1596 token.authorized_ok(HttpResponse::Ok().finish())
1597}
1598
1599#[instrument(skip(pool))]
1603async fn get_course_with_join_code(
1604 join_code: web::Path<String>,
1605 user: AuthUser,
1606 pool: web::Data<PgPool>,
1607) -> ControllerResult<web::Json<Course>> {
1608 let mut conn = pool.acquire().await?;
1609 let token = skip_authorize();
1610 let course =
1611 models::courses::get_course_with_join_code(&mut conn, join_code.to_string()).await?;
1612
1613 token.authorized_ok(web::Json(course))
1614}
1615
1616#[instrument(skip(payload, pool))]
1620async fn post_partners_block(
1621 path: web::Path<Uuid>,
1622 payload: web::Json<Option<serde_json::Value>>,
1623 pool: web::Data<PgPool>,
1624 user: AuthUser,
1625) -> ControllerResult<web::Json<PartnersBlock>> {
1626 let course_id = path.into_inner();
1627
1628 let content = payload.into_inner();
1629 let mut conn = pool.acquire().await?;
1630 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1631
1632 let upserted_partner_block =
1633 models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
1634
1635 token.authorized_ok(web::Json(upserted_partner_block))
1636}
1637
1638#[instrument(skip(pool))]
1642async fn get_partners_block(
1643 path: web::Path<Uuid>,
1644 user: AuthUser,
1645 pool: web::Data<PgPool>,
1646) -> ControllerResult<web::Json<PartnersBlock>> {
1647 let course_id = path.into_inner();
1648 let mut conn = pool.acquire().await?;
1649 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1650
1651 let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
1653
1654 let partner_block = if course_exists {
1655 models::partner_block::get_partner_block(&mut conn, course_id).await?
1657 } else {
1658 let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
1660
1661 models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
1663 };
1664
1665 token.authorized_ok(web::Json(partner_block))
1666}
1667
1668#[instrument(skip(pool))]
1672async fn delete_partners_block(
1673 path: web::Path<Uuid>,
1674 pool: web::Data<PgPool>,
1675 user: AuthUser,
1676) -> ControllerResult<web::Json<PartnersBlock>> {
1677 let course_id = path.into_inner();
1678 let mut conn = pool.acquire().await?;
1679 let token = authorize(
1680 &mut conn,
1681 Act::UsuallyUnacceptableDeletion,
1682 Some(user.id),
1683 Res::Course(course_id),
1684 )
1685 .await?;
1686 let deleted_partners_block =
1687 models::partner_block::delete_partner_block(&mut conn, course_id).await?;
1688
1689 token.authorized_ok(web::Json(deleted_partners_block))
1690}
1691
1692pub fn _add_routes(cfg: &mut ServiceConfig) {
1700 cfg.service(web::scope("/{course_id}/stats").configure(stats::_add_routes))
1701 .service(web::scope("/{course_id}/chatbots").configure(chatbots::_add_routes))
1702 .route("/{course_id}", web::get().to(get_course))
1703 .route("", web::post().to(post_new_course))
1704 .route("/{course_id}", web::put().to(update_course))
1705 .route("/{course_id}", web::delete().to(delete_course))
1706 .route(
1707 "/{course_id}/daily-submission-counts",
1708 web::get().to(get_daily_submission_counts),
1709 )
1710 .route(
1711 "/{course_id}/daily-users-who-have-submitted-something",
1712 web::get().to(get_daily_user_counts_with_submissions),
1713 )
1714 .route("/{course_id}/exercises", web::get().to(get_all_exercises))
1715 .route(
1716 "/{course_id}/exercises-and-count-of-answers-requiring-attention",
1717 web::get().to(get_all_exercises_and_count_of_answers_requiring_attention),
1718 )
1719 .route(
1720 "/{course_id}/structure",
1721 web::get().to(get_course_structure),
1722 )
1723 .route(
1724 "/{course_id}/language-versions",
1725 web::get().to(get_all_course_language_versions),
1726 )
1727 .route(
1728 "/{course_id}/create-copy",
1729 web::post().to(create_course_copy),
1730 )
1731 .route("/{course_id}/upload", web::post().to(add_media_for_course))
1732 .route(
1733 "/{course_id}/weekday-hour-submission-counts",
1734 web::get().to(get_weekday_hour_submission_counts),
1735 )
1736 .route(
1737 "/{course_id}/submission-counts-by-exercise",
1738 web::get().to(get_submission_counts_by_exercise),
1739 )
1740 .route(
1741 "/{course_id}/course-instances",
1742 web::get().to(get_course_instances),
1743 )
1744 .route("/{course_id}/feedback", web::get().to(get_feedback))
1745 .route(
1746 "/{course_id}/feedback-count",
1747 web::get().to(get_feedback_count),
1748 )
1749 .route(
1750 "/{course_id}/new-course-instance",
1751 web::post().to(new_course_instance),
1752 )
1753 .route("/{course_id}/glossary", web::get().to(glossary))
1754 .route("/{course_id}/glossary", web::post().to(new_glossary_term))
1755 .route(
1756 "/{course_id}/course-users-counts-by-exercise",
1757 web::get().to(get_course_users_counts_by_exercise),
1758 )
1759 .route(
1760 "/{course_id}/new-page-ordering",
1761 web::post().to(post_new_page_ordering),
1762 )
1763 .route(
1764 "/{course_id}/new-chapter-ordering",
1765 web::post().to(post_new_chapter_ordering),
1766 )
1767 .route(
1768 "/{course_id}/references",
1769 web::get().to(get_material_references_by_course_id),
1770 )
1771 .route(
1772 "/{course_id}/references",
1773 web::post().to(insert_material_references),
1774 )
1775 .route(
1776 "/{course_id}/references/{reference_id}",
1777 web::post().to(update_material_reference),
1778 )
1779 .route(
1780 "/{course_id}/references/{reference_id}",
1781 web::delete().to(delete_material_reference_by_id),
1782 )
1783 .route(
1784 "/{course_id}/course-modules",
1785 web::post().to(update_modules),
1786 )
1787 .route(
1788 "/{course_id}/default-peer-review",
1789 web::get().to(get_course_default_peer_review),
1790 )
1791 .route(
1792 "/{course_id}/update-peer-review-queue-reviews-received",
1793 web::post().to(post_update_peer_review_queue_reviews_received),
1794 )
1795 .route(
1796 "/{course_id}/breadcrumb-info",
1797 web::get().to(get_course_breadcrumb_info),
1798 )
1799 .route(
1800 "/{course_id}/user-settings/{user_id}",
1801 web::get().to(get_user_course_settings),
1802 )
1803 .route(
1804 "/{course_id}/export-submissions",
1805 web::get().to(submission_export),
1806 )
1807 .route(
1808 "/{course_id}/export-user-details",
1809 web::get().to(user_details_export),
1810 )
1811 .route(
1812 "/{course_id}/export-exercise-tasks",
1813 web::get().to(exercise_tasks_export),
1814 )
1815 .route(
1816 "/{course_id}/export-course-instances",
1817 web::get().to(course_instances_export),
1818 )
1819 .route(
1820 "/{course_id}/export-course-user-consents",
1821 web::get().to(course_consent_form_answers_export),
1822 )
1823 .route(
1824 "/{course_id}/export-user-exercise-states",
1825 web::get().to(user_exercise_states_export),
1826 )
1827 .route(
1828 "/{course_id}/page-visit-datum-summary",
1829 web::get().to(get_page_visit_datum_summary),
1830 )
1831 .route(
1832 "/{course_id}/page-visit-datum-summary-by-pages",
1833 web::get().to(get_page_visit_datum_summary_by_pages),
1834 )
1835 .route(
1836 "/{course_id}/page-visit-datum-summary-by-device-types",
1837 web::get().to(get_page_visit_datum_summary_by_device_types),
1838 )
1839 .route(
1840 "/{course_id}/page-visit-datum-summary-by-countries",
1841 web::get().to(get_page_visit_datum_summary_by_countries),
1842 )
1843 .route(
1844 "/{course_id}/teacher-reset-course-progress-for-themselves",
1845 web::delete().to(teacher_reset_course_progress_for_themselves),
1846 )
1847 .route("/{course_id}/threshold", web::post().to(insert_threshold))
1848 .route(
1849 "/{course_id}/suspected-cheaters",
1850 web::get().to(get_all_suspected_cheaters),
1851 )
1852 .route(
1853 "/{course_id}/suspected-cheaters/archive/{id}",
1854 web::post().to(teacher_archive_suspected_cheater),
1855 )
1856 .route(
1857 "/{course_id}/suspected-cheaters/approve/{id}",
1858 web::post().to(teacher_approve_suspected_cheater),
1859 )
1860 .route(
1861 "/{course_id}/teacher-reset-course-progress-for-everyone",
1862 web::delete().to(teacher_reset_course_progress_for_everyone),
1863 )
1864 .route(
1865 "/{course_id}/join-course-with-join-code",
1866 web::post().to(add_user_to_course_with_join_code),
1867 )
1868 .route(
1869 "/{course_id}/partners-block",
1870 web::post().to(post_partners_block),
1871 )
1872 .route(
1873 "/{course_id}/partners-block",
1874 web::get().to(get_partners_block),
1875 )
1876 .route(
1877 "/{course_id}/partners-block",
1878 web::delete().to(delete_partners_block),
1879 )
1880 .route(
1881 "/{course_id}/set-join-code",
1882 web::post().to(set_join_code_for_course),
1883 )
1884 .route(
1885 "/{course_id}/reprocess-completions",
1886 web::post().to(post_reprocess_module_completions),
1887 )
1888 .route(
1889 "/join/{join_code}",
1890 web::get().to(get_course_with_join_code),
1891 );
1892}