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 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_module_completions::CourseModuleCompletion,
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, ExerciseStatusSummaryForUser},
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, UserCourseProgress},
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_all_exercise_statuses_by_course_id(
92 params: web::Path<(Uuid, Uuid)>,
93 pool: web::Data<PgPool>,
94 user: AuthUser,
95) -> ControllerResult<web::Json<Vec<ExerciseStatusSummaryForUser>>> {
96 let (course_id, user_id) = params.into_inner();
97 let mut conn = pool.acquire().await?;
98 let token = authorize(
99 &mut conn,
100 Act::ViewUserProgressOrDetails,
101 Some(user.id),
102 Res::Course(course_id),
103 )
104 .await?;
105 let res = models::exercises::get_all_exercise_statuses_by_user_id_and_course_id(
106 &mut conn, course_id, user_id,
107 )
108 .await?;
109 token.authorized_ok(web::Json(res))
110}
111
112#[instrument(skip(pool))]
116async fn get_all_course_module_completions_for_user_by_course_id(
117 params: web::Path<(Uuid, Uuid)>,
118 pool: web::Data<PgPool>,
119 user: AuthUser,
120) -> ControllerResult<web::Json<Vec<CourseModuleCompletion>>> {
121 let (course_id, user_id) = params.into_inner();
122 let mut conn = pool.acquire().await?;
123 let token = authorize(
124 &mut conn,
125 Act::ViewUserProgressOrDetails,
126 Some(user.id),
127 Res::Course(course_id),
128 )
129 .await?;
130 let res = models::course_module_completions::get_all_by_course_id_and_user_id(
131 &mut conn, course_id, user_id,
132 )
133 .await?;
134 token.authorized_ok(web::Json(res))
135}
136
137#[instrument(skip(pool))]
141async fn get_user_progress_for_course(
142 path: web::Path<(Uuid, Uuid)>,
143 pool: web::Data<PgPool>,
144 user: AuthUser,
145) -> ControllerResult<web::Json<Vec<UserCourseProgress>>> {
146 let (course_id, target_user_id) = path.into_inner();
147 let mut conn = pool.acquire().await?;
148 let token = authorize(
149 &mut conn,
150 Act::ViewUserProgressOrDetails,
151 Some(user.id),
152 Res::Course(course_id),
153 )
154 .await?;
155 let user_course_progress = models::user_exercise_states::get_user_course_progress(
156 &mut conn,
157 course_id,
158 target_user_id,
159 false,
160 )
161 .await?;
162 token.authorized_ok(web::Json(user_course_progress))
163}
164
165#[instrument(skip(pool))]
169async fn get_user_course_settings(
170 path: web::Path<(Uuid, Uuid)>,
171 pool: web::Data<PgPool>,
172 user: AuthUser,
173) -> ControllerResult<web::Json<Option<UserCourseSettings>>> {
174 let (course_id, target_user_id) = path.into_inner();
175 let mut conn = pool.acquire().await?;
176 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
177 let settings = models::user_course_settings::get_user_course_settings_by_course_id(
178 &mut conn,
179 target_user_id,
180 course_id,
181 )
182 .await?;
183 token.authorized_ok(web::Json(settings))
184}
185
186#[instrument(skip(pool, user))]
193async fn post_reprocess_module_completions(
194 pool: web::Data<PgPool>,
195 user: AuthUser,
196 course_id: web::Path<Uuid>,
197) -> ControllerResult<web::Json<bool>> {
198 let mut conn = pool.acquire().await?;
199 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
200 models::library::progressing::process_all_course_completions(&mut conn, *course_id).await?;
201 token.authorized_ok(web::Json(true))
202}
203
204#[instrument(skip(pool, app_conf))]
222async fn post_new_course(
223 request_id: RequestId,
224 pool: web::Data<PgPool>,
225 payload: web::Json<NewCourse>,
226 user: AuthUser,
227 app_conf: web::Data<ApplicationConfiguration>,
228 jwt_key: web::Data<JwtKey>,
229) -> ControllerResult<web::Json<Course>> {
230 let mut conn = pool.acquire().await?;
231 let new_course = payload.0;
232 if !is_ietf_language_code_like(&new_course.language_code) {
233 return Err(ControllerError::new(
234 ControllerErrorType::BadRequest,
235 "Malformed language code.".to_string(),
236 None,
237 ));
238 }
239 let token = authorize(
240 &mut conn,
241 Act::CreateCoursesOrExams,
242 Some(user.id),
243 Res::Organization(new_course.organization_id),
244 )
245 .await?;
246
247 let mut tx = conn.begin().await?;
248 let (course, ..) = library::content_management::create_new_course(
249 &mut tx,
250 PKeyPolicy::Generate,
251 new_course,
252 user.id,
253 models_requests::make_spec_fetcher(
254 app_conf.base_url.clone(),
255 request_id.0,
256 Arc::clone(&jwt_key),
257 ),
258 models_requests::fetch_service_info,
259 )
260 .await?;
261 models::roles::insert(
262 &mut tx,
263 user.id,
264 models::roles::UserRole::Teacher,
265 models::roles::RoleDomain::Course(course.id),
266 )
267 .await?;
268 tx.commit().await?;
269
270 token.authorized_ok(web::Json(course))
271}
272
273#[instrument(skip(pool))]
289async fn update_course(
290 payload: web::Json<CourseUpdate>,
291 course_id: web::Path<Uuid>,
292 pool: web::Data<PgPool>,
293 user: AuthUser,
294) -> ControllerResult<web::Json<Course>> {
295 let mut conn = pool.acquire().await?;
296 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
297 let course_update = payload.0;
298 let course_before_update = models::courses::get_course(&mut conn, *course_id).await?;
299 if course_update.can_add_chatbot != course_before_update.can_add_chatbot {
300 let _token2 =
302 authorize(&mut conn, Act::Teach, Some(user.id), Res::GlobalPermissions).await?;
303 }
304
305 let locking_just_enabled =
306 !course_before_update.chapter_locking_enabled && course_update.chapter_locking_enabled;
307
308 let course = models::courses::update_course(&mut conn, *course_id, course_update).await?;
309
310 if locking_just_enabled {
311 use models::{user_chapter_locking_statuses, user_course_settings};
312
313 let all_user_settings =
314 user_course_settings::get_all_by_course_id(&mut conn, *course_id).await?;
315
316 for settings in all_user_settings {
317 let _ = user_chapter_locking_statuses::get_or_init_all_for_course(
318 &mut conn,
319 settings.user_id,
320 *course_id,
321 )
322 .await?;
323 }
324 }
325
326 token.authorized_ok(web::Json(course))
327}
328
329#[instrument(skip(pool))]
333async fn delete_course(
334 course_id: web::Path<Uuid>,
335 pool: web::Data<PgPool>,
336 user: AuthUser,
337) -> ControllerResult<web::Json<Course>> {
338 let mut conn = pool.acquire().await?;
339 let token = authorize(
340 &mut conn,
341 Act::UsuallyUnacceptableDeletion,
342 Some(user.id),
343 Res::Course(*course_id),
344 )
345 .await?;
346 let course = models::courses::delete_course(&mut conn, *course_id).await?;
347
348 token.authorized_ok(web::Json(course))
349}
350
351#[instrument(skip(pool, file_store, app_conf))]
399async fn get_course_structure(
400 course_id: web::Path<Uuid>,
401 pool: web::Data<PgPool>,
402 user: AuthUser,
403 file_store: web::Data<dyn FileStore>,
404 app_conf: web::Data<ApplicationConfiguration>,
405) -> ControllerResult<web::Json<CourseStructure>> {
406 let mut conn = pool.acquire().await?;
407 let token = authorize(
408 &mut conn,
409 Act::ViewInternalCourseStructure,
410 Some(user.id),
411 Res::Course(*course_id),
412 )
413 .await?;
414 let course_structure = models::courses::get_course_structure(
415 &mut conn,
416 *course_id,
417 file_store.as_ref(),
418 app_conf.as_ref(),
419 )
420 .await?;
421
422 token.authorized_ok(web::Json(course_structure))
423}
424
425#[instrument(skip(payload, request, pool, file_store, app_conf))]
441async fn add_media_for_course(
442 course_id: web::Path<Uuid>,
443 payload: Multipart,
444 request: HttpRequest,
445 pool: web::Data<PgPool>,
446 user: AuthUser,
447 file_store: web::Data<dyn FileStore>,
448 app_conf: web::Data<ApplicationConfiguration>,
449) -> ControllerResult<web::Json<UploadResult>> {
450 let mut conn = pool.acquire().await?;
451 let course = models::courses::get_course(&mut conn, *course_id).await?;
452 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
453 let media_path = upload_file_from_cms(
454 request.headers(),
455 payload,
456 StoreKind::Course(course.id),
457 file_store.as_ref(),
458 &mut conn,
459 user,
460 )
461 .await?;
462 let download_url = file_store.get_download_url(media_path.as_path(), app_conf.as_ref());
463
464 token.authorized_ok(web::Json(UploadResult { url: download_url }))
465}
466
467#[instrument(skip(pool))]
471async fn get_all_exercises(
472 pool: web::Data<PgPool>,
473 course_id: web::Path<Uuid>,
474 user: AuthUser,
475) -> ControllerResult<web::Json<Vec<Exercise>>> {
476 let mut conn = pool.acquire().await?;
477 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
478 let exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
479
480 token.authorized_ok(web::Json(exercises))
481}
482
483#[instrument(skip(pool))]
487async fn get_all_exercises_and_count_of_answers_requiring_attention(
488 pool: web::Data<PgPool>,
489 course_id: web::Path<Uuid>,
490 user: AuthUser,
491) -> ControllerResult<web::Json<Vec<ExerciseAnswersInCourseRequiringAttentionCount>>> {
492 let mut conn = pool.acquire().await?;
493 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
494 let _exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
495 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?;
496 token.authorized_ok(web::Json(count_of_answers_requiring_attention))
497}
498
499#[instrument(skip(pool))]
511async fn get_all_course_language_versions(
512 pool: web::Data<PgPool>,
513 course_id: web::Path<Uuid>,
514 user: AuthUser,
515) -> ControllerResult<web::Json<Vec<Course>>> {
516 let mut conn = pool.acquire().await?;
517 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
518 let course = models::courses::get_course(&mut conn, *course_id).await?;
519 let language_versions =
520 models::courses::get_all_language_versions_of_course(&mut conn, &course).await?;
521
522 token.authorized_ok(web::Json(language_versions))
523}
524
525#[derive(Deserialize, Debug)]
526#[serde(tag = "mode", rename_all = "snake_case")]
527#[cfg_attr(feature = "ts_rs", derive(TS))]
528pub enum CopyCourseMode {
529 Duplicate,
531 SameLanguageGroup,
533 ExistingLanguageGroup { target_course_id: Uuid },
535 NewLanguageGroup,
537}
538
539#[derive(Deserialize, Debug)]
540#[cfg_attr(feature = "ts_rs", derive(TS))]
541pub struct CopyCourseRequest {
542 #[serde(flatten)]
543 pub new_course: NewCourse,
544 pub mode: CopyCourseMode,
545}
546
547#[instrument(skip(pool))]
591pub async fn create_course_copy(
592 pool: web::Data<PgPool>,
593 course_id: web::Path<Uuid>,
594 payload: web::Json<CopyCourseRequest>,
595 user: AuthUser,
596) -> ControllerResult<web::Json<Course>> {
597 let mut conn = pool.acquire().await?;
598 let token = authorize(
599 &mut conn,
600 Act::Duplicate,
601 Some(user.id),
602 Res::Course(*course_id),
603 )
604 .await?;
605
606 let mut tx = conn.begin().await?;
607
608 let new_course = payload.new_course.clone();
609
610 let copied_course = match &payload.mode {
611 CopyCourseMode::Duplicate => {
612 models::library::copying::copy_course(&mut tx, *course_id, &new_course, false, user.id)
613 .await?
614 }
615 CopyCourseMode::SameLanguageGroup => {
616 models::library::copying::copy_course(&mut tx, *course_id, &new_course, true, user.id)
617 .await?
618 }
619 CopyCourseMode::ExistingLanguageGroup { target_course_id } => {
620 let target_course = models::courses::get_course(&mut tx, *target_course_id).await?;
621 authorize(
623 &mut tx,
624 Act::Duplicate,
625 Some(user.id),
626 Res::Course(*target_course_id),
627 )
628 .await?;
629 models::library::copying::copy_course_with_language_group(
630 &mut tx,
631 *course_id,
632 target_course.course_language_group_id,
633 &new_course,
634 user.id,
635 )
636 .await?
637 }
638 CopyCourseMode::NewLanguageGroup => {
639 let new_clg_id = course_language_groups::insert(
640 &mut tx,
641 PKeyPolicy::Generate,
642 new_course.slug.as_str(),
643 )
644 .await?;
645 models::library::copying::copy_course_with_language_group(
646 &mut tx,
647 *course_id,
648 new_clg_id,
649 &new_course,
650 user.id,
651 )
652 .await?
653 }
654 };
655
656 models::roles::insert(
657 &mut tx,
658 user.id,
659 models::roles::UserRole::Teacher,
660 models::roles::RoleDomain::Course(copied_course.id),
661 )
662 .await?;
663
664 tx.commit().await?;
665
666 token.authorized_ok(web::Json(copied_course))
667}
668
669#[instrument(skip(pool))]
673async fn get_daily_submission_counts(
674 pool: web::Data<PgPool>,
675 course_id: web::Path<Uuid>,
676 user: AuthUser,
677) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
678 let mut conn = pool.acquire().await?;
679 let token = authorize(
680 &mut conn,
681 Act::ViewStats,
682 Some(user.id),
683 Res::Course(*course_id),
684 )
685 .await?;
686 let course = models::courses::get_course(&mut conn, *course_id).await?;
687 let res =
688 exercise_slide_submissions::get_course_daily_slide_submission_counts(&mut conn, &course)
689 .await?;
690
691 token.authorized_ok(web::Json(res))
692}
693
694#[instrument(skip(pool))]
698async fn get_daily_user_counts_with_submissions(
699 pool: web::Data<PgPool>,
700 course_id: web::Path<Uuid>,
701 user: AuthUser,
702) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
703 let mut conn = pool.acquire().await?;
704 let token = authorize(
705 &mut conn,
706 Act::ViewStats,
707 Some(user.id),
708 Res::Course(*course_id),
709 )
710 .await?;
711 let course = models::courses::get_course(&mut conn, *course_id).await?;
712 let res = exercise_slide_submissions::get_course_daily_user_counts_with_submissions(
713 &mut conn, &course,
714 )
715 .await?;
716
717 token.authorized_ok(web::Json(res))
718}
719
720#[instrument(skip(pool))]
724async fn get_weekday_hour_submission_counts(
725 pool: web::Data<PgPool>,
726 course_id: web::Path<Uuid>,
727 user: AuthUser,
728) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByWeekAndHour>>> {
729 let mut conn = pool.acquire().await?;
730 let token = authorize(
731 &mut conn,
732 Act::ViewStats,
733 Some(user.id),
734 Res::Course(*course_id),
735 )
736 .await?;
737 let course = models::courses::get_course(&mut conn, *course_id).await?;
738 let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_weekday_and_hour(
739 &mut conn, &course,
740 )
741 .await?;
742
743 token.authorized_ok(web::Json(res))
744}
745
746#[instrument(skip(pool))]
750async fn get_submission_counts_by_exercise(
751 pool: web::Data<PgPool>,
752 course_id: web::Path<Uuid>,
753 user: AuthUser,
754) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByExercise>>> {
755 let mut conn = pool.acquire().await?;
756 let token = authorize(
757 &mut conn,
758 Act::ViewStats,
759 Some(user.id),
760 Res::Course(*course_id),
761 )
762 .await?;
763 let course = models::courses::get_course(&mut conn, *course_id).await?;
764 let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_exercise(
765 &mut conn, &course,
766 )
767 .await?;
768
769 token.authorized_ok(web::Json(res))
770}
771
772#[instrument(skip(pool))]
776async fn get_course_instances(
777 pool: web::Data<PgPool>,
778 course_id: web::Path<Uuid>,
779 user: AuthUser,
780) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
781 let mut conn = pool.acquire().await?;
782 let token = authorize(
783 &mut conn,
784 Act::Teach,
785 Some(user.id),
786 Res::Course(*course_id),
787 )
788 .await?;
789 let course_instances =
790 models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
791
792 token.authorized_ok(web::Json(course_instances))
793}
794
795#[derive(Debug, Deserialize)]
796#[cfg_attr(feature = "ts_rs", derive(TS))]
797pub struct GetFeedbackQuery {
798 read: bool,
799 #[serde(flatten)]
800 pagination: Pagination,
801}
802
803#[instrument(skip(pool))]
807pub async fn get_feedback(
808 course_id: web::Path<Uuid>,
809 pool: web::Data<PgPool>,
810 read: web::Query<GetFeedbackQuery>,
811 user: AuthUser,
812) -> ControllerResult<web::Json<Vec<Feedback>>> {
813 let mut conn = pool.acquire().await?;
814 let token = authorize(
815 &mut conn,
816 Act::Teach,
817 Some(user.id),
818 Res::Course(*course_id),
819 )
820 .await?;
821 let feedback =
822 feedback::get_feedback_for_course(&mut conn, *course_id, read.read, read.pagination)
823 .await?;
824
825 token.authorized_ok(web::Json(feedback))
826}
827
828#[instrument(skip(pool))]
832pub async fn get_feedback_count(
833 course_id: web::Path<Uuid>,
834 pool: web::Data<PgPool>,
835 user: AuthUser,
836) -> ControllerResult<web::Json<FeedbackCount>> {
837 let mut conn = pool.acquire().await?;
838 let token = authorize(
839 &mut conn,
840 Act::Teach,
841 Some(user.id),
842 Res::Course(*course_id),
843 )
844 .await?;
845
846 let feedback_count = feedback::get_feedback_count_for_course(&mut conn, *course_id).await?;
847
848 token.authorized_ok(web::Json(feedback_count))
849}
850
851#[instrument(skip(pool))]
855async fn new_course_instance(
856 form: web::Json<CourseInstanceForm>,
857 course_id: web::Path<Uuid>,
858 pool: web::Data<PgPool>,
859 user: AuthUser,
860) -> ControllerResult<web::Json<Uuid>> {
861 let mut conn = pool.acquire().await?;
862 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
863 let form = form.into_inner();
864 let new = NewCourseInstance {
865 course_id: *course_id,
866 name: form.name.as_deref(),
867 description: form.description.as_deref(),
868 support_email: form.support_email.as_deref(),
869 teacher_in_charge_name: &form.teacher_in_charge_name,
870 teacher_in_charge_email: &form.teacher_in_charge_email,
871 opening_time: form.opening_time,
872 closing_time: form.closing_time,
873 };
874 let ci = models::course_instances::insert(&mut conn, PKeyPolicy::Generate, new).await?;
875
876 token.authorized_ok(web::Json(ci.id))
877}
878
879#[instrument(skip(pool))]
880async fn glossary(
881 pool: web::Data<PgPool>,
882 course_id: web::Path<Uuid>,
883 user: AuthUser,
884) -> ControllerResult<web::Json<Vec<Term>>> {
885 let mut conn = pool.acquire().await?;
886 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
887 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
888
889 token.authorized_ok(web::Json(glossary))
890}
891
892#[instrument(skip(pool))]
895async fn _new_term(
896 pool: web::Data<PgPool>,
897 course_id: web::Path<Uuid>,
898 user: AuthUser,
899) -> ControllerResult<web::Json<Vec<Term>>> {
900 let mut conn = pool.acquire().await?;
901 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
902 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
903
904 token.authorized_ok(web::Json(glossary))
905}
906
907#[instrument(skip(pool))]
908async fn new_glossary_term(
909 pool: web::Data<PgPool>,
910 course_id: web::Path<Uuid>,
911 new_term: web::Json<TermUpdate>,
912 user: AuthUser,
913) -> ControllerResult<web::Json<Uuid>> {
914 let mut conn = pool.acquire().await?;
915 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
916 let TermUpdate { term, definition } = new_term.into_inner();
917 let term = models::glossary::insert(&mut conn, &term, &definition, *course_id).await?;
918
919 token.authorized_ok(web::Json(term))
920}
921
922#[instrument(skip(pool))]
926pub async fn get_course_users_counts_by_exercise(
927 course_id: web::Path<Uuid>,
928 pool: web::Data<PgPool>,
929 user: AuthUser,
930) -> ControllerResult<web::Json<Vec<ExerciseUserCounts>>> {
931 let mut conn = pool.acquire().await?;
932 let course_id = course_id.into_inner();
933 let token = authorize(
934 &mut conn,
935 Act::ViewStats,
936 Some(user.id),
937 Res::Course(course_id),
938 )
939 .await?;
940
941 let res =
942 models::user_exercise_states::get_course_users_counts_by_exercise(&mut conn, course_id)
943 .await?;
944
945 token.authorized_ok(web::Json(res))
946}
947
948#[instrument(skip(pool))]
956pub async fn post_new_page_ordering(
957 course_id: web::Path<Uuid>,
958 pool: web::Data<PgPool>,
959 user: AuthUser,
960 payload: web::Json<Vec<Page>>,
961) -> ControllerResult<web::Json<()>> {
962 let mut conn = pool.acquire().await?;
963 let course_id = course_id.into_inner();
964 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
965
966 models::pages::reorder_pages(&mut conn, &payload, course_id).await?;
967
968 token.authorized_ok(web::Json(()))
969}
970
971#[instrument(skip(pool))]
977pub async fn post_new_chapter_ordering(
978 course_id: web::Path<Uuid>,
979 pool: web::Data<PgPool>,
980 user: AuthUser,
981 payload: web::Json<Vec<Chapter>>,
982) -> ControllerResult<web::Json<()>> {
983 let mut conn = pool.acquire().await?;
984 let course_id = course_id.into_inner();
985 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
986
987 models::pages::reorder_chapters(&mut conn, &payload, course_id).await?;
988
989 token.authorized_ok(web::Json(()))
990}
991
992#[instrument(skip(pool))]
993async fn get_material_references_by_course_id(
994 course_id: web::Path<Uuid>,
995 pool: web::Data<PgPool>,
996 user: AuthUser,
997) -> ControllerResult<web::Json<Vec<MaterialReference>>> {
998 let mut conn = pool.acquire().await?;
999 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1000
1001 let res =
1002 models::material_references::get_references_by_course_id(&mut conn, *course_id).await?;
1003 token.authorized_ok(web::Json(res))
1004}
1005
1006#[instrument(skip(pool))]
1007async fn insert_material_references(
1008 course_id: web::Path<Uuid>,
1009 payload: web::Json<Vec<NewMaterialReference>>,
1010 pool: web::Data<PgPool>,
1011 user: AuthUser,
1012) -> ControllerResult<web::Json<()>> {
1013 let mut conn = pool.acquire().await?;
1014 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1015
1016 models::material_references::insert_reference(&mut conn, *course_id, payload.0).await?;
1017
1018 token.authorized_ok(web::Json(()))
1019}
1020
1021#[instrument(skip(pool))]
1022async fn update_material_reference(
1023 path: web::Path<(Uuid, Uuid)>,
1024 pool: web::Data<PgPool>,
1025 user: AuthUser,
1026 payload: web::Json<NewMaterialReference>,
1027) -> ControllerResult<web::Json<()>> {
1028 let (course_id, reference_id) = path.into_inner();
1029 let mut conn = pool.acquire().await?;
1030 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1031
1032 models::material_references::update_material_reference_by_id(
1033 &mut conn,
1034 reference_id,
1035 payload.0,
1036 )
1037 .await?;
1038 token.authorized_ok(web::Json(()))
1039}
1040
1041#[instrument(skip(pool))]
1042async fn delete_material_reference_by_id(
1043 path: web::Path<(Uuid, Uuid)>,
1044 pool: web::Data<PgPool>,
1045 user: AuthUser,
1046) -> ControllerResult<web::Json<()>> {
1047 let (course_id, reference_id) = path.into_inner();
1048 let mut conn = pool.acquire().await?;
1049 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1050
1051 models::material_references::delete_reference(&mut conn, reference_id).await?;
1052 token.authorized_ok(web::Json(()))
1053}
1054
1055#[instrument(skip(pool))]
1056pub async fn update_modules(
1057 course_id: web::Path<Uuid>,
1058 pool: web::Data<PgPool>,
1059 user: AuthUser,
1060 payload: web::Json<ModuleUpdates>,
1061) -> ControllerResult<web::Json<()>> {
1062 let mut conn = pool.acquire().await?;
1063 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1064
1065 models::course_modules::update_modules(&mut conn, *course_id, payload.into_inner()).await?;
1066 token.authorized_ok(web::Json(()))
1067}
1068
1069async fn get_course_default_peer_review(
1070 course_id: web::Path<Uuid>,
1071 pool: web::Data<PgPool>,
1072 user: AuthUser,
1073) -> ControllerResult<web::Json<(PeerOrSelfReviewConfig, Vec<PeerOrSelfReviewQuestion>)>> {
1074 let mut conn = pool.acquire().await?;
1075 let token = authorize(
1076 &mut conn,
1077 Act::Teach,
1078 Some(user.id),
1079 Res::Course(*course_id),
1080 )
1081 .await?;
1082
1083 let peer_review = models::peer_or_self_review_configs::get_default_for_course_by_course_id(
1084 &mut conn, *course_id,
1085 )
1086 .await?;
1087 let peer_or_self_review_questions =
1088 models::peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
1089 &mut conn,
1090 peer_review.id,
1091 )
1092 .await?;
1093 token.authorized_ok(web::Json((peer_review, peer_or_self_review_questions)))
1094}
1095
1096#[instrument(skip(pool, user))]
1103async fn post_update_peer_review_queue_reviews_received(
1104 pool: web::Data<PgPool>,
1105 user: AuthUser,
1106 course_id: web::Path<Uuid>,
1107) -> ControllerResult<web::Json<bool>> {
1108 let mut conn = pool.acquire().await?;
1109 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
1110 models::library::peer_or_self_reviewing::update_peer_review_queue_reviews_received(
1111 &mut conn, *course_id,
1112 )
1113 .await?;
1114 token.authorized_ok(web::Json(true))
1115}
1116
1117#[instrument(skip(pool))]
1123pub async fn submission_export(
1124 course_id: web::Path<Uuid>,
1125 pool: web::Data<PgPool>,
1126 user: AuthUser,
1127) -> ControllerResult<HttpResponse> {
1128 let mut conn = pool.acquire().await?;
1129
1130 let token = authorize(
1131 &mut conn,
1132 Act::Teach,
1133 Some(user.id),
1134 Res::Course(*course_id),
1135 )
1136 .await?;
1137
1138 let course = models::courses::get_course(&mut conn, *course_id).await?;
1139
1140 general_export(
1141 pool,
1142 &format!(
1143 "attachment; filename=\"Course: {} - Submissions (exercise tasks) {}.csv\"",
1144 course.name,
1145 Utc::now().format("%Y-%m-%d")
1146 ),
1147 CourseSubmissionExportOperation {
1148 course_id: *course_id,
1149 },
1150 token,
1151 )
1152 .await
1153}
1154
1155#[instrument(skip(pool))]
1161pub async fn user_details_export(
1162 course_id: web::Path<Uuid>,
1163 pool: web::Data<PgPool>,
1164 user: AuthUser,
1165) -> ControllerResult<HttpResponse> {
1166 let mut conn = pool.acquire().await?;
1167
1168 let token = authorize(
1169 &mut conn,
1170 Act::Teach,
1171 Some(user.id),
1172 Res::Course(*course_id),
1173 )
1174 .await?;
1175
1176 let course = models::courses::get_course(&mut conn, *course_id).await?;
1177
1178 general_export(
1179 pool,
1180 &format!(
1181 "attachment; filename=\"Course: {} - User Details {}.csv\"",
1182 course.name,
1183 Utc::now().format("%Y-%m-%d")
1184 ),
1185 UsersExportOperation {
1186 course_id: *course_id,
1187 },
1188 token,
1189 )
1190 .await
1191}
1192
1193#[instrument(skip(pool))]
1199pub async fn exercise_tasks_export(
1200 course_id: web::Path<Uuid>,
1201 pool: web::Data<PgPool>,
1202 user: AuthUser,
1203) -> ControllerResult<HttpResponse> {
1204 let mut conn = pool.acquire().await?;
1205
1206 let token = authorize(
1207 &mut conn,
1208 Act::Teach,
1209 Some(user.id),
1210 Res::Course(*course_id),
1211 )
1212 .await?;
1213
1214 let course = models::courses::get_course(&mut conn, *course_id).await?;
1215
1216 general_export(
1217 pool,
1218 &format!(
1219 "attachment; filename=\"Course: {} - Exercise tasks {}.csv\"",
1220 course.name,
1221 Utc::now().format("%Y-%m-%d")
1222 ),
1223 CourseExerciseTasksExportOperation {
1224 course_id: *course_id,
1225 },
1226 token,
1227 )
1228 .await
1229}
1230
1231#[instrument(skip(pool))]
1237pub async fn course_instances_export(
1238 course_id: web::Path<Uuid>,
1239 pool: web::Data<PgPool>,
1240 user: AuthUser,
1241) -> ControllerResult<HttpResponse> {
1242 let mut conn = pool.acquire().await?;
1243
1244 let token = authorize(
1245 &mut conn,
1246 Act::Teach,
1247 Some(user.id),
1248 Res::Course(*course_id),
1249 )
1250 .await?;
1251
1252 let course = models::courses::get_course(&mut conn, *course_id).await?;
1253
1254 general_export(
1255 pool,
1256 &format!(
1257 "attachment; filename=\"Course: {} - Instances {}.csv\"",
1258 course.name,
1259 Utc::now().format("%Y-%m-%d")
1260 ),
1261 CourseInstancesExportOperation {
1262 course_id: *course_id,
1263 },
1264 token,
1265 )
1266 .await
1267}
1268
1269#[instrument(skip(pool))]
1275pub async fn course_consent_form_answers_export(
1276 course_id: web::Path<Uuid>,
1277 pool: web::Data<PgPool>,
1278 user: AuthUser,
1279) -> ControllerResult<HttpResponse> {
1280 let mut conn = pool.acquire().await?;
1281
1282 let token = authorize(
1283 &mut conn,
1284 Act::Teach,
1285 Some(user.id),
1286 Res::Course(*course_id),
1287 )
1288 .await?;
1289
1290 let course = models::courses::get_course(&mut conn, *course_id).await?;
1291
1292 general_export(
1293 pool,
1294 &format!(
1295 "attachment; filename=\"Course: {} - User Consents {}.csv\"",
1296 course.name,
1297 Utc::now().format("%Y-%m-%d")
1298 ),
1299 CourseResearchFormExportOperation {
1300 course_id: *course_id,
1301 },
1302 token,
1303 )
1304 .await
1305}
1306
1307#[instrument(skip(pool))]
1313pub async fn user_exercise_states_export(
1314 course_id: web::Path<Uuid>,
1315 pool: web::Data<PgPool>,
1316 user: AuthUser,
1317) -> ControllerResult<HttpResponse> {
1318 let mut conn = pool.acquire().await?;
1319
1320 let token = authorize(
1321 &mut conn,
1322 Act::Teach,
1323 Some(user.id),
1324 Res::Course(*course_id),
1325 )
1326 .await?;
1327
1328 let course = models::courses::get_course(&mut conn, *course_id).await?;
1329
1330 general_export(
1331 pool,
1332 &format!(
1333 "attachment; filename=\"Course: {} - User exercise states {}.csv\"",
1334 course.name,
1335 Utc::now().format("%Y-%m-%d")
1336 ),
1337 UserExerciseStatesExportOperation {
1338 course_id: *course_id,
1339 },
1340 token,
1341 )
1342 .await
1343}
1344
1345pub async fn get_page_visit_datum_summary(
1349 course_id: web::Path<Uuid>,
1350 pool: web::Data<PgPool>,
1351 user: AuthUser,
1352) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourse>>> {
1353 let mut conn = pool.acquire().await?;
1354 let course_id = course_id.into_inner();
1355 let token = authorize(
1356 &mut conn,
1357 Act::ViewStats,
1358 Some(user.id),
1359 Res::Course(course_id),
1360 )
1361 .await?;
1362
1363 let res = models::page_visit_datum_summary_by_courses::get_all_for_course(&mut conn, course_id)
1364 .await?;
1365
1366 token.authorized_ok(web::Json(res))
1367}
1368
1369pub async fn get_page_visit_datum_summary_by_pages(
1373 course_id: web::Path<Uuid>,
1374 pool: web::Data<PgPool>,
1375 user: AuthUser,
1376) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByPages>>> {
1377 let mut conn = pool.acquire().await?;
1378 let course_id = course_id.into_inner();
1379 let token = authorize(
1380 &mut conn,
1381 Act::ViewStats,
1382 Some(user.id),
1383 Res::Course(course_id),
1384 )
1385 .await?;
1386
1387 let res =
1388 models::page_visit_datum_summary_by_pages::get_all_for_course(&mut conn, course_id).await?;
1389
1390 token.authorized_ok(web::Json(res))
1391}
1392
1393pub async fn get_page_visit_datum_summary_by_device_types(
1397 course_id: web::Path<Uuid>,
1398 pool: web::Data<PgPool>,
1399 user: AuthUser,
1400) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourseDeviceTypes>>> {
1401 let mut conn = pool.acquire().await?;
1402 let course_id = course_id.into_inner();
1403 let token = authorize(
1404 &mut conn,
1405 Act::ViewStats,
1406 Some(user.id),
1407 Res::Course(course_id),
1408 )
1409 .await?;
1410
1411 let res = models::page_visit_datum_summary_by_courses_device_types::get_all_for_course(
1412 &mut conn, course_id,
1413 )
1414 .await?;
1415
1416 token.authorized_ok(web::Json(res))
1417}
1418
1419pub async fn get_page_visit_datum_summary_by_countries(
1423 course_id: web::Path<Uuid>,
1424 pool: web::Data<PgPool>,
1425 user: AuthUser,
1426) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCoursesCountries>>> {
1427 let mut conn = pool.acquire().await?;
1428 let course_id = course_id.into_inner();
1429 let token = authorize(
1430 &mut conn,
1431 Act::ViewStats,
1432 Some(user.id),
1433 Res::Course(course_id),
1434 )
1435 .await?;
1436
1437 let res = models::page_visit_datum_summary_by_courses_countries::get_all_for_course(
1438 &mut conn, course_id,
1439 )
1440 .await?;
1441
1442 token.authorized_ok(web::Json(res))
1443}
1444
1445pub async fn teacher_reset_course_progress_for_themselves(
1451 course_id: web::Path<Uuid>,
1452 pool: web::Data<PgPool>,
1453 user: AuthUser,
1454) -> ControllerResult<web::Json<bool>> {
1455 let mut conn = pool.acquire().await?;
1456 let course_id = course_id.into_inner();
1457 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1458
1459 let mut tx = conn.begin().await?;
1460 let course_instances =
1461 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
1462 for course_instance in course_instances {
1463 models::course_instances::reset_progress_on_course_instance_for_user(
1464 &mut tx,
1465 user.id,
1466 course_instance.course_id,
1467 )
1468 .await?;
1469 }
1470
1471 tx.commit().await?;
1472 token.authorized_ok(web::Json(true))
1473}
1474
1475pub async fn teacher_reset_course_progress_for_everyone(
1481 course_id: web::Path<Uuid>,
1482 pool: web::Data<PgPool>,
1483 user: AuthUser,
1484) -> ControllerResult<web::Json<bool>> {
1485 let mut conn = pool.acquire().await?;
1486 let course_id = course_id.into_inner();
1487 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1488 let course = models::courses::get_course(&mut conn, course_id).await?;
1489 if !course.is_draft {
1490 return Err(ControllerError::new(
1491 ControllerErrorType::BadRequest,
1492 "Can only reset progress for a draft course.".to_string(),
1493 None,
1494 ));
1495 }
1496 let n_course_module_completions =
1498 models::course_module_completions::get_count_of_distinct_completors_by_course_id(
1499 &mut conn, course_id,
1500 )
1501 .await?;
1502 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(
1503 &mut conn, course_id,
1504 ).await?;
1505 if n_course_module_completions > 200 {
1506 return Err(ControllerError::new(
1507 ControllerErrorType::BadRequest,
1508 "Too many students have completed the course.".to_string(),
1509 None,
1510 ));
1511 }
1512 if n_completions_registered_to_study_registry > 2 {
1513 return Err(ControllerError::new(
1514 ControllerErrorType::BadRequest,
1515 "Too many students have registered their completion to a study registry".to_string(),
1516 None,
1517 ));
1518 }
1519
1520 let mut tx = conn.begin().await?;
1521 let course_instances =
1522 models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
1523
1524 for course_instance in course_instances {
1526 let users_in_course_instance =
1527 models::users::get_users_by_course_instance_enrollment(&mut tx, course_instance.id)
1528 .await?;
1529 for user_in_course_instance in users_in_course_instance {
1530 models::course_instances::reset_progress_on_course_instance_for_user(
1531 &mut tx,
1532 user_in_course_instance.id,
1533 course_instance.course_id,
1534 )
1535 .await?;
1536 }
1537 }
1538
1539 tx.commit().await?;
1540 token.authorized_ok(web::Json(true))
1541}
1542
1543#[derive(Debug, Deserialize)]
1544#[cfg_attr(feature = "ts_rs", derive(TS))]
1545pub struct GetSuspectedCheatersQuery {
1546 archive: bool,
1547}
1548
1549#[instrument(skip(pool))]
1553async fn get_all_suspected_cheaters(
1554 user: AuthUser,
1555 params: web::Path<Uuid>,
1556 query: web::Query<GetSuspectedCheatersQuery>,
1557 pool: web::Data<PgPool>,
1558) -> ControllerResult<web::Json<Vec<SuspectedCheaters>>> {
1559 let course_id = params.into_inner();
1560
1561 let mut conn = pool.acquire().await?;
1562 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1563
1564 let course_cheaters = models::suspected_cheaters::get_all_suspected_cheaters_in_course(
1565 &mut conn,
1566 course_id,
1567 query.archive,
1568 )
1569 .await?;
1570
1571 token.authorized_ok(web::Json(course_cheaters))
1572}
1573
1574#[instrument(skip(pool))]
1578async fn get_all_thresholds(
1579 user: AuthUser,
1580 params: web::Path<Uuid>,
1581 pool: web::Data<PgPool>,
1582) -> ControllerResult<web::Json<Vec<Threshold>>> {
1583 let mut conn = pool.acquire().await?;
1584 let course_id = params.into_inner();
1585
1586 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1587
1588 let thresholds =
1589 models::suspected_cheaters::get_all_thresholds_for_course(&mut conn, course_id).await?;
1590
1591 token.authorized_ok(web::Json(thresholds))
1592}
1593
1594#[instrument(skip(pool))]
1598async fn teacher_archive_suspected_cheater(
1599 user: AuthUser,
1600 path: web::Path<(Uuid, Uuid)>,
1601 pool: web::Data<PgPool>,
1602) -> ControllerResult<web::Json<()>> {
1603 let (course_id, user_id) = path.into_inner();
1604
1605 let mut conn = pool.acquire().await?;
1606 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1607
1608 models::suspected_cheaters::archive_suspected_cheater(&mut conn, user_id).await?;
1609
1610 token.authorized_ok(web::Json(()))
1611}
1612
1613#[instrument(skip(pool))]
1617async fn teacher_approve_suspected_cheater(
1618 user: AuthUser,
1619 path: web::Path<(Uuid, Uuid)>,
1620 pool: web::Data<PgPool>,
1621) -> ControllerResult<web::Json<()>> {
1622 let (course_id, user_id) = path.into_inner();
1623
1624 let mut conn = pool.acquire().await?;
1625 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1626
1627 models::suspected_cheaters::approve_suspected_cheater(&mut conn, user_id).await?;
1628
1629 models::course_module_completions::update_passed_and_grade_status(
1632 &mut conn, course_id, user_id, false, 0,
1633 )
1634 .await?;
1635
1636 token.authorized_ok(web::Json(()))
1637}
1638
1639#[instrument(skip(pool))]
1643async fn add_user_to_course_with_join_code(
1644 course_id: web::Path<Uuid>,
1645 user: AuthUser,
1646 pool: web::Data<PgPool>,
1647) -> ControllerResult<web::Json<Uuid>> {
1648 let mut conn = pool.acquire().await?;
1649 let token = skip_authorize();
1650
1651 let joined =
1652 models::join_code_uses::insert(&mut conn, PKeyPolicy::Generate, user.id, *course_id)
1653 .await?;
1654 token.authorized_ok(web::Json(joined))
1655}
1656
1657#[instrument(skip(pool))]
1661async fn set_join_code_for_course(
1662 id: web::Path<Uuid>,
1663 pool: web::Data<PgPool>,
1664 user: AuthUser,
1665) -> ControllerResult<HttpResponse> {
1666 let mut conn = pool.acquire().await?;
1667 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*id)).await?;
1668
1669 const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
1670 abcdefghjkmnpqrstuvwxyz";
1671 const PASSWORD_LEN: usize = 64;
1672 let mut rng = rand::rng();
1673
1674 let code: String = (0..PASSWORD_LEN)
1675 .map(|_| {
1676 let idx = rng.random_range(0..CHARSET.len());
1677 CHARSET[idx] as char
1678 })
1679 .collect();
1680
1681 models::courses::set_join_code_for_course(&mut conn, *id, code).await?;
1682 token.authorized_ok(HttpResponse::Ok().finish())
1683}
1684
1685#[instrument(skip(pool))]
1689async fn get_course_with_join_code(
1690 join_code: web::Path<String>,
1691 user: AuthUser,
1692 pool: web::Data<PgPool>,
1693) -> ControllerResult<web::Json<Course>> {
1694 let mut conn = pool.acquire().await?;
1695 let token = skip_authorize();
1696 let course =
1697 models::courses::get_course_with_join_code(&mut conn, join_code.to_string()).await?;
1698
1699 token.authorized_ok(web::Json(course))
1700}
1701
1702#[instrument(skip(payload, pool))]
1706async fn post_partners_block(
1707 path: web::Path<Uuid>,
1708 payload: web::Json<Option<serde_json::Value>>,
1709 pool: web::Data<PgPool>,
1710 user: AuthUser,
1711) -> ControllerResult<web::Json<PartnersBlock>> {
1712 let course_id = path.into_inner();
1713
1714 let content = payload.into_inner();
1715 let mut conn = pool.acquire().await?;
1716 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1717
1718 let upserted_partner_block =
1719 models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
1720
1721 token.authorized_ok(web::Json(upserted_partner_block))
1722}
1723
1724#[instrument(skip(pool))]
1728async fn get_partners_block(
1729 path: web::Path<Uuid>,
1730 user: AuthUser,
1731 pool: web::Data<PgPool>,
1732) -> ControllerResult<web::Json<PartnersBlock>> {
1733 let course_id = path.into_inner();
1734 let mut conn = pool.acquire().await?;
1735 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1736
1737 let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
1739
1740 let partner_block = if course_exists {
1741 models::partner_block::get_partner_block(&mut conn, course_id).await?
1743 } else {
1744 let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
1746
1747 models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
1749 };
1750
1751 token.authorized_ok(web::Json(partner_block))
1752}
1753
1754#[instrument(skip(pool))]
1758async fn delete_partners_block(
1759 path: web::Path<Uuid>,
1760 pool: web::Data<PgPool>,
1761 user: AuthUser,
1762) -> ControllerResult<web::Json<PartnersBlock>> {
1763 let course_id = path.into_inner();
1764 let mut conn = pool.acquire().await?;
1765 let token = authorize(
1766 &mut conn,
1767 Act::UsuallyUnacceptableDeletion,
1768 Some(user.id),
1769 Res::Course(course_id),
1770 )
1771 .await?;
1772 let deleted_partners_block =
1773 models::partner_block::delete_partner_block(&mut conn, course_id).await?;
1774
1775 token.authorized_ok(web::Json(deleted_partners_block))
1776}
1777
1778pub fn _add_routes(cfg: &mut ServiceConfig) {
1786 cfg.service(web::scope("/{course_id}/stats").configure(stats::_add_routes))
1787 .service(web::scope("/{course_id}/chatbots").configure(chatbots::_add_routes))
1788 .service(web::scope("/{course_id}/students").configure(students::_add_routes))
1789 .route("/{course_id}", web::get().to(get_course))
1790 .route("", web::post().to(post_new_course))
1791 .route("/{course_id}", web::put().to(update_course))
1792 .route("/{course_id}", web::delete().to(delete_course))
1793 .route(
1794 "/{course_id}/status-for-all-exercises/{user_id}",
1795 web::get().to(get_all_exercise_statuses_by_course_id),
1796 )
1797 .route(
1798 "/{course_id}/course-module-completions/{user_id}",
1799 web::get().to(get_all_course_module_completions_for_user_by_course_id),
1800 )
1801 .route(
1802 "/{course_id}/daily-submission-counts",
1803 web::get().to(get_daily_submission_counts),
1804 )
1805 .route(
1806 "/{course_id}/daily-users-who-have-submitted-something",
1807 web::get().to(get_daily_user_counts_with_submissions),
1808 )
1809 .route("/{course_id}/exercises", web::get().to(get_all_exercises))
1810 .route(
1811 "/{course_id}/exercises-and-count-of-answers-requiring-attention",
1812 web::get().to(get_all_exercises_and_count_of_answers_requiring_attention),
1813 )
1814 .route(
1815 "/{course_id}/structure",
1816 web::get().to(get_course_structure),
1817 )
1818 .route(
1819 "/{course_id}/language-versions",
1820 web::get().to(get_all_course_language_versions),
1821 )
1822 .route(
1823 "/{course_id}/create-copy",
1824 web::post().to(create_course_copy),
1825 )
1826 .route("/{course_id}/upload", web::post().to(add_media_for_course))
1827 .route(
1828 "/{course_id}/weekday-hour-submission-counts",
1829 web::get().to(get_weekday_hour_submission_counts),
1830 )
1831 .route(
1832 "/{course_id}/submission-counts-by-exercise",
1833 web::get().to(get_submission_counts_by_exercise),
1834 )
1835 .route(
1836 "/{course_id}/course-instances",
1837 web::get().to(get_course_instances),
1838 )
1839 .route("/{course_id}/feedback", web::get().to(get_feedback))
1840 .route(
1841 "/{course_id}/feedback-count",
1842 web::get().to(get_feedback_count),
1843 )
1844 .route(
1845 "/{course_id}/new-course-instance",
1846 web::post().to(new_course_instance),
1847 )
1848 .route("/{course_id}/glossary", web::get().to(glossary))
1849 .route("/{course_id}/glossary", web::post().to(new_glossary_term))
1850 .route(
1851 "/{course_id}/course-users-counts-by-exercise",
1852 web::get().to(get_course_users_counts_by_exercise),
1853 )
1854 .route(
1855 "/{course_id}/new-page-ordering",
1856 web::post().to(post_new_page_ordering),
1857 )
1858 .route(
1859 "/{course_id}/new-chapter-ordering",
1860 web::post().to(post_new_chapter_ordering),
1861 )
1862 .route(
1863 "/{course_id}/references",
1864 web::get().to(get_material_references_by_course_id),
1865 )
1866 .route(
1867 "/{course_id}/references",
1868 web::post().to(insert_material_references),
1869 )
1870 .route(
1871 "/{course_id}/references/{reference_id}",
1872 web::post().to(update_material_reference),
1873 )
1874 .route(
1875 "/{course_id}/references/{reference_id}",
1876 web::delete().to(delete_material_reference_by_id),
1877 )
1878 .route(
1879 "/{course_id}/course-modules",
1880 web::post().to(update_modules),
1881 )
1882 .route(
1883 "/{course_id}/default-peer-review",
1884 web::get().to(get_course_default_peer_review),
1885 )
1886 .route(
1887 "/{course_id}/update-peer-review-queue-reviews-received",
1888 web::post().to(post_update_peer_review_queue_reviews_received),
1889 )
1890 .route(
1891 "/{course_id}/breadcrumb-info",
1892 web::get().to(get_course_breadcrumb_info),
1893 )
1894 .route(
1895 "/{course_id}/progress/{user_id}",
1896 web::get().to(get_user_progress_for_course),
1897 )
1898 .route(
1899 "/{course_id}/user-settings/{user_id}",
1900 web::get().to(get_user_course_settings),
1901 )
1902 .route(
1903 "/{course_id}/export-submissions",
1904 web::get().to(submission_export),
1905 )
1906 .route(
1907 "/{course_id}/export-user-details",
1908 web::get().to(user_details_export),
1909 )
1910 .route(
1911 "/{course_id}/export-exercise-tasks",
1912 web::get().to(exercise_tasks_export),
1913 )
1914 .route(
1915 "/{course_id}/export-course-instances",
1916 web::get().to(course_instances_export),
1917 )
1918 .route(
1919 "/{course_id}/export-course-user-consents",
1920 web::get().to(course_consent_form_answers_export),
1921 )
1922 .route(
1923 "/{course_id}/export-user-exercise-states",
1924 web::get().to(user_exercise_states_export),
1925 )
1926 .route(
1927 "/{course_id}/page-visit-datum-summary",
1928 web::get().to(get_page_visit_datum_summary),
1929 )
1930 .route(
1931 "/{course_id}/page-visit-datum-summary-by-pages",
1932 web::get().to(get_page_visit_datum_summary_by_pages),
1933 )
1934 .route(
1935 "/{course_id}/page-visit-datum-summary-by-device-types",
1936 web::get().to(get_page_visit_datum_summary_by_device_types),
1937 )
1938 .route(
1939 "/{course_id}/page-visit-datum-summary-by-countries",
1940 web::get().to(get_page_visit_datum_summary_by_countries),
1941 )
1942 .route(
1943 "/{course_id}/teacher-reset-course-progress-for-themselves",
1944 web::delete().to(teacher_reset_course_progress_for_themselves),
1945 )
1946 .route("/{course_id}/thresholds", web::get().to(get_all_thresholds))
1947 .route(
1948 "/{course_id}/suspected-cheaters",
1949 web::get().to(get_all_suspected_cheaters),
1950 )
1951 .route(
1952 "/{course_id}/suspected-cheaters/archive/{id}",
1953 web::post().to(teacher_archive_suspected_cheater),
1954 )
1955 .route(
1956 "/{course_id}/suspected-cheaters/approve/{id}",
1957 web::post().to(teacher_approve_suspected_cheater),
1958 )
1959 .route(
1960 "/{course_id}/teacher-reset-course-progress-for-everyone",
1961 web::delete().to(teacher_reset_course_progress_for_everyone),
1962 )
1963 .route(
1964 "/{course_id}/join-course-with-join-code",
1965 web::post().to(add_user_to_course_with_join_code),
1966 )
1967 .route(
1968 "/{course_id}/partners-block",
1969 web::post().to(post_partners_block),
1970 )
1971 .route(
1972 "/{course_id}/partners-block",
1973 web::get().to(get_partners_block),
1974 )
1975 .route(
1976 "/{course_id}/partners-block",
1977 web::delete().to(delete_partners_block),
1978 )
1979 .route(
1980 "/{course_id}/set-join-code",
1981 web::post().to(set_join_code_for_course),
1982 )
1983 .route(
1984 "/{course_id}/reprocess-completions",
1985 web::post().to(post_reprocess_module_completions),
1986 )
1987 .route(
1988 "/join/{join_code}",
1989 web::get().to(get_course_with_join_code),
1990 );
1991}