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