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 mut tx = conn.begin().await?;
877
878    let new_course = payload.new_course.clone();
879
880    let copied_course = match &payload.mode {
881        CopyCourseMode::Duplicate => {
882            models::library::copying::copy_course(&mut tx, *course_id, &new_course, false, user.id)
883                .await?
884        }
885        CopyCourseMode::SameLanguageGroup => {
886            models::library::copying::copy_course(&mut tx, *course_id, &new_course, true, user.id)
887                .await?
888        }
889        CopyCourseMode::ExistingLanguageGroup { target_course_id } => {
890            let target_course = models::courses::get_course(&mut tx, *target_course_id).await?;
891            // Verify that the user has permissions also to the course of the custom language group
892            authorize(
893                &mut tx,
894                Act::Duplicate,
895                Some(user.id),
896                Res::Course(*target_course_id),
897            )
898            .await?;
899            models::library::copying::copy_course_with_language_group(
900                &mut tx,
901                *course_id,
902                target_course.course_language_group_id,
903                &new_course,
904                user.id,
905            )
906            .await?
907        }
908        CopyCourseMode::NewLanguageGroup => {
909            let new_clg_id = course_language_groups::insert(
910                &mut tx,
911                PKeyPolicy::Generate,
912                new_course.slug.as_str(),
913            )
914            .await?;
915            models::library::copying::copy_course_with_language_group(
916                &mut tx,
917                *course_id,
918                new_clg_id,
919                &new_course,
920                user.id,
921            )
922            .await?
923        }
924    };
925
926    models::roles::insert(
927        &mut tx,
928        user.id,
929        models::roles::UserRole::Teacher,
930        models::roles::RoleDomain::Course(copied_course.id),
931    )
932    .await?;
933
934    tx.commit().await?;
935
936    token.authorized_ok(web::Json(copied_course))
937}
938
939/**
940GET `/api/v0/main-frontend/courses/:id/daily-submission-counts` - Returns submission counts grouped by day.
941*/
942#[utoipa::path(
943    get,
944    path = "/{course_id}/daily-submission-counts",
945    operation_id = "getCourseDailySubmissionCounts",
946    tag = "courses",
947    params(
948        ("course_id" = Uuid, Path, description = "Course id")
949    ),
950    responses(
951        (status = 200, description = "Course daily submission counts", body = [ExerciseSlideSubmissionCount])
952    )
953)]
954#[instrument(skip(pool))]
955async fn get_daily_submission_counts(
956    pool: web::Data<PgPool>,
957    course_id: web::Path<Uuid>,
958    user: AuthUser,
959) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
960    let mut conn = pool.acquire().await?;
961    let token = authorize(
962        &mut conn,
963        Act::ViewStats,
964        Some(user.id),
965        Res::Course(*course_id),
966    )
967    .await?;
968    let course = models::courses::get_course(&mut conn, *course_id).await?;
969    let res =
970        exercise_slide_submissions::get_course_daily_slide_submission_counts(&mut conn, &course)
971            .await?;
972
973    token.authorized_ok(web::Json(res))
974}
975
976/**
977GET `/api/v0/main-frontend/courses/:id/daily-users-who-have-submitted-something` - Returns a count of users who have submitted something grouped by day.
978*/
979#[utoipa::path(
980    get,
981    path = "/{course_id}/daily-users-who-have-submitted-something",
982    operation_id = "getCourseDailyUsersWhoSubmittedSomething",
983    tag = "courses",
984    params(
985        ("course_id" = Uuid, Path, description = "Course id")
986    ),
987    responses(
988        (status = 200, description = "Course daily user submission counts", body = [ExerciseSlideSubmissionCount])
989    )
990)]
991#[instrument(skip(pool))]
992async fn get_daily_user_counts_with_submissions(
993    pool: web::Data<PgPool>,
994    course_id: web::Path<Uuid>,
995    user: AuthUser,
996) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
997    let mut conn = pool.acquire().await?;
998    let token = authorize(
999        &mut conn,
1000        Act::ViewStats,
1001        Some(user.id),
1002        Res::Course(*course_id),
1003    )
1004    .await?;
1005    let course = models::courses::get_course(&mut conn, *course_id).await?;
1006    let res = exercise_slide_submissions::get_course_daily_user_counts_with_submissions(
1007        &mut conn, &course,
1008    )
1009    .await?;
1010
1011    token.authorized_ok(web::Json(res))
1012}
1013
1014/**
1015GET `/api/v0/main-frontend/courses/:id/weekday-hour-submission-counts` - Returns submission counts grouped by weekday and hour.
1016*/
1017#[utoipa::path(
1018    get,
1019    path = "/{course_id}/weekday-hour-submission-counts",
1020    operation_id = "getCourseWeekdayHourSubmissionCounts",
1021    tag = "courses",
1022    params(
1023        ("course_id" = Uuid, Path, description = "Course id")
1024    ),
1025    responses(
1026        (status = 200, description = "Course weekday and hour submission counts", body = [ExerciseSlideSubmissionCountByWeekAndHour])
1027    )
1028)]
1029#[instrument(skip(pool))]
1030async fn get_weekday_hour_submission_counts(
1031    pool: web::Data<PgPool>,
1032    course_id: web::Path<Uuid>,
1033    user: AuthUser,
1034) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByWeekAndHour>>> {
1035    let mut conn = pool.acquire().await?;
1036    let token = authorize(
1037        &mut conn,
1038        Act::ViewStats,
1039        Some(user.id),
1040        Res::Course(*course_id),
1041    )
1042    .await?;
1043    let course = models::courses::get_course(&mut conn, *course_id).await?;
1044    let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_weekday_and_hour(
1045        &mut conn, &course,
1046    )
1047    .await?;
1048
1049    token.authorized_ok(web::Json(res))
1050}
1051
1052/**
1053GET `/api/v0/main-frontend/courses/:id/submission-counts-by-exercise` - Returns submission counts grouped by weekday and hour.
1054*/
1055#[utoipa::path(
1056    get,
1057    path = "/{course_id}/submission-counts-by-exercise",
1058    operation_id = "getCourseSubmissionCountsByExercise",
1059    tag = "courses",
1060    params(
1061        ("course_id" = Uuid, Path, description = "Course id")
1062    ),
1063    responses(
1064        (status = 200, description = "Course submission counts by exercise", body = [ExerciseSlideSubmissionCountByExercise])
1065    )
1066)]
1067#[instrument(skip(pool))]
1068async fn get_submission_counts_by_exercise(
1069    pool: web::Data<PgPool>,
1070    course_id: web::Path<Uuid>,
1071    user: AuthUser,
1072) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByExercise>>> {
1073    let mut conn = pool.acquire().await?;
1074    let token = authorize(
1075        &mut conn,
1076        Act::ViewStats,
1077        Some(user.id),
1078        Res::Course(*course_id),
1079    )
1080    .await?;
1081    let course = models::courses::get_course(&mut conn, *course_id).await?;
1082    let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_exercise(
1083        &mut conn, &course,
1084    )
1085    .await?;
1086
1087    token.authorized_ok(web::Json(res))
1088}
1089
1090/**
1091GET `/api/v0/main-frontend/courses/:id/course-instances` - Returns all course instances for given course id.
1092*/
1093#[utoipa::path(
1094    get,
1095    path = "/{course_id}/course-instances",
1096    operation_id = "getCourseInstances",
1097    tag = "courses",
1098    params(
1099        ("course_id" = Uuid, Path, description = "Course id")
1100    ),
1101    responses(
1102        (status = 200, description = "Course instances", body = [CourseInstance])
1103    )
1104)]
1105#[instrument(skip(pool))]
1106async fn get_course_instances(
1107    pool: web::Data<PgPool>,
1108    course_id: web::Path<Uuid>,
1109    user: AuthUser,
1110) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
1111    let mut conn = pool.acquire().await?;
1112    let token = authorize(
1113        &mut conn,
1114        Act::Teach,
1115        Some(user.id),
1116        Res::Course(*course_id),
1117    )
1118    .await?;
1119    let course_instances =
1120        models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
1121
1122    token.authorized_ok(web::Json(course_instances))
1123}
1124
1125#[derive(Debug, Deserialize)]
1126
1127pub struct GetFeedbackQuery {
1128    read: bool,
1129    #[serde(flatten)]
1130    pagination: Pagination,
1131}
1132
1133/**
1134GET `/api/v0/main-frontend/courses/:id/feedback?read=true` - Returns feedback for the given course.
1135*/
1136#[utoipa::path(
1137    get,
1138    path = "/{course_id}/feedback",
1139    operation_id = "getCourseFeedback",
1140    tag = "courses",
1141    params(
1142        ("course_id" = String, Path, description = "Course id"),
1143        ("read" = bool, Query, description = "Whether to fetch read feedback"),
1144        ("page" = Option<i64>, Query, description = "Page number"),
1145        ("limit" = Option<i64>, Query, description = "Page size")
1146    ),
1147    responses(
1148        (status = 200, description = "Feedback for the course", body = [Feedback])
1149    )
1150)]
1151#[instrument(skip(pool))]
1152pub async fn get_feedback(
1153    course_id: web::Path<Uuid>,
1154    pool: web::Data<PgPool>,
1155    read: web::Query<GetFeedbackQuery>,
1156    user: AuthUser,
1157) -> ControllerResult<web::Json<Vec<Feedback>>> {
1158    let mut conn = pool.acquire().await?;
1159    let token = authorize(
1160        &mut conn,
1161        Act::Teach,
1162        Some(user.id),
1163        Res::Course(*course_id),
1164    )
1165    .await?;
1166    let feedback =
1167        feedback::get_feedback_for_course(&mut conn, *course_id, read.read, read.pagination)
1168            .await?;
1169
1170    token.authorized_ok(web::Json(feedback))
1171}
1172
1173/**
1174GET `/api/v0/main-frontend/courses/:id/feedback-count` - Returns the amount of feedback for the given course.
1175*/
1176#[utoipa::path(
1177    get,
1178    path = "/{course_id}/feedback-count",
1179    operation_id = "getCourseFeedbackCount",
1180    tag = "courses",
1181    params(
1182        ("course_id" = Uuid, Path, description = "Course id")
1183    ),
1184    responses(
1185        (status = 200, description = "Feedback counts for the course", body = FeedbackCount)
1186    )
1187)]
1188#[instrument(skip(pool))]
1189pub async fn get_feedback_count(
1190    course_id: web::Path<Uuid>,
1191    pool: web::Data<PgPool>,
1192    user: AuthUser,
1193) -> ControllerResult<web::Json<FeedbackCount>> {
1194    let mut conn = pool.acquire().await?;
1195    let token = authorize(
1196        &mut conn,
1197        Act::Teach,
1198        Some(user.id),
1199        Res::Course(*course_id),
1200    )
1201    .await?;
1202
1203    let feedback_count = feedback::get_feedback_count_for_course(&mut conn, *course_id).await?;
1204
1205    token.authorized_ok(web::Json(feedback_count))
1206}
1207
1208/**
1209POST `/api/v0/main-frontend/courses/:id/new-course-instance`
1210*/
1211#[utoipa::path(
1212    post,
1213    path = "/{course_id}/new-course-instance",
1214    operation_id = "createCourseInstance",
1215    tag = "courses",
1216    params(
1217        ("course_id" = Uuid, Path, description = "Course id")
1218    ),
1219    request_body = CourseInstanceForm,
1220    responses(
1221        (status = 200, description = "Created course instance id", body = Uuid)
1222    )
1223)]
1224#[instrument(skip(pool))]
1225async fn new_course_instance(
1226    form: web::Json<CourseInstanceForm>,
1227    course_id: web::Path<Uuid>,
1228    pool: web::Data<PgPool>,
1229    user: AuthUser,
1230) -> ControllerResult<web::Json<Uuid>> {
1231    let mut conn = pool.acquire().await?;
1232    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1233    let form = form.into_inner();
1234    let new = NewCourseInstance {
1235        course_id: *course_id,
1236        name: form.name.as_deref(),
1237        description: form.description.as_deref(),
1238        support_email: form.support_email.as_deref(),
1239        teacher_in_charge_name: &form.teacher_in_charge_name,
1240        teacher_in_charge_email: &form.teacher_in_charge_email,
1241        opening_time: form.opening_time,
1242        closing_time: form.closing_time,
1243    };
1244    let ci = models::course_instances::insert(&mut conn, PKeyPolicy::Generate, new).await?;
1245
1246    token.authorized_ok(web::Json(ci.id))
1247}
1248
1249#[instrument(skip(pool))]
1250#[utoipa::path(
1251    get,
1252    path = "/{course_id}/glossary",
1253    operation_id = "getCourseGlossary",
1254    tag = "glossary",
1255    params(
1256        ("course_id" = Uuid, Path, description = "Course id")
1257    ),
1258    responses(
1259        (status = 200, description = "Glossary terms for the course", body = [Term]),
1260        (status = 401, description = "Authentication required"),
1261        (status = 403, description = "User is not allowed to manage the course glossary")
1262    )
1263)]
1264pub(crate) async fn glossary(
1265    pool: web::Data<PgPool>,
1266    course_id: web::Path<Uuid>,
1267    user: AuthUser,
1268) -> ControllerResult<web::Json<Vec<Term>>> {
1269    let mut conn = pool.acquire().await?;
1270    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1271    let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
1272
1273    token.authorized_ok(web::Json(glossary))
1274}
1275
1276// unused?
1277
1278#[instrument(skip(pool))]
1279async fn _new_term(
1280    pool: web::Data<PgPool>,
1281    course_id: web::Path<Uuid>,
1282    user: AuthUser,
1283) -> ControllerResult<web::Json<Vec<Term>>> {
1284    let mut conn = pool.acquire().await?;
1285    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1286    let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
1287
1288    token.authorized_ok(web::Json(glossary))
1289}
1290
1291#[instrument(skip(pool))]
1292#[utoipa::path(
1293    post,
1294    path = "/{course_id}/glossary",
1295    operation_id = "createCourseGlossaryTerm",
1296    tag = "glossary",
1297    params(
1298        ("course_id" = Uuid, Path, description = "Course id")
1299    ),
1300    request_body = TermUpdate,
1301    responses(
1302        (status = 200, description = "Created glossary term id", body = Uuid),
1303        (status = 401, description = "Authentication required"),
1304        (status = 403, description = "User is not allowed to manage the course glossary")
1305    )
1306)]
1307pub(crate) async fn new_glossary_term(
1308    pool: web::Data<PgPool>,
1309    course_id: web::Path<Uuid>,
1310    new_term: web::Json<TermUpdate>,
1311    user: AuthUser,
1312) -> ControllerResult<web::Json<Uuid>> {
1313    let mut conn = pool.acquire().await?;
1314    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1315    let TermUpdate { term, definition } = new_term.into_inner();
1316    let term = models::glossary::insert(&mut conn, &term, &definition, *course_id).await?;
1317
1318    token.authorized_ok(web::Json(term))
1319}
1320
1321/**
1322GET `/api/v0/main-frontend/courses/:id/course-users-counts-by-exercise` - Returns the amount of users for each exercise.
1323*/
1324#[utoipa::path(
1325    get,
1326    path = "/{course_id}/course-users-counts-by-exercise",
1327    operation_id = "getCourseUsersCountsByExercise",
1328    tag = "courses",
1329    params(
1330        ("course_id" = Uuid, Path, description = "Course id")
1331    ),
1332    responses(
1333        (status = 200, description = "Course users counts by exercise", body = [ExerciseUserCounts])
1334    )
1335)]
1336#[instrument(skip(pool))]
1337pub async fn get_course_users_counts_by_exercise(
1338    course_id: web::Path<Uuid>,
1339    pool: web::Data<PgPool>,
1340    user: AuthUser,
1341) -> ControllerResult<web::Json<Vec<ExerciseUserCounts>>> {
1342    let mut conn = pool.acquire().await?;
1343    let course_id = course_id.into_inner();
1344    let token = authorize(
1345        &mut conn,
1346        Act::ViewStats,
1347        Some(user.id),
1348        Res::Course(course_id),
1349    )
1350    .await?;
1351
1352    let res =
1353        models::user_exercise_states::get_course_users_counts_by_exercise(&mut conn, course_id)
1354            .await?;
1355
1356    token.authorized_ok(web::Json(res))
1357}
1358
1359/**
1360POST `/api/v0/main-frontend/courses/:id/new-page-ordering` - Reorders pages to the given order numbers and given chapters.
1361
1362Note 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.
1363
1364Creates redirects if url_path changes.
1365*/
1366#[utoipa::path(
1367    post,
1368    path = "/{course_id}/new-page-ordering",
1369    operation_id = "updateCoursePageOrdering",
1370    tag = "courses",
1371    params(
1372        ("course_id" = Uuid, Path, description = "Course id")
1373    ),
1374    request_body = Vec<Page>,
1375    responses(
1376        (status = 200, description = "Course page ordering updated")
1377    )
1378)]
1379#[instrument(skip(pool))]
1380pub async fn post_new_page_ordering(
1381    course_id: web::Path<Uuid>,
1382    pool: web::Data<PgPool>,
1383    user: AuthUser,
1384    payload: web::Json<Vec<Page>>,
1385) -> ControllerResult<web::Json<()>> {
1386    let mut conn = pool.acquire().await?;
1387    let course_id = course_id.into_inner();
1388    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1389
1390    models::pages::reorder_pages(&mut conn, &payload, course_id).await?;
1391
1392    token.authorized_ok(web::Json(()))
1393}
1394
1395/**
1396POST `/api/v0/main-frontend/courses/:id/new-chapter-ordering` - Reorders chapters based on modified chapter number.#
1397
1398Creates redirects if url_path changes.
1399*/
1400#[utoipa::path(
1401    post,
1402    path = "/{course_id}/new-chapter-ordering",
1403    operation_id = "updateCourseChapterOrdering",
1404    tag = "courses",
1405    params(
1406        ("course_id" = Uuid, Path, description = "Course id")
1407    ),
1408    request_body = Vec<Chapter>,
1409    responses(
1410        (status = 200, description = "Course chapter ordering updated")
1411    )
1412)]
1413#[instrument(skip(pool))]
1414pub async fn post_new_chapter_ordering(
1415    course_id: web::Path<Uuid>,
1416    pool: web::Data<PgPool>,
1417    user: AuthUser,
1418    payload: web::Json<Vec<Chapter>>,
1419) -> ControllerResult<web::Json<()>> {
1420    let mut conn = pool.acquire().await?;
1421    let course_id = course_id.into_inner();
1422    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1423
1424    models::pages::reorder_chapters(&mut conn, &payload, course_id).await?;
1425
1426    token.authorized_ok(web::Json(()))
1427}
1428
1429#[utoipa::path(
1430    get,
1431    path = "/{course_id}/references",
1432    operation_id = "getCourseReferences",
1433    tag = "courses",
1434    params(
1435        ("course_id" = Uuid, Path, description = "Course id")
1436    ),
1437    responses(
1438        (status = 200, description = "Course references", body = [MaterialReference])
1439    )
1440)]
1441#[instrument(skip(pool))]
1442async fn get_material_references_by_course_id(
1443    course_id: web::Path<Uuid>,
1444    pool: web::Data<PgPool>,
1445    user: AuthUser,
1446) -> ControllerResult<web::Json<Vec<MaterialReference>>> {
1447    let mut conn = pool.acquire().await?;
1448    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1449
1450    let res =
1451        models::material_references::get_references_by_course_id(&mut conn, *course_id).await?;
1452    token.authorized_ok(web::Json(res))
1453}
1454
1455#[utoipa::path(
1456    post,
1457    path = "/{course_id}/references",
1458    operation_id = "createCourseReferences",
1459    tag = "courses",
1460    params(
1461        ("course_id" = Uuid, Path, description = "Course id")
1462    ),
1463    request_body = [NewMaterialReference],
1464    responses(
1465        (status = 200, description = "Course references created")
1466    )
1467)]
1468#[instrument(skip(pool))]
1469async fn insert_material_references(
1470    course_id: web::Path<Uuid>,
1471    payload: web::Json<Vec<NewMaterialReference>>,
1472    pool: web::Data<PgPool>,
1473    user: AuthUser,
1474) -> ControllerResult<web::Json<()>> {
1475    let mut conn = pool.acquire().await?;
1476    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1477
1478    models::material_references::insert_reference(&mut conn, *course_id, payload.0).await?;
1479
1480    token.authorized_ok(web::Json(()))
1481}
1482
1483#[utoipa::path(
1484    post,
1485    path = "/{course_id}/references/{reference_id}",
1486    operation_id = "updateCourseReference",
1487    tag = "courses",
1488    params(
1489        ("course_id" = Uuid, Path, description = "Course id"),
1490        ("reference_id" = Uuid, Path, description = "Reference id")
1491    ),
1492    request_body = NewMaterialReference,
1493    responses(
1494        (status = 200, description = "Course reference updated")
1495    )
1496)]
1497#[instrument(skip(pool))]
1498async fn update_material_reference(
1499    path: web::Path<(Uuid, Uuid)>,
1500    pool: web::Data<PgPool>,
1501    user: AuthUser,
1502    payload: web::Json<NewMaterialReference>,
1503) -> ControllerResult<web::Json<()>> {
1504    let (course_id, reference_id) = path.into_inner();
1505    let mut conn = pool.acquire().await?;
1506    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1507
1508    models::material_references::update_material_reference_by_id(
1509        &mut conn,
1510        reference_id,
1511        payload.0,
1512    )
1513    .await?;
1514    token.authorized_ok(web::Json(()))
1515}
1516
1517#[utoipa::path(
1518    delete,
1519    path = "/{course_id}/references/{reference_id}",
1520    operation_id = "deleteCourseReference",
1521    tag = "courses",
1522    params(
1523        ("course_id" = Uuid, Path, description = "Course id"),
1524        ("reference_id" = Uuid, Path, description = "Reference id")
1525    ),
1526    responses(
1527        (status = 200, description = "Course reference deleted")
1528    )
1529)]
1530#[instrument(skip(pool))]
1531async fn delete_material_reference_by_id(
1532    path: web::Path<(Uuid, Uuid)>,
1533    pool: web::Data<PgPool>,
1534    user: AuthUser,
1535) -> ControllerResult<web::Json<()>> {
1536    let (course_id, reference_id) = path.into_inner();
1537    let mut conn = pool.acquire().await?;
1538    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1539
1540    models::material_references::delete_reference(&mut conn, reference_id).await?;
1541    token.authorized_ok(web::Json(()))
1542}
1543
1544#[utoipa::path(
1545    post,
1546    path = "/{course_id}/course-modules",
1547    operation_id = "updateCourseModules",
1548    tag = "courses",
1549    params(
1550        ("course_id" = Uuid, Path, description = "Course id")
1551    ),
1552    request_body = ModuleUpdates,
1553    responses(
1554        (status = 200, description = "Course modules updated")
1555    )
1556)]
1557#[instrument(skip(pool))]
1558pub async fn update_modules(
1559    course_id: web::Path<Uuid>,
1560    pool: web::Data<PgPool>,
1561    user: AuthUser,
1562    payload: web::Json<ModuleUpdates>,
1563) -> ControllerResult<web::Json<()>> {
1564    let mut conn = pool.acquire().await?;
1565    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1566
1567    models::course_modules::update_modules(&mut conn, *course_id, payload.into_inner()).await?;
1568    token.authorized_ok(web::Json(()))
1569}
1570
1571#[utoipa::path(
1572    get,
1573    path = "/{course_id}/default-peer-review",
1574    operation_id = "getCourseDefaultPeerReview",
1575    tag = "courses",
1576    params(
1577        ("course_id" = Uuid, Path, description = "Course id")
1578    ),
1579    responses(
1580        (status = 200, description = "Default peer review configuration", body = serde_json::Value)
1581    )
1582)]
1583async fn get_course_default_peer_review(
1584    course_id: web::Path<Uuid>,
1585    pool: web::Data<PgPool>,
1586    user: AuthUser,
1587) -> ControllerResult<web::Json<(PeerOrSelfReviewConfig, Vec<PeerOrSelfReviewQuestion>)>> {
1588    let mut conn = pool.acquire().await?;
1589    let token = authorize(
1590        &mut conn,
1591        Act::Teach,
1592        Some(user.id),
1593        Res::Course(*course_id),
1594    )
1595    .await?;
1596
1597    let peer_review = models::peer_or_self_review_configs::get_default_for_course_by_course_id(
1598        &mut conn, *course_id,
1599    )
1600    .await?;
1601    let peer_or_self_review_questions =
1602        models::peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
1603            &mut conn,
1604            peer_review.id,
1605        )
1606        .await?;
1607    token.authorized_ok(web::Json((peer_review, peer_or_self_review_questions)))
1608}
1609
1610/**
1611POST `/api/v0/main-frontend/courses/${course_id}/update-peer-review-queue-reviews-received`
1612
1613Updates 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.
1614*/
1615#[utoipa::path(
1616    post,
1617    path = "/{course_id}/update-peer-review-queue-reviews-received",
1618    operation_id = "updateCoursePeerReviewQueueReviewsReceived",
1619    tag = "courses",
1620    params(
1621        ("course_id" = Uuid, Path, description = "Course id")
1622    ),
1623    responses(
1624        (status = 200, description = "Peer review queue updated", body = bool)
1625    )
1626)]
1627#[instrument(skip(pool, user))]
1628async fn post_update_peer_review_queue_reviews_received(
1629    pool: web::Data<PgPool>,
1630    user: AuthUser,
1631    course_id: web::Path<Uuid>,
1632) -> ControllerResult<web::Json<bool>> {
1633    let mut conn = pool.acquire().await?;
1634    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
1635    models::library::peer_or_self_reviewing::update_peer_review_queue_reviews_received(
1636        &mut conn, *course_id,
1637    )
1638    .await?;
1639    token.authorized_ok(web::Json(true))
1640}
1641
1642/**
1643GET `/api/v0/main-frontend/courses/${courseId}/export-submissions`
1644
1645gets SCV of course exercise submissions
1646*/
1647#[utoipa::path(
1648    get,
1649    path = "/{course_id}/export-submissions",
1650    operation_id = "exportCourseSubmissionsCsv",
1651    tag = "courses",
1652    params(
1653        ("course_id" = Uuid, Path, description = "Course id")
1654    ),
1655    responses(
1656        (status = 200, description = "Course submissions CSV", body = String, content_type = "text/csv")
1657    )
1658)]
1659#[instrument(skip(pool))]
1660pub async fn submission_export(
1661    course_id: web::Path<Uuid>,
1662    pool: web::Data<PgPool>,
1663    user: AuthUser,
1664) -> ControllerResult<HttpResponse> {
1665    let mut conn = pool.acquire().await?;
1666
1667    let token = authorize(
1668        &mut conn,
1669        Act::Teach,
1670        Some(user.id),
1671        Res::Course(*course_id),
1672    )
1673    .await?;
1674
1675    let course = models::courses::get_course(&mut conn, *course_id).await?;
1676
1677    general_export(
1678        pool,
1679        &format!(
1680            "attachment; filename=\"Course: {} - Submissions (exercise tasks) {}.csv\"",
1681            course.name,
1682            Utc::now().format("%Y-%m-%d")
1683        ),
1684        CourseSubmissionExportOperation {
1685            course_id: *course_id,
1686        },
1687        token,
1688    )
1689    .await
1690}
1691
1692/**
1693GET `/api/v0/main-frontend/courses/${course.id}/export-user-details`
1694
1695gets SCV of user details for all users having submitted an exercise in the course
1696*/
1697#[utoipa::path(
1698    get,
1699    path = "/{course_id}/export-user-details",
1700    operation_id = "exportCourseUserDetailsCsv",
1701    tag = "courses",
1702    params(
1703        ("course_id" = Uuid, Path, description = "Course id")
1704    ),
1705    responses(
1706        (status = 200, description = "Course user details CSV", body = String, content_type = "text/csv")
1707    )
1708)]
1709#[instrument(skip(pool))]
1710pub async fn user_details_export(
1711    course_id: web::Path<Uuid>,
1712    pool: web::Data<PgPool>,
1713    user: AuthUser,
1714) -> ControllerResult<HttpResponse> {
1715    let mut conn = pool.acquire().await?;
1716
1717    let token = authorize(
1718        &mut conn,
1719        Act::Teach,
1720        Some(user.id),
1721        Res::Course(*course_id),
1722    )
1723    .await?;
1724
1725    let course = models::courses::get_course(&mut conn, *course_id).await?;
1726
1727    general_export(
1728        pool,
1729        &format!(
1730            "attachment; filename=\"Course: {} - User Details {}.csv\"",
1731            course.name,
1732            Utc::now().format("%Y-%m-%d")
1733        ),
1734        UsersExportOperation {
1735            course_id: *course_id,
1736        },
1737        token,
1738    )
1739    .await
1740}
1741
1742/**
1743GET `/api/v0/main-frontend/courses/${course.id}/export-exercise-tasks`
1744
1745gets SCV all exercise-tasks' private specs in course
1746*/
1747#[utoipa::path(
1748    get,
1749    path = "/{course_id}/export-exercise-tasks",
1750    operation_id = "exportCourseExerciseTasksCsv",
1751    tag = "courses",
1752    params(
1753        ("course_id" = Uuid, Path, description = "Course id")
1754    ),
1755    responses(
1756        (status = 200, description = "Course exercise tasks CSV", body = String, content_type = "text/csv")
1757    )
1758)]
1759#[instrument(skip(pool))]
1760pub async fn exercise_tasks_export(
1761    course_id: web::Path<Uuid>,
1762    pool: web::Data<PgPool>,
1763    user: AuthUser,
1764) -> ControllerResult<HttpResponse> {
1765    let mut conn = pool.acquire().await?;
1766
1767    let token = authorize(
1768        &mut conn,
1769        Act::Teach,
1770        Some(user.id),
1771        Res::Course(*course_id),
1772    )
1773    .await?;
1774
1775    let course = models::courses::get_course(&mut conn, *course_id).await?;
1776
1777    general_export(
1778        pool,
1779        &format!(
1780            "attachment; filename=\"Course: {} - Exercise tasks {}.csv\"",
1781            course.name,
1782            Utc::now().format("%Y-%m-%d")
1783        ),
1784        CourseExerciseTasksExportOperation {
1785            course_id: *course_id,
1786        },
1787        token,
1788    )
1789    .await
1790}
1791
1792/**
1793GET `/api/v0/main-frontend/courses/${course.id}/export-course-instances`
1794
1795gets SCV course instances for course
1796*/
1797#[utoipa::path(
1798    get,
1799    path = "/{course_id}/export-course-instances",
1800    operation_id = "exportCourseInstancesCsv",
1801    tag = "courses",
1802    params(
1803        ("course_id" = Uuid, Path, description = "Course id")
1804    ),
1805    responses(
1806        (status = 200, description = "Course instances CSV", body = String, content_type = "text/csv")
1807    )
1808)]
1809#[instrument(skip(pool))]
1810pub async fn course_instances_export(
1811    course_id: web::Path<Uuid>,
1812    pool: web::Data<PgPool>,
1813    user: AuthUser,
1814) -> ControllerResult<HttpResponse> {
1815    let mut conn = pool.acquire().await?;
1816
1817    let token = authorize(
1818        &mut conn,
1819        Act::Teach,
1820        Some(user.id),
1821        Res::Course(*course_id),
1822    )
1823    .await?;
1824
1825    let course = models::courses::get_course(&mut conn, *course_id).await?;
1826
1827    general_export(
1828        pool,
1829        &format!(
1830            "attachment; filename=\"Course: {} - Instances {}.csv\"",
1831            course.name,
1832            Utc::now().format("%Y-%m-%d")
1833        ),
1834        CourseInstancesExportOperation {
1835            course_id: *course_id,
1836        },
1837        token,
1838    )
1839    .await
1840}
1841
1842/**
1843GET `/api/v0/main-frontend/courses/${course.id}/export-course-user-consents`
1844
1845gets SCV course specific research form questions and user answers for course
1846*/
1847#[utoipa::path(
1848    get,
1849    path = "/{course_id}/export-course-user-consents",
1850    operation_id = "exportCourseUserConsentsCsv",
1851    tag = "courses",
1852    params(
1853        ("course_id" = Uuid, Path, description = "Course id")
1854    ),
1855    responses(
1856        (status = 200, description = "Course user consents CSV", body = String, content_type = "text/csv")
1857    )
1858)]
1859#[instrument(skip(pool))]
1860pub async fn course_consent_form_answers_export(
1861    course_id: web::Path<Uuid>,
1862    pool: web::Data<PgPool>,
1863    user: AuthUser,
1864) -> ControllerResult<HttpResponse> {
1865    let mut conn = pool.acquire().await?;
1866
1867    let token = authorize(
1868        &mut conn,
1869        Act::Teach,
1870        Some(user.id),
1871        Res::Course(*course_id),
1872    )
1873    .await?;
1874
1875    let course = models::courses::get_course(&mut conn, *course_id).await?;
1876
1877    general_export(
1878        pool,
1879        &format!(
1880            "attachment; filename=\"Course: {} - User Consents {}.csv\"",
1881            course.name,
1882            Utc::now().format("%Y-%m-%d")
1883        ),
1884        CourseResearchFormExportOperation {
1885            course_id: *course_id,
1886        },
1887        token,
1888    )
1889    .await
1890}
1891
1892/**
1893GET `/api/v0/main-frontend/courses/${course.id}/export-user-exercise-states`
1894
1895gets CSV for course specific user exercise states
1896*/
1897#[utoipa::path(
1898    get,
1899    path = "/{course_id}/export-user-exercise-states",
1900    operation_id = "exportCourseUserExerciseStatesCsv",
1901    tag = "courses",
1902    params(
1903        ("course_id" = Uuid, Path, description = "Course id")
1904    ),
1905    responses(
1906        (status = 200, description = "Course user exercise states CSV", body = String, content_type = "text/csv")
1907    )
1908)]
1909#[instrument(skip(pool))]
1910pub async fn user_exercise_states_export(
1911    course_id: web::Path<Uuid>,
1912    pool: web::Data<PgPool>,
1913    user: AuthUser,
1914) -> ControllerResult<HttpResponse> {
1915    let mut conn = pool.acquire().await?;
1916
1917    let token = authorize(
1918        &mut conn,
1919        Act::Teach,
1920        Some(user.id),
1921        Res::Course(*course_id),
1922    )
1923    .await?;
1924
1925    let course = models::courses::get_course(&mut conn, *course_id).await?;
1926
1927    general_export(
1928        pool,
1929        &format!(
1930            "attachment; filename=\"Course: {} - User exercise states {}.csv\"",
1931            course.name,
1932            Utc::now().format("%Y-%m-%d")
1933        ),
1934        UserExerciseStatesExportOperation {
1935            course_id: *course_id,
1936        },
1937        token,
1938    )
1939    .await
1940}
1941
1942/**
1943GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary` - Gets aggregated statistics for page visits for the course.
1944*/
1945#[utoipa::path(
1946    get,
1947    path = "/{course_id}/page-visit-datum-summary",
1948    operation_id = "getCoursePageVisitDatumSummary",
1949    tag = "courses",
1950    params(
1951        ("course_id" = Uuid, Path, description = "Course id")
1952    ),
1953    responses(
1954        (status = 200, description = "Course page visit summary", body = [PageVisitDatumSummaryByCourse])
1955    )
1956)]
1957pub async fn get_page_visit_datum_summary(
1958    course_id: web::Path<Uuid>,
1959    pool: web::Data<PgPool>,
1960    user: AuthUser,
1961) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourse>>> {
1962    let mut conn = pool.acquire().await?;
1963    let course_id = course_id.into_inner();
1964    let token = authorize(
1965        &mut conn,
1966        Act::ViewStats,
1967        Some(user.id),
1968        Res::Course(course_id),
1969    )
1970    .await?;
1971
1972    let res = models::page_visit_datum_summary_by_courses::get_all_for_course(&mut conn, course_id)
1973        .await?;
1974
1975    token.authorized_ok(web::Json(res))
1976}
1977
1978/**
1979GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary-by-pages` - Gets aggregated statistics for page visits for the course.
1980*/
1981#[utoipa::path(
1982    get,
1983    path = "/{course_id}/page-visit-datum-summary-by-pages",
1984    operation_id = "getCoursePageVisitDatumSummaryByPages",
1985    tag = "courses",
1986    params(
1987        ("course_id" = Uuid, Path, description = "Course id")
1988    ),
1989    responses(
1990        (status = 200, description = "Course page visit summary by pages", body = [PageVisitDatumSummaryByPages])
1991    )
1992)]
1993pub async fn get_page_visit_datum_summary_by_pages(
1994    course_id: web::Path<Uuid>,
1995    pool: web::Data<PgPool>,
1996    user: AuthUser,
1997) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByPages>>> {
1998    let mut conn = pool.acquire().await?;
1999    let course_id = course_id.into_inner();
2000    let token = authorize(
2001        &mut conn,
2002        Act::ViewStats,
2003        Some(user.id),
2004        Res::Course(course_id),
2005    )
2006    .await?;
2007
2008    let res =
2009        models::page_visit_datum_summary_by_pages::get_all_for_course(&mut conn, course_id).await?;
2010
2011    token.authorized_ok(web::Json(res))
2012}
2013
2014/**
2015GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary-by-device-types` - Gets aggregated statistics for page visits for the course.
2016*/
2017#[utoipa::path(
2018    get,
2019    path = "/{course_id}/page-visit-datum-summary-by-device-types",
2020    operation_id = "getCoursePageVisitDatumSummaryByDeviceTypes",
2021    tag = "courses",
2022    params(
2023        ("course_id" = Uuid, Path, description = "Course id")
2024    ),
2025    responses(
2026        (status = 200, description = "Course page visit summary by device types", body = [PageVisitDatumSummaryByCourseDeviceTypes])
2027    )
2028)]
2029pub async fn get_page_visit_datum_summary_by_device_types(
2030    course_id: web::Path<Uuid>,
2031    pool: web::Data<PgPool>,
2032    user: AuthUser,
2033) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourseDeviceTypes>>> {
2034    let mut conn = pool.acquire().await?;
2035    let course_id = course_id.into_inner();
2036    let token = authorize(
2037        &mut conn,
2038        Act::ViewStats,
2039        Some(user.id),
2040        Res::Course(course_id),
2041    )
2042    .await?;
2043
2044    let res = models::page_visit_datum_summary_by_courses_device_types::get_all_for_course(
2045        &mut conn, course_id,
2046    )
2047    .await?;
2048
2049    token.authorized_ok(web::Json(res))
2050}
2051
2052/**
2053GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary-by-countries` - Gets aggregated statistics for page visits for the course.
2054*/
2055#[utoipa::path(
2056    get,
2057    path = "/{course_id}/page-visit-datum-summary-by-countries",
2058    operation_id = "getCoursePageVisitDatumSummaryByCountries",
2059    tag = "courses",
2060    params(
2061        ("course_id" = Uuid, Path, description = "Course id")
2062    ),
2063    responses(
2064        (status = 200, description = "Course page visit summary by countries", body = [PageVisitDatumSummaryByCoursesCountries])
2065    )
2066)]
2067pub async fn get_page_visit_datum_summary_by_countries(
2068    course_id: web::Path<Uuid>,
2069    pool: web::Data<PgPool>,
2070    user: AuthUser,
2071) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCoursesCountries>>> {
2072    let mut conn = pool.acquire().await?;
2073    let course_id = course_id.into_inner();
2074    let token = authorize(
2075        &mut conn,
2076        Act::ViewStats,
2077        Some(user.id),
2078        Res::Course(course_id),
2079    )
2080    .await?;
2081
2082    let res = models::page_visit_datum_summary_by_courses_countries::get_all_for_course(
2083        &mut conn, course_id,
2084    )
2085    .await?;
2086
2087    token.authorized_ok(web::Json(res))
2088}
2089
2090/**
2091DELETE `/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.
2092
2093Deletes submissions, user exercise states, and peer reviews etc. for all the course instances of this course.
2094*/
2095#[utoipa::path(
2096    delete,
2097    path = "/{course_id}/teacher-reset-course-progress-for-themselves",
2098    operation_id = "resetCourseProgressForTeacherThemselves",
2099    tag = "courses",
2100    params(
2101        ("course_id" = Uuid, Path, description = "Course id")
2102    ),
2103    responses(
2104        (status = 200, description = "Teacher course progress reset", body = bool)
2105    )
2106)]
2107pub async fn teacher_reset_course_progress_for_themselves(
2108    course_id: web::Path<Uuid>,
2109    pool: web::Data<PgPool>,
2110    user: AuthUser,
2111) -> ControllerResult<web::Json<bool>> {
2112    let mut conn = pool.acquire().await?;
2113    let course_id = course_id.into_inner();
2114    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2115
2116    let mut tx = conn.begin().await?;
2117    let course_instances =
2118        models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2119    for course_instance in course_instances {
2120        models::course_instances::reset_progress_on_course_instance_for_user(
2121            &mut tx,
2122            user.id,
2123            course_instance.course_id,
2124        )
2125        .await?;
2126    }
2127
2128    tx.commit().await?;
2129    token.authorized_ok(web::Json(true))
2130}
2131
2132/**
2133DELETE `/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.
2134
2135Deletes submissions, user exercise states, and peer reviews etc. for all the course instances of this course.
2136*/
2137#[utoipa::path(
2138    delete,
2139    path = "/{course_id}/teacher-reset-course-progress-for-everyone",
2140    operation_id = "resetCourseProgressForEveryone",
2141    tag = "courses",
2142    params(
2143        ("course_id" = Uuid, Path, description = "Course id")
2144    ),
2145    responses(
2146        (status = 200, description = "Course progress reset for everyone", body = bool)
2147    )
2148)]
2149pub async fn teacher_reset_course_progress_for_everyone(
2150    course_id: web::Path<Uuid>,
2151    pool: web::Data<PgPool>,
2152    user: AuthUser,
2153) -> ControllerResult<web::Json<bool>> {
2154    let mut conn = pool.acquire().await?;
2155    let course_id = course_id.into_inner();
2156    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2157    let course = models::courses::get_course(&mut conn, course_id).await?;
2158    if !course.is_draft {
2159        return Err(ControllerError::new(
2160            ControllerErrorType::BadRequest,
2161            "Can only reset progress for a draft course.".to_string(),
2162            None,
2163        ));
2164    }
2165    // 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.
2166    let n_course_module_completions =
2167        models::course_module_completions::get_count_of_distinct_completors_by_course_id(
2168            &mut conn, course_id,
2169        )
2170        .await?;
2171    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(
2172        &mut conn, course_id,
2173    ).await?;
2174    if n_course_module_completions > 200 {
2175        return Err(ControllerError::new(
2176            ControllerErrorType::BadRequest,
2177            "Too many students have completed the course.".to_string(),
2178            None,
2179        ));
2180    }
2181    if n_completions_registered_to_study_registry > 2 {
2182        return Err(ControllerError::new(
2183            ControllerErrorType::BadRequest,
2184            "Too many students have registered their completion to a study registry".to_string(),
2185            None,
2186        ));
2187    }
2188
2189    let mut tx = conn.begin().await?;
2190    let course_instances =
2191        models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2192
2193    // Looping though the data since this is only for draft courses and the amount of data is not expected to be large.
2194    for course_instance in course_instances {
2195        let users_in_course_instance =
2196            models::users::get_users_by_course_instance_enrollment(&mut tx, course_instance.id)
2197                .await?;
2198        for user_in_course_instance in users_in_course_instance {
2199            models::course_instances::reset_progress_on_course_instance_for_user(
2200                &mut tx,
2201                user_in_course_instance.id,
2202                course_instance.course_id,
2203            )
2204            .await?;
2205        }
2206    }
2207
2208    tx.commit().await?;
2209    token.authorized_ok(web::Json(true))
2210}
2211
2212#[derive(Debug, Deserialize)]
2213
2214pub struct GetSuspectedCheatersQuery {
2215    archive: bool,
2216}
2217
2218/**
2219 GET /api/v0/main-frontend/courses/${course.id}/suspected-cheaters?archive=true - returns all suspected cheaters related to a course instance.
2220*/
2221#[utoipa::path(
2222    get,
2223    path = "/{course_id}/suspected-cheaters",
2224    operation_id = "getCourseSuspectedCheaters",
2225    tag = "courses",
2226    params(
2227        ("course_id" = Uuid, Path, description = "Course id"),
2228        ("archive" = bool, Query, description = "Whether to fetch archived suspected cheaters")
2229    ),
2230    responses(
2231        (status = 200, description = "Suspected cheaters for course", body = [SuspectedCheaters])
2232    )
2233)]
2234#[instrument(skip(pool))]
2235async fn get_all_suspected_cheaters(
2236    user: AuthUser,
2237    params: web::Path<Uuid>,
2238    query: web::Query<GetSuspectedCheatersQuery>,
2239    pool: web::Data<PgPool>,
2240) -> ControllerResult<web::Json<Vec<SuspectedCheaters>>> {
2241    let course_id = params.into_inner();
2242
2243    let mut conn = pool.acquire().await?;
2244    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2245
2246    let course_cheaters = models::suspected_cheaters::get_all_suspected_cheaters_in_course(
2247        &mut conn,
2248        course_id,
2249        query.archive,
2250    )
2251    .await?;
2252
2253    token.authorized_ok(web::Json(course_cheaters))
2254}
2255
2256/**
2257 GET /api/v0/main-frontend/courses/${course.id}/thresholds - get all thresholds for all modules in a course.
2258*/
2259#[utoipa::path(
2260    get,
2261    path = "/{course_id}/thresholds",
2262    operation_id = "getCourseThresholds",
2263    tag = "courses",
2264    params(
2265        ("course_id" = Uuid, Path, description = "Course id")
2266    ),
2267    responses(
2268        (status = 200, description = "Course thresholds", body = serde_json::Value)
2269    )
2270)]
2271#[instrument(skip(pool))]
2272async fn get_all_thresholds(
2273    user: AuthUser,
2274    params: web::Path<Uuid>,
2275    pool: web::Data<PgPool>,
2276) -> ControllerResult<web::Json<Vec<Threshold>>> {
2277    let mut conn = pool.acquire().await?;
2278    let course_id = params.into_inner();
2279
2280    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2281
2282    let thresholds =
2283        models::suspected_cheaters::get_all_thresholds_for_course(&mut conn, course_id).await?;
2284
2285    token.authorized_ok(web::Json(thresholds))
2286}
2287
2288/**
2289 POST /api/v0/main-frontend/courses/${course.id}/suspected-cheaters/archive/:id - UPDATE is_archived to TRUE.
2290*/
2291#[utoipa::path(
2292    post,
2293    path = "/{course_id}/suspected-cheaters/archive/{id}",
2294    operation_id = "archiveCourseSuspectedCheater",
2295    tag = "courses",
2296    params(
2297        ("course_id" = Uuid, Path, description = "Course id"),
2298        ("id" = Uuid, Path, description = "Suspected cheater user id")
2299    ),
2300    responses(
2301        (status = 200, description = "Suspected cheater archived")
2302    )
2303)]
2304#[instrument(skip(pool))]
2305async fn teacher_archive_suspected_cheater(
2306    user: AuthUser,
2307    path: web::Path<(Uuid, Uuid)>,
2308    pool: web::Data<PgPool>,
2309) -> ControllerResult<web::Json<()>> {
2310    let (course_id, user_id) = path.into_inner();
2311
2312    let mut conn = pool.acquire().await?;
2313    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2314
2315    models::suspected_cheaters::archive_suspected_cheater(&mut conn, user_id).await?;
2316
2317    token.authorized_ok(web::Json(()))
2318}
2319
2320/**
2321 POST /api/v0/main-frontend/courses/${course.id}/suspected-cheaters/approve/:id - UPDATE is_archived to FALSE.
2322*/
2323#[utoipa::path(
2324    post,
2325    path = "/{course_id}/suspected-cheaters/approve/{id}",
2326    operation_id = "approveCourseSuspectedCheater",
2327    tag = "courses",
2328    params(
2329        ("course_id" = Uuid, Path, description = "Course id"),
2330        ("id" = Uuid, Path, description = "Suspected cheater user id")
2331    ),
2332    responses(
2333        (status = 200, description = "Suspected cheater approved")
2334    )
2335)]
2336#[instrument(skip(pool))]
2337async fn teacher_approve_suspected_cheater(
2338    user: AuthUser,
2339    path: web::Path<(Uuid, Uuid)>,
2340    pool: web::Data<PgPool>,
2341) -> ControllerResult<web::Json<()>> {
2342    let (course_id, user_id) = path.into_inner();
2343
2344    let mut conn = pool.acquire().await?;
2345    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2346
2347    models::suspected_cheaters::approve_suspected_cheater(&mut conn, user_id).await?;
2348
2349    // Fail student
2350    //find by user_id and course_id
2351    models::course_module_completions::update_passed_and_grade_status(
2352        &mut conn, course_id, user_id, false, 0,
2353    )
2354    .await?;
2355
2356    token.authorized_ok(web::Json(()))
2357}
2358
2359/**
2360POST /courses/:course_id/join-course-with-join-code - Adds the user to join_code_uses so the user gets access to the course
2361*/
2362#[utoipa::path(
2363    post,
2364    path = "/{course_id}/join-course-with-join-code",
2365    operation_id = "joinCourseWithJoinCode",
2366    tag = "courses",
2367    params(
2368        ("course_id" = Uuid, Path, description = "Course id")
2369    ),
2370    responses(
2371        (status = 200, description = "Joined course id", body = Uuid)
2372    )
2373)]
2374#[instrument(skip(pool))]
2375async fn add_user_to_course_with_join_code(
2376    course_id: web::Path<Uuid>,
2377    user: AuthUser,
2378    pool: web::Data<PgPool>,
2379) -> ControllerResult<web::Json<Uuid>> {
2380    let mut conn = pool.acquire().await?;
2381    let token = skip_authorize();
2382
2383    let joined =
2384        models::join_code_uses::insert(&mut conn, PKeyPolicy::Generate, user.id, *course_id)
2385            .await?;
2386    token.authorized_ok(web::Json(joined))
2387}
2388
2389/**
2390 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
2391*/
2392#[utoipa::path(
2393    post,
2394    path = "/{course_id}/set-join-code",
2395    operation_id = "setCourseJoinCode",
2396    tag = "courses",
2397    params(
2398        ("course_id" = Uuid, Path, description = "Course id")
2399    ),
2400    responses(
2401        (status = 200, description = "Course join code set")
2402    )
2403)]
2404#[instrument(skip(pool))]
2405async fn set_join_code_for_course(
2406    id: web::Path<Uuid>,
2407    pool: web::Data<PgPool>,
2408    user: AuthUser,
2409) -> ControllerResult<HttpResponse> {
2410    let mut conn = pool.acquire().await?;
2411    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*id)).await?;
2412
2413    const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
2414                            abcdefghjkmnpqrstuvwxyz";
2415    const PASSWORD_LEN: usize = 64;
2416    let mut rng = rand::rng();
2417
2418    let code: String = (0..PASSWORD_LEN)
2419        .map(|_| {
2420            let idx = rng.random_range(0..CHARSET.len());
2421            CHARSET[idx] as char
2422        })
2423        .collect();
2424
2425    models::courses::set_join_code_for_course(&mut conn, *id, code).await?;
2426    token.authorized_ok(HttpResponse::Ok().finish())
2427}
2428
2429/**
2430GET /courses/join/:join_code - Gets the course related to join code
2431*/
2432#[utoipa::path(
2433    get,
2434    path = "/join/{join_code}",
2435    operation_id = "getCourseByJoinCode",
2436    tag = "courses",
2437    params(
2438        ("join_code" = String, Path, description = "Course join code")
2439    ),
2440    responses(
2441        (status = 200, description = "Course for join code", body = Course)
2442    )
2443)]
2444#[instrument(skip(pool))]
2445async fn get_course_with_join_code(
2446    join_code: web::Path<String>,
2447    user: AuthUser,
2448    pool: web::Data<PgPool>,
2449) -> ControllerResult<web::Json<Course>> {
2450    let mut conn = pool.acquire().await?;
2451    let token = skip_authorize();
2452    let course =
2453        models::courses::get_course_with_join_code(&mut conn, join_code.to_string()).await?;
2454
2455    token.authorized_ok(web::Json(course))
2456}
2457
2458/**
2459 POST /api/v0/main-frontend/courses/:course_id/partners_block - Create or updates a partners block for a course
2460*/
2461#[utoipa::path(
2462    post,
2463    path = "/{course_id}/partners-block",
2464    operation_id = "upsertCoursePartnersBlock",
2465    tag = "courses",
2466    params(
2467        ("course_id" = Uuid, Path, description = "Course id")
2468    ),
2469    request_body = Option<serde_json::Value>,
2470    responses(
2471        (status = 200, description = "Partners block", body = serde_json::Value)
2472    )
2473)]
2474#[instrument(skip(payload, pool))]
2475async fn post_partners_block(
2476    path: web::Path<Uuid>,
2477    payload: web::Json<Option<serde_json::Value>>,
2478    pool: web::Data<PgPool>,
2479    user: AuthUser,
2480) -> ControllerResult<web::Json<PartnersBlock>> {
2481    let course_id = path.into_inner();
2482
2483    let content = payload.into_inner();
2484    let mut conn = pool.acquire().await?;
2485    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2486
2487    let upserted_partner_block =
2488        models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
2489
2490    token.authorized_ok(web::Json(upserted_partner_block))
2491}
2492
2493/**
2494GET /courses/:course_id/partners_blocks - Gets a partners block related to a course
2495*/
2496#[utoipa::path(
2497    get,
2498    path = "/{course_id}/partners-block",
2499    operation_id = "getCoursePartnersBlock",
2500    tag = "courses",
2501    params(
2502        ("course_id" = Uuid, Path, description = "Course id")
2503    ),
2504    responses(
2505        (status = 200, description = "Partners block", body = serde_json::Value)
2506    )
2507)]
2508#[instrument(skip(pool))]
2509async fn get_partners_block(
2510    path: web::Path<Uuid>,
2511    user: AuthUser,
2512    pool: web::Data<PgPool>,
2513) -> ControllerResult<web::Json<PartnersBlock>> {
2514    let course_id = path.into_inner();
2515    let mut conn = pool.acquire().await?;
2516    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2517
2518    // Check if the course exists in the partners_blocks table
2519    let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
2520
2521    let partner_block = if course_exists {
2522        // If the course exists, fetch the partner block
2523        models::partner_block::get_partner_block(&mut conn, course_id).await?
2524    } else {
2525        // If the course does not exist, create a new partner block with an empty content array
2526        let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
2527
2528        // Upsert the partner block with the empty content
2529        models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
2530    };
2531
2532    token.authorized_ok(web::Json(partner_block))
2533}
2534
2535/**
2536DELETE `/api/v0/main-frontend/courses/:course_id` - Delete a partners block in a course.
2537*/
2538#[utoipa::path(
2539    delete,
2540    path = "/{course_id}/partners-block",
2541    operation_id = "deleteCoursePartnersBlock",
2542    tag = "courses",
2543    params(
2544        ("course_id" = Uuid, Path, description = "Course id")
2545    ),
2546    responses(
2547        (status = 200, description = "Deleted partners block", body = serde_json::Value)
2548    )
2549)]
2550#[instrument(skip(pool))]
2551async fn delete_partners_block(
2552    path: web::Path<Uuid>,
2553    pool: web::Data<PgPool>,
2554    user: AuthUser,
2555) -> ControllerResult<web::Json<PartnersBlock>> {
2556    let course_id = path.into_inner();
2557    let mut conn = pool.acquire().await?;
2558    let token = authorize(
2559        &mut conn,
2560        Act::UsuallyUnacceptableDeletion,
2561        Some(user.id),
2562        Res::Course(course_id),
2563    )
2564    .await?;
2565    let deleted_partners_block =
2566        models::partner_block::delete_partner_block(&mut conn, course_id).await?;
2567
2568    token.authorized_ok(web::Json(deleted_partners_block))
2569}
2570
2571/**
2572Add a route for each controller in this module.
2573
2574The name starts with an underline in order to appear before other functions in the module documentation.
2575
2576We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
2577*/
2578pub fn _add_routes(cfg: &mut ServiceConfig) {
2579    cfg.service(web::scope("/{course_id}/stats").configure(stats::_add_routes))
2580        .service(web::scope("/{course_id}/chatbots").configure(chatbots::_add_routes))
2581        .service(web::scope("/{course_id}/students").configure(students::_add_routes))
2582        .route("/{course_id}", web::get().to(get_course))
2583        .route("", web::post().to(post_new_course))
2584        .route("/{course_id}", web::put().to(update_course))
2585        .route("/{course_id}", web::delete().to(delete_course))
2586        .route(
2587            "/{course_id}/status-for-all-exercises/{user_id}",
2588            web::get().to(get_all_exercise_statuses_by_course_id),
2589        )
2590        .route(
2591            "/{course_id}/course-module-completions/{user_id}",
2592            web::get().to(get_all_course_module_completions_for_user_by_course_id),
2593        )
2594        .route(
2595            "/{course_id}/daily-submission-counts",
2596            web::get().to(get_daily_submission_counts),
2597        )
2598        .route(
2599            "/{course_id}/daily-users-who-have-submitted-something",
2600            web::get().to(get_daily_user_counts_with_submissions),
2601        )
2602        .route("/{course_id}/exercises", web::get().to(get_all_exercises))
2603        .route(
2604            "/{course_id}/exercises-and-count-of-answers-requiring-attention",
2605            web::get().to(get_all_exercises_and_count_of_answers_requiring_attention),
2606        )
2607        .route(
2608            "/{course_id}/structure",
2609            web::get().to(get_course_structure),
2610        )
2611        .route(
2612            "/{course_id}/language-versions",
2613            web::get().to(get_all_course_language_versions),
2614        )
2615        .route(
2616            "/{course_id}/create-copy",
2617            web::post().to(create_course_copy),
2618        )
2619        .route("/{course_id}/upload", web::post().to(add_media_for_course))
2620        .route(
2621            "/{course_id}/weekday-hour-submission-counts",
2622            web::get().to(get_weekday_hour_submission_counts),
2623        )
2624        .route(
2625            "/{course_id}/submission-counts-by-exercise",
2626            web::get().to(get_submission_counts_by_exercise),
2627        )
2628        .route(
2629            "/{course_id}/course-instances",
2630            web::get().to(get_course_instances),
2631        )
2632        .route("/{course_id}/feedback", web::get().to(get_feedback))
2633        .route(
2634            "/{course_id}/feedback-count",
2635            web::get().to(get_feedback_count),
2636        )
2637        .route(
2638            "/{course_id}/new-course-instance",
2639            web::post().to(new_course_instance),
2640        )
2641        .route("/{course_id}/glossary", web::get().to(glossary))
2642        .route("/{course_id}/glossary", web::post().to(new_glossary_term))
2643        .route(
2644            "/{course_id}/course-users-counts-by-exercise",
2645            web::get().to(get_course_users_counts_by_exercise),
2646        )
2647        .route(
2648            "/{course_id}/new-page-ordering",
2649            web::post().to(post_new_page_ordering),
2650        )
2651        .route(
2652            "/{course_id}/new-chapter-ordering",
2653            web::post().to(post_new_chapter_ordering),
2654        )
2655        .route(
2656            "/{course_id}/references",
2657            web::get().to(get_material_references_by_course_id),
2658        )
2659        .route(
2660            "/{course_id}/references",
2661            web::post().to(insert_material_references),
2662        )
2663        .route(
2664            "/{course_id}/references/{reference_id}",
2665            web::post().to(update_material_reference),
2666        )
2667        .route(
2668            "/{course_id}/references/{reference_id}",
2669            web::delete().to(delete_material_reference_by_id),
2670        )
2671        .route(
2672            "/{course_id}/course-modules",
2673            web::post().to(update_modules),
2674        )
2675        .route(
2676            "/{course_id}/default-peer-review",
2677            web::get().to(get_course_default_peer_review),
2678        )
2679        .route(
2680            "/{course_id}/update-peer-review-queue-reviews-received",
2681            web::post().to(post_update_peer_review_queue_reviews_received),
2682        )
2683        .route(
2684            "/{course_id}/breadcrumb-info",
2685            web::get().to(get_course_breadcrumb_info),
2686        )
2687        .route(
2688            "/{course_id}/progress/{user_id}",
2689            web::get().to(get_user_progress_for_course),
2690        )
2691        .route(
2692            "/{course_id}/user-settings/{user_id}",
2693            web::get().to(get_user_course_settings),
2694        )
2695        .route(
2696            "/{course_id}/export-submissions",
2697            web::get().to(submission_export),
2698        )
2699        .route(
2700            "/{course_id}/export-user-details",
2701            web::get().to(user_details_export),
2702        )
2703        .route(
2704            "/{course_id}/export-exercise-tasks",
2705            web::get().to(exercise_tasks_export),
2706        )
2707        .route(
2708            "/{course_id}/export-course-instances",
2709            web::get().to(course_instances_export),
2710        )
2711        .route(
2712            "/{course_id}/export-course-user-consents",
2713            web::get().to(course_consent_form_answers_export),
2714        )
2715        .route(
2716            "/{course_id}/export-user-exercise-states",
2717            web::get().to(user_exercise_states_export),
2718        )
2719        .route(
2720            "/{course_id}/page-visit-datum-summary",
2721            web::get().to(get_page_visit_datum_summary),
2722        )
2723        .route(
2724            "/{course_id}/page-visit-datum-summary-by-pages",
2725            web::get().to(get_page_visit_datum_summary_by_pages),
2726        )
2727        .route(
2728            "/{course_id}/page-visit-datum-summary-by-device-types",
2729            web::get().to(get_page_visit_datum_summary_by_device_types),
2730        )
2731        .route(
2732            "/{course_id}/page-visit-datum-summary-by-countries",
2733            web::get().to(get_page_visit_datum_summary_by_countries),
2734        )
2735        .route(
2736            "/{course_id}/teacher-reset-course-progress-for-themselves",
2737            web::delete().to(teacher_reset_course_progress_for_themselves),
2738        )
2739        .route("/{course_id}/thresholds", web::get().to(get_all_thresholds))
2740        .route(
2741            "/{course_id}/suspected-cheaters",
2742            web::get().to(get_all_suspected_cheaters),
2743        )
2744        .route(
2745            "/{course_id}/suspected-cheaters/archive/{id}",
2746            web::post().to(teacher_archive_suspected_cheater),
2747        )
2748        .route(
2749            "/{course_id}/suspected-cheaters/approve/{id}",
2750            web::post().to(teacher_approve_suspected_cheater),
2751        )
2752        .route(
2753            "/{course_id}/teacher-reset-course-progress-for-everyone",
2754            web::delete().to(teacher_reset_course_progress_for_everyone),
2755        )
2756        .route(
2757            "/{course_id}/join-course-with-join-code",
2758            web::post().to(add_user_to_course_with_join_code),
2759        )
2760        .route(
2761            "/{course_id}/partners-block",
2762            web::post().to(post_partners_block),
2763        )
2764        .route(
2765            "/{course_id}/partners-block",
2766            web::get().to(get_partners_block),
2767        )
2768        .route(
2769            "/{course_id}/partners-block",
2770            web::delete().to(delete_partners_block),
2771        )
2772        .route(
2773            "/{course_id}/set-join-code",
2774            web::post().to(set_join_code_for_course),
2775        )
2776        .route(
2777            "/{course_id}/reprocess-completions",
2778            web::post().to(post_reprocess_module_completions),
2779        )
2780        .route(
2781            "/join/{join_code}",
2782            web::get().to(get_course_with_join_code),
2783        );
2784}