1pub mod stats;
4
5use chrono::Utc;
6use domain::csv_export::user_exericse_states_export::UserExerciseStatesExportOperation;
7use headless_lms_models::{
8 partner_block::PartnersBlock,
9 suspected_cheaters::{SuspectedCheaters, ThresholdData},
10};
11use rand::Rng;
12use std::sync::Arc;
13
14use headless_lms_utils::strings::is_ietf_language_code_like;
15use models::{
16 chapters::Chapter,
17 course_instances::{CourseInstance, CourseInstanceForm, NewCourseInstance},
18 course_modules::ModuleUpdates,
19 courses::{Course, CourseBreadcrumbInfo, CourseStructure, CourseUpdate, NewCourse},
20 exercise_slide_submissions::{
21 self, ExerciseAnswersInCourseRequiringAttentionCount, ExerciseSlideSubmissionCount,
22 ExerciseSlideSubmissionCountByExercise, ExerciseSlideSubmissionCountByWeekAndHour,
23 },
24 exercises::Exercise,
25 feedback::{self, Feedback, FeedbackCount},
26 glossary::{Term, TermUpdate},
27 library,
28 material_references::{MaterialReference, NewMaterialReference},
29 page_visit_datum_summary_by_courses::PageVisitDatumSummaryByCourse,
30 page_visit_datum_summary_by_courses_countries::PageVisitDatumSummaryByCoursesCountries,
31 page_visit_datum_summary_by_courses_device_types::PageVisitDatumSummaryByCourseDeviceTypes,
32 page_visit_datum_summary_by_pages::PageVisitDatumSummaryByPages,
33 pages::Page,
34 peer_or_self_review_configs::PeerOrSelfReviewConfig,
35 peer_or_self_review_questions::PeerOrSelfReviewQuestion,
36 user_exercise_states::ExerciseUserCounts,
37};
38
39use crate::{
40 domain::models_requests::{self, JwtKey},
41 prelude::*,
42};
43
44use headless_lms_models::course_language_groups;
45
46use crate::domain::csv_export::course_instance_export::CourseInstancesExportOperation;
47use crate::domain::csv_export::course_research_form_questions_answers_export::CourseResearchFormExportOperation;
48use crate::domain::csv_export::exercise_tasks_export::CourseExerciseTasksExportOperation;
49use crate::domain::csv_export::general_export;
50use crate::domain::csv_export::submissions::CourseSubmissionExportOperation;
51use crate::domain::csv_export::users_export::UsersExportOperation;
52
53#[instrument(skip(pool))]
57async fn get_course(
58 course_id: web::Path<Uuid>,
59 pool: web::Data<PgPool>,
60 user: AuthUser,
61) -> ControllerResult<web::Json<Course>> {
62 let mut conn = pool.acquire().await?;
63 let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
64 let course = models::courses::get_course(&mut conn, *course_id).await?;
65 token.authorized_ok(web::Json(course))
66}
67
68#[instrument(skip(pool))]
72async fn get_course_breadcrumb_info(
73 course_id: web::Path<Uuid>,
74 pool: web::Data<PgPool>,
75 user: AuthUser,
76) -> ControllerResult<web::Json<CourseBreadcrumbInfo>> {
77 let mut conn = pool.acquire().await?;
78 let user_id = Some(user.id);
79 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
80 let info = models::courses::get_course_breadcrumb_info(&mut conn, *course_id).await?;
81 token.authorized_ok(web::Json(info))
82}
83
84#[instrument(skip(pool, app_conf))]
102async fn post_new_course(
103 request_id: RequestId,
104 pool: web::Data<PgPool>,
105 payload: web::Json<NewCourse>,
106 user: AuthUser,
107 app_conf: web::Data<ApplicationConfiguration>,
108 jwt_key: web::Data<JwtKey>,
109) -> ControllerResult<web::Json<Course>> {
110 let mut conn = pool.acquire().await?;
111 let new_course = payload.0;
112 if !is_ietf_language_code_like(&new_course.language_code) {
113 return Err(ControllerError::new(
114 ControllerErrorType::BadRequest,
115 "Malformed language code.".to_string(),
116 None,
117 ));
118 }
119 let token = authorize(
120 &mut conn,
121 Act::CreateCoursesOrExams,
122 Some(user.id),
123 Res::Organization(new_course.organization_id),
124 )
125 .await?;
126
127 let mut tx = conn.begin().await?;
128 let (course, ..) = library::content_management::create_new_course(
129 &mut tx,
130 PKeyPolicy::Generate,
131 new_course,
132 user.id,
133 models_requests::make_spec_fetcher(
134 app_conf.base_url.clone(),
135 request_id.0,
136 Arc::clone(&jwt_key),
137 ),
138 models_requests::fetch_service_info,
139 )
140 .await?;
141 models::roles::insert(
142 &mut tx,
143 user.id,
144 models::roles::UserRole::Teacher,
145 models::roles::RoleDomain::Course(course.id),
146 )
147 .await?;
148 tx.commit().await?;
149
150 token.authorized_ok(web::Json(course))
151}
152
153#[instrument(skip(pool))]
169async fn update_course(
170 payload: web::Json<CourseUpdate>,
171 course_id: web::Path<Uuid>,
172 pool: web::Data<PgPool>,
173 user: AuthUser,
174) -> ControllerResult<web::Json<Course>> {
175 let mut conn = pool.acquire().await?;
176 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
177 let course_update = payload.0;
178 let course_before_update = models::courses::get_course(&mut conn, *course_id).await?;
179 if course_update.can_add_chatbot != course_before_update.can_add_chatbot {
180 let _token2 =
182 authorize(&mut conn, Act::Teach, Some(user.id), Res::GlobalPermissions).await?;
183 }
184 let course = models::courses::update_course(&mut conn, *course_id, course_update).await?;
185 token.authorized_ok(web::Json(course))
186}
187
188#[instrument(skip(pool))]
192async fn delete_course(
193 course_id: web::Path<Uuid>,
194 pool: web::Data<PgPool>,
195 user: AuthUser,
196) -> ControllerResult<web::Json<Course>> {
197 let mut conn = pool.acquire().await?;
198 let token = authorize(
199 &mut conn,
200 Act::UsuallyUnacceptableDeletion,
201 Some(user.id),
202 Res::Course(*course_id),
203 )
204 .await?;
205 let course = models::courses::delete_course(&mut conn, *course_id).await?;
206
207 token.authorized_ok(web::Json(course))
208}
209
210#[instrument(skip(pool, file_store, app_conf))]
258async fn get_course_structure(
259 course_id: web::Path<Uuid>,
260 pool: web::Data<PgPool>,
261 user: AuthUser,
262 file_store: web::Data<dyn FileStore>,
263 app_conf: web::Data<ApplicationConfiguration>,
264) -> ControllerResult<web::Json<CourseStructure>> {
265 let mut conn = pool.acquire().await?;
266 let token = authorize(
267 &mut conn,
268 Act::ViewInternalCourseStructure,
269 Some(user.id),
270 Res::Course(*course_id),
271 )
272 .await?;
273 let course_structure = models::courses::get_course_structure(
274 &mut conn,
275 *course_id,
276 file_store.as_ref(),
277 app_conf.as_ref(),
278 )
279 .await?;
280
281 token.authorized_ok(web::Json(course_structure))
282}
283
284#[instrument(skip(payload, request, pool, file_store, app_conf))]
300async fn add_media_for_course(
301 course_id: web::Path<Uuid>,
302 payload: Multipart,
303 request: HttpRequest,
304 pool: web::Data<PgPool>,
305 user: AuthUser,
306 file_store: web::Data<dyn FileStore>,
307 app_conf: web::Data<ApplicationConfiguration>,
308) -> ControllerResult<web::Json<UploadResult>> {
309 let mut conn = pool.acquire().await?;
310 let course = models::courses::get_course(&mut conn, *course_id).await?;
311 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
312 let media_path = upload_file_from_cms(
313 request.headers(),
314 payload,
315 StoreKind::Course(course.id),
316 file_store.as_ref(),
317 &mut conn,
318 user,
319 )
320 .await?;
321 let download_url = file_store.get_download_url(media_path.as_path(), app_conf.as_ref());
322
323 token.authorized_ok(web::Json(UploadResult { url: download_url }))
324}
325
326#[instrument(skip(pool))]
330async fn get_all_exercises(
331 pool: web::Data<PgPool>,
332 course_id: web::Path<Uuid>,
333 user: AuthUser,
334) -> ControllerResult<web::Json<Vec<Exercise>>> {
335 let mut conn = pool.acquire().await?;
336 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
337 let exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
338
339 token.authorized_ok(web::Json(exercises))
340}
341
342#[instrument(skip(pool))]
346async fn get_all_exercises_and_count_of_answers_requiring_attention(
347 pool: web::Data<PgPool>,
348 course_id: web::Path<Uuid>,
349 user: AuthUser,
350) -> ControllerResult<web::Json<Vec<ExerciseAnswersInCourseRequiringAttentionCount>>> {
351 let mut conn = pool.acquire().await?;
352 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
353 let _exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
354 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?;
355 token.authorized_ok(web::Json(count_of_answers_requiring_attention))
356}
357
358#[instrument(skip(pool))]
370async fn get_all_course_language_versions(
371 pool: web::Data<PgPool>,
372 course_id: web::Path<Uuid>,
373 user: AuthUser,
374) -> ControllerResult<web::Json<Vec<Course>>> {
375 let mut conn = pool.acquire().await?;
376 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
377 let course = models::courses::get_course(&mut conn, *course_id).await?;
378 let language_versions =
379 models::courses::get_all_language_versions_of_course(&mut conn, &course).await?;
380
381 token.authorized_ok(web::Json(language_versions))
382}
383
384#[derive(Deserialize, Debug)]
385#[serde(tag = "mode", rename_all = "snake_case")]
386#[cfg_attr(feature = "ts_rs", derive(TS))]
387pub enum CopyCourseMode {
388 Duplicate,
390 SameLanguageGroup,
392 ExistingLanguageGroup { target_course_id: Uuid },
394 NewLanguageGroup,
396}
397
398#[derive(Deserialize, Debug)]
399#[cfg_attr(feature = "ts_rs", derive(TS))]
400pub struct CopyCourseRequest {
401 #[serde(flatten)]
402 pub new_course: NewCourse,
403 pub mode: CopyCourseMode,
404}
405
406#[instrument(skip(pool))]
450pub async fn create_course_copy(
451 pool: web::Data<PgPool>,
452 course_id: web::Path<Uuid>,
453 payload: web::Json<CopyCourseRequest>,
454 user: AuthUser,
455) -> ControllerResult<web::Json<Course>> {
456 let mut conn = pool.acquire().await?;
457 let token = authorize(
458 &mut conn,
459 Act::Duplicate,
460 Some(user.id),
461 Res::Course(*course_id),
462 )
463 .await?;
464
465 let mut tx = conn.begin().await?;
466
467 let copied_course = match &payload.mode {
468 CopyCourseMode::Duplicate => {
469 models::library::copying::copy_course(
470 &mut tx,
471 *course_id,
472 &payload.new_course,
473 false,
474 user.id,
475 )
476 .await?
477 }
478 CopyCourseMode::SameLanguageGroup => {
479 models::library::copying::copy_course(
480 &mut tx,
481 *course_id,
482 &payload.new_course,
483 true,
484 user.id,
485 )
486 .await?
487 }
488 CopyCourseMode::ExistingLanguageGroup { target_course_id } => {
489 let target_course = models::courses::get_course(&mut tx, *target_course_id).await?;
490 authorize(
492 &mut tx,
493 Act::Duplicate,
494 Some(user.id),
495 Res::Course(*target_course_id),
496 )
497 .await?;
498 models::library::copying::copy_course_with_language_group(
499 &mut tx,
500 *course_id,
501 target_course.course_language_group_id,
502 &payload.new_course,
503 user.id,
504 )
505 .await?
506 }
507 CopyCourseMode::NewLanguageGroup => {
508 let new_clg_id = course_language_groups::insert(&mut tx, PKeyPolicy::Generate).await?;
509 models::library::copying::copy_course_with_language_group(
510 &mut tx,
511 *course_id,
512 new_clg_id,
513 &payload.new_course,
514 user.id,
515 )
516 .await?
517 }
518 };
519
520 models::roles::insert(
521 &mut tx,
522 user.id,
523 models::roles::UserRole::Teacher,
524 models::roles::RoleDomain::Course(copied_course.id),
525 )
526 .await?;
527
528 tx.commit().await?;
529
530 token.authorized_ok(web::Json(copied_course))
531}
532
533#[instrument(skip(pool))]
537async fn get_daily_submission_counts(
538 pool: web::Data<PgPool>,
539 course_id: web::Path<Uuid>,
540 user: AuthUser,
541) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
542 let mut conn = pool.acquire().await?;
543 let token = authorize(
544 &mut conn,
545 Act::ViewStats,
546 Some(user.id),
547 Res::Course(*course_id),
548 )
549 .await?;
550 let course = models::courses::get_course(&mut conn, *course_id).await?;
551 let res =
552 exercise_slide_submissions::get_course_daily_slide_submission_counts(&mut conn, &course)
553 .await?;
554
555 token.authorized_ok(web::Json(res))
556}
557
558#[instrument(skip(pool))]
562async fn get_daily_user_counts_with_submissions(
563 pool: web::Data<PgPool>,
564 course_id: web::Path<Uuid>,
565 user: AuthUser,
566) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
567 let mut conn = pool.acquire().await?;
568 let token = authorize(
569 &mut conn,
570 Act::ViewStats,
571 Some(user.id),
572 Res::Course(*course_id),
573 )
574 .await?;
575 let course = models::courses::get_course(&mut conn, *course_id).await?;
576 let res = exercise_slide_submissions::get_course_daily_user_counts_with_submissions(
577 &mut conn, &course,
578 )
579 .await?;
580
581 token.authorized_ok(web::Json(res))
582}
583
584#[instrument(skip(pool))]
588async fn get_weekday_hour_submission_counts(
589 pool: web::Data<PgPool>,
590 course_id: web::Path<Uuid>,
591 user: AuthUser,
592) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByWeekAndHour>>> {
593 let mut conn = pool.acquire().await?;
594 let token = authorize(
595 &mut conn,
596 Act::ViewStats,
597 Some(user.id),
598 Res::Course(*course_id),
599 )
600 .await?;
601 let course = models::courses::get_course(&mut conn, *course_id).await?;
602 let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_weekday_and_hour(
603 &mut conn, &course,
604 )
605 .await?;
606
607 token.authorized_ok(web::Json(res))
608}
609
610#[instrument(skip(pool))]
614async fn get_submission_counts_by_exercise(
615 pool: web::Data<PgPool>,
616 course_id: web::Path<Uuid>,
617 user: AuthUser,
618) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByExercise>>> {
619 let mut conn = pool.acquire().await?;
620 let token = authorize(
621 &mut conn,
622 Act::ViewStats,
623 Some(user.id),
624 Res::Course(*course_id),
625 )
626 .await?;
627 let course = models::courses::get_course(&mut conn, *course_id).await?;
628 let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_exercise(
629 &mut conn, &course,
630 )
631 .await?;
632
633 token.authorized_ok(web::Json(res))
634}
635
636#[instrument(skip(pool))]
640async fn get_course_instances(
641 pool: web::Data<PgPool>,
642 course_id: web::Path<Uuid>,
643 user: AuthUser,
644) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
645 let mut conn = pool.acquire().await?;
646 let token = authorize(
647 &mut conn,
648 Act::Teach,
649 Some(user.id),
650 Res::Course(*course_id),
651 )
652 .await?;
653 let course_instances =
654 models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
655
656 token.authorized_ok(web::Json(course_instances))
657}
658
659#[derive(Debug, Deserialize)]
660#[cfg_attr(feature = "ts_rs", derive(TS))]
661pub struct GetFeedbackQuery {
662 read: bool,
663 #[serde(flatten)]
664 pagination: Pagination,
665}
666
667#[instrument(skip(pool))]
671pub async fn get_feedback(
672 course_id: web::Path<Uuid>,
673 pool: web::Data<PgPool>,
674 read: web::Query<GetFeedbackQuery>,
675 user: AuthUser,
676) -> ControllerResult<web::Json<Vec<Feedback>>> {
677 let mut conn = pool.acquire().await?;
678 let token = authorize(
679 &mut conn,
680 Act::Teach,
681 Some(user.id),
682 Res::Course(*course_id),
683 )
684 .await?;
685 let feedback =
686 feedback::get_feedback_for_course(&mut conn, *course_id, read.read, read.pagination)
687 .await?;
688
689 token.authorized_ok(web::Json(feedback))
690}
691
692#[instrument(skip(pool))]
696pub async fn get_feedback_count(
697 course_id: web::Path<Uuid>,
698 pool: web::Data<PgPool>,
699 user: AuthUser,
700) -> ControllerResult<web::Json<FeedbackCount>> {
701 let mut conn = pool.acquire().await?;
702 let token = authorize(
703 &mut conn,
704 Act::Teach,
705 Some(user.id),
706 Res::Course(*course_id),
707 )
708 .await?;
709
710 let feedback_count = feedback::get_feedback_count_for_course(&mut conn, *course_id).await?;
711
712 token.authorized_ok(web::Json(feedback_count))
713}
714
715#[instrument(skip(pool))]
719async fn new_course_instance(
720 form: web::Json<CourseInstanceForm>,
721 course_id: web::Path<Uuid>,
722 pool: web::Data<PgPool>,
723 user: AuthUser,
724) -> ControllerResult<web::Json<Uuid>> {
725 let mut conn = pool.acquire().await?;
726 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
727 let form = form.into_inner();
728 let new = NewCourseInstance {
729 course_id: *course_id,
730 name: form.name.as_deref(),
731 description: form.description.as_deref(),
732 support_email: form.support_email.as_deref(),
733 teacher_in_charge_name: &form.teacher_in_charge_name,
734 teacher_in_charge_email: &form.teacher_in_charge_email,
735 opening_time: form.opening_time,
736 closing_time: form.closing_time,
737 };
738 let ci = models::course_instances::insert(&mut conn, PKeyPolicy::Generate, new).await?;
739
740 token.authorized_ok(web::Json(ci.id))
741}
742
743#[instrument(skip(pool))]
744async fn glossary(
745 pool: web::Data<PgPool>,
746 course_id: web::Path<Uuid>,
747 user: AuthUser,
748) -> ControllerResult<web::Json<Vec<Term>>> {
749 let mut conn = pool.acquire().await?;
750 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
751 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
752
753 token.authorized_ok(web::Json(glossary))
754}
755
756#[instrument(skip(pool))]
759async fn _new_term(
760 pool: web::Data<PgPool>,
761 course_id: web::Path<Uuid>,
762 user: AuthUser,
763) -> ControllerResult<web::Json<Vec<Term>>> {
764 let mut conn = pool.acquire().await?;
765 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
766 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
767
768 token.authorized_ok(web::Json(glossary))
769}
770
771#[instrument(skip(pool))]
772async fn new_glossary_term(
773 pool: web::Data<PgPool>,
774 course_id: web::Path<Uuid>,
775 new_term: web::Json<TermUpdate>,
776 user: AuthUser,
777) -> ControllerResult<web::Json<Uuid>> {
778 let mut conn = pool.acquire().await?;
779 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
780 let TermUpdate { term, definition } = new_term.into_inner();
781 let term = models::glossary::insert(&mut conn, &term, &definition, *course_id).await?;
782
783 token.authorized_ok(web::Json(term))
784}
785
786#[instrument(skip(pool))]
790pub async fn get_course_users_counts_by_exercise(
791 course_id: web::Path<Uuid>,
792 pool: web::Data<PgPool>,
793 user: AuthUser,
794) -> ControllerResult<web::Json<Vec<ExerciseUserCounts>>> {
795 let mut conn = pool.acquire().await?;
796 let course_id = course_id.into_inner();
797 let token = authorize(
798 &mut conn,
799 Act::ViewStats,
800 Some(user.id),
801 Res::Course(course_id),
802 )
803 .await?;
804
805 let res =
806 models::user_exercise_states::get_course_users_counts_by_exercise(&mut conn, course_id)
807 .await?;
808
809 token.authorized_ok(web::Json(res))
810}
811
812#[instrument(skip(pool))]
820pub async fn post_new_page_ordering(
821 course_id: web::Path<Uuid>,
822 pool: web::Data<PgPool>,
823 user: AuthUser,
824 payload: web::Json<Vec<Page>>,
825) -> ControllerResult<web::Json<()>> {
826 let mut conn = pool.acquire().await?;
827 let course_id = course_id.into_inner();
828 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
829
830 models::pages::reorder_pages(&mut conn, &payload, course_id).await?;
831
832 token.authorized_ok(web::Json(()))
833}
834
835#[instrument(skip(pool))]
841pub async fn post_new_chapter_ordering(
842 course_id: web::Path<Uuid>,
843 pool: web::Data<PgPool>,
844 user: AuthUser,
845 payload: web::Json<Vec<Chapter>>,
846) -> ControllerResult<web::Json<()>> {
847 let mut conn = pool.acquire().await?;
848 let course_id = course_id.into_inner();
849 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
850
851 models::pages::reorder_chapters(&mut conn, &payload, course_id).await?;
852
853 token.authorized_ok(web::Json(()))
854}
855
856#[instrument(skip(pool))]
857async fn get_material_references_by_course_id(
858 course_id: web::Path<Uuid>,
859 pool: web::Data<PgPool>,
860 user: AuthUser,
861) -> ControllerResult<web::Json<Vec<MaterialReference>>> {
862 let mut conn = pool.acquire().await?;
863 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
864
865 let res =
866 models::material_references::get_references_by_course_id(&mut conn, *course_id).await?;
867 token.authorized_ok(web::Json(res))
868}
869
870#[instrument(skip(pool))]
871async fn insert_material_references(
872 course_id: web::Path<Uuid>,
873 payload: web::Json<Vec<NewMaterialReference>>,
874 pool: web::Data<PgPool>,
875 user: AuthUser,
876) -> ControllerResult<web::Json<()>> {
877 let mut conn = pool.acquire().await?;
878 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
879
880 models::material_references::insert_reference(&mut conn, *course_id, payload.0).await?;
881
882 token.authorized_ok(web::Json(()))
883}
884
885#[instrument(skip(pool))]
886async fn update_material_reference(
887 path: web::Path<(Uuid, Uuid)>,
888 pool: web::Data<PgPool>,
889 user: AuthUser,
890 payload: web::Json<NewMaterialReference>,
891) -> ControllerResult<web::Json<()>> {
892 let (course_id, reference_id) = path.into_inner();
893 let mut conn = pool.acquire().await?;
894 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
895
896 models::material_references::update_material_reference_by_id(
897 &mut conn,
898 reference_id,
899 payload.0,
900 )
901 .await?;
902 token.authorized_ok(web::Json(()))
903}
904
905#[instrument(skip(pool))]
906async fn delete_material_reference_by_id(
907 path: web::Path<(Uuid, Uuid)>,
908 pool: web::Data<PgPool>,
909 user: AuthUser,
910) -> ControllerResult<web::Json<()>> {
911 let (course_id, reference_id) = path.into_inner();
912 let mut conn = pool.acquire().await?;
913 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
914
915 models::material_references::delete_reference(&mut conn, reference_id).await?;
916 token.authorized_ok(web::Json(()))
917}
918
919#[instrument(skip(pool))]
920pub async fn update_modules(
921 course_id: web::Path<Uuid>,
922 pool: web::Data<PgPool>,
923 user: AuthUser,
924 payload: web::Json<ModuleUpdates>,
925) -> ControllerResult<web::Json<()>> {
926 let mut conn = pool.acquire().await?;
927 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
928
929 models::course_modules::update_modules(&mut conn, *course_id, payload.into_inner()).await?;
930 token.authorized_ok(web::Json(()))
931}
932
933async fn get_course_default_peer_review(
934 course_id: web::Path<Uuid>,
935 pool: web::Data<PgPool>,
936 user: AuthUser,
937) -> ControllerResult<web::Json<(PeerOrSelfReviewConfig, Vec<PeerOrSelfReviewQuestion>)>> {
938 let mut conn = pool.acquire().await?;
939 let token = authorize(
940 &mut conn,
941 Act::Teach,
942 Some(user.id),
943 Res::Course(*course_id),
944 )
945 .await?;
946
947 let peer_review = models::peer_or_self_review_configs::get_default_for_course_by_course_id(
948 &mut conn, *course_id,
949 )
950 .await?;
951 let peer_or_self_review_questions =
952 models::peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
953 &mut conn,
954 peer_review.id,
955 )
956 .await?;
957 token.authorized_ok(web::Json((peer_review, peer_or_self_review_questions)))
958}
959
960#[instrument(skip(pool, user))]
967async fn post_update_peer_review_queue_reviews_received(
968 pool: web::Data<PgPool>,
969 user: AuthUser,
970 course_id: web::Path<Uuid>,
971) -> ControllerResult<web::Json<bool>> {
972 let mut conn = pool.acquire().await?;
973 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
974 models::library::peer_or_self_reviewing::update_peer_review_queue_reviews_received(
975 &mut conn, *course_id,
976 )
977 .await?;
978 token.authorized_ok(web::Json(true))
979}
980
981#[instrument(skip(pool))]
987pub async fn submission_export(
988 course_id: web::Path<Uuid>,
989 pool: web::Data<PgPool>,
990 user: AuthUser,
991) -> ControllerResult<HttpResponse> {
992 let mut conn = pool.acquire().await?;
993
994 let token = authorize(
995 &mut conn,
996 Act::Teach,
997 Some(user.id),
998 Res::Course(*course_id),
999 )
1000 .await?;
1001
1002 let course = models::courses::get_course(&mut conn, *course_id).await?;
1003
1004 general_export(
1005 pool,
1006 &format!(
1007 "attachment; filename=\"Course: {} - Submissions (exercise tasks) {}.csv\"",
1008 course.name,
1009 Utc::now().format("%Y-%m-%d")
1010 ),
1011 CourseSubmissionExportOperation {
1012 course_id: *course_id,
1013 },
1014 token,
1015 )
1016 .await
1017}
1018
1019#[instrument(skip(pool))]
1025pub async fn user_details_export(
1026 course_id: web::Path<Uuid>,
1027 pool: web::Data<PgPool>,
1028 user: AuthUser,
1029) -> ControllerResult<HttpResponse> {
1030 let mut conn = pool.acquire().await?;
1031
1032 let token = authorize(
1033 &mut conn,
1034 Act::Teach,
1035 Some(user.id),
1036 Res::Course(*course_id),
1037 )
1038 .await?;
1039
1040 let course = models::courses::get_course(&mut conn, *course_id).await?;
1041
1042 general_export(
1043 pool,
1044 &format!(
1045 "attachment; filename=\"Course: {} - User Details {}.csv\"",
1046 course.name,
1047 Utc::now().format("%Y-%m-%d")
1048 ),
1049 UsersExportOperation {
1050 course_id: *course_id,
1051 },
1052 token,
1053 )
1054 .await
1055}
1056
1057#[instrument(skip(pool))]
1063pub async fn exercise_tasks_export(
1064 course_id: web::Path<Uuid>,
1065 pool: web::Data<PgPool>,
1066 user: AuthUser,
1067) -> ControllerResult<HttpResponse> {
1068 let mut conn = pool.acquire().await?;
1069
1070 let token = authorize(
1071 &mut conn,
1072 Act::Teach,
1073 Some(user.id),
1074 Res::Course(*course_id),
1075 )
1076 .await?;
1077
1078 let course = models::courses::get_course(&mut conn, *course_id).await?;
1079
1080 general_export(
1081 pool,
1082 &format!(
1083 "attachment; filename=\"Course: {} - Exercise tasks {}.csv\"",
1084 course.name,
1085 Utc::now().format("%Y-%m-%d")
1086 ),
1087 CourseExerciseTasksExportOperation {
1088 course_id: *course_id,
1089 },
1090 token,
1091 )
1092 .await
1093}
1094
1095#[instrument(skip(pool))]
1101pub async fn course_instances_export(
1102 course_id: web::Path<Uuid>,
1103 pool: web::Data<PgPool>,
1104 user: AuthUser,
1105) -> ControllerResult<HttpResponse> {
1106 let mut conn = pool.acquire().await?;
1107
1108 let token = authorize(
1109 &mut conn,
1110 Act::Teach,
1111 Some(user.id),
1112 Res::Course(*course_id),
1113 )
1114 .await?;
1115
1116 let course = models::courses::get_course(&mut conn, *course_id).await?;
1117
1118 general_export(
1119 pool,
1120 &format!(
1121 "attachment; filename=\"Course: {} - Instances {}.csv\"",
1122 course.name,
1123 Utc::now().format("%Y-%m-%d")
1124 ),
1125 CourseInstancesExportOperation {
1126 course_id: *course_id,
1127 },
1128 token,
1129 )
1130 .await
1131}
1132
1133#[instrument(skip(pool))]
1139pub async fn course_consent_form_answers_export(
1140 course_id: web::Path<Uuid>,
1141 pool: web::Data<PgPool>,
1142 user: AuthUser,
1143) -> ControllerResult<HttpResponse> {
1144 let mut conn = pool.acquire().await?;
1145
1146 let token = authorize(
1147 &mut conn,
1148 Act::Teach,
1149 Some(user.id),
1150 Res::Course(*course_id),
1151 )
1152 .await?;
1153
1154 let course = models::courses::get_course(&mut conn, *course_id).await?;
1155
1156 general_export(
1157 pool,
1158 &format!(
1159 "attachment; filename=\"Course: {} - User Consents {}.csv\"",
1160 course.name,
1161 Utc::now().format("%Y-%m-%d")
1162 ),
1163 CourseResearchFormExportOperation {
1164 course_id: *course_id,
1165 },
1166 token,
1167 )
1168 .await
1169}
1170
1171#[instrument(skip(pool))]
1177pub async fn user_exercise_states_export(
1178 course_id: web::Path<Uuid>,
1179 pool: web::Data<PgPool>,
1180 user: AuthUser,
1181) -> ControllerResult<HttpResponse> {
1182 let mut conn = pool.acquire().await?;
1183
1184 let token = authorize(
1185 &mut conn,
1186 Act::Teach,
1187 Some(user.id),
1188 Res::Course(*course_id),
1189 )
1190 .await?;
1191
1192 let course = models::courses::get_course(&mut conn, *course_id).await?;
1193
1194 general_export(
1195 pool,
1196 &format!(
1197 "attachment; filename=\"Course: {} - User exercise states {}.csv\"",
1198 course.name,
1199 Utc::now().format("%Y-%m-%d")
1200 ),
1201 UserExerciseStatesExportOperation {
1202 course_id: *course_id,
1203 },
1204 token,
1205 )
1206 .await
1207}
1208
1209pub async fn get_page_visit_datum_summary(
1213 course_id: web::Path<Uuid>,
1214 pool: web::Data<PgPool>,
1215 user: AuthUser,
1216) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourse>>> {
1217 let mut conn = pool.acquire().await?;
1218 let course_id = course_id.into_inner();
1219 let token = authorize(
1220 &mut conn,
1221 Act::ViewStats,
1222 Some(user.id),
1223 Res::Course(course_id),
1224 )
1225 .await?;
1226
1227 let res = models::page_visit_datum_summary_by_courses::get_all_for_course(&mut conn, course_id)
1228 .await?;
1229
1230 token.authorized_ok(web::Json(res))
1231}
1232
1233pub async fn get_page_visit_datum_summary_by_pages(
1237 course_id: web::Path<Uuid>,
1238 pool: web::Data<PgPool>,
1239 user: AuthUser,
1240) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByPages>>> {
1241 let mut conn = pool.acquire().await?;
1242 let course_id = course_id.into_inner();
1243 let token = authorize(
1244 &mut conn,
1245 Act::ViewStats,
1246 Some(user.id),
1247 Res::Course(course_id),
1248 )
1249 .await?;
1250
1251 let res =
1252 models::page_visit_datum_summary_by_pages::get_all_for_course(&mut conn, course_id).await?;
1253
1254 token.authorized_ok(web::Json(res))
1255}
1256
1257pub async fn get_page_visit_datum_summary_by_device_types(
1261 course_id: web::Path<Uuid>,
1262 pool: web::Data<PgPool>,
1263 user: AuthUser,
1264) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourseDeviceTypes>>> {
1265 let mut conn = pool.acquire().await?;
1266 let course_id = course_id.into_inner();
1267 let token = authorize(
1268 &mut conn,
1269 Act::ViewStats,
1270 Some(user.id),
1271 Res::Course(course_id),
1272 )
1273 .await?;
1274
1275 let res = models::page_visit_datum_summary_by_courses_device_types::get_all_for_course(
1276 &mut conn, course_id,
1277 )
1278 .await?;
1279
1280 token.authorized_ok(web::Json(res))
1281}
1282
1283pub async fn get_page_visit_datum_summary_by_countries(
1287 course_id: web::Path<Uuid>,
1288 pool: web::Data<PgPool>,
1289 user: AuthUser,
1290) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCoursesCountries>>> {
1291 let mut conn = pool.acquire().await?;
1292 let course_id = course_id.into_inner();
1293 let token = authorize(
1294 &mut conn,
1295 Act::ViewStats,
1296 Some(user.id),
1297 Res::Course(course_id),
1298 )
1299 .await?;
1300
1301 let res = models::page_visit_datum_summary_by_courses_countries::get_all_for_course(
1302 &mut conn, course_id,
1303 )
1304 .await?;
1305
1306 token.authorized_ok(web::Json(res))
1307}
1308
1309pub async fn teacher_reset_course_progress_for_themselves(
1315 course_id: web::Path<Uuid>,
1316 pool: web::Data<PgPool>,
1317 user: AuthUser,
1318) -> ControllerResult<web::Json<bool>> {
1319 let mut conn = pool.acquire().await?;
1320 let course_id = course_id.into_inner();
1321 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1322
1323 let mut tx = conn.begin().await?;
1324 let course_instances =
1325 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
1326 for course_instance in course_instances {
1327 models::course_instances::reset_progress_on_course_instance_for_user(
1328 &mut tx,
1329 user.id,
1330 course_instance.id,
1331 )
1332 .await?;
1333 }
1334
1335 tx.commit().await?;
1336 token.authorized_ok(web::Json(true))
1337}
1338
1339pub async fn teacher_reset_course_progress_for_everyone(
1345 course_id: web::Path<Uuid>,
1346 pool: web::Data<PgPool>,
1347 user: AuthUser,
1348) -> ControllerResult<web::Json<bool>> {
1349 let mut conn = pool.acquire().await?;
1350 let course_id = course_id.into_inner();
1351 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1352 let course = models::courses::get_course(&mut conn, course_id).await?;
1353 if !course.is_draft {
1354 return Err(ControllerError::new(
1355 ControllerErrorType::BadRequest,
1356 "Can only reset progress for a draft course.".to_string(),
1357 None,
1358 ));
1359 }
1360 let n_course_module_completions =
1362 models::course_module_completions::get_count_of_distinct_completors_by_course_id(
1363 &mut conn, course_id,
1364 )
1365 .await?;
1366 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(
1367 &mut conn, course_id,
1368 ).await?;
1369 if n_course_module_completions > 200 {
1370 return Err(ControllerError::new(
1371 ControllerErrorType::BadRequest,
1372 "Too many students have completed the course.".to_string(),
1373 None,
1374 ));
1375 }
1376 if n_completions_registered_to_study_registry > 2 {
1377 return Err(ControllerError::new(
1378 ControllerErrorType::BadRequest,
1379 "Too many students have registered their completion to a study registry".to_string(),
1380 None,
1381 ));
1382 }
1383
1384 let mut tx = conn.begin().await?;
1385 let course_instances =
1386 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
1387
1388 for course_instance in course_instances {
1390 let users_in_course_instance =
1391 models::users::get_users_by_course_instance_enrollment(&mut tx, course_instance.id)
1392 .await?;
1393 for user_in_course_instance in users_in_course_instance {
1394 models::course_instances::reset_progress_on_course_instance_for_user(
1395 &mut tx,
1396 user_in_course_instance.id,
1397 course_instance.id,
1398 )
1399 .await?;
1400 }
1401 }
1402
1403 tx.commit().await?;
1404 token.authorized_ok(web::Json(true))
1405}
1406
1407#[derive(Debug, Deserialize)]
1408#[cfg_attr(feature = "ts_rs", derive(TS))]
1409pub struct GetSuspectedCheatersQuery {
1410 archive: bool,
1411}
1412
1413#[instrument(skip(pool))]
1417async fn get_all_suspected_cheaters(
1418 user: AuthUser,
1419 params: web::Path<Uuid>,
1420 query: web::Query<GetSuspectedCheatersQuery>,
1421 pool: web::Data<PgPool>,
1422) -> ControllerResult<web::Json<Vec<SuspectedCheaters>>> {
1423 let course_id = params.into_inner();
1424
1425 let mut conn = pool.acquire().await?;
1426 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1427
1428 let course_cheaters = models::suspected_cheaters::get_all_suspected_cheaters_in_course(
1429 &mut conn,
1430 course_id,
1431 query.archive,
1432 )
1433 .await?;
1434
1435 token.authorized_ok(web::Json(course_cheaters))
1436}
1437
1438#[instrument(skip(pool))]
1442async fn insert_threshold(
1443 pool: web::Data<PgPool>,
1444 params: web::Path<Uuid>,
1445 payload: web::Json<ThresholdData>,
1446 user: AuthUser,
1447) -> ControllerResult<web::Json<()>> {
1448 let mut conn = pool.acquire().await?;
1449
1450 let course_id = params.into_inner();
1451 let new_threshold = payload.0;
1452 let duration: Option<i32> = new_threshold.duration_seconds;
1453
1454 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1455
1456 models::suspected_cheaters::insert_thresholds(
1457 &mut conn,
1458 course_id,
1459 duration,
1460 new_threshold.points,
1461 )
1462 .await?;
1463
1464 token.authorized_ok(web::Json(()))
1465}
1466
1467#[instrument(skip(pool))]
1471async fn teacher_archive_suspected_cheater(
1472 user: AuthUser,
1473 path: web::Path<(Uuid, Uuid)>,
1474 pool: web::Data<PgPool>,
1475) -> ControllerResult<web::Json<()>> {
1476 let (course_id, user_id) = path.into_inner();
1477
1478 let mut conn = pool.acquire().await?;
1479 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1480
1481 models::suspected_cheaters::archive_suspected_cheater(&mut conn, user_id).await?;
1482
1483 token.authorized_ok(web::Json(()))
1484}
1485
1486#[instrument(skip(pool))]
1490async fn teacher_approve_suspected_cheater(
1491 user: AuthUser,
1492 path: web::Path<(Uuid, Uuid)>,
1493 pool: web::Data<PgPool>,
1494) -> ControllerResult<web::Json<()>> {
1495 let (course_id, user_id) = path.into_inner();
1496
1497 let mut conn = pool.acquire().await?;
1498 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1499
1500 models::suspected_cheaters::approve_suspected_cheater(&mut conn, user_id).await?;
1501
1502 models::course_module_completions::update_passed_and_grade_status(
1505 &mut conn, course_id, user_id, false, 0,
1506 )
1507 .await?;
1508
1509 token.authorized_ok(web::Json(()))
1510}
1511
1512#[instrument(skip(pool))]
1516async fn add_user_to_course_with_join_code(
1517 course_id: web::Path<Uuid>,
1518 user: AuthUser,
1519 pool: web::Data<PgPool>,
1520) -> ControllerResult<web::Json<Uuid>> {
1521 let mut conn = pool.acquire().await?;
1522 let token = skip_authorize();
1523
1524 let joined =
1525 models::join_code_uses::insert(&mut conn, PKeyPolicy::Generate, user.id, *course_id)
1526 .await?;
1527 token.authorized_ok(web::Json(joined))
1528}
1529
1530#[instrument(skip(pool))]
1534async fn set_join_code_for_course(
1535 id: web::Path<Uuid>,
1536 pool: web::Data<PgPool>,
1537 user: AuthUser,
1538) -> ControllerResult<HttpResponse> {
1539 let mut conn = pool.acquire().await?;
1540 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*id)).await?;
1541
1542 const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
1543 abcdefghjkmnpqrstuvwxyz";
1544 const PASSWORD_LEN: usize = 64;
1545 let mut rng = rand::rng();
1546
1547 let code: String = (0..PASSWORD_LEN)
1548 .map(|_| {
1549 let idx = rng.random_range(0..CHARSET.len());
1550 CHARSET[idx] as char
1551 })
1552 .collect();
1553
1554 models::courses::set_join_code_for_course(&mut conn, *id, code).await?;
1555 token.authorized_ok(HttpResponse::Ok().finish())
1556}
1557
1558#[instrument(skip(pool))]
1562async fn get_course_with_join_code(
1563 join_code: web::Path<String>,
1564 user: AuthUser,
1565 pool: web::Data<PgPool>,
1566) -> ControllerResult<web::Json<Course>> {
1567 let mut conn = pool.acquire().await?;
1568 let token = skip_authorize();
1569 let course =
1570 models::courses::get_course_with_join_code(&mut conn, join_code.to_string()).await?;
1571
1572 token.authorized_ok(web::Json(course))
1573}
1574
1575#[instrument(skip(payload, pool))]
1579async fn post_partners_block(
1580 path: web::Path<Uuid>,
1581 payload: web::Json<Option<serde_json::Value>>,
1582 pool: web::Data<PgPool>,
1583 user: AuthUser,
1584) -> ControllerResult<web::Json<PartnersBlock>> {
1585 let course_id = path.into_inner();
1586
1587 let content = payload.into_inner();
1588 let mut conn = pool.acquire().await?;
1589 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1590
1591 let upserted_partner_block =
1592 models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
1593
1594 token.authorized_ok(web::Json(upserted_partner_block))
1595}
1596
1597#[instrument(skip(pool))]
1601async fn get_partners_block(
1602 path: web::Path<Uuid>,
1603 user: AuthUser,
1604 pool: web::Data<PgPool>,
1605) -> ControllerResult<web::Json<PartnersBlock>> {
1606 let course_id = path.into_inner();
1607 let mut conn = pool.acquire().await?;
1608 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1609
1610 let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
1612
1613 let partner_block = if course_exists {
1614 models::partner_block::get_partner_block(&mut conn, course_id).await?
1616 } else {
1617 let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
1619
1620 models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
1622 };
1623
1624 token.authorized_ok(web::Json(partner_block))
1625}
1626
1627#[instrument(skip(pool))]
1631async fn delete_partners_block(
1632 path: web::Path<Uuid>,
1633 pool: web::Data<PgPool>,
1634 user: AuthUser,
1635) -> ControllerResult<web::Json<PartnersBlock>> {
1636 let course_id = path.into_inner();
1637 let mut conn = pool.acquire().await?;
1638 let token = authorize(
1639 &mut conn,
1640 Act::UsuallyUnacceptableDeletion,
1641 Some(user.id),
1642 Res::Course(course_id),
1643 )
1644 .await?;
1645 let deleted_partners_block =
1646 models::partner_block::delete_partner_block(&mut conn, course_id).await?;
1647
1648 token.authorized_ok(web::Json(deleted_partners_block))
1649}
1650
1651pub fn _add_routes(cfg: &mut ServiceConfig) {
1659 cfg.service(web::scope("/{course_id}/stats").configure(stats::_add_routes))
1660 .route("/{course_id}", web::get().to(get_course))
1661 .route("", web::post().to(post_new_course))
1662 .route("/{course_id}", web::put().to(update_course))
1663 .route("/{course_id}", web::delete().to(delete_course))
1664 .route(
1665 "/{course_id}/daily-submission-counts",
1666 web::get().to(get_daily_submission_counts),
1667 )
1668 .route(
1669 "/{course_id}/daily-users-who-have-submitted-something",
1670 web::get().to(get_daily_user_counts_with_submissions),
1671 )
1672 .route("/{course_id}/exercises", web::get().to(get_all_exercises))
1673 .route(
1674 "/{course_id}/exercises-and-count-of-answers-requiring-attention",
1675 web::get().to(get_all_exercises_and_count_of_answers_requiring_attention),
1676 )
1677 .route(
1678 "/{course_id}/structure",
1679 web::get().to(get_course_structure),
1680 )
1681 .route(
1682 "/{course_id}/language-versions",
1683 web::get().to(get_all_course_language_versions),
1684 )
1685 .route(
1686 "/{course_id}/create-copy",
1687 web::post().to(create_course_copy),
1688 )
1689 .route("/{course_id}/upload", web::post().to(add_media_for_course))
1690 .route(
1691 "/{course_id}/weekday-hour-submission-counts",
1692 web::get().to(get_weekday_hour_submission_counts),
1693 )
1694 .route(
1695 "/{course_id}/submission-counts-by-exercise",
1696 web::get().to(get_submission_counts_by_exercise),
1697 )
1698 .route(
1699 "/{course_id}/course-instances",
1700 web::get().to(get_course_instances),
1701 )
1702 .route("/{course_id}/feedback", web::get().to(get_feedback))
1703 .route(
1704 "/{course_id}/feedback-count",
1705 web::get().to(get_feedback_count),
1706 )
1707 .route(
1708 "/{course_id}/new-course-instance",
1709 web::post().to(new_course_instance),
1710 )
1711 .route("/{course_id}/glossary", web::get().to(glossary))
1712 .route("/{course_id}/glossary", web::post().to(new_glossary_term))
1713 .route(
1714 "/{course_id}/course-users-counts-by-exercise",
1715 web::get().to(get_course_users_counts_by_exercise),
1716 )
1717 .route(
1718 "/{course_id}/new-page-ordering",
1719 web::post().to(post_new_page_ordering),
1720 )
1721 .route(
1722 "/{course_id}/new-chapter-ordering",
1723 web::post().to(post_new_chapter_ordering),
1724 )
1725 .route(
1726 "/{course_id}/references",
1727 web::get().to(get_material_references_by_course_id),
1728 )
1729 .route(
1730 "/{course_id}/references",
1731 web::post().to(insert_material_references),
1732 )
1733 .route(
1734 "/{course_id}/references/{reference_id}",
1735 web::post().to(update_material_reference),
1736 )
1737 .route(
1738 "/{course_id}/references/{reference_id}",
1739 web::delete().to(delete_material_reference_by_id),
1740 )
1741 .route(
1742 "/{course_id}/course-modules",
1743 web::post().to(update_modules),
1744 )
1745 .route(
1746 "/{course_id}/default-peer-review",
1747 web::get().to(get_course_default_peer_review),
1748 )
1749 .route(
1750 "/{course_id}/update-peer-review-queue-reviews-received",
1751 web::post().to(post_update_peer_review_queue_reviews_received),
1752 )
1753 .route(
1754 "/{course_id}/breadcrumb-info",
1755 web::get().to(get_course_breadcrumb_info),
1756 )
1757 .route(
1758 "/{course_id}/export-submissions",
1759 web::get().to(submission_export),
1760 )
1761 .route(
1762 "/{course_id}/export-user-details",
1763 web::get().to(user_details_export),
1764 )
1765 .route(
1766 "/{course_id}/export-exercise-tasks",
1767 web::get().to(exercise_tasks_export),
1768 )
1769 .route(
1770 "/{course_id}/export-course-instances",
1771 web::get().to(course_instances_export),
1772 )
1773 .route(
1774 "/{course_id}/export-course-user-consents",
1775 web::get().to(course_consent_form_answers_export),
1776 )
1777 .route(
1778 "/{course_id}/export-user-exercise-states",
1779 web::get().to(user_exercise_states_export),
1780 )
1781 .route(
1782 "/{course_id}/page-visit-datum-summary",
1783 web::get().to(get_page_visit_datum_summary),
1784 )
1785 .route(
1786 "/{course_id}/page-visit-datum-summary-by-pages",
1787 web::get().to(get_page_visit_datum_summary_by_pages),
1788 )
1789 .route(
1790 "/{course_id}/page-visit-datum-summary-by-device-types",
1791 web::get().to(get_page_visit_datum_summary_by_device_types),
1792 )
1793 .route(
1794 "/{course_id}/page-visit-datum-summary-by-countries",
1795 web::get().to(get_page_visit_datum_summary_by_countries),
1796 )
1797 .route(
1798 "/{course_id}/teacher-reset-course-progress-for-themselves",
1799 web::delete().to(teacher_reset_course_progress_for_themselves),
1800 )
1801 .route("/{course_id}/threshold", web::post().to(insert_threshold))
1802 .route(
1803 "/{course_id}/suspected-cheaters",
1804 web::get().to(get_all_suspected_cheaters),
1805 )
1806 .route(
1807 "/{course_id}/suspected-cheaters/archive/{id}",
1808 web::post().to(teacher_archive_suspected_cheater),
1809 )
1810 .route(
1811 "/{course_id}/suspected-cheaters/approve/{id}",
1812 web::post().to(teacher_approve_suspected_cheater),
1813 )
1814 .route(
1815 "/{course_id}/teacher-reset-course-progress-for-everyone",
1816 web::delete().to(teacher_reset_course_progress_for_everyone),
1817 )
1818 .route(
1819 "/{course_id}/join-course-with-join-code",
1820 web::post().to(add_user_to_course_with_join_code),
1821 )
1822 .route(
1823 "/{course_id}/partners-block",
1824 web::post().to(post_partners_block),
1825 )
1826 .route(
1827 "/{course_id}/partners-block",
1828 web::get().to(get_partners_block),
1829 )
1830 .route(
1831 "/{course_id}/partners-block",
1832 web::delete().to(delete_partners_block),
1833 )
1834 .route(
1835 "/{course_id}/set-join-code",
1836 web::post().to(set_join_code_for_course),
1837 )
1838 .route(
1839 "/join/{join_code}",
1840 web::get().to(get_course_with_join_code),
1841 );
1842}