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, Threshold},
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 get_all_thresholds(
1484 user: AuthUser,
1485 params: web::Path<Uuid>,
1486 pool: web::Data<PgPool>,
1487) -> ControllerResult<web::Json<Vec<Threshold>>> {
1488 let mut conn = pool.acquire().await?;
1489 let course_id = params.into_inner();
1490
1491 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1492
1493 let thresholds =
1494 models::suspected_cheaters::get_all_thresholds_for_course(&mut conn, course_id).await?;
1495
1496 token.authorized_ok(web::Json(thresholds))
1497}
1498
1499#[instrument(skip(pool))]
1503async fn teacher_archive_suspected_cheater(
1504 user: AuthUser,
1505 path: web::Path<(Uuid, Uuid)>,
1506 pool: web::Data<PgPool>,
1507) -> ControllerResult<web::Json<()>> {
1508 let (course_id, user_id) = path.into_inner();
1509
1510 let mut conn = pool.acquire().await?;
1511 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1512
1513 models::suspected_cheaters::archive_suspected_cheater(&mut conn, user_id).await?;
1514
1515 token.authorized_ok(web::Json(()))
1516}
1517
1518#[instrument(skip(pool))]
1522async fn teacher_approve_suspected_cheater(
1523 user: AuthUser,
1524 path: web::Path<(Uuid, Uuid)>,
1525 pool: web::Data<PgPool>,
1526) -> ControllerResult<web::Json<()>> {
1527 let (course_id, user_id) = path.into_inner();
1528
1529 let mut conn = pool.acquire().await?;
1530 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1531
1532 models::suspected_cheaters::approve_suspected_cheater(&mut conn, user_id).await?;
1533
1534 models::course_module_completions::update_passed_and_grade_status(
1537 &mut conn, course_id, user_id, false, 0,
1538 )
1539 .await?;
1540
1541 token.authorized_ok(web::Json(()))
1542}
1543
1544#[instrument(skip(pool))]
1548async fn add_user_to_course_with_join_code(
1549 course_id: web::Path<Uuid>,
1550 user: AuthUser,
1551 pool: web::Data<PgPool>,
1552) -> ControllerResult<web::Json<Uuid>> {
1553 let mut conn = pool.acquire().await?;
1554 let token = skip_authorize();
1555
1556 let joined =
1557 models::join_code_uses::insert(&mut conn, PKeyPolicy::Generate, user.id, *course_id)
1558 .await?;
1559 token.authorized_ok(web::Json(joined))
1560}
1561
1562#[instrument(skip(pool))]
1566async fn set_join_code_for_course(
1567 id: web::Path<Uuid>,
1568 pool: web::Data<PgPool>,
1569 user: AuthUser,
1570) -> ControllerResult<HttpResponse> {
1571 let mut conn = pool.acquire().await?;
1572 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*id)).await?;
1573
1574 const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
1575 abcdefghjkmnpqrstuvwxyz";
1576 const PASSWORD_LEN: usize = 64;
1577 let mut rng = rand::rng();
1578
1579 let code: String = (0..PASSWORD_LEN)
1580 .map(|_| {
1581 let idx = rng.random_range(0..CHARSET.len());
1582 CHARSET[idx] as char
1583 })
1584 .collect();
1585
1586 models::courses::set_join_code_for_course(&mut conn, *id, code).await?;
1587 token.authorized_ok(HttpResponse::Ok().finish())
1588}
1589
1590#[instrument(skip(pool))]
1594async fn get_course_with_join_code(
1595 join_code: web::Path<String>,
1596 user: AuthUser,
1597 pool: web::Data<PgPool>,
1598) -> ControllerResult<web::Json<Course>> {
1599 let mut conn = pool.acquire().await?;
1600 let token = skip_authorize();
1601 let course =
1602 models::courses::get_course_with_join_code(&mut conn, join_code.to_string()).await?;
1603
1604 token.authorized_ok(web::Json(course))
1605}
1606
1607#[instrument(skip(payload, pool))]
1611async fn post_partners_block(
1612 path: web::Path<Uuid>,
1613 payload: web::Json<Option<serde_json::Value>>,
1614 pool: web::Data<PgPool>,
1615 user: AuthUser,
1616) -> ControllerResult<web::Json<PartnersBlock>> {
1617 let course_id = path.into_inner();
1618
1619 let content = payload.into_inner();
1620 let mut conn = pool.acquire().await?;
1621 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1622
1623 let upserted_partner_block =
1624 models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
1625
1626 token.authorized_ok(web::Json(upserted_partner_block))
1627}
1628
1629#[instrument(skip(pool))]
1633async fn get_partners_block(
1634 path: web::Path<Uuid>,
1635 user: AuthUser,
1636 pool: web::Data<PgPool>,
1637) -> ControllerResult<web::Json<PartnersBlock>> {
1638 let course_id = path.into_inner();
1639 let mut conn = pool.acquire().await?;
1640 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1641
1642 let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
1644
1645 let partner_block = if course_exists {
1646 models::partner_block::get_partner_block(&mut conn, course_id).await?
1648 } else {
1649 let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
1651
1652 models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
1654 };
1655
1656 token.authorized_ok(web::Json(partner_block))
1657}
1658
1659#[instrument(skip(pool))]
1663async fn delete_partners_block(
1664 path: web::Path<Uuid>,
1665 pool: web::Data<PgPool>,
1666 user: AuthUser,
1667) -> ControllerResult<web::Json<PartnersBlock>> {
1668 let course_id = path.into_inner();
1669 let mut conn = pool.acquire().await?;
1670 let token = authorize(
1671 &mut conn,
1672 Act::UsuallyUnacceptableDeletion,
1673 Some(user.id),
1674 Res::Course(course_id),
1675 )
1676 .await?;
1677 let deleted_partners_block =
1678 models::partner_block::delete_partner_block(&mut conn, course_id).await?;
1679
1680 token.authorized_ok(web::Json(deleted_partners_block))
1681}
1682
1683pub fn _add_routes(cfg: &mut ServiceConfig) {
1691 cfg.service(web::scope("/{course_id}/stats").configure(stats::_add_routes))
1692 .service(web::scope("/{course_id}/chatbots").configure(chatbots::_add_routes))
1693 .route("/{course_id}", web::get().to(get_course))
1694 .route("", web::post().to(post_new_course))
1695 .route("/{course_id}", web::put().to(update_course))
1696 .route("/{course_id}", web::delete().to(delete_course))
1697 .route(
1698 "/{course_id}/daily-submission-counts",
1699 web::get().to(get_daily_submission_counts),
1700 )
1701 .route(
1702 "/{course_id}/daily-users-who-have-submitted-something",
1703 web::get().to(get_daily_user_counts_with_submissions),
1704 )
1705 .route("/{course_id}/exercises", web::get().to(get_all_exercises))
1706 .route(
1707 "/{course_id}/exercises-and-count-of-answers-requiring-attention",
1708 web::get().to(get_all_exercises_and_count_of_answers_requiring_attention),
1709 )
1710 .route(
1711 "/{course_id}/structure",
1712 web::get().to(get_course_structure),
1713 )
1714 .route(
1715 "/{course_id}/language-versions",
1716 web::get().to(get_all_course_language_versions),
1717 )
1718 .route(
1719 "/{course_id}/create-copy",
1720 web::post().to(create_course_copy),
1721 )
1722 .route("/{course_id}/upload", web::post().to(add_media_for_course))
1723 .route(
1724 "/{course_id}/weekday-hour-submission-counts",
1725 web::get().to(get_weekday_hour_submission_counts),
1726 )
1727 .route(
1728 "/{course_id}/submission-counts-by-exercise",
1729 web::get().to(get_submission_counts_by_exercise),
1730 )
1731 .route(
1732 "/{course_id}/course-instances",
1733 web::get().to(get_course_instances),
1734 )
1735 .route("/{course_id}/feedback", web::get().to(get_feedback))
1736 .route(
1737 "/{course_id}/feedback-count",
1738 web::get().to(get_feedback_count),
1739 )
1740 .route(
1741 "/{course_id}/new-course-instance",
1742 web::post().to(new_course_instance),
1743 )
1744 .route("/{course_id}/glossary", web::get().to(glossary))
1745 .route("/{course_id}/glossary", web::post().to(new_glossary_term))
1746 .route(
1747 "/{course_id}/course-users-counts-by-exercise",
1748 web::get().to(get_course_users_counts_by_exercise),
1749 )
1750 .route(
1751 "/{course_id}/new-page-ordering",
1752 web::post().to(post_new_page_ordering),
1753 )
1754 .route(
1755 "/{course_id}/new-chapter-ordering",
1756 web::post().to(post_new_chapter_ordering),
1757 )
1758 .route(
1759 "/{course_id}/references",
1760 web::get().to(get_material_references_by_course_id),
1761 )
1762 .route(
1763 "/{course_id}/references",
1764 web::post().to(insert_material_references),
1765 )
1766 .route(
1767 "/{course_id}/references/{reference_id}",
1768 web::post().to(update_material_reference),
1769 )
1770 .route(
1771 "/{course_id}/references/{reference_id}",
1772 web::delete().to(delete_material_reference_by_id),
1773 )
1774 .route(
1775 "/{course_id}/course-modules",
1776 web::post().to(update_modules),
1777 )
1778 .route(
1779 "/{course_id}/default-peer-review",
1780 web::get().to(get_course_default_peer_review),
1781 )
1782 .route(
1783 "/{course_id}/update-peer-review-queue-reviews-received",
1784 web::post().to(post_update_peer_review_queue_reviews_received),
1785 )
1786 .route(
1787 "/{course_id}/breadcrumb-info",
1788 web::get().to(get_course_breadcrumb_info),
1789 )
1790 .route(
1791 "/{course_id}/user-settings/{user_id}",
1792 web::get().to(get_user_course_settings),
1793 )
1794 .route(
1795 "/{course_id}/export-submissions",
1796 web::get().to(submission_export),
1797 )
1798 .route(
1799 "/{course_id}/export-user-details",
1800 web::get().to(user_details_export),
1801 )
1802 .route(
1803 "/{course_id}/export-exercise-tasks",
1804 web::get().to(exercise_tasks_export),
1805 )
1806 .route(
1807 "/{course_id}/export-course-instances",
1808 web::get().to(course_instances_export),
1809 )
1810 .route(
1811 "/{course_id}/export-course-user-consents",
1812 web::get().to(course_consent_form_answers_export),
1813 )
1814 .route(
1815 "/{course_id}/export-user-exercise-states",
1816 web::get().to(user_exercise_states_export),
1817 )
1818 .route(
1819 "/{course_id}/page-visit-datum-summary",
1820 web::get().to(get_page_visit_datum_summary),
1821 )
1822 .route(
1823 "/{course_id}/page-visit-datum-summary-by-pages",
1824 web::get().to(get_page_visit_datum_summary_by_pages),
1825 )
1826 .route(
1827 "/{course_id}/page-visit-datum-summary-by-device-types",
1828 web::get().to(get_page_visit_datum_summary_by_device_types),
1829 )
1830 .route(
1831 "/{course_id}/page-visit-datum-summary-by-countries",
1832 web::get().to(get_page_visit_datum_summary_by_countries),
1833 )
1834 .route(
1835 "/{course_id}/teacher-reset-course-progress-for-themselves",
1836 web::delete().to(teacher_reset_course_progress_for_themselves),
1837 )
1838 .route("/{course_id}/thresholds", web::get().to(get_all_thresholds))
1839 .route(
1840 "/{course_id}/suspected-cheaters",
1841 web::get().to(get_all_suspected_cheaters),
1842 )
1843 .route(
1844 "/{course_id}/suspected-cheaters/archive/{id}",
1845 web::post().to(teacher_archive_suspected_cheater),
1846 )
1847 .route(
1848 "/{course_id}/suspected-cheaters/approve/{id}",
1849 web::post().to(teacher_approve_suspected_cheater),
1850 )
1851 .route(
1852 "/{course_id}/teacher-reset-course-progress-for-everyone",
1853 web::delete().to(teacher_reset_course_progress_for_everyone),
1854 )
1855 .route(
1856 "/{course_id}/join-course-with-join-code",
1857 web::post().to(add_user_to_course_with_join_code),
1858 )
1859 .route(
1860 "/{course_id}/partners-block",
1861 web::post().to(post_partners_block),
1862 )
1863 .route(
1864 "/{course_id}/partners-block",
1865 web::get().to(get_partners_block),
1866 )
1867 .route(
1868 "/{course_id}/partners-block",
1869 web::delete().to(delete_partners_block),
1870 )
1871 .route(
1872 "/{course_id}/set-join-code",
1873 web::post().to(set_join_code_for_course),
1874 )
1875 .route(
1876 "/{course_id}/reprocess-completions",
1877 web::post().to(post_reprocess_module_completions),
1878 )
1879 .route(
1880 "/join/{join_code}",
1881 web::get().to(get_course_with_join_code),
1882 );
1883}