Skip to main content

headless_lms_server/controllers/main_frontend/courses/
mod.rs

1//! Controllers for requests starting with `/api/v0/main-frontend/courses`.
2
3pub 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::{CourseModuleThresholdInfo, SuspectedCheaterStatus, SuspectedCheaters},
12};
13use std::sync::Arc;
14use utoipa::OpenApi;
15
16use headless_lms_utils::strings::is_ietf_language_code_like;
17use models::{
18    chapters::Chapter,
19    course_instances::{CourseInstance, CourseInstanceForm, NewCourseInstance},
20    course_module_completions::CourseModuleCompletion,
21    course_modules::ModuleUpdates,
22    courses::{Course, CourseBreadcrumbInfo, CourseStructure, CourseUpdate, NewCourse},
23    exercise_slide_submissions::{
24        self, ExerciseAnswersInCourseRequiringAttentionCount, ExerciseSlideSubmissionCount,
25        ExerciseSlideSubmissionCountByExercise, ExerciseSlideSubmissionCountByWeekAndHour,
26    },
27    exercises::{Exercise, ExerciseStatusSummaryForUser},
28    feedback::{self, Feedback, FeedbackCount},
29    glossary::{Term, TermUpdate},
30    library,
31    material_references::{MaterialReference, NewMaterialReference},
32    page_visit_datum_summary_by_courses::PageVisitDatumSummaryByCourse,
33    page_visit_datum_summary_by_courses_countries::PageVisitDatumSummaryByCoursesCountries,
34    page_visit_datum_summary_by_courses_device_types::PageVisitDatumSummaryByCourseDeviceTypes,
35    page_visit_datum_summary_by_pages::PageVisitDatumSummaryByPages,
36    pages::Page,
37    peer_or_self_review_configs::PeerOrSelfReviewConfig,
38    peer_or_self_review_questions::PeerOrSelfReviewQuestion,
39    user_course_settings::UserCourseSettings,
40    user_exercise_states::{ExerciseUserCounts, UserCourseProgress},
41};
42
43use crate::{
44    domain::models_requests::{self, JwtKey},
45    prelude::*,
46};
47
48use headless_lms_models::course_language_groups;
49
50use crate::domain::csv_export::course_instance_export::CourseInstancesExportOperation;
51use crate::domain::csv_export::course_research_form_questions_answers_export::CourseResearchFormExportOperation;
52use crate::domain::csv_export::exercise_tasks_export::CourseExerciseTasksExportOperation;
53use crate::domain::csv_export::general_export;
54use crate::domain::csv_export::submissions::CourseSubmissionExportOperation;
55use crate::domain::csv_export::users_export::UsersExportOperation;
56
57#[derive(OpenApi)]
58#[openapi(
59    paths(
60        get_course,
61        get_course_breadcrumb_info,
62        get_all_exercise_statuses_by_course_id,
63        get_all_course_module_completions_for_user_by_course_id,
64        get_user_progress_for_course,
65        get_user_course_settings,
66        post_reprocess_module_completions,
67        post_new_course,
68        update_course,
69        delete_course,
70        get_course_structure,
71        add_media_for_course,
72        get_all_exercises,
73        get_all_exercises_and_count_of_answers_requiring_attention,
74        get_all_course_language_versions,
75        create_course_copy,
76        get_daily_submission_counts,
77        get_daily_user_counts_with_submissions,
78        get_weekday_hour_submission_counts,
79        get_submission_counts_by_exercise,
80        get_course_instances,
81        get_feedback,
82        get_feedback_count,
83        new_course_instance,
84        glossary,
85        new_glossary_term,
86        get_course_users_counts_by_exercise,
87        post_new_page_ordering,
88        post_new_chapter_ordering,
89        get_material_references_by_course_id,
90        insert_material_references,
91        update_material_reference,
92        delete_material_reference_by_id,
93        update_modules,
94        get_course_default_peer_review,
95        post_update_peer_review_queue_reviews_received,
96        submission_export,
97        user_details_export,
98        exercise_tasks_export,
99        course_instances_export,
100        course_consent_form_answers_export,
101        user_exercise_states_export,
102        get_page_visit_datum_summary,
103        get_page_visit_datum_summary_by_pages,
104        get_page_visit_datum_summary_by_device_types,
105        get_page_visit_datum_summary_by_countries,
106        teacher_reset_course_progress_for_themselves,
107        teacher_reset_course_progress_for_everyone,
108        get_all_suspected_cheaters,
109        get_flagged_suspected_cheaters_count,
110        get_all_thresholds,
111        teacher_dismiss_suspected_cheater,
112        teacher_confirm_suspected_cheater,
113        add_user_to_course_with_join_code,
114        set_join_code_for_course,
115        get_course_with_join_code,
116        post_partners_block,
117        get_partners_block,
118        delete_partners_block
119    ),
120    nest(
121        (path = "/{course_id}/chatbots", api = chatbots::MainFrontendCourseChatbotsApiDoc),
122        (path = "/{course_id}/stats", api = stats::MainFrontendCourseStatsApiDoc),
123        (path = "/{course_id}/students", api = students::MainFrontendCourseStudentsApiDoc)
124    )
125)]
126pub(crate) struct MainFrontendCoursesApiDoc;
127
128/**
129GET `/api/v0/main-frontend/courses/:course_id` - Get course.
130*/
131#[utoipa::path(
132    get,
133    path = "/{course_id}",
134    operation_id = "getCourse",
135    tag = "courses",
136    params(
137        ("course_id" = Uuid, Path, description = "Course id")
138    ),
139    responses(
140        (status = 200, description = "Course", body = Course)
141    )
142)]
143#[instrument(skip(pool))]
144async fn get_course(
145    course_id: web::Path<Uuid>,
146    pool: web::Data<PgPool>,
147    user: AuthUser,
148) -> ControllerResult<web::Json<Course>> {
149    let mut conn = pool.acquire().await?;
150    let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
151    let course = models::courses::get_course(&mut conn, *course_id).await?;
152    token.authorized_ok(web::Json(course))
153}
154
155/**
156GET `/api/v0/main-frontend/courses/:course_id/breadcrumb-info` - Get information to display breadcrumbs on the manage course pages.
157*/
158#[utoipa::path(
159    get,
160    path = "/{course_id}/breadcrumb-info",
161    operation_id = "getCourseBreadcrumbInfo",
162    tag = "courses",
163    params(
164        ("course_id" = Uuid, Path, description = "Course id")
165    ),
166    responses(
167        (status = 200, description = "Course breadcrumb information", body = CourseBreadcrumbInfo)
168    )
169)]
170#[instrument(skip(pool))]
171async fn get_course_breadcrumb_info(
172    course_id: web::Path<Uuid>,
173    pool: web::Data<PgPool>,
174    user: AuthUser,
175) -> ControllerResult<web::Json<CourseBreadcrumbInfo>> {
176    let mut conn = pool.acquire().await?;
177    let user_id = Some(user.id);
178    let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
179    let info = models::courses::get_course_breadcrumb_info(&mut conn, *course_id).await?;
180    token.authorized_ok(web::Json(info))
181}
182
183/**
184GET `/api/v0/main-frontend/courses/:course_id/status-for-all-exercises/:user_id` - Returns status for all exercises in the course for a given user.
185*/
186#[utoipa::path(
187    get,
188    path = "/{course_id}/status-for-all-exercises/{user_id}",
189    operation_id = "getCourseExerciseStatusesForUser",
190    tag = "courses",
191    params(
192        ("course_id" = Uuid, Path, description = "Course id"),
193        ("user_id" = Uuid, Path, description = "User id")
194    ),
195    responses(
196        (status = 200, description = "Exercise statuses for course user", body = [ExerciseStatusSummaryForUser])
197    )
198)]
199#[instrument(skip(pool))]
200async fn get_all_exercise_statuses_by_course_id(
201    params: web::Path<(Uuid, Uuid)>,
202    pool: web::Data<PgPool>,
203    user: AuthUser,
204) -> ControllerResult<web::Json<Vec<ExerciseStatusSummaryForUser>>> {
205    let (course_id, user_id) = params.into_inner();
206    let mut conn = pool.acquire().await?;
207    let token = authorize(
208        &mut conn,
209        Act::ViewUserProgressOrDetails,
210        Some(user.id),
211        Res::Course(course_id),
212    )
213    .await?;
214    let res = models::exercises::get_all_exercise_statuses_by_user_id_and_course_id(
215        &mut conn, course_id, user_id,
216    )
217    .await?;
218    token.authorized_ok(web::Json(res))
219}
220
221/**
222GET `/api/v0/main-frontend/courses/:course_id/course-module-completions/:user_id` - Returns all course module completions for a given user for this course.
223*/
224#[utoipa::path(
225    get,
226    path = "/{course_id}/course-module-completions/{user_id}",
227    operation_id = "getCourseModuleCompletionsForUser",
228    tag = "courses",
229    params(
230        ("course_id" = Uuid, Path, description = "Course id"),
231        ("user_id" = Uuid, Path, description = "User id")
232    ),
233    responses(
234        (status = 200, description = "Course module completions for course user", body = [CourseModuleCompletion])
235    )
236)]
237#[instrument(skip(pool))]
238async fn get_all_course_module_completions_for_user_by_course_id(
239    params: web::Path<(Uuid, Uuid)>,
240    pool: web::Data<PgPool>,
241    user: AuthUser,
242) -> ControllerResult<web::Json<Vec<CourseModuleCompletion>>> {
243    let (course_id, user_id) = params.into_inner();
244    let mut conn = pool.acquire().await?;
245    let token = authorize(
246        &mut conn,
247        Act::ViewUserProgressOrDetails,
248        Some(user.id),
249        Res::Course(course_id),
250    )
251    .await?;
252    let res = models::course_module_completions::get_all_by_course_id_and_user_id(
253        &mut conn, course_id, user_id,
254    )
255    .await?;
256    token.authorized_ok(web::Json(res))
257}
258
259/**
260GET `/api/v0/main-frontend/courses/:course_id/progress/:user_id` - Returns user progress for the course.
261*/
262#[utoipa::path(
263    get,
264    path = "/{course_id}/progress/{user_id}",
265    operation_id = "getCourseProgressForUser",
266    tag = "courses",
267    params(
268        ("course_id" = Uuid, Path, description = "Course id"),
269        ("user_id" = Uuid, Path, description = "User id")
270    ),
271    responses(
272        (status = 200, description = "User progress for course", body = [UserCourseProgress])
273    )
274)]
275#[instrument(skip(pool))]
276async fn get_user_progress_for_course(
277    path: web::Path<(Uuid, Uuid)>,
278    pool: web::Data<PgPool>,
279    user: AuthUser,
280) -> ControllerResult<web::Json<Vec<UserCourseProgress>>> {
281    let (course_id, target_user_id) = path.into_inner();
282    let mut conn = pool.acquire().await?;
283    let token = authorize(
284        &mut conn,
285        Act::ViewUserProgressOrDetails,
286        Some(user.id),
287        Res::Course(course_id),
288    )
289    .await?;
290    let user_course_progress = models::user_exercise_states::get_user_course_progress(
291        &mut conn,
292        course_id,
293        target_user_id,
294        false,
295    )
296    .await?;
297    token.authorized_ok(web::Json(user_course_progress))
298}
299
300/**
301GET `/api/v0/main-frontend/courses/:course_id/user-settings/:user_id` - Get current course settings for a specific user.
302*/
303#[utoipa::path(
304    get,
305    path = "/{course_id}/user-settings/{user_id}",
306    operation_id = "getCourseUserSettingsForUser",
307    tag = "courses",
308    params(
309        ("course_id" = Uuid, Path, description = "Course id"),
310        ("user_id" = Uuid, Path, description = "User id")
311    ),
312    responses(
313        (status = 200, description = "User course settings", body = Option<UserCourseSettings>)
314    )
315)]
316#[instrument(skip(pool))]
317async fn get_user_course_settings(
318    path: web::Path<(Uuid, Uuid)>,
319    pool: web::Data<PgPool>,
320    user: AuthUser,
321) -> ControllerResult<web::Json<Option<UserCourseSettings>>> {
322    let (course_id, target_user_id) = path.into_inner();
323    let mut conn = pool.acquire().await?;
324    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
325    let settings = models::user_course_settings::get_user_course_settings_by_course_id(
326        &mut conn,
327        target_user_id,
328        course_id,
329    )
330    .await?;
331    token.authorized_ok(web::Json(settings))
332}
333
334/**
335POST `/api/v0/main-frontend/courses/{course_id}/reprocess-completions`
336
337Reprocesses all module completions for the given course instance. Only available to admins.
338*/
339#[utoipa::path(
340    post,
341    path = "/{course_id}/reprocess-completions",
342    operation_id = "reprocessCourseCompletions",
343    tag = "courses",
344    params(
345        ("course_id" = Uuid, Path, description = "Course id")
346    ),
347    responses(
348        (status = 200, description = "Course completions reprocessed", body = bool)
349    )
350)]
351#[instrument(skip(pool, user))]
352async fn post_reprocess_module_completions(
353    pool: web::Data<PgPool>,
354    user: AuthUser,
355    course_id: web::Path<Uuid>,
356) -> ControllerResult<web::Json<bool>> {
357    let mut conn = pool.acquire().await?;
358    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
359    models::library::progressing::process_all_course_completions(&mut conn, *course_id).await?;
360    token.authorized_ok(web::Json(true))
361}
362
363/**
364POST `/api/v0/main-frontend/courses` - Create a new course.
365# Example
366
367Request:
368```http
369POST /api/v0/main-frontend/courses HTTP/1.1
370Content-Type: application/json
371
372{
373  "name": "Introduction to introduction",
374  "slug": "introduction-to-introduction",
375  "organization_id": "1b89e57e-8b57-42f2-9fed-c7a6736e3eec"
376}
377```
378*/
379#[utoipa::path(
380    post,
381    path = "",
382    operation_id = "createCourse",
383    tag = "courses",
384    request_body = NewCourse,
385    responses(
386        (status = 200, description = "Created course", body = Course)
387    )
388)]
389#[instrument(skip(pool, app_conf))]
390async fn post_new_course(
391    request_id: RequestId,
392    pool: web::Data<PgPool>,
393    payload: web::Json<NewCourse>,
394    user: AuthUser,
395    app_conf: web::Data<ApplicationConfiguration>,
396    jwt_key: web::Data<JwtKey>,
397) -> ControllerResult<web::Json<Course>> {
398    let mut conn = pool.acquire().await?;
399    let new_course = payload.0;
400    if !is_ietf_language_code_like(&new_course.language_code) {
401        return Err(ControllerError::new(
402            ControllerErrorType::BadRequest,
403            "Malformed language code.".to_string(),
404            None,
405        ));
406    }
407    let token = authorize(
408        &mut conn,
409        Act::CreateCoursesOrExams,
410        Some(user.id),
411        Res::Organization(new_course.organization_id),
412    )
413    .await?;
414
415    let mut tx = conn.begin().await?;
416    let (course, ..) = library::content_management::create_new_course(
417        &mut tx,
418        PKeyPolicy::Generate,
419        new_course,
420        user.id,
421        models_requests::make_spec_fetcher(
422            app_conf.base_url.clone(),
423            request_id.0,
424            Arc::clone(&jwt_key),
425        ),
426        models_requests::fetch_service_info,
427    )
428    .await?;
429    models::roles::insert(
430        &mut tx,
431        user.id,
432        models::roles::UserRole::Teacher,
433        models::roles::RoleDomain::Course(course.id),
434    )
435    .await?;
436    tx.commit().await?;
437
438    token.authorized_ok(web::Json(course))
439}
440
441/**
442POST `/api/v0/main-frontend/courses/:course_id` - Update course.
443# Example
444
445Request:
446```http
447PUT /api/v0/main-frontend/courses/ab4541d8-6db4-4561-bdb2-45f35b2544a1 HTTP/1.1
448Content-Type: application/json
449
450{
451  "name": "Introduction to Introduction"
452}
453
454```
455*/
456#[utoipa::path(
457    put,
458    path = "/{course_id}",
459    operation_id = "updateCourse",
460    tag = "courses",
461    params(
462        ("course_id" = Uuid, Path, description = "Course id")
463    ),
464    request_body = CourseUpdate,
465    responses(
466        (status = 200, description = "Updated course", body = Course)
467    )
468)]
469#[instrument(skip(pool))]
470async fn update_course(
471    payload: web::Json<CourseUpdate>,
472    course_id: web::Path<Uuid>,
473    pool: web::Data<PgPool>,
474    user: AuthUser,
475) -> ControllerResult<web::Json<Course>> {
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 course_update = payload.0;
479    let course_before_update = models::courses::get_course(&mut conn, *course_id).await?;
480    if course_update.can_add_chatbot != course_before_update.can_add_chatbot {
481        // Only global admins can change the chatbot status
482        let _token2 =
483            authorize(&mut conn, Act::Teach, Some(user.id), Res::GlobalPermissions).await?;
484    }
485
486    let locking_just_enabled =
487        !course_before_update.chapter_locking_enabled && course_update.chapter_locking_enabled;
488
489    let course = models::courses::update_course(&mut conn, *course_id, course_update).await?;
490
491    if locking_just_enabled {
492        use models::{user_chapter_locking_statuses, user_course_settings};
493
494        let all_user_settings =
495            user_course_settings::get_all_by_course_id(&mut conn, *course_id).await?;
496
497        for settings in all_user_settings {
498            let _ = user_chapter_locking_statuses::get_or_init_all_for_course(
499                &mut conn,
500                settings.user_id,
501                *course_id,
502            )
503            .await?;
504        }
505    }
506
507    token.authorized_ok(web::Json(course))
508}
509
510/**
511DELETE `/api/v0/main-frontend/courses/:course_id` - Delete a course.
512*/
513#[utoipa::path(
514    delete,
515    path = "/{course_id}",
516    operation_id = "deleteCourse",
517    tag = "courses",
518    params(
519        ("course_id" = Uuid, Path, description = "Course id")
520    ),
521    responses(
522        (status = 200, description = "Deleted course", body = serde_json::Value)
523    )
524)]
525#[instrument(skip(pool))]
526async fn delete_course(
527    course_id: web::Path<Uuid>,
528    pool: web::Data<PgPool>,
529    user: AuthUser,
530) -> ControllerResult<web::Json<Course>> {
531    let mut conn = pool.acquire().await?;
532    let token = authorize(
533        &mut conn,
534        Act::UsuallyUnacceptableDeletion,
535        Some(user.id),
536        Res::Course(*course_id),
537    )
538    .await?;
539    let course = models::courses::delete_course(&mut conn, *course_id).await?;
540
541    token.authorized_ok(web::Json(course))
542}
543
544/**
545GET `/api/v0/main-frontend/courses/:course_id/structure` - Returns the structure of a course.
546# Example
547```json
548{
549  "course": {
550    "id": "d86cf910-4d26-40e9-8c9c-1cc35294fdbb",
551    "slug": "introduction-to-everything",
552    "created_at": "2021-04-28T10:40:54.503917",
553    "updated_at": "2021-04-28T10:40:54.503917",
554    "name": "Introduction to everything",
555    "organization_id": "1b89e57e-8b57-42f2-9fed-c7a6736e3eec",
556    "deleted_at": null,
557    "language_code": "en-US",
558    "copied_from": null,
559    "language_version_of_course_id": null
560  },
561  "pages": [
562    {
563      "id": "f3b0d699-c9be-4d56-bd0a-9d40e5547e4d",
564      "created_at": "2021-04-28T13:51:51.024118",
565      "updated_at": "2021-04-28T14:36:18.179490",
566      "course_id": "d86cf910-4d26-40e9-8c9c-1cc35294fdbb",
567      "content": [],
568      "url_path": "/",
569      "title": "Welcome to Introduction to Everything",
570      "deleted_at": null,
571      "chapter_id": "d332f3d9-39a5-4a18-80f4-251727693c37"
572    }
573  ],
574  "chapters": [
575    {
576      "id": "d332f3d9-39a5-4a18-80f4-251727693c37",
577      "created_at": "2021-04-28T16:11:47.477850",
578      "updated_at": "2021-04-28T16:11:47.477850",
579      "name": "The Basics",
580      "course_id": "d86cf910-4d26-40e9-8c9c-1cc35294fdbb",
581      "deleted_at": null,
582      "chapter_image_url": "http://project-331.local/api/v0/files/uploads/organizations/1b89e57e-8b57-42f2-9fed-c7a6736e3eec/courses/d86cf910-4d26-40e9-8c9c-1cc35294fdbb/images/mbPQh8th96TdUwX96Y0ch1fjbJLRFr.png",
583      "chapter_number": 1,
584      "front_page_id": null
585    }
586  ]
587}
588```
589*/
590#[utoipa::path(
591    get,
592    path = "/{course_id}/structure",
593    operation_id = "getCourseStructure",
594    tag = "courses",
595    params(
596        ("course_id" = Uuid, Path, description = "Course id")
597    ),
598    responses(
599        (status = 200, description = "Course structure", body = CourseStructure)
600    )
601)]
602#[instrument(skip(pool, file_store, app_conf))]
603async fn get_course_structure(
604    course_id: web::Path<Uuid>,
605    pool: web::Data<PgPool>,
606    user: AuthUser,
607    file_store: web::Data<dyn FileStore>,
608    app_conf: web::Data<ApplicationConfiguration>,
609) -> ControllerResult<web::Json<CourseStructure>> {
610    let mut conn = pool.acquire().await?;
611    let token = authorize(
612        &mut conn,
613        Act::ViewInternalCourseStructure,
614        Some(user.id),
615        Res::Course(*course_id),
616    )
617    .await?;
618    let course_structure = models::courses::get_course_structure(
619        &mut conn,
620        *course_id,
621        file_store.as_ref(),
622        app_conf.as_ref(),
623    )
624    .await?;
625
626    token.authorized_ok(web::Json(course_structure))
627}
628
629/**
630POST `/api/v0/main-frontend/courses/:course_id/upload` - Uploads a media (image, audio, file) for the course from Gutenberg page edit.
631
632Put the the contents of the media in a form and add a content type header multipart/form-data.
633# Example
634
635Request:
636```http
637POST /api/v0/main-frontend/pages/d86cf910-4d26-40e9-8c9c-1cc35294fdbb/upload HTTP/1.1
638Content-Type: multipart/form-data
639
640BINARY_DATA
641```
642*/
643#[utoipa::path(
644    post,
645    path = "/{course_id}/upload",
646    operation_id = "uploadCourseMedia",
647    tag = "courses",
648    params(
649        ("course_id" = Uuid, Path, description = "Course id")
650    ),
651    request_body(
652        content = String,
653        content_type = "multipart/form-data"
654    ),
655    responses(
656        (status = 200, description = "Uploaded media result", body = UploadResult)
657    )
658)]
659#[instrument(skip(payload, request, pool, file_store, app_conf))]
660async fn add_media_for_course(
661    course_id: web::Path<Uuid>,
662    payload: Multipart,
663    request: HttpRequest,
664    pool: web::Data<PgPool>,
665    user: AuthUser,
666    file_store: web::Data<dyn FileStore>,
667    app_conf: web::Data<ApplicationConfiguration>,
668) -> ControllerResult<web::Json<UploadResult>> {
669    let mut conn = pool.acquire().await?;
670    let course = models::courses::get_course(&mut conn, *course_id).await?;
671    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
672    let media_path = upload_file_from_cms(
673        request.headers(),
674        payload,
675        StoreKind::Course(course.id),
676        file_store.as_ref(),
677        &mut conn,
678        user,
679    )
680    .await?;
681    let download_url = file_store.get_download_url(media_path.as_path(), app_conf.as_ref());
682
683    token.authorized_ok(web::Json(UploadResult { url: download_url }))
684}
685
686/**
687GET `/api/v0/main-frontend/courses/:id/exercises` - Returns all exercises for the course.
688*/
689#[utoipa::path(
690    get,
691    path = "/{course_id}/exercises",
692    operation_id = "getCourseExercises",
693    tag = "courses",
694    params(
695        ("course_id" = Uuid, Path, description = "Course id")
696    ),
697    responses(
698        (status = 200, description = "Exercises for course", body = [Exercise])
699    )
700)]
701#[instrument(skip(pool))]
702async fn get_all_exercises(
703    pool: web::Data<PgPool>,
704    course_id: web::Path<Uuid>,
705    user: AuthUser,
706) -> ControllerResult<web::Json<Vec<Exercise>>> {
707    let mut conn = pool.acquire().await?;
708    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
709    let exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
710
711    token.authorized_ok(web::Json(exercises))
712}
713
714/**
715GET `/api/v0/main-frontend/courses/:id/exercises-and-count-of-answers-requiring-attention` - Returns all exercises for the course and count of answers requiring attention in them.
716*/
717#[utoipa::path(
718    get,
719    path = "/{course_id}/exercises-and-count-of-answers-requiring-attention",
720    operation_id = "getCourseExercisesAndAnswersRequiringAttentionCounts",
721    tag = "courses",
722    params(
723        ("course_id" = Uuid, Path, description = "Course id")
724    ),
725    responses(
726        (
727            status = 200,
728            description = "Exercises and answer attention counts",
729            body = [ExerciseAnswersInCourseRequiringAttentionCount]
730        )
731    )
732)]
733#[instrument(skip(pool))]
734async fn get_all_exercises_and_count_of_answers_requiring_attention(
735    pool: web::Data<PgPool>,
736    course_id: web::Path<Uuid>,
737    user: AuthUser,
738) -> ControllerResult<web::Json<Vec<ExerciseAnswersInCourseRequiringAttentionCount>>> {
739    let mut conn = pool.acquire().await?;
740    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
741    let _exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
742    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?;
743    token.authorized_ok(web::Json(count_of_answers_requiring_attention))
744}
745
746/**
747GET `/api/v0/main-frontend/courses/:id/language-versions` - Returns all language versions of the same course.
748
749# Example
750
751Request:
752```http
753GET /api/v0/main-frontend/courses/fd484707-25b6-4c51-a4ff-32d8259e3e47/language-versions HTTP/1.1
754Content-Type: application/json
755```
756*/
757#[utoipa::path(
758    get,
759    path = "/{course_id}/language-versions",
760    operation_id = "getCourseLanguageVersions",
761    tag = "courses",
762    params(
763        ("course_id" = Uuid, Path, description = "Course id")
764    ),
765    responses(
766        (status = 200, description = "Course language versions", body = [Course])
767    )
768)]
769#[instrument(skip(pool))]
770async fn get_all_course_language_versions(
771    pool: web::Data<PgPool>,
772    course_id: web::Path<Uuid>,
773    user: AuthUser,
774) -> ControllerResult<web::Json<Vec<Course>>> {
775    let mut conn = pool.acquire().await?;
776    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
777    let course = models::courses::get_course(&mut conn, *course_id).await?;
778    let language_versions =
779        models::courses::get_all_language_versions_of_course(&mut conn, &course).await?;
780
781    token.authorized_ok(web::Json(language_versions))
782}
783
784#[derive(Deserialize, Debug, utoipa::ToSchema)]
785#[serde(tag = "mode", rename_all = "snake_case")]
786pub enum CopyCourseMode {
787    /// Create a completely separate copy with a new course language group
788    Duplicate,
789    /// Create a new language version within the same language group as the source
790    SameLanguageGroup,
791    /// Create a new language version in a specified language group
792    ExistingLanguageGroup { target_course_id: Uuid },
793    /// Create a new language version in a new language group
794    NewLanguageGroup,
795}
796
797#[derive(Deserialize, Debug, utoipa::ToSchema)]
798
799pub struct CopyCourseRequest {
800    #[serde(flatten)]
801    pub new_course: NewCourse,
802    pub mode: CopyCourseMode,
803}
804
805/**
806POST `/api/v0/main-frontend/courses/:id/create-copy` - Create a copy of a course with specified mode.
807
808Different copy modes:
809- `duplicate`: Creates a completely separate copy with new language group
810- `same_language_group`: Creates a new language version within the same language group
811- `existing_language_group`: Creates a new language version in the specified language group
812- `new_language_group`: Creates a new language version in a new language group
813
814# Example
815
816Request:
817```http
818POST /api/v0/main-frontend/courses/fd484707-25b6-4c51-a4ff-32d8259e3e47/create-copy HTTP/1.1
819Content-Type: application/json
820
821{
822  "name": "Johdatus kaikkeen",
823  "slug": "johdatus-kaikkeen",
824  "organization_id": "1b89e57e-8b57-42f2-9fed-c7a6736e3eec",
825  "language_code": "fi-FI",
826  "mode": "duplicate"
827}
828```
829
830Or with an existing language group:
831```http
832POST /api/v0/main-frontend/courses/fd484707-25b6-4c51-a4ff-32d8259e3e47/create-copy HTTP/1.1
833Content-Type: application/json
834
835{
836  "name": "Johdatus kaikkeen",
837  "slug": "johdatus-kaikkeen",
838  "organization_id": "1b89e57e-8b57-42f2-9fed-c7a6736e3eec",
839  "language_code": "fi-FI",
840  "mode": {
841    "existing_language_group": {
842      "target_course_id": "1b89e57e-8b57-42f2-9fed-c7a6736e3eec"
843    }
844  }
845}
846```
847*/
848#[utoipa::path(
849    post,
850    path = "/{course_id}/create-copy",
851    operation_id = "createCourseCopy",
852    tag = "courses",
853    params(
854        ("course_id" = Uuid, Path, description = "Course id")
855    ),
856    request_body = CopyCourseRequest,
857    responses(
858        (status = 200, description = "Created course copy", body = Course)
859    )
860)]
861#[instrument(skip(pool))]
862pub async fn create_course_copy(
863    pool: web::Data<PgPool>,
864    course_id: web::Path<Uuid>,
865    payload: web::Json<CopyCourseRequest>,
866    user: AuthUser,
867) -> ControllerResult<web::Json<Course>> {
868    let mut conn = pool.acquire().await?;
869    let token = authorize(
870        &mut conn,
871        Act::Duplicate,
872        Some(user.id),
873        Res::Course(*course_id),
874    )
875    .await?;
876
877    let new_course = payload.new_course.clone();
878    authorize(
879        &mut conn,
880        Act::CreateCoursesOrExams,
881        Some(user.id),
882        Res::Organization(new_course.organization_id),
883    )
884    .await?;
885
886    let mut tx = conn.begin().await?;
887
888    let copied_course = match &payload.mode {
889        CopyCourseMode::Duplicate => {
890            models::library::copying::copy_course(&mut tx, *course_id, &new_course, false, user.id)
891                .await?
892        }
893        CopyCourseMode::SameLanguageGroup => {
894            models::library::copying::copy_course(&mut tx, *course_id, &new_course, true, user.id)
895                .await?
896        }
897        CopyCourseMode::ExistingLanguageGroup { target_course_id } => {
898            let target_course = models::courses::get_course(&mut tx, *target_course_id).await?;
899            // Verify that the user has permissions also to the course of the custom language group
900            authorize(
901                &mut tx,
902                Act::Duplicate,
903                Some(user.id),
904                Res::Course(*target_course_id),
905            )
906            .await?;
907            models::library::copying::copy_course_with_language_group(
908                &mut tx,
909                *course_id,
910                target_course.course_language_group_id,
911                &new_course,
912                user.id,
913            )
914            .await?
915        }
916        CopyCourseMode::NewLanguageGroup => {
917            let new_clg_id = course_language_groups::insert(
918                &mut tx,
919                PKeyPolicy::Generate,
920                new_course.slug.as_str(),
921            )
922            .await?;
923            models::library::copying::copy_course_with_language_group(
924                &mut tx,
925                *course_id,
926                new_clg_id,
927                &new_course,
928                user.id,
929            )
930            .await?
931        }
932    };
933
934    models::roles::insert(
935        &mut tx,
936        user.id,
937        models::roles::UserRole::Teacher,
938        models::roles::RoleDomain::Course(copied_course.id),
939    )
940    .await?;
941
942    tx.commit().await?;
943
944    token.authorized_ok(web::Json(copied_course))
945}
946
947/**
948GET `/api/v0/main-frontend/courses/:id/daily-submission-counts` - Returns submission counts grouped by day.
949*/
950#[utoipa::path(
951    get,
952    path = "/{course_id}/daily-submission-counts",
953    operation_id = "getCourseDailySubmissionCounts",
954    tag = "courses",
955    params(
956        ("course_id" = Uuid, Path, description = "Course id")
957    ),
958    responses(
959        (status = 200, description = "Course daily submission counts", body = [ExerciseSlideSubmissionCount])
960    )
961)]
962#[instrument(skip(pool))]
963async fn get_daily_submission_counts(
964    pool: web::Data<PgPool>,
965    course_id: web::Path<Uuid>,
966    user: AuthUser,
967) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
968    let mut conn = pool.acquire().await?;
969    let token = authorize(
970        &mut conn,
971        Act::ViewStats,
972        Some(user.id),
973        Res::Course(*course_id),
974    )
975    .await?;
976    let course = models::courses::get_course(&mut conn, *course_id).await?;
977    let res =
978        exercise_slide_submissions::get_course_daily_slide_submission_counts(&mut conn, &course)
979            .await?;
980
981    token.authorized_ok(web::Json(res))
982}
983
984/**
985GET `/api/v0/main-frontend/courses/:id/daily-users-who-have-submitted-something` - Returns a count of users who have submitted something grouped by day.
986*/
987#[utoipa::path(
988    get,
989    path = "/{course_id}/daily-users-who-have-submitted-something",
990    operation_id = "getCourseDailyUsersWhoSubmittedSomething",
991    tag = "courses",
992    params(
993        ("course_id" = Uuid, Path, description = "Course id")
994    ),
995    responses(
996        (status = 200, description = "Course daily user submission counts", body = [ExerciseSlideSubmissionCount])
997    )
998)]
999#[instrument(skip(pool))]
1000async fn get_daily_user_counts_with_submissions(
1001    pool: web::Data<PgPool>,
1002    course_id: web::Path<Uuid>,
1003    user: AuthUser,
1004) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
1005    let mut conn = pool.acquire().await?;
1006    let token = authorize(
1007        &mut conn,
1008        Act::ViewStats,
1009        Some(user.id),
1010        Res::Course(*course_id),
1011    )
1012    .await?;
1013    let course = models::courses::get_course(&mut conn, *course_id).await?;
1014    let res = exercise_slide_submissions::get_course_daily_user_counts_with_submissions(
1015        &mut conn, &course,
1016    )
1017    .await?;
1018
1019    token.authorized_ok(web::Json(res))
1020}
1021
1022/**
1023GET `/api/v0/main-frontend/courses/:id/weekday-hour-submission-counts` - Returns submission counts grouped by weekday and hour.
1024*/
1025#[utoipa::path(
1026    get,
1027    path = "/{course_id}/weekday-hour-submission-counts",
1028    operation_id = "getCourseWeekdayHourSubmissionCounts",
1029    tag = "courses",
1030    params(
1031        ("course_id" = Uuid, Path, description = "Course id")
1032    ),
1033    responses(
1034        (status = 200, description = "Course weekday and hour submission counts", body = [ExerciseSlideSubmissionCountByWeekAndHour])
1035    )
1036)]
1037#[instrument(skip(pool))]
1038async fn get_weekday_hour_submission_counts(
1039    pool: web::Data<PgPool>,
1040    course_id: web::Path<Uuid>,
1041    user: AuthUser,
1042) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByWeekAndHour>>> {
1043    let mut conn = pool.acquire().await?;
1044    let token = authorize(
1045        &mut conn,
1046        Act::ViewStats,
1047        Some(user.id),
1048        Res::Course(*course_id),
1049    )
1050    .await?;
1051    let course = models::courses::get_course(&mut conn, *course_id).await?;
1052    let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_weekday_and_hour(
1053        &mut conn, &course,
1054    )
1055    .await?;
1056
1057    token.authorized_ok(web::Json(res))
1058}
1059
1060/**
1061GET `/api/v0/main-frontend/courses/:id/submission-counts-by-exercise` - Returns submission counts grouped by weekday and hour.
1062*/
1063#[utoipa::path(
1064    get,
1065    path = "/{course_id}/submission-counts-by-exercise",
1066    operation_id = "getCourseSubmissionCountsByExercise",
1067    tag = "courses",
1068    params(
1069        ("course_id" = Uuid, Path, description = "Course id")
1070    ),
1071    responses(
1072        (status = 200, description = "Course submission counts by exercise", body = [ExerciseSlideSubmissionCountByExercise])
1073    )
1074)]
1075#[instrument(skip(pool))]
1076async fn get_submission_counts_by_exercise(
1077    pool: web::Data<PgPool>,
1078    course_id: web::Path<Uuid>,
1079    user: AuthUser,
1080) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByExercise>>> {
1081    let mut conn = pool.acquire().await?;
1082    let token = authorize(
1083        &mut conn,
1084        Act::ViewStats,
1085        Some(user.id),
1086        Res::Course(*course_id),
1087    )
1088    .await?;
1089    let course = models::courses::get_course(&mut conn, *course_id).await?;
1090    let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_exercise(
1091        &mut conn, &course,
1092    )
1093    .await?;
1094
1095    token.authorized_ok(web::Json(res))
1096}
1097
1098/**
1099GET `/api/v0/main-frontend/courses/:id/course-instances` - Returns all course instances for given course id.
1100*/
1101#[utoipa::path(
1102    get,
1103    path = "/{course_id}/course-instances",
1104    operation_id = "getCourseInstances",
1105    tag = "courses",
1106    params(
1107        ("course_id" = Uuid, Path, description = "Course id")
1108    ),
1109    responses(
1110        (status = 200, description = "Course instances", body = [CourseInstance])
1111    )
1112)]
1113#[instrument(skip(pool))]
1114async fn get_course_instances(
1115    pool: web::Data<PgPool>,
1116    course_id: web::Path<Uuid>,
1117    user: AuthUser,
1118) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
1119    let mut conn = pool.acquire().await?;
1120    let token = authorize(
1121        &mut conn,
1122        Act::Teach,
1123        Some(user.id),
1124        Res::Course(*course_id),
1125    )
1126    .await?;
1127    let course_instances =
1128        models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
1129
1130    token.authorized_ok(web::Json(course_instances))
1131}
1132
1133#[derive(Debug, Deserialize)]
1134
1135pub struct GetFeedbackQuery {
1136    read: bool,
1137    #[serde(flatten)]
1138    pagination: Pagination,
1139}
1140
1141/**
1142GET `/api/v0/main-frontend/courses/:id/feedback?read=true` - Returns feedback for the given course.
1143*/
1144#[utoipa::path(
1145    get,
1146    path = "/{course_id}/feedback",
1147    operation_id = "getCourseFeedback",
1148    tag = "courses",
1149    params(
1150        ("course_id" = String, Path, description = "Course id"),
1151        ("read" = bool, Query, description = "Whether to fetch read feedback"),
1152        ("page" = Option<i64>, Query, description = "Page number"),
1153        ("limit" = Option<i64>, Query, description = "Page size")
1154    ),
1155    responses(
1156        (status = 200, description = "Feedback for the course", body = [Feedback])
1157    )
1158)]
1159#[instrument(skip(pool))]
1160pub async fn get_feedback(
1161    course_id: web::Path<Uuid>,
1162    pool: web::Data<PgPool>,
1163    read: web::Query<GetFeedbackQuery>,
1164    user: AuthUser,
1165) -> ControllerResult<web::Json<Vec<Feedback>>> {
1166    let mut conn = pool.acquire().await?;
1167    let token = authorize(
1168        &mut conn,
1169        Act::Teach,
1170        Some(user.id),
1171        Res::Course(*course_id),
1172    )
1173    .await?;
1174    let feedback =
1175        feedback::get_feedback_for_course(&mut conn, *course_id, read.read, read.pagination)
1176            .await?;
1177
1178    token.authorized_ok(web::Json(feedback))
1179}
1180
1181/**
1182GET `/api/v0/main-frontend/courses/:id/feedback-count` - Returns the amount of feedback for the given course.
1183*/
1184#[utoipa::path(
1185    get,
1186    path = "/{course_id}/feedback-count",
1187    operation_id = "getCourseFeedbackCount",
1188    tag = "courses",
1189    params(
1190        ("course_id" = Uuid, Path, description = "Course id")
1191    ),
1192    responses(
1193        (status = 200, description = "Feedback counts for the course", body = FeedbackCount)
1194    )
1195)]
1196#[instrument(skip(pool))]
1197pub async fn get_feedback_count(
1198    course_id: web::Path<Uuid>,
1199    pool: web::Data<PgPool>,
1200    user: AuthUser,
1201) -> ControllerResult<web::Json<FeedbackCount>> {
1202    let mut conn = pool.acquire().await?;
1203    let token = authorize(
1204        &mut conn,
1205        Act::Teach,
1206        Some(user.id),
1207        Res::Course(*course_id),
1208    )
1209    .await?;
1210
1211    let feedback_count = feedback::get_feedback_count_for_course(&mut conn, *course_id).await?;
1212
1213    token.authorized_ok(web::Json(feedback_count))
1214}
1215
1216/**
1217POST `/api/v0/main-frontend/courses/:id/new-course-instance`
1218*/
1219#[utoipa::path(
1220    post,
1221    path = "/{course_id}/new-course-instance",
1222    operation_id = "createCourseInstance",
1223    tag = "courses",
1224    params(
1225        ("course_id" = Uuid, Path, description = "Course id")
1226    ),
1227    request_body = CourseInstanceForm,
1228    responses(
1229        (status = 200, description = "Created course instance id", body = Uuid)
1230    )
1231)]
1232#[instrument(skip(pool))]
1233async fn new_course_instance(
1234    form: web::Json<CourseInstanceForm>,
1235    course_id: web::Path<Uuid>,
1236    pool: web::Data<PgPool>,
1237    user: AuthUser,
1238) -> ControllerResult<web::Json<Uuid>> {
1239    let mut conn = pool.acquire().await?;
1240    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1241    let form = form.into_inner();
1242    let new = NewCourseInstance {
1243        course_id: *course_id,
1244        name: form.name.as_deref(),
1245        description: form.description.as_deref(),
1246        support_email: form.support_email.as_deref(),
1247        teacher_in_charge_name: &form.teacher_in_charge_name,
1248        teacher_in_charge_email: &form.teacher_in_charge_email,
1249        opening_time: form.opening_time,
1250        closing_time: form.closing_time,
1251    };
1252    let ci = models::course_instances::insert(&mut conn, PKeyPolicy::Generate, new).await?;
1253
1254    token.authorized_ok(web::Json(ci.id))
1255}
1256
1257#[instrument(skip(pool))]
1258#[utoipa::path(
1259    get,
1260    path = "/{course_id}/glossary",
1261    operation_id = "getCourseGlossary",
1262    tag = "glossary",
1263    params(
1264        ("course_id" = Uuid, Path, description = "Course id")
1265    ),
1266    responses(
1267        (status = 200, description = "Glossary terms for the course", body = [Term]),
1268        (status = 401, description = "Authentication required"),
1269        (status = 403, description = "User is not allowed to manage the course glossary")
1270    )
1271)]
1272pub(crate) async fn glossary(
1273    pool: web::Data<PgPool>,
1274    course_id: web::Path<Uuid>,
1275    user: AuthUser,
1276) -> ControllerResult<web::Json<Vec<Term>>> {
1277    let mut conn = pool.acquire().await?;
1278    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1279    let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
1280
1281    token.authorized_ok(web::Json(glossary))
1282}
1283
1284// unused?
1285
1286#[instrument(skip(pool))]
1287async fn _new_term(
1288    pool: web::Data<PgPool>,
1289    course_id: web::Path<Uuid>,
1290    user: AuthUser,
1291) -> ControllerResult<web::Json<Vec<Term>>> {
1292    let mut conn = pool.acquire().await?;
1293    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1294    let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
1295
1296    token.authorized_ok(web::Json(glossary))
1297}
1298
1299#[instrument(skip(pool))]
1300#[utoipa::path(
1301    post,
1302    path = "/{course_id}/glossary",
1303    operation_id = "createCourseGlossaryTerm",
1304    tag = "glossary",
1305    params(
1306        ("course_id" = Uuid, Path, description = "Course id")
1307    ),
1308    request_body = TermUpdate,
1309    responses(
1310        (status = 200, description = "Created glossary term id", body = Uuid),
1311        (status = 401, description = "Authentication required"),
1312        (status = 403, description = "User is not allowed to manage the course glossary")
1313    )
1314)]
1315pub(crate) async fn new_glossary_term(
1316    pool: web::Data<PgPool>,
1317    course_id: web::Path<Uuid>,
1318    new_term: web::Json<TermUpdate>,
1319    user: AuthUser,
1320) -> ControllerResult<web::Json<Uuid>> {
1321    let mut conn = pool.acquire().await?;
1322    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1323    let TermUpdate { term, definition } = new_term.into_inner();
1324    let term = models::glossary::insert(&mut conn, &term, &definition, *course_id).await?;
1325
1326    token.authorized_ok(web::Json(term))
1327}
1328
1329/**
1330GET `/api/v0/main-frontend/courses/:id/course-users-counts-by-exercise` - Returns the amount of users for each exercise.
1331*/
1332#[utoipa::path(
1333    get,
1334    path = "/{course_id}/course-users-counts-by-exercise",
1335    operation_id = "getCourseUsersCountsByExercise",
1336    tag = "courses",
1337    params(
1338        ("course_id" = Uuid, Path, description = "Course id")
1339    ),
1340    responses(
1341        (status = 200, description = "Course users counts by exercise", body = [ExerciseUserCounts])
1342    )
1343)]
1344#[instrument(skip(pool))]
1345pub async fn get_course_users_counts_by_exercise(
1346    course_id: web::Path<Uuid>,
1347    pool: web::Data<PgPool>,
1348    user: AuthUser,
1349) -> ControllerResult<web::Json<Vec<ExerciseUserCounts>>> {
1350    let mut conn = pool.acquire().await?;
1351    let course_id = course_id.into_inner();
1352    let token = authorize(
1353        &mut conn,
1354        Act::ViewStats,
1355        Some(user.id),
1356        Res::Course(course_id),
1357    )
1358    .await?;
1359
1360    let res =
1361        models::user_exercise_states::get_course_users_counts_by_exercise(&mut conn, course_id)
1362            .await?;
1363
1364    token.authorized_ok(web::Json(res))
1365}
1366
1367/**
1368POST `/api/v0/main-frontend/courses/:id/new-page-ordering` - Reorders pages to the given order numbers and given chapters.
1369
1370Note that the page objects posted here might have the content omitted because it is not needed here and the content makes the request body to be very large.
1371
1372Creates redirects if url_path changes.
1373*/
1374#[utoipa::path(
1375    post,
1376    path = "/{course_id}/new-page-ordering",
1377    operation_id = "updateCoursePageOrdering",
1378    tag = "courses",
1379    params(
1380        ("course_id" = Uuid, Path, description = "Course id")
1381    ),
1382    request_body = Vec<Page>,
1383    responses(
1384        (status = 200, description = "Course page ordering updated")
1385    )
1386)]
1387#[instrument(skip(pool))]
1388pub async fn post_new_page_ordering(
1389    course_id: web::Path<Uuid>,
1390    pool: web::Data<PgPool>,
1391    user: AuthUser,
1392    payload: web::Json<Vec<Page>>,
1393) -> ControllerResult<web::Json<()>> {
1394    let mut conn = pool.acquire().await?;
1395    let course_id = course_id.into_inner();
1396    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1397
1398    models::pages::reorder_pages(&mut conn, &payload, course_id).await?;
1399
1400    token.authorized_ok(web::Json(()))
1401}
1402
1403/**
1404POST `/api/v0/main-frontend/courses/:id/new-chapter-ordering` - Reorders chapters based on modified chapter number.#
1405
1406Creates redirects if url_path changes.
1407*/
1408#[utoipa::path(
1409    post,
1410    path = "/{course_id}/new-chapter-ordering",
1411    operation_id = "updateCourseChapterOrdering",
1412    tag = "courses",
1413    params(
1414        ("course_id" = Uuid, Path, description = "Course id")
1415    ),
1416    request_body = Vec<Chapter>,
1417    responses(
1418        (status = 200, description = "Course chapter ordering updated")
1419    )
1420)]
1421#[instrument(skip(pool))]
1422pub async fn post_new_chapter_ordering(
1423    course_id: web::Path<Uuid>,
1424    pool: web::Data<PgPool>,
1425    user: AuthUser,
1426    payload: web::Json<Vec<Chapter>>,
1427) -> ControllerResult<web::Json<()>> {
1428    let mut conn = pool.acquire().await?;
1429    let course_id = course_id.into_inner();
1430    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1431
1432    models::pages::reorder_chapters(&mut conn, &payload, course_id).await?;
1433
1434    token.authorized_ok(web::Json(()))
1435}
1436
1437#[utoipa::path(
1438    get,
1439    path = "/{course_id}/references",
1440    operation_id = "getCourseReferences",
1441    tag = "courses",
1442    params(
1443        ("course_id" = Uuid, Path, description = "Course id")
1444    ),
1445    responses(
1446        (status = 200, description = "Course references", body = [MaterialReference])
1447    )
1448)]
1449#[instrument(skip(pool))]
1450async fn get_material_references_by_course_id(
1451    course_id: web::Path<Uuid>,
1452    pool: web::Data<PgPool>,
1453    user: AuthUser,
1454) -> ControllerResult<web::Json<Vec<MaterialReference>>> {
1455    let mut conn = pool.acquire().await?;
1456    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1457
1458    let res =
1459        models::material_references::get_references_by_course_id(&mut conn, *course_id).await?;
1460    token.authorized_ok(web::Json(res))
1461}
1462
1463#[utoipa::path(
1464    post,
1465    path = "/{course_id}/references",
1466    operation_id = "createCourseReferences",
1467    tag = "courses",
1468    params(
1469        ("course_id" = Uuid, Path, description = "Course id")
1470    ),
1471    request_body = [NewMaterialReference],
1472    responses(
1473        (status = 200, description = "Course references created")
1474    )
1475)]
1476#[instrument(skip(pool))]
1477async fn insert_material_references(
1478    course_id: web::Path<Uuid>,
1479    payload: web::Json<Vec<NewMaterialReference>>,
1480    pool: web::Data<PgPool>,
1481    user: AuthUser,
1482) -> ControllerResult<web::Json<()>> {
1483    let mut conn = pool.acquire().await?;
1484    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1485
1486    models::material_references::insert_reference(&mut conn, *course_id, payload.0).await?;
1487
1488    token.authorized_ok(web::Json(()))
1489}
1490
1491#[utoipa::path(
1492    post,
1493    path = "/{course_id}/references/{reference_id}",
1494    operation_id = "updateCourseReference",
1495    tag = "courses",
1496    params(
1497        ("course_id" = Uuid, Path, description = "Course id"),
1498        ("reference_id" = Uuid, Path, description = "Reference id")
1499    ),
1500    request_body = NewMaterialReference,
1501    responses(
1502        (status = 200, description = "Course reference updated")
1503    )
1504)]
1505#[instrument(skip(pool))]
1506async fn update_material_reference(
1507    path: web::Path<(Uuid, Uuid)>,
1508    pool: web::Data<PgPool>,
1509    user: AuthUser,
1510    payload: web::Json<NewMaterialReference>,
1511) -> ControllerResult<web::Json<()>> {
1512    let (course_id, reference_id) = path.into_inner();
1513    let mut conn = pool.acquire().await?;
1514    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1515
1516    models::material_references::update_by_id_and_course_id(
1517        &mut conn,
1518        reference_id,
1519        course_id,
1520        payload.0,
1521    )
1522    .await?;
1523    token.authorized_ok(web::Json(()))
1524}
1525
1526#[utoipa::path(
1527    delete,
1528    path = "/{course_id}/references/{reference_id}",
1529    operation_id = "deleteCourseReference",
1530    tag = "courses",
1531    params(
1532        ("course_id" = Uuid, Path, description = "Course id"),
1533        ("reference_id" = Uuid, Path, description = "Reference id")
1534    ),
1535    responses(
1536        (status = 200, description = "Course reference deleted")
1537    )
1538)]
1539#[instrument(skip(pool))]
1540async fn delete_material_reference_by_id(
1541    path: web::Path<(Uuid, Uuid)>,
1542    pool: web::Data<PgPool>,
1543    user: AuthUser,
1544) -> ControllerResult<web::Json<()>> {
1545    let (course_id, reference_id) = path.into_inner();
1546    let mut conn = pool.acquire().await?;
1547    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1548
1549    models::material_references::delete_by_id_and_course_id(&mut conn, reference_id, course_id)
1550        .await?;
1551    token.authorized_ok(web::Json(()))
1552}
1553
1554#[utoipa::path(
1555    post,
1556    path = "/{course_id}/course-modules",
1557    operation_id = "updateCourseModules",
1558    tag = "courses",
1559    params(
1560        ("course_id" = Uuid, Path, description = "Course id")
1561    ),
1562    request_body = ModuleUpdates,
1563    responses(
1564        (status = 200, description = "Course modules updated")
1565    )
1566)]
1567#[instrument(skip(pool))]
1568pub async fn update_modules(
1569    course_id: web::Path<Uuid>,
1570    pool: web::Data<PgPool>,
1571    user: AuthUser,
1572    payload: web::Json<ModuleUpdates>,
1573) -> ControllerResult<web::Json<()>> {
1574    let mut conn = pool.acquire().await?;
1575    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1576
1577    models::course_modules::update_modules(&mut conn, *course_id, payload.into_inner()).await?;
1578    token.authorized_ok(web::Json(()))
1579}
1580
1581#[utoipa::path(
1582    get,
1583    path = "/{course_id}/default-peer-review",
1584    operation_id = "getCourseDefaultPeerReview",
1585    tag = "courses",
1586    params(
1587        ("course_id" = Uuid, Path, description = "Course id")
1588    ),
1589    responses(
1590        (status = 200, description = "Default peer review configuration", body = serde_json::Value)
1591    )
1592)]
1593async fn get_course_default_peer_review(
1594    course_id: web::Path<Uuid>,
1595    pool: web::Data<PgPool>,
1596    user: AuthUser,
1597) -> ControllerResult<web::Json<(PeerOrSelfReviewConfig, Vec<PeerOrSelfReviewQuestion>)>> {
1598    let mut conn = pool.acquire().await?;
1599    let token = authorize(
1600        &mut conn,
1601        Act::Teach,
1602        Some(user.id),
1603        Res::Course(*course_id),
1604    )
1605    .await?;
1606
1607    let peer_review = models::peer_or_self_review_configs::get_default_for_course_by_course_id(
1608        &mut conn, *course_id,
1609    )
1610    .await?;
1611    let peer_or_self_review_questions =
1612        models::peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
1613            &mut conn,
1614            peer_review.id,
1615        )
1616        .await?;
1617    token.authorized_ok(web::Json((peer_review, peer_or_self_review_questions)))
1618}
1619
1620/**
1621POST `/api/v0/main-frontend/courses/${course_id}/update-peer-review-queue-reviews-received`
1622
1623Updates reviews received for all the students in the peer review queue for a specific course. Updates only entries that have not received enough peer reviews in the table. Only available to admins.
1624*/
1625#[utoipa::path(
1626    post,
1627    path = "/{course_id}/update-peer-review-queue-reviews-received",
1628    operation_id = "updateCoursePeerReviewQueueReviewsReceived",
1629    tag = "courses",
1630    params(
1631        ("course_id" = Uuid, Path, description = "Course id")
1632    ),
1633    responses(
1634        (status = 200, description = "Peer review queue updated", body = bool)
1635    )
1636)]
1637#[instrument(skip(pool, user))]
1638async fn post_update_peer_review_queue_reviews_received(
1639    pool: web::Data<PgPool>,
1640    user: AuthUser,
1641    course_id: web::Path<Uuid>,
1642) -> ControllerResult<web::Json<bool>> {
1643    let mut conn = pool.acquire().await?;
1644    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
1645    models::library::peer_or_self_reviewing::update_peer_review_queue_reviews_received(
1646        &mut conn, *course_id,
1647    )
1648    .await?;
1649    token.authorized_ok(web::Json(true))
1650}
1651
1652/**
1653GET `/api/v0/main-frontend/courses/${courseId}/export-submissions`
1654
1655gets SCV of course exercise submissions
1656*/
1657#[utoipa::path(
1658    get,
1659    path = "/{course_id}/export-submissions",
1660    operation_id = "exportCourseSubmissionsCsv",
1661    tag = "courses",
1662    params(
1663        ("course_id" = Uuid, Path, description = "Course id")
1664    ),
1665    responses(
1666        (status = 200, description = "Course submissions CSV", body = String, content_type = "text/csv")
1667    )
1668)]
1669#[instrument(skip(pool))]
1670pub async fn submission_export(
1671    course_id: web::Path<Uuid>,
1672    pool: web::Data<PgPool>,
1673    user: AuthUser,
1674) -> ControllerResult<HttpResponse> {
1675    let mut conn = pool.acquire().await?;
1676
1677    let token = authorize(
1678        &mut conn,
1679        Act::Teach,
1680        Some(user.id),
1681        Res::Course(*course_id),
1682    )
1683    .await?;
1684
1685    let course = models::courses::get_course(&mut conn, *course_id).await?;
1686
1687    general_export(
1688        pool,
1689        &format!(
1690            "attachment; filename=\"Course: {} - Submissions (exercise tasks) {}.csv\"",
1691            course.name,
1692            Utc::now().format("%Y-%m-%d")
1693        ),
1694        CourseSubmissionExportOperation {
1695            course_id: *course_id,
1696        },
1697        token,
1698    )
1699    .await
1700}
1701
1702/**
1703GET `/api/v0/main-frontend/courses/${course.id}/export-user-details`
1704
1705gets SCV of user details for all users having submitted an exercise in the course
1706*/
1707#[utoipa::path(
1708    get,
1709    path = "/{course_id}/export-user-details",
1710    operation_id = "exportCourseUserDetailsCsv",
1711    tag = "courses",
1712    params(
1713        ("course_id" = Uuid, Path, description = "Course id")
1714    ),
1715    responses(
1716        (status = 200, description = "Course user details CSV", body = String, content_type = "text/csv")
1717    )
1718)]
1719#[instrument(skip(pool))]
1720pub async fn user_details_export(
1721    course_id: web::Path<Uuid>,
1722    pool: web::Data<PgPool>,
1723    user: AuthUser,
1724) -> ControllerResult<HttpResponse> {
1725    let mut conn = pool.acquire().await?;
1726
1727    let token = authorize(
1728        &mut conn,
1729        Act::Teach,
1730        Some(user.id),
1731        Res::Course(*course_id),
1732    )
1733    .await?;
1734
1735    let course = models::courses::get_course(&mut conn, *course_id).await?;
1736
1737    general_export(
1738        pool,
1739        &format!(
1740            "attachment; filename=\"Course: {} - User Details {}.csv\"",
1741            course.name,
1742            Utc::now().format("%Y-%m-%d")
1743        ),
1744        UsersExportOperation {
1745            course_id: *course_id,
1746        },
1747        token,
1748    )
1749    .await
1750}
1751
1752/**
1753GET `/api/v0/main-frontend/courses/${course.id}/export-exercise-tasks`
1754
1755gets SCV all exercise-tasks' private specs in course
1756*/
1757#[utoipa::path(
1758    get,
1759    path = "/{course_id}/export-exercise-tasks",
1760    operation_id = "exportCourseExerciseTasksCsv",
1761    tag = "courses",
1762    params(
1763        ("course_id" = Uuid, Path, description = "Course id")
1764    ),
1765    responses(
1766        (status = 200, description = "Course exercise tasks CSV", body = String, content_type = "text/csv")
1767    )
1768)]
1769#[instrument(skip(pool))]
1770pub async fn exercise_tasks_export(
1771    course_id: web::Path<Uuid>,
1772    pool: web::Data<PgPool>,
1773    user: AuthUser,
1774) -> ControllerResult<HttpResponse> {
1775    let mut conn = pool.acquire().await?;
1776
1777    let token = authorize(
1778        &mut conn,
1779        Act::Teach,
1780        Some(user.id),
1781        Res::Course(*course_id),
1782    )
1783    .await?;
1784
1785    let course = models::courses::get_course(&mut conn, *course_id).await?;
1786
1787    general_export(
1788        pool,
1789        &format!(
1790            "attachment; filename=\"Course: {} - Exercise tasks {}.csv\"",
1791            course.name,
1792            Utc::now().format("%Y-%m-%d")
1793        ),
1794        CourseExerciseTasksExportOperation {
1795            course_id: *course_id,
1796        },
1797        token,
1798    )
1799    .await
1800}
1801
1802/**
1803GET `/api/v0/main-frontend/courses/${course.id}/export-course-instances`
1804
1805gets SCV course instances for course
1806*/
1807#[utoipa::path(
1808    get,
1809    path = "/{course_id}/export-course-instances",
1810    operation_id = "exportCourseInstancesCsv",
1811    tag = "courses",
1812    params(
1813        ("course_id" = Uuid, Path, description = "Course id")
1814    ),
1815    responses(
1816        (status = 200, description = "Course instances CSV", body = String, content_type = "text/csv")
1817    )
1818)]
1819#[instrument(skip(pool))]
1820pub async fn course_instances_export(
1821    course_id: web::Path<Uuid>,
1822    pool: web::Data<PgPool>,
1823    user: AuthUser,
1824) -> ControllerResult<HttpResponse> {
1825    let mut conn = pool.acquire().await?;
1826
1827    let token = authorize(
1828        &mut conn,
1829        Act::Teach,
1830        Some(user.id),
1831        Res::Course(*course_id),
1832    )
1833    .await?;
1834
1835    let course = models::courses::get_course(&mut conn, *course_id).await?;
1836
1837    general_export(
1838        pool,
1839        &format!(
1840            "attachment; filename=\"Course: {} - Instances {}.csv\"",
1841            course.name,
1842            Utc::now().format("%Y-%m-%d")
1843        ),
1844        CourseInstancesExportOperation {
1845            course_id: *course_id,
1846        },
1847        token,
1848    )
1849    .await
1850}
1851
1852/**
1853GET `/api/v0/main-frontend/courses/${course.id}/export-course-user-consents`
1854
1855gets SCV course specific research form questions and user answers for course
1856*/
1857#[utoipa::path(
1858    get,
1859    path = "/{course_id}/export-course-user-consents",
1860    operation_id = "exportCourseUserConsentsCsv",
1861    tag = "courses",
1862    params(
1863        ("course_id" = Uuid, Path, description = "Course id")
1864    ),
1865    responses(
1866        (status = 200, description = "Course user consents CSV", body = String, content_type = "text/csv")
1867    )
1868)]
1869#[instrument(skip(pool))]
1870pub async fn course_consent_form_answers_export(
1871    course_id: web::Path<Uuid>,
1872    pool: web::Data<PgPool>,
1873    user: AuthUser,
1874) -> ControllerResult<HttpResponse> {
1875    let mut conn = pool.acquire().await?;
1876
1877    let token = authorize(
1878        &mut conn,
1879        Act::Teach,
1880        Some(user.id),
1881        Res::Course(*course_id),
1882    )
1883    .await?;
1884
1885    let course = models::courses::get_course(&mut conn, *course_id).await?;
1886
1887    general_export(
1888        pool,
1889        &format!(
1890            "attachment; filename=\"Course: {} - User Consents {}.csv\"",
1891            course.name,
1892            Utc::now().format("%Y-%m-%d")
1893        ),
1894        CourseResearchFormExportOperation {
1895            course_id: *course_id,
1896        },
1897        token,
1898    )
1899    .await
1900}
1901
1902/**
1903GET `/api/v0/main-frontend/courses/${course.id}/export-user-exercise-states`
1904
1905gets CSV for course specific user exercise states
1906*/
1907#[utoipa::path(
1908    get,
1909    path = "/{course_id}/export-user-exercise-states",
1910    operation_id = "exportCourseUserExerciseStatesCsv",
1911    tag = "courses",
1912    params(
1913        ("course_id" = Uuid, Path, description = "Course id")
1914    ),
1915    responses(
1916        (status = 200, description = "Course user exercise states CSV", body = String, content_type = "text/csv")
1917    )
1918)]
1919#[instrument(skip(pool))]
1920pub async fn user_exercise_states_export(
1921    course_id: web::Path<Uuid>,
1922    pool: web::Data<PgPool>,
1923    user: AuthUser,
1924) -> ControllerResult<HttpResponse> {
1925    let mut conn = pool.acquire().await?;
1926
1927    let token = authorize(
1928        &mut conn,
1929        Act::Teach,
1930        Some(user.id),
1931        Res::Course(*course_id),
1932    )
1933    .await?;
1934
1935    let course = models::courses::get_course(&mut conn, *course_id).await?;
1936
1937    general_export(
1938        pool,
1939        &format!(
1940            "attachment; filename=\"Course: {} - User exercise states {}.csv\"",
1941            course.name,
1942            Utc::now().format("%Y-%m-%d")
1943        ),
1944        UserExerciseStatesExportOperation {
1945            course_id: *course_id,
1946        },
1947        token,
1948    )
1949    .await
1950}
1951
1952/**
1953GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary` - Gets aggregated statistics for page visits for the course.
1954*/
1955#[utoipa::path(
1956    get,
1957    path = "/{course_id}/page-visit-datum-summary",
1958    operation_id = "getCoursePageVisitDatumSummary",
1959    tag = "courses",
1960    params(
1961        ("course_id" = Uuid, Path, description = "Course id")
1962    ),
1963    responses(
1964        (status = 200, description = "Course page visit summary", body = [PageVisitDatumSummaryByCourse])
1965    )
1966)]
1967pub async fn get_page_visit_datum_summary(
1968    course_id: web::Path<Uuid>,
1969    pool: web::Data<PgPool>,
1970    user: AuthUser,
1971) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourse>>> {
1972    let mut conn = pool.acquire().await?;
1973    let course_id = course_id.into_inner();
1974    let token = authorize(
1975        &mut conn,
1976        Act::ViewStats,
1977        Some(user.id),
1978        Res::Course(course_id),
1979    )
1980    .await?;
1981
1982    let res = models::page_visit_datum_summary_by_courses::get_all_for_course(&mut conn, course_id)
1983        .await?;
1984
1985    token.authorized_ok(web::Json(res))
1986}
1987
1988/**
1989GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary-by-pages` - Gets aggregated statistics for page visits for the course.
1990*/
1991#[utoipa::path(
1992    get,
1993    path = "/{course_id}/page-visit-datum-summary-by-pages",
1994    operation_id = "getCoursePageVisitDatumSummaryByPages",
1995    tag = "courses",
1996    params(
1997        ("course_id" = Uuid, Path, description = "Course id")
1998    ),
1999    responses(
2000        (status = 200, description = "Course page visit summary by pages", body = [PageVisitDatumSummaryByPages])
2001    )
2002)]
2003pub async fn get_page_visit_datum_summary_by_pages(
2004    course_id: web::Path<Uuid>,
2005    pool: web::Data<PgPool>,
2006    user: AuthUser,
2007) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByPages>>> {
2008    let mut conn = pool.acquire().await?;
2009    let course_id = course_id.into_inner();
2010    let token = authorize(
2011        &mut conn,
2012        Act::ViewStats,
2013        Some(user.id),
2014        Res::Course(course_id),
2015    )
2016    .await?;
2017
2018    let res =
2019        models::page_visit_datum_summary_by_pages::get_all_for_course(&mut conn, course_id).await?;
2020
2021    token.authorized_ok(web::Json(res))
2022}
2023
2024/**
2025GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary-by-device-types` - Gets aggregated statistics for page visits for the course.
2026*/
2027#[utoipa::path(
2028    get,
2029    path = "/{course_id}/page-visit-datum-summary-by-device-types",
2030    operation_id = "getCoursePageVisitDatumSummaryByDeviceTypes",
2031    tag = "courses",
2032    params(
2033        ("course_id" = Uuid, Path, description = "Course id")
2034    ),
2035    responses(
2036        (status = 200, description = "Course page visit summary by device types", body = [PageVisitDatumSummaryByCourseDeviceTypes])
2037    )
2038)]
2039pub async fn get_page_visit_datum_summary_by_device_types(
2040    course_id: web::Path<Uuid>,
2041    pool: web::Data<PgPool>,
2042    user: AuthUser,
2043) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourseDeviceTypes>>> {
2044    let mut conn = pool.acquire().await?;
2045    let course_id = course_id.into_inner();
2046    let token = authorize(
2047        &mut conn,
2048        Act::ViewStats,
2049        Some(user.id),
2050        Res::Course(course_id),
2051    )
2052    .await?;
2053
2054    let res = models::page_visit_datum_summary_by_courses_device_types::get_all_for_course(
2055        &mut conn, course_id,
2056    )
2057    .await?;
2058
2059    token.authorized_ok(web::Json(res))
2060}
2061
2062/**
2063GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary-by-countries` - Gets aggregated statistics for page visits for the course.
2064*/
2065#[utoipa::path(
2066    get,
2067    path = "/{course_id}/page-visit-datum-summary-by-countries",
2068    operation_id = "getCoursePageVisitDatumSummaryByCountries",
2069    tag = "courses",
2070    params(
2071        ("course_id" = Uuid, Path, description = "Course id")
2072    ),
2073    responses(
2074        (status = 200, description = "Course page visit summary by countries", body = [PageVisitDatumSummaryByCoursesCountries])
2075    )
2076)]
2077pub async fn get_page_visit_datum_summary_by_countries(
2078    course_id: web::Path<Uuid>,
2079    pool: web::Data<PgPool>,
2080    user: AuthUser,
2081) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCoursesCountries>>> {
2082    let mut conn = pool.acquire().await?;
2083    let course_id = course_id.into_inner();
2084    let token = authorize(
2085        &mut conn,
2086        Act::ViewStats,
2087        Some(user.id),
2088        Res::Course(course_id),
2089    )
2090    .await?;
2091
2092    let res = models::page_visit_datum_summary_by_courses_countries::get_all_for_course(
2093        &mut conn, course_id,
2094    )
2095    .await?;
2096
2097    token.authorized_ok(web::Json(res))
2098}
2099
2100/**
2101DELETE `/api/v0/main-frontend/courses/${course.id}/teacher-reset-course-progress-for-themselves` - Allows a teacher to reset the course progress for themselves. Cannot be used to reset the course for others.
2102
2103Deletes submissions, user exercise states, and peer reviews etc. for all the course instances of this course.
2104*/
2105#[utoipa::path(
2106    delete,
2107    path = "/{course_id}/teacher-reset-course-progress-for-themselves",
2108    operation_id = "resetCourseProgressForTeacherThemselves",
2109    tag = "courses",
2110    params(
2111        ("course_id" = Uuid, Path, description = "Course id")
2112    ),
2113    responses(
2114        (status = 200, description = "Teacher course progress reset", body = bool)
2115    )
2116)]
2117pub async fn teacher_reset_course_progress_for_themselves(
2118    course_id: web::Path<Uuid>,
2119    pool: web::Data<PgPool>,
2120    user: AuthUser,
2121) -> ControllerResult<web::Json<bool>> {
2122    let mut conn = pool.acquire().await?;
2123    let course_id = course_id.into_inner();
2124    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2125
2126    let mut tx = conn.begin().await?;
2127    let course_instances =
2128        models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2129    for course_instance in course_instances {
2130        models::course_instances::reset_progress_on_course_instance_for_user(
2131            &mut tx,
2132            user.id,
2133            course_instance.course_id,
2134        )
2135        .await?;
2136    }
2137
2138    tx.commit().await?;
2139    token.authorized_ok(web::Json(true))
2140}
2141
2142/**
2143DELETE `/api/v0/main-frontend/courses/${course.id}/teacher-reset-course-progress-for-everyone` - Can be used by teachers to reset the course progress for all students. Only works when the course is a draft and not published to students. Cannot be used to delete a course that some students have taken.
2144
2145Deletes submissions, user exercise states, and peer reviews etc. for all the course instances of this course.
2146*/
2147#[utoipa::path(
2148    delete,
2149    path = "/{course_id}/teacher-reset-course-progress-for-everyone",
2150    operation_id = "resetCourseProgressForEveryone",
2151    tag = "courses",
2152    params(
2153        ("course_id" = Uuid, Path, description = "Course id")
2154    ),
2155    responses(
2156        (status = 200, description = "Course progress reset for everyone", body = bool)
2157    )
2158)]
2159pub async fn teacher_reset_course_progress_for_everyone(
2160    course_id: web::Path<Uuid>,
2161    pool: web::Data<PgPool>,
2162    user: AuthUser,
2163) -> ControllerResult<web::Json<bool>> {
2164    let mut conn = pool.acquire().await?;
2165    let course_id = course_id.into_inner();
2166    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2167    let course = models::courses::get_course(&mut conn, course_id).await?;
2168    if !course.is_draft {
2169        return Err(ControllerError::new(
2170            ControllerErrorType::BadRequest,
2171            "Can only reset progress for a draft course.".to_string(),
2172            None,
2173        ));
2174    }
2175    // To prevent teachers from deleting courses that real students have been taking, we need to address the case where the teacher turns the course back to draft to enable resetting progress for everyone. We'll counteract this by checking the number of course module completions to the course.
2176    let n_course_module_completions =
2177        models::course_module_completions::get_count_of_distinct_completors_by_course_id(
2178            &mut conn, course_id,
2179        )
2180        .await?;
2181    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(
2182        &mut conn, course_id,
2183    ).await?;
2184    if n_course_module_completions > 200 {
2185        return Err(ControllerError::new(
2186            ControllerErrorType::BadRequest,
2187            "Too many students have completed the course.".to_string(),
2188            None,
2189        ));
2190    }
2191    if n_completions_registered_to_study_registry > 2 {
2192        return Err(ControllerError::new(
2193            ControllerErrorType::BadRequest,
2194            "Too many students have registered their completion to a study registry".to_string(),
2195            None,
2196        ));
2197    }
2198
2199    let mut tx = conn.begin().await?;
2200    let course_instances =
2201        models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2202
2203    // Looping though the data since this is only for draft courses and the amount of data is not expected to be large.
2204    for course_instance in course_instances {
2205        let users_in_course_instance =
2206            models::users::get_users_by_course_instance_enrollment(&mut tx, course_instance.id)
2207                .await?;
2208        for user_in_course_instance in users_in_course_instance {
2209            models::course_instances::reset_progress_on_course_instance_for_user(
2210                &mut tx,
2211                user_in_course_instance.id,
2212                course_instance.course_id,
2213            )
2214            .await?;
2215        }
2216    }
2217
2218    tx.commit().await?;
2219    token.authorized_ok(web::Json(true))
2220}
2221
2222#[derive(Debug, Deserialize)]
2223
2224pub struct GetSuspectedCheatersQuery {
2225    status: SuspectedCheaterStatus,
2226}
2227
2228/**
2229 GET /api/v0/main-frontend/courses/${course.id}/suspected-cheaters?status=Flagged - returns the suspected cheaters in the given review state for a course.
2230*/
2231#[utoipa::path(
2232    get,
2233    path = "/{course_id}/suspected-cheaters",
2234    operation_id = "getCourseSuspectedCheaters",
2235    tag = "courses",
2236    params(
2237        ("course_id" = Uuid, Path, description = "Course id"),
2238        ("status" = SuspectedCheaterStatus, Query, description = "Which review state of suspected cheaters to fetch")
2239    ),
2240    responses(
2241        (status = 200, description = "Suspected cheaters for course", body = [SuspectedCheaters])
2242    )
2243)]
2244#[instrument(skip(pool))]
2245async fn get_all_suspected_cheaters(
2246    user: AuthUser,
2247    params: web::Path<Uuid>,
2248    query: web::Query<GetSuspectedCheatersQuery>,
2249    pool: web::Data<PgPool>,
2250) -> ControllerResult<web::Json<Vec<SuspectedCheaters>>> {
2251    let course_id = params.into_inner();
2252
2253    let mut conn = pool.acquire().await?;
2254    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2255
2256    let course_cheaters = models::suspected_cheaters::get_all_suspected_cheaters_in_course(
2257        &mut conn,
2258        course_id,
2259        query.status,
2260    )
2261    .await?;
2262
2263    token.authorized_ok(web::Json(course_cheaters))
2264}
2265
2266/**
2267 GET /api/v0/main-frontend/courses/${course.id}/suspected-cheaters/flagged-count - number of suspected cheaters awaiting review (status flagged). Used to show a review notification to teachers.
2268*/
2269#[utoipa::path(
2270    get,
2271    path = "/{course_id}/suspected-cheaters/flagged-count",
2272    operation_id = "getCourseFlaggedSuspectedCheatersCount",
2273    tag = "courses",
2274    params(
2275        ("course_id" = Uuid, Path, description = "Course id")
2276    ),
2277    responses(
2278        (status = 200, description = "Number of suspected cheaters awaiting review", body = i64, content_type = "application/json")
2279    )
2280)]
2281#[instrument(skip(pool))]
2282async fn get_flagged_suspected_cheaters_count(
2283    user: AuthUser,
2284    params: web::Path<Uuid>,
2285    pool: web::Data<PgPool>,
2286) -> ControllerResult<web::Json<i64>> {
2287    let course_id = params.into_inner();
2288
2289    let mut conn = pool.acquire().await?;
2290    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2291
2292    let count = models::suspected_cheaters::get_count_in_course_by_status(
2293        &mut conn,
2294        course_id,
2295        SuspectedCheaterStatus::Flagged,
2296    )
2297    .await?;
2298
2299    token.authorized_ok(web::Json(count))
2300}
2301
2302/**
2303 GET /api/v0/main-frontend/courses/${course.id}/thresholds - get all thresholds for all modules in a course.
2304*/
2305#[utoipa::path(
2306    get,
2307    path = "/{course_id}/thresholds",
2308    operation_id = "getCourseThresholds",
2309    tag = "courses",
2310    params(
2311        ("course_id" = Uuid, Path, description = "Course id")
2312    ),
2313    responses(
2314        (status = 200, description = "Course thresholds", body = Vec<CourseModuleThresholdInfo>)
2315    )
2316)]
2317#[instrument(skip(pool))]
2318async fn get_all_thresholds(
2319    user: AuthUser,
2320    params: web::Path<Uuid>,
2321    pool: web::Data<PgPool>,
2322) -> ControllerResult<web::Json<Vec<CourseModuleThresholdInfo>>> {
2323    let mut conn = pool.acquire().await?;
2324    let course_id = params.into_inner();
2325
2326    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2327
2328    let thresholds =
2329        models::suspected_cheaters::get_threshold_info_for_course(&mut conn, course_id).await?;
2330
2331    token.authorized_ok(web::Json(thresholds))
2332}
2333
2334/**
2335 POST /api/v0/main-frontend/courses/${course.id}/suspected-cheaters/dismiss/:user_id - dismisses the suspicion as a false alarm (sets status to 'Dismissed').
2336*/
2337#[utoipa::path(
2338    post,
2339    path = "/{course_id}/suspected-cheaters/dismiss/{user_id}",
2340    operation_id = "dismissCourseSuspectedCheater",
2341    tag = "courses",
2342    params(
2343        ("course_id" = Uuid, Path, description = "Course id"),
2344        ("user_id" = Uuid, Path, description = "Suspected cheater's user id")
2345    ),
2346    responses(
2347        (status = 200, description = "Suspicion dismissed")
2348    )
2349)]
2350#[instrument(skip(pool))]
2351async fn teacher_dismiss_suspected_cheater(
2352    user: AuthUser,
2353    path: web::Path<(Uuid, Uuid)>,
2354    pool: web::Data<PgPool>,
2355) -> ControllerResult<web::Json<()>> {
2356    let (course_id, user_id) = path.into_inner();
2357
2358    let mut conn = pool.acquire().await?;
2359    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2360
2361    models::suspected_cheaters::dismiss_by_user_id_and_course_id(&mut conn, user_id, course_id)
2362        .await?;
2363
2364    token.authorized_ok(web::Json(()))
2365}
2366
2367/**
2368 POST /api/v0/main-frontend/courses/${course.id}/suspected-cheaters/confirm/:user_id - confirms the student cheated (sets status to 'ConfirmedCheating') and fails the student.
2369*/
2370#[utoipa::path(
2371    post,
2372    path = "/{course_id}/suspected-cheaters/confirm/{user_id}",
2373    operation_id = "confirmCourseSuspectedCheater",
2374    tag = "courses",
2375    params(
2376        ("course_id" = Uuid, Path, description = "Course id"),
2377        ("user_id" = Uuid, Path, description = "Suspected cheater's user id")
2378    ),
2379    responses(
2380        (status = 200, description = "Cheating confirmed")
2381    )
2382)]
2383#[instrument(skip(pool))]
2384async fn teacher_confirm_suspected_cheater(
2385    user: AuthUser,
2386    path: web::Path<(Uuid, Uuid)>,
2387    pool: web::Data<PgPool>,
2388) -> ControllerResult<web::Json<()>> {
2389    let (course_id, user_id) = path.into_inner();
2390
2391    let mut conn = pool.acquire().await?;
2392    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2393
2394    // Confirming sets the status and fails the student's completions (snapshotting the previous
2395    // grade so a later dismiss can restore it); see confirm_cheater_by_user_id_and_course_id.
2396    models::suspected_cheaters::confirm_cheater_by_user_id_and_course_id(
2397        &mut conn, user_id, course_id,
2398    )
2399    .await?;
2400
2401    token.authorized_ok(web::Json(()))
2402}
2403
2404#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
2405pub struct JoinCourseWithJoinCodePayload {
2406    join_code: String,
2407}
2408
2409/**
2410POST /courses/:course_id/join-course-with-join-code - Adds the user to join_code_uses so the user gets access to the course
2411*/
2412#[utoipa::path(
2413    post,
2414    path = "/{course_id}/join-course-with-join-code",
2415    operation_id = "joinCourseWithJoinCode",
2416    tag = "courses",
2417    params(
2418        ("course_id" = Uuid, Path, description = "Course id")
2419    ),
2420    request_body = JoinCourseWithJoinCodePayload,
2421    responses(
2422        (status = 200, description = "Joined course id", body = Uuid)
2423    )
2424)]
2425#[instrument(skip(pool))]
2426async fn add_user_to_course_with_join_code(
2427    course_id: web::Path<Uuid>,
2428    payload: web::Json<JoinCourseWithJoinCodePayload>,
2429    user: AuthUser,
2430    pool: web::Data<PgPool>,
2431) -> ControllerResult<web::Json<Uuid>> {
2432    let mut conn = pool.acquire().await?;
2433    let token = skip_authorize();
2434
2435    models::courses::get_by_id_and_join_code(&mut conn, *course_id, &payload.join_code).await?;
2436    let joined =
2437        models::join_code_uses::insert(&mut conn, PKeyPolicy::Generate, user.id, *course_id)
2438            .await?;
2439    token.authorized_ok(web::Json(joined))
2440}
2441
2442/**
2443 POST /api/v0/main-frontend/courses/:course_id/generate-join-code - Generates a code that is used as a part of URL to join course
2444*/
2445#[utoipa::path(
2446    post,
2447    path = "/{course_id}/set-join-code",
2448    operation_id = "setCourseJoinCode",
2449    tag = "courses",
2450    params(
2451        ("course_id" = Uuid, Path, description = "Course id")
2452    ),
2453    responses(
2454        (status = 200, description = "Course join code set")
2455    )
2456)]
2457#[instrument(skip(pool))]
2458async fn set_join_code_for_course(
2459    id: web::Path<Uuid>,
2460    pool: web::Data<PgPool>,
2461    user: AuthUser,
2462) -> ControllerResult<HttpResponse> {
2463    let mut conn = pool.acquire().await?;
2464    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*id)).await?;
2465
2466    const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
2467                            abcdefghjkmnpqrstuvwxyz";
2468    const PASSWORD_LEN: usize = 64;
2469    let mut rng = rand::rng();
2470
2471    let code: String = (0..PASSWORD_LEN)
2472        .map(|_| {
2473            let idx = rng.random_range(0..CHARSET.len());
2474            CHARSET[idx] as char
2475        })
2476        .collect();
2477
2478    models::courses::set_join_code_for_course(&mut conn, *id, code).await?;
2479    token.authorized_ok(HttpResponse::Ok().finish())
2480}
2481
2482/**
2483GET /courses/join/:join_code - Gets the course related to join code
2484*/
2485#[utoipa::path(
2486    get,
2487    path = "/join/{join_code}",
2488    operation_id = "getCourseByJoinCode",
2489    tag = "courses",
2490    params(
2491        ("join_code" = String, Path, description = "Course join code")
2492    ),
2493    responses(
2494        (status = 200, description = "Course for join code", body = Course)
2495    )
2496)]
2497#[instrument(skip(pool))]
2498async fn get_course_with_join_code(
2499    join_code: web::Path<String>,
2500    user: AuthUser,
2501    pool: web::Data<PgPool>,
2502) -> ControllerResult<web::Json<Course>> {
2503    let mut conn = pool.acquire().await?;
2504    let token = skip_authorize();
2505    let course =
2506        models::courses::get_course_with_join_code(&mut conn, join_code.to_string()).await?;
2507
2508    token.authorized_ok(web::Json(course))
2509}
2510
2511/**
2512 POST /api/v0/main-frontend/courses/:course_id/partners_block - Create or updates a partners block for a course
2513*/
2514#[utoipa::path(
2515    post,
2516    path = "/{course_id}/partners-block",
2517    operation_id = "upsertCoursePartnersBlock",
2518    tag = "courses",
2519    params(
2520        ("course_id" = Uuid, Path, description = "Course id")
2521    ),
2522    request_body = Option<serde_json::Value>,
2523    responses(
2524        (status = 200, description = "Partners block", body = serde_json::Value)
2525    )
2526)]
2527#[instrument(skip(payload, pool))]
2528async fn post_partners_block(
2529    path: web::Path<Uuid>,
2530    payload: web::Json<Option<serde_json::Value>>,
2531    pool: web::Data<PgPool>,
2532    user: AuthUser,
2533) -> ControllerResult<web::Json<PartnersBlock>> {
2534    let course_id = path.into_inner();
2535
2536    let content = payload.into_inner();
2537    let mut conn = pool.acquire().await?;
2538    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2539
2540    let upserted_partner_block =
2541        models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
2542
2543    token.authorized_ok(web::Json(upserted_partner_block))
2544}
2545
2546/**
2547GET /courses/:course_id/partners_blocks - Gets a partners block related to a course
2548*/
2549#[utoipa::path(
2550    get,
2551    path = "/{course_id}/partners-block",
2552    operation_id = "getCoursePartnersBlock",
2553    tag = "courses",
2554    params(
2555        ("course_id" = Uuid, Path, description = "Course id")
2556    ),
2557    responses(
2558        (status = 200, description = "Partners block", body = serde_json::Value)
2559    )
2560)]
2561#[instrument(skip(pool))]
2562async fn get_partners_block(
2563    path: web::Path<Uuid>,
2564    user: AuthUser,
2565    pool: web::Data<PgPool>,
2566) -> ControllerResult<web::Json<PartnersBlock>> {
2567    let course_id = path.into_inner();
2568    let mut conn = pool.acquire().await?;
2569    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2570
2571    // Check if the course exists in the partners_blocks table
2572    let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
2573
2574    let partner_block = if course_exists {
2575        // If the course exists, fetch the partner block
2576        models::partner_block::get_partner_block(&mut conn, course_id).await?
2577    } else {
2578        // If the course does not exist, create a new partner block with an empty content array
2579        let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
2580
2581        // Upsert the partner block with the empty content
2582        models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
2583    };
2584
2585    token.authorized_ok(web::Json(partner_block))
2586}
2587
2588/**
2589DELETE `/api/v0/main-frontend/courses/:course_id` - Delete a partners block in a course.
2590*/
2591#[utoipa::path(
2592    delete,
2593    path = "/{course_id}/partners-block",
2594    operation_id = "deleteCoursePartnersBlock",
2595    tag = "courses",
2596    params(
2597        ("course_id" = Uuid, Path, description = "Course id")
2598    ),
2599    responses(
2600        (status = 200, description = "Deleted partners block", body = serde_json::Value)
2601    )
2602)]
2603#[instrument(skip(pool))]
2604async fn delete_partners_block(
2605    path: web::Path<Uuid>,
2606    pool: web::Data<PgPool>,
2607    user: AuthUser,
2608) -> ControllerResult<web::Json<PartnersBlock>> {
2609    let course_id = path.into_inner();
2610    let mut conn = pool.acquire().await?;
2611    let token = authorize(
2612        &mut conn,
2613        Act::UsuallyUnacceptableDeletion,
2614        Some(user.id),
2615        Res::Course(course_id),
2616    )
2617    .await?;
2618    let deleted_partners_block =
2619        models::partner_block::delete_partner_block(&mut conn, course_id).await?;
2620
2621    token.authorized_ok(web::Json(deleted_partners_block))
2622}
2623
2624/**
2625Add a route for each controller in this module.
2626
2627The name starts with an underline in order to appear before other functions in the module documentation.
2628
2629We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
2630*/
2631pub fn _add_routes(cfg: &mut ServiceConfig) {
2632    cfg.service(web::scope("/{course_id}/stats").configure(stats::_add_routes))
2633        .service(web::scope("/{course_id}/chatbots").configure(chatbots::_add_routes))
2634        .service(web::scope("/{course_id}/students").configure(students::_add_routes))
2635        .route("/{course_id}", web::get().to(get_course))
2636        .route("", web::post().to(post_new_course))
2637        .route("/{course_id}", web::put().to(update_course))
2638        .route("/{course_id}", web::delete().to(delete_course))
2639        .route(
2640            "/{course_id}/status-for-all-exercises/{user_id}",
2641            web::get().to(get_all_exercise_statuses_by_course_id),
2642        )
2643        .route(
2644            "/{course_id}/course-module-completions/{user_id}",
2645            web::get().to(get_all_course_module_completions_for_user_by_course_id),
2646        )
2647        .route(
2648            "/{course_id}/daily-submission-counts",
2649            web::get().to(get_daily_submission_counts),
2650        )
2651        .route(
2652            "/{course_id}/daily-users-who-have-submitted-something",
2653            web::get().to(get_daily_user_counts_with_submissions),
2654        )
2655        .route("/{course_id}/exercises", web::get().to(get_all_exercises))
2656        .route(
2657            "/{course_id}/exercises-and-count-of-answers-requiring-attention",
2658            web::get().to(get_all_exercises_and_count_of_answers_requiring_attention),
2659        )
2660        .route(
2661            "/{course_id}/structure",
2662            web::get().to(get_course_structure),
2663        )
2664        .route(
2665            "/{course_id}/language-versions",
2666            web::get().to(get_all_course_language_versions),
2667        )
2668        .route(
2669            "/{course_id}/create-copy",
2670            web::post().to(create_course_copy),
2671        )
2672        .route("/{course_id}/upload", web::post().to(add_media_for_course))
2673        .route(
2674            "/{course_id}/weekday-hour-submission-counts",
2675            web::get().to(get_weekday_hour_submission_counts),
2676        )
2677        .route(
2678            "/{course_id}/submission-counts-by-exercise",
2679            web::get().to(get_submission_counts_by_exercise),
2680        )
2681        .route(
2682            "/{course_id}/course-instances",
2683            web::get().to(get_course_instances),
2684        )
2685        .route("/{course_id}/feedback", web::get().to(get_feedback))
2686        .route(
2687            "/{course_id}/feedback-count",
2688            web::get().to(get_feedback_count),
2689        )
2690        .route(
2691            "/{course_id}/new-course-instance",
2692            web::post().to(new_course_instance),
2693        )
2694        .route("/{course_id}/glossary", web::get().to(glossary))
2695        .route("/{course_id}/glossary", web::post().to(new_glossary_term))
2696        .route(
2697            "/{course_id}/course-users-counts-by-exercise",
2698            web::get().to(get_course_users_counts_by_exercise),
2699        )
2700        .route(
2701            "/{course_id}/new-page-ordering",
2702            web::post().to(post_new_page_ordering),
2703        )
2704        .route(
2705            "/{course_id}/new-chapter-ordering",
2706            web::post().to(post_new_chapter_ordering),
2707        )
2708        .route(
2709            "/{course_id}/references",
2710            web::get().to(get_material_references_by_course_id),
2711        )
2712        .route(
2713            "/{course_id}/references",
2714            web::post().to(insert_material_references),
2715        )
2716        .route(
2717            "/{course_id}/references/{reference_id}",
2718            web::post().to(update_material_reference),
2719        )
2720        .route(
2721            "/{course_id}/references/{reference_id}",
2722            web::delete().to(delete_material_reference_by_id),
2723        )
2724        .route(
2725            "/{course_id}/course-modules",
2726            web::post().to(update_modules),
2727        )
2728        .route(
2729            "/{course_id}/default-peer-review",
2730            web::get().to(get_course_default_peer_review),
2731        )
2732        .route(
2733            "/{course_id}/update-peer-review-queue-reviews-received",
2734            web::post().to(post_update_peer_review_queue_reviews_received),
2735        )
2736        .route(
2737            "/{course_id}/breadcrumb-info",
2738            web::get().to(get_course_breadcrumb_info),
2739        )
2740        .route(
2741            "/{course_id}/progress/{user_id}",
2742            web::get().to(get_user_progress_for_course),
2743        )
2744        .route(
2745            "/{course_id}/user-settings/{user_id}",
2746            web::get().to(get_user_course_settings),
2747        )
2748        .route(
2749            "/{course_id}/export-submissions",
2750            web::get().to(submission_export),
2751        )
2752        .route(
2753            "/{course_id}/export-user-details",
2754            web::get().to(user_details_export),
2755        )
2756        .route(
2757            "/{course_id}/export-exercise-tasks",
2758            web::get().to(exercise_tasks_export),
2759        )
2760        .route(
2761            "/{course_id}/export-course-instances",
2762            web::get().to(course_instances_export),
2763        )
2764        .route(
2765            "/{course_id}/export-course-user-consents",
2766            web::get().to(course_consent_form_answers_export),
2767        )
2768        .route(
2769            "/{course_id}/export-user-exercise-states",
2770            web::get().to(user_exercise_states_export),
2771        )
2772        .route(
2773            "/{course_id}/page-visit-datum-summary",
2774            web::get().to(get_page_visit_datum_summary),
2775        )
2776        .route(
2777            "/{course_id}/page-visit-datum-summary-by-pages",
2778            web::get().to(get_page_visit_datum_summary_by_pages),
2779        )
2780        .route(
2781            "/{course_id}/page-visit-datum-summary-by-device-types",
2782            web::get().to(get_page_visit_datum_summary_by_device_types),
2783        )
2784        .route(
2785            "/{course_id}/page-visit-datum-summary-by-countries",
2786            web::get().to(get_page_visit_datum_summary_by_countries),
2787        )
2788        .route(
2789            "/{course_id}/teacher-reset-course-progress-for-themselves",
2790            web::delete().to(teacher_reset_course_progress_for_themselves),
2791        )
2792        .route("/{course_id}/thresholds", web::get().to(get_all_thresholds))
2793        .route(
2794            "/{course_id}/suspected-cheaters",
2795            web::get().to(get_all_suspected_cheaters),
2796        )
2797        .route(
2798            "/{course_id}/suspected-cheaters/flagged-count",
2799            web::get().to(get_flagged_suspected_cheaters_count),
2800        )
2801        .route(
2802            "/{course_id}/suspected-cheaters/dismiss/{user_id}",
2803            web::post().to(teacher_dismiss_suspected_cheater),
2804        )
2805        .route(
2806            "/{course_id}/suspected-cheaters/confirm/{user_id}",
2807            web::post().to(teacher_confirm_suspected_cheater),
2808        )
2809        .route(
2810            "/{course_id}/teacher-reset-course-progress-for-everyone",
2811            web::delete().to(teacher_reset_course_progress_for_everyone),
2812        )
2813        .route(
2814            "/{course_id}/join-course-with-join-code",
2815            web::post().to(add_user_to_course_with_join_code),
2816        )
2817        .route(
2818            "/{course_id}/partners-block",
2819            web::post().to(post_partners_block),
2820        )
2821        .route(
2822            "/{course_id}/partners-block",
2823            web::get().to(get_partners_block),
2824        )
2825        .route(
2826            "/{course_id}/partners-block",
2827            web::delete().to(delete_partners_block),
2828        )
2829        .route(
2830            "/{course_id}/set-join-code",
2831            web::post().to(set_join_code_for_course),
2832        )
2833        .route(
2834            "/{course_id}/reprocess-completions",
2835            web::post().to(post_reprocess_module_completions),
2836        )
2837        .route(
2838            "/join/{join_code}",
2839            web::get().to(get_course_with_join_code),
2840        );
2841}