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