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