Skip to main content

headless_lms_server/controllers/main_frontend/courses/
mod.rs

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