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