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        (status = 400, description = "A module or chapter is not in the course, or a completion registration link is not https")
1584    )
1585)]
1586#[instrument(skip(pool))]
1587pub async fn update_modules(
1588    course_id: web::Path<Uuid>,
1589    pool: web::Data<PgPool>,
1590    user: AuthUser,
1591    payload: web::Json<ModuleUpdates>,
1592) -> ControllerResult<web::Json<()>> {
1593    let mut conn = pool.acquire().await?;
1594    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1595
1596    let updates = payload.into_inner();
1597    if models::course_modules::would_change_credit_registration_via_suotar(
1598        &mut conn, *course_id, &updates,
1599    )
1600    .await?
1601    {
1602        // Editing a course is not enough to put a module on the live study registry path.
1603        authorize(
1604            &mut conn,
1605            Act::Administrate,
1606            Some(user.id),
1607            Res::GlobalPermissions,
1608        )
1609        .await?;
1610    }
1611
1612    models::course_modules::update_modules(&mut conn, *course_id, updates).await?;
1613    token.authorized_ok(web::Json(()))
1614}
1615
1616#[utoipa::path(
1617    get,
1618    path = "/{course_id}/default-peer-review",
1619    operation_id = "getCourseDefaultPeerReview",
1620    tag = "courses",
1621    params(
1622        ("course_id" = Uuid, Path, description = "Course id")
1623    ),
1624    responses(
1625        (status = 200, description = "Default peer review configuration", body = serde_json::Value)
1626    )
1627)]
1628async fn get_course_default_peer_review(
1629    course_id: web::Path<Uuid>,
1630    pool: web::Data<PgPool>,
1631    user: AuthUser,
1632) -> ControllerResult<web::Json<(PeerOrSelfReviewConfig, Vec<PeerOrSelfReviewQuestion>)>> {
1633    let mut conn = pool.acquire().await?;
1634    let token = authorize(
1635        &mut conn,
1636        Act::Teach,
1637        Some(user.id),
1638        Res::Course(*course_id),
1639    )
1640    .await?;
1641
1642    let peer_review = models::peer_or_self_review_configs::get_default_for_course_by_course_id(
1643        &mut conn, *course_id,
1644    )
1645    .await?;
1646    let peer_or_self_review_questions =
1647        models::peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
1648            &mut conn,
1649            peer_review.id,
1650        )
1651        .await?;
1652    token.authorized_ok(web::Json((peer_review, peer_or_self_review_questions)))
1653}
1654
1655/**
1656POST `/api/v0/main-frontend/courses/${course_id}/update-peer-review-queue-reviews-received`
1657
1658Updates 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.
1659*/
1660#[utoipa::path(
1661    post,
1662    path = "/{course_id}/update-peer-review-queue-reviews-received",
1663    operation_id = "updateCoursePeerReviewQueueReviewsReceived",
1664    tag = "courses",
1665    params(
1666        ("course_id" = Uuid, Path, description = "Course id")
1667    ),
1668    responses(
1669        (status = 200, description = "Peer review queue updated", body = bool)
1670    )
1671)]
1672#[instrument(skip(pool, user))]
1673async fn post_update_peer_review_queue_reviews_received(
1674    pool: web::Data<PgPool>,
1675    user: AuthUser,
1676    course_id: web::Path<Uuid>,
1677) -> ControllerResult<web::Json<bool>> {
1678    let mut conn = pool.acquire().await?;
1679    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
1680    models::library::peer_or_self_reviewing::update_peer_review_queue_reviews_received(
1681        &mut conn, *course_id,
1682    )
1683    .await?;
1684    token.authorized_ok(web::Json(true))
1685}
1686
1687/**
1688GET `/api/v0/main-frontend/courses/${courseId}/export-submissions`
1689
1690gets SCV of course exercise submissions
1691*/
1692#[utoipa::path(
1693    get,
1694    path = "/{course_id}/export-submissions",
1695    operation_id = "exportCourseSubmissionsCsv",
1696    tag = "courses",
1697    params(
1698        ("course_id" = Uuid, Path, description = "Course id")
1699    ),
1700    responses(
1701        (status = 200, description = "Course submissions CSV", body = String, content_type = "text/csv")
1702    )
1703)]
1704#[instrument(skip(pool, file_store, app_conf))]
1705pub async fn submission_export(
1706    course_id: web::Path<Uuid>,
1707    pool: web::Data<PgPool>,
1708    user: AuthUser,
1709    file_store: web::Data<dyn FileStore>,
1710    app_conf: web::Data<ApplicationConfiguration>,
1711) -> ControllerResult<HttpResponse> {
1712    let mut conn = pool.acquire().await?;
1713
1714    let token = authorize(
1715        &mut conn,
1716        Act::Teach,
1717        Some(user.id),
1718        Res::Course(*course_id),
1719    )
1720    .await?;
1721
1722    let course = models::courses::get_course(&mut conn, *course_id).await?;
1723
1724    general_export(
1725        pool,
1726        &format!(
1727            "attachment; filename=\"Course: {} - Submissions (exercise tasks) {}.csv\"",
1728            course.name,
1729            Utc::now().format("%Y-%m-%d")
1730        ),
1731        CourseSubmissionExportOperation {
1732            course_id: *course_id,
1733            file_store,
1734            app_conf,
1735        },
1736        token,
1737    )
1738    .await
1739}
1740
1741/**
1742GET `/api/v0/main-frontend/courses/${course.id}/export-user-details`
1743
1744gets SCV of user details for all users having submitted an exercise in the course
1745*/
1746#[utoipa::path(
1747    get,
1748    path = "/{course_id}/export-user-details",
1749    operation_id = "exportCourseUserDetailsCsv",
1750    tag = "courses",
1751    params(
1752        ("course_id" = Uuid, Path, description = "Course id")
1753    ),
1754    responses(
1755        (status = 200, description = "Course user details CSV", body = String, content_type = "text/csv")
1756    )
1757)]
1758#[instrument(skip(pool))]
1759pub async fn user_details_export(
1760    course_id: web::Path<Uuid>,
1761    pool: web::Data<PgPool>,
1762    user: AuthUser,
1763) -> ControllerResult<HttpResponse> {
1764    let mut conn = pool.acquire().await?;
1765
1766    let token = authorize(
1767        &mut conn,
1768        Act::Teach,
1769        Some(user.id),
1770        Res::Course(*course_id),
1771    )
1772    .await?;
1773
1774    let course = models::courses::get_course(&mut conn, *course_id).await?;
1775
1776    general_export(
1777        pool,
1778        &format!(
1779            "attachment; filename=\"Course: {} - User Details {}.csv\"",
1780            course.name,
1781            Utc::now().format("%Y-%m-%d")
1782        ),
1783        UsersExportOperation {
1784            course_id: *course_id,
1785        },
1786        token,
1787    )
1788    .await
1789}
1790
1791/**
1792GET `/api/v0/main-frontend/courses/${course.id}/export-exercise-tasks`
1793
1794gets SCV all exercise-tasks' private specs in course
1795*/
1796#[utoipa::path(
1797    get,
1798    path = "/{course_id}/export-exercise-tasks",
1799    operation_id = "exportCourseExerciseTasksCsv",
1800    tag = "courses",
1801    params(
1802        ("course_id" = Uuid, Path, description = "Course id")
1803    ),
1804    responses(
1805        (status = 200, description = "Course exercise tasks CSV", body = String, content_type = "text/csv")
1806    )
1807)]
1808#[instrument(skip(pool))]
1809pub async fn exercise_tasks_export(
1810    course_id: web::Path<Uuid>,
1811    pool: web::Data<PgPool>,
1812    user: AuthUser,
1813) -> ControllerResult<HttpResponse> {
1814    let mut conn = pool.acquire().await?;
1815
1816    let token = authorize(
1817        &mut conn,
1818        Act::Teach,
1819        Some(user.id),
1820        Res::Course(*course_id),
1821    )
1822    .await?;
1823
1824    let course = models::courses::get_course(&mut conn, *course_id).await?;
1825
1826    general_export(
1827        pool,
1828        &format!(
1829            "attachment; filename=\"Course: {} - Exercise tasks {}.csv\"",
1830            course.name,
1831            Utc::now().format("%Y-%m-%d")
1832        ),
1833        CourseExerciseTasksExportOperation {
1834            course_id: *course_id,
1835        },
1836        token,
1837    )
1838    .await
1839}
1840
1841/**
1842GET `/api/v0/main-frontend/courses/${course.id}/export-course-instances`
1843
1844gets SCV course instances for course
1845*/
1846#[utoipa::path(
1847    get,
1848    path = "/{course_id}/export-course-instances",
1849    operation_id = "exportCourseInstancesCsv",
1850    tag = "courses",
1851    params(
1852        ("course_id" = Uuid, Path, description = "Course id")
1853    ),
1854    responses(
1855        (status = 200, description = "Course instances CSV", body = String, content_type = "text/csv")
1856    )
1857)]
1858#[instrument(skip(pool))]
1859pub async fn course_instances_export(
1860    course_id: web::Path<Uuid>,
1861    pool: web::Data<PgPool>,
1862    user: AuthUser,
1863) -> ControllerResult<HttpResponse> {
1864    let mut conn = pool.acquire().await?;
1865
1866    let token = authorize(
1867        &mut conn,
1868        Act::Teach,
1869        Some(user.id),
1870        Res::Course(*course_id),
1871    )
1872    .await?;
1873
1874    let course = models::courses::get_course(&mut conn, *course_id).await?;
1875
1876    general_export(
1877        pool,
1878        &format!(
1879            "attachment; filename=\"Course: {} - Instances {}.csv\"",
1880            course.name,
1881            Utc::now().format("%Y-%m-%d")
1882        ),
1883        CourseInstancesExportOperation {
1884            course_id: *course_id,
1885        },
1886        token,
1887    )
1888    .await
1889}
1890
1891/**
1892GET `/api/v0/main-frontend/courses/${course.id}/export-course-user-consents`
1893
1894gets SCV course specific research form questions and user answers for course
1895*/
1896#[utoipa::path(
1897    get,
1898    path = "/{course_id}/export-course-user-consents",
1899    operation_id = "exportCourseUserConsentsCsv",
1900    tag = "courses",
1901    params(
1902        ("course_id" = Uuid, Path, description = "Course id")
1903    ),
1904    responses(
1905        (status = 200, description = "Course user consents CSV", body = String, content_type = "text/csv")
1906    )
1907)]
1908#[instrument(skip(pool))]
1909pub async fn course_consent_form_answers_export(
1910    course_id: web::Path<Uuid>,
1911    pool: web::Data<PgPool>,
1912    user: AuthUser,
1913) -> ControllerResult<HttpResponse> {
1914    let mut conn = pool.acquire().await?;
1915
1916    let token = authorize(
1917        &mut conn,
1918        Act::Teach,
1919        Some(user.id),
1920        Res::Course(*course_id),
1921    )
1922    .await?;
1923
1924    let course = models::courses::get_course(&mut conn, *course_id).await?;
1925
1926    general_export(
1927        pool,
1928        &format!(
1929            "attachment; filename=\"Course: {} - User Consents {}.csv\"",
1930            course.name,
1931            Utc::now().format("%Y-%m-%d")
1932        ),
1933        CourseResearchFormExportOperation {
1934            course_id: *course_id,
1935        },
1936        token,
1937    )
1938    .await
1939}
1940
1941/**
1942GET `/api/v0/main-frontend/courses/${course.id}/export-user-exercise-states`
1943
1944gets CSV for course specific user exercise states
1945*/
1946#[utoipa::path(
1947    get,
1948    path = "/{course_id}/export-user-exercise-states",
1949    operation_id = "exportCourseUserExerciseStatesCsv",
1950    tag = "courses",
1951    params(
1952        ("course_id" = Uuid, Path, description = "Course id")
1953    ),
1954    responses(
1955        (status = 200, description = "Course user exercise states CSV", body = String, content_type = "text/csv")
1956    )
1957)]
1958#[instrument(skip(pool))]
1959pub async fn user_exercise_states_export(
1960    course_id: web::Path<Uuid>,
1961    pool: web::Data<PgPool>,
1962    user: AuthUser,
1963) -> ControllerResult<HttpResponse> {
1964    let mut conn = pool.acquire().await?;
1965
1966    let token = authorize(
1967        &mut conn,
1968        Act::Teach,
1969        Some(user.id),
1970        Res::Course(*course_id),
1971    )
1972    .await?;
1973
1974    let course = models::courses::get_course(&mut conn, *course_id).await?;
1975
1976    general_export(
1977        pool,
1978        &format!(
1979            "attachment; filename=\"Course: {} - User exercise states {}.csv\"",
1980            course.name,
1981            Utc::now().format("%Y-%m-%d")
1982        ),
1983        UserExerciseStatesExportOperation {
1984            course_id: *course_id,
1985        },
1986        token,
1987    )
1988    .await
1989}
1990
1991/**
1992GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary` - Gets aggregated statistics for page visits for the course.
1993*/
1994#[utoipa::path(
1995    get,
1996    path = "/{course_id}/page-visit-datum-summary",
1997    operation_id = "getCoursePageVisitDatumSummary",
1998    tag = "courses",
1999    params(
2000        ("course_id" = Uuid, Path, description = "Course id")
2001    ),
2002    responses(
2003        (status = 200, description = "Course page visit summary", body = [PageVisitDatumSummaryByCourse])
2004    )
2005)]
2006pub async fn get_page_visit_datum_summary(
2007    course_id: web::Path<Uuid>,
2008    pool: web::Data<PgPool>,
2009    user: AuthUser,
2010) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourse>>> {
2011    let mut conn = pool.acquire().await?;
2012    let course_id = course_id.into_inner();
2013    let token = authorize(
2014        &mut conn,
2015        Act::ViewStats,
2016        Some(user.id),
2017        Res::Course(course_id),
2018    )
2019    .await?;
2020
2021    let res = models::page_visit_datum_summary_by_courses::get_all_for_course(&mut conn, course_id)
2022        .await?;
2023
2024    token.authorized_ok(web::Json(res))
2025}
2026
2027/**
2028GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary-by-pages` - Gets aggregated statistics for page visits for the course.
2029*/
2030#[utoipa::path(
2031    get,
2032    path = "/{course_id}/page-visit-datum-summary-by-pages",
2033    operation_id = "getCoursePageVisitDatumSummaryByPages",
2034    tag = "courses",
2035    params(
2036        ("course_id" = Uuid, Path, description = "Course id")
2037    ),
2038    responses(
2039        (status = 200, description = "Course page visit summary by pages", body = [PageVisitDatumSummaryByPages])
2040    )
2041)]
2042pub async fn get_page_visit_datum_summary_by_pages(
2043    course_id: web::Path<Uuid>,
2044    pool: web::Data<PgPool>,
2045    user: AuthUser,
2046) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByPages>>> {
2047    let mut conn = pool.acquire().await?;
2048    let course_id = course_id.into_inner();
2049    let token = authorize(
2050        &mut conn,
2051        Act::ViewStats,
2052        Some(user.id),
2053        Res::Course(course_id),
2054    )
2055    .await?;
2056
2057    let res =
2058        models::page_visit_datum_summary_by_pages::get_all_for_course(&mut conn, course_id).await?;
2059
2060    token.authorized_ok(web::Json(res))
2061}
2062
2063/**
2064GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary-by-device-types` - Gets aggregated statistics for page visits for the course.
2065*/
2066#[utoipa::path(
2067    get,
2068    path = "/{course_id}/page-visit-datum-summary-by-device-types",
2069    operation_id = "getCoursePageVisitDatumSummaryByDeviceTypes",
2070    tag = "courses",
2071    params(
2072        ("course_id" = Uuid, Path, description = "Course id")
2073    ),
2074    responses(
2075        (status = 200, description = "Course page visit summary by device types", body = [PageVisitDatumSummaryByCourseDeviceTypes])
2076    )
2077)]
2078pub async fn get_page_visit_datum_summary_by_device_types(
2079    course_id: web::Path<Uuid>,
2080    pool: web::Data<PgPool>,
2081    user: AuthUser,
2082) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourseDeviceTypes>>> {
2083    let mut conn = pool.acquire().await?;
2084    let course_id = course_id.into_inner();
2085    let token = authorize(
2086        &mut conn,
2087        Act::ViewStats,
2088        Some(user.id),
2089        Res::Course(course_id),
2090    )
2091    .await?;
2092
2093    let res = models::page_visit_datum_summary_by_courses_device_types::get_all_for_course(
2094        &mut conn, course_id,
2095    )
2096    .await?;
2097
2098    token.authorized_ok(web::Json(res))
2099}
2100
2101/**
2102GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary-by-countries` - Gets aggregated statistics for page visits for the course.
2103*/
2104#[utoipa::path(
2105    get,
2106    path = "/{course_id}/page-visit-datum-summary-by-countries",
2107    operation_id = "getCoursePageVisitDatumSummaryByCountries",
2108    tag = "courses",
2109    params(
2110        ("course_id" = Uuid, Path, description = "Course id")
2111    ),
2112    responses(
2113        (status = 200, description = "Course page visit summary by countries", body = [PageVisitDatumSummaryByCoursesCountries])
2114    )
2115)]
2116pub async fn get_page_visit_datum_summary_by_countries(
2117    course_id: web::Path<Uuid>,
2118    pool: web::Data<PgPool>,
2119    user: AuthUser,
2120) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCoursesCountries>>> {
2121    let mut conn = pool.acquire().await?;
2122    let course_id = course_id.into_inner();
2123    let token = authorize(
2124        &mut conn,
2125        Act::ViewStats,
2126        Some(user.id),
2127        Res::Course(course_id),
2128    )
2129    .await?;
2130
2131    let res = models::page_visit_datum_summary_by_courses_countries::get_all_for_course(
2132        &mut conn, course_id,
2133    )
2134    .await?;
2135
2136    token.authorized_ok(web::Json(res))
2137}
2138
2139/**
2140DELETE `/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.
2141
2142Deletes submissions, user exercise states, and peer reviews etc. for all the course instances of this course.
2143*/
2144#[utoipa::path(
2145    delete,
2146    path = "/{course_id}/teacher-reset-course-progress-for-themselves",
2147    operation_id = "resetCourseProgressForTeacherThemselves",
2148    tag = "courses",
2149    params(
2150        ("course_id" = Uuid, Path, description = "Course id")
2151    ),
2152    responses(
2153        (status = 200, description = "Teacher course progress reset", body = bool)
2154    )
2155)]
2156pub async fn teacher_reset_course_progress_for_themselves(
2157    course_id: web::Path<Uuid>,
2158    pool: web::Data<PgPool>,
2159    user: AuthUser,
2160) -> ControllerResult<web::Json<bool>> {
2161    let mut conn = pool.acquire().await?;
2162    let course_id = course_id.into_inner();
2163    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2164
2165    let mut tx = conn.begin().await?;
2166    let course_instances =
2167        models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2168    for course_instance in course_instances {
2169        models::course_instances::reset_progress_on_course_instance_for_user(
2170            &mut tx,
2171            user.id,
2172            course_instance.course_id,
2173        )
2174        .await?;
2175    }
2176
2177    tx.commit().await?;
2178    token.authorized_ok(web::Json(true))
2179}
2180
2181/**
2182DELETE `/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.
2183
2184Deletes submissions, user exercise states, and peer reviews etc. for all the course instances of this course.
2185*/
2186#[utoipa::path(
2187    delete,
2188    path = "/{course_id}/teacher-reset-course-progress-for-everyone",
2189    operation_id = "resetCourseProgressForEveryone",
2190    tag = "courses",
2191    params(
2192        ("course_id" = Uuid, Path, description = "Course id")
2193    ),
2194    responses(
2195        (status = 200, description = "Course progress reset for everyone", body = bool)
2196    )
2197)]
2198pub async fn teacher_reset_course_progress_for_everyone(
2199    course_id: web::Path<Uuid>,
2200    pool: web::Data<PgPool>,
2201    user: AuthUser,
2202) -> ControllerResult<web::Json<bool>> {
2203    let mut conn = pool.acquire().await?;
2204    let course_id = course_id.into_inner();
2205    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2206    let course = models::courses::get_course(&mut conn, course_id).await?;
2207    if !course.is_draft {
2208        return Err(ControllerError::new(
2209            ControllerErrorType::BadRequest,
2210            "Can only reset progress for a draft course.".to_string(),
2211            None,
2212        ));
2213    }
2214    // 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.
2215    let n_course_module_completions =
2216        models::course_module_completions::get_count_of_distinct_completors_by_course_id(
2217            &mut conn, course_id,
2218        )
2219        .await?;
2220    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(
2221        &mut conn, course_id,
2222    ).await?;
2223    if n_course_module_completions > 200 {
2224        return Err(ControllerError::new(
2225            ControllerErrorType::BadRequest,
2226            "Too many students have completed the course.".to_string(),
2227            None,
2228        ));
2229    }
2230    if n_completions_registered_to_study_registry > 2 {
2231        return Err(ControllerError::new(
2232            ControllerErrorType::BadRequest,
2233            "Too many students have registered their completion to a study registry".to_string(),
2234            None,
2235        ));
2236    }
2237
2238    let mut tx = conn.begin().await?;
2239    let course_instances =
2240        models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
2241
2242    // Looping though the data since this is only for draft courses and the amount of data is not expected to be large.
2243    for course_instance in course_instances {
2244        let users_in_course_instance =
2245            models::users::get_users_by_course_instance_enrollment(&mut tx, course_instance.id)
2246                .await?;
2247        for user_in_course_instance in users_in_course_instance {
2248            models::course_instances::reset_progress_on_course_instance_for_user(
2249                &mut tx,
2250                user_in_course_instance.id,
2251                course_instance.course_id,
2252            )
2253            .await?;
2254        }
2255    }
2256
2257    tx.commit().await?;
2258    token.authorized_ok(web::Json(true))
2259}
2260
2261#[derive(Debug, Deserialize)]
2262
2263pub struct GetSuspectedCheatersQuery {
2264    status: SuspectedCheaterStatus,
2265}
2266
2267/**
2268 GET /api/v0/main-frontend/courses/${course.id}/suspected-cheaters?status=Flagged - returns the suspected cheaters in the given review state for a course.
2269*/
2270#[utoipa::path(
2271    get,
2272    path = "/{course_id}/suspected-cheaters",
2273    operation_id = "getCourseSuspectedCheaters",
2274    tag = "courses",
2275    params(
2276        ("course_id" = Uuid, Path, description = "Course id"),
2277        ("status" = SuspectedCheaterStatus, Query, description = "Which review state of suspected cheaters to fetch")
2278    ),
2279    responses(
2280        (status = 200, description = "Suspected cheaters for course", body = [SuspectedCheaters])
2281    )
2282)]
2283#[instrument(skip(pool))]
2284async fn get_all_suspected_cheaters(
2285    user: AuthUser,
2286    params: web::Path<Uuid>,
2287    query: web::Query<GetSuspectedCheatersQuery>,
2288    pool: web::Data<PgPool>,
2289) -> ControllerResult<web::Json<Vec<SuspectedCheaters>>> {
2290    let course_id = params.into_inner();
2291
2292    let mut conn = pool.acquire().await?;
2293    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2294
2295    let course_cheaters = models::suspected_cheaters::get_all_suspected_cheaters_in_course(
2296        &mut conn,
2297        course_id,
2298        query.status,
2299    )
2300    .await?;
2301
2302    token.authorized_ok(web::Json(course_cheaters))
2303}
2304
2305/**
2306 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.
2307*/
2308#[utoipa::path(
2309    get,
2310    path = "/{course_id}/suspected-cheaters/flagged-count",
2311    operation_id = "getCourseFlaggedSuspectedCheatersCount",
2312    tag = "courses",
2313    params(
2314        ("course_id" = Uuid, Path, description = "Course id")
2315    ),
2316    responses(
2317        (status = 200, description = "Number of suspected cheaters awaiting review", body = i64, content_type = "application/json")
2318    )
2319)]
2320#[instrument(skip(pool))]
2321async fn get_flagged_suspected_cheaters_count(
2322    user: AuthUser,
2323    params: web::Path<Uuid>,
2324    pool: web::Data<PgPool>,
2325) -> ControllerResult<web::Json<i64>> {
2326    let course_id = params.into_inner();
2327
2328    let mut conn = pool.acquire().await?;
2329    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2330
2331    let count = models::suspected_cheaters::get_count_in_course_by_status(
2332        &mut conn,
2333        course_id,
2334        SuspectedCheaterStatus::Flagged,
2335    )
2336    .await?;
2337
2338    token.authorized_ok(web::Json(count))
2339}
2340
2341/**
2342 GET /api/v0/main-frontend/courses/${course.id}/thresholds - get all thresholds for all modules in a course.
2343*/
2344#[utoipa::path(
2345    get,
2346    path = "/{course_id}/thresholds",
2347    operation_id = "getCourseThresholds",
2348    tag = "courses",
2349    params(
2350        ("course_id" = Uuid, Path, description = "Course id")
2351    ),
2352    responses(
2353        (status = 200, description = "Course thresholds", body = Vec<CourseModuleThresholdInfo>)
2354    )
2355)]
2356#[instrument(skip(pool))]
2357async fn get_all_thresholds(
2358    user: AuthUser,
2359    params: web::Path<Uuid>,
2360    pool: web::Data<PgPool>,
2361) -> ControllerResult<web::Json<Vec<CourseModuleThresholdInfo>>> {
2362    let mut conn = pool.acquire().await?;
2363    let course_id = params.into_inner();
2364
2365    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2366
2367    let thresholds =
2368        models::suspected_cheaters::get_threshold_info_for_course(&mut conn, course_id).await?;
2369
2370    token.authorized_ok(web::Json(thresholds))
2371}
2372
2373/**
2374 POST /api/v0/main-frontend/courses/${course.id}/suspected-cheaters/dismiss/:user_id - dismisses the suspicion as a false alarm (sets status to 'Dismissed').
2375*/
2376#[utoipa::path(
2377    post,
2378    path = "/{course_id}/suspected-cheaters/dismiss/{user_id}",
2379    operation_id = "dismissCourseSuspectedCheater",
2380    tag = "courses",
2381    params(
2382        ("course_id" = Uuid, Path, description = "Course id"),
2383        ("user_id" = Uuid, Path, description = "Suspected cheater's user id")
2384    ),
2385    responses(
2386        (status = 200, description = "Suspicion dismissed")
2387    )
2388)]
2389#[instrument(skip(pool))]
2390async fn teacher_dismiss_suspected_cheater(
2391    user: AuthUser,
2392    path: web::Path<(Uuid, Uuid)>,
2393    pool: web::Data<PgPool>,
2394) -> ControllerResult<web::Json<()>> {
2395    let (course_id, user_id) = path.into_inner();
2396
2397    let mut conn = pool.acquire().await?;
2398    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2399
2400    models::suspected_cheaters::dismiss_by_user_id_and_course_id(&mut conn, user_id, course_id)
2401        .await?;
2402
2403    token.authorized_ok(web::Json(()))
2404}
2405
2406/**
2407 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.
2408*/
2409#[utoipa::path(
2410    post,
2411    path = "/{course_id}/suspected-cheaters/confirm/{user_id}",
2412    operation_id = "confirmCourseSuspectedCheater",
2413    tag = "courses",
2414    params(
2415        ("course_id" = Uuid, Path, description = "Course id"),
2416        ("user_id" = Uuid, Path, description = "Suspected cheater's user id")
2417    ),
2418    responses(
2419        (status = 200, description = "Cheating confirmed")
2420    )
2421)]
2422#[instrument(skip(pool))]
2423async fn teacher_confirm_suspected_cheater(
2424    user: AuthUser,
2425    path: web::Path<(Uuid, Uuid)>,
2426    pool: web::Data<PgPool>,
2427) -> ControllerResult<web::Json<()>> {
2428    let (course_id, user_id) = path.into_inner();
2429
2430    let mut conn = pool.acquire().await?;
2431    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2432
2433    // Confirming sets the status and fails the student's completions (snapshotting the previous
2434    // grade so a later dismiss can restore it); see confirm_cheater_by_user_id_and_course_id.
2435    models::suspected_cheaters::confirm_cheater_by_user_id_and_course_id(
2436        &mut conn, user_id, course_id,
2437    )
2438    .await?;
2439
2440    token.authorized_ok(web::Json(()))
2441}
2442
2443#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
2444pub struct JoinCourseWithJoinCodePayload {
2445    join_code: String,
2446}
2447
2448/**
2449POST /courses/:course_id/join-course-with-join-code - Adds the user to join_code_uses so the user gets access to the course
2450*/
2451#[utoipa::path(
2452    post,
2453    path = "/{course_id}/join-course-with-join-code",
2454    operation_id = "joinCourseWithJoinCode",
2455    tag = "courses",
2456    params(
2457        ("course_id" = Uuid, Path, description = "Course id")
2458    ),
2459    request_body = JoinCourseWithJoinCodePayload,
2460    responses(
2461        (status = 200, description = "Joined course id", body = Uuid)
2462    )
2463)]
2464#[instrument(skip(pool))]
2465async fn add_user_to_course_with_join_code(
2466    course_id: web::Path<Uuid>,
2467    payload: web::Json<JoinCourseWithJoinCodePayload>,
2468    user: AuthUser,
2469    pool: web::Data<PgPool>,
2470) -> ControllerResult<web::Json<Uuid>> {
2471    let mut conn = pool.acquire().await?;
2472    let token = skip_authorize();
2473
2474    models::courses::get_by_id_and_join_code(&mut conn, *course_id, &payload.join_code).await?;
2475    let joined =
2476        models::join_code_uses::insert(&mut conn, PKeyPolicy::Generate, user.id, *course_id)
2477            .await?;
2478    token.authorized_ok(web::Json(joined))
2479}
2480
2481/**
2482 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
2483*/
2484#[utoipa::path(
2485    post,
2486    path = "/{course_id}/set-join-code",
2487    operation_id = "setCourseJoinCode",
2488    tag = "courses",
2489    params(
2490        ("course_id" = Uuid, Path, description = "Course id")
2491    ),
2492    responses(
2493        (status = 200, description = "Course join code set")
2494    )
2495)]
2496#[instrument(skip(pool))]
2497async fn set_join_code_for_course(
2498    id: web::Path<Uuid>,
2499    pool: web::Data<PgPool>,
2500    user: AuthUser,
2501) -> ControllerResult<HttpResponse> {
2502    let mut conn = pool.acquire().await?;
2503    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*id)).await?;
2504
2505    const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
2506                            abcdefghjkmnpqrstuvwxyz";
2507    const PASSWORD_LEN: usize = 64;
2508    let mut rng = rand::rng();
2509
2510    let code: String = (0..PASSWORD_LEN)
2511        .map(|_| {
2512            let idx = rng.random_range(0..CHARSET.len());
2513            CHARSET[idx] as char
2514        })
2515        .collect();
2516
2517    models::courses::set_join_code_for_course(&mut conn, *id, code).await?;
2518    token.authorized_ok(HttpResponse::Ok().finish())
2519}
2520
2521/**
2522GET /courses/join/:join_code - Gets the course related to join code
2523*/
2524#[utoipa::path(
2525    get,
2526    path = "/join/{join_code}",
2527    operation_id = "getCourseByJoinCode",
2528    tag = "courses",
2529    params(
2530        ("join_code" = String, Path, description = "Course join code")
2531    ),
2532    responses(
2533        (status = 200, description = "Course for join code", body = Course)
2534    )
2535)]
2536#[instrument(skip(pool))]
2537async fn get_course_with_join_code(
2538    join_code: web::Path<String>,
2539    user: AuthUser,
2540    pool: web::Data<PgPool>,
2541) -> ControllerResult<web::Json<Course>> {
2542    let mut conn = pool.acquire().await?;
2543    let token = skip_authorize();
2544    let course =
2545        models::courses::get_course_with_join_code(&mut conn, join_code.to_string()).await?;
2546
2547    token.authorized_ok(web::Json(course))
2548}
2549
2550/**
2551 POST /api/v0/main-frontend/courses/:course_id/partners_block - Create or updates a partners block for a course
2552*/
2553#[utoipa::path(
2554    post,
2555    path = "/{course_id}/partners-block",
2556    operation_id = "upsertCoursePartnersBlock",
2557    tag = "courses",
2558    params(
2559        ("course_id" = Uuid, Path, description = "Course id")
2560    ),
2561    request_body = Option<serde_json::Value>,
2562    responses(
2563        (status = 200, description = "Partners block", body = serde_json::Value)
2564    )
2565)]
2566#[instrument(skip(payload, pool))]
2567async fn post_partners_block(
2568    path: web::Path<Uuid>,
2569    payload: web::Json<Option<serde_json::Value>>,
2570    pool: web::Data<PgPool>,
2571    user: AuthUser,
2572) -> ControllerResult<web::Json<PartnersBlock>> {
2573    let course_id = path.into_inner();
2574
2575    let content = payload.into_inner();
2576    let mut conn = pool.acquire().await?;
2577    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2578
2579    let upserted_partner_block =
2580        models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
2581
2582    token.authorized_ok(web::Json(upserted_partner_block))
2583}
2584
2585/**
2586GET /courses/:course_id/partners_blocks - Gets a partners block related to a course
2587*/
2588#[utoipa::path(
2589    get,
2590    path = "/{course_id}/partners-block",
2591    operation_id = "getCoursePartnersBlock",
2592    tag = "courses",
2593    params(
2594        ("course_id" = Uuid, Path, description = "Course id")
2595    ),
2596    responses(
2597        (status = 200, description = "Partners block", body = serde_json::Value)
2598    )
2599)]
2600#[instrument(skip(pool))]
2601async fn get_partners_block(
2602    path: web::Path<Uuid>,
2603    user: AuthUser,
2604    pool: web::Data<PgPool>,
2605) -> ControllerResult<web::Json<PartnersBlock>> {
2606    let course_id = path.into_inner();
2607    let mut conn = pool.acquire().await?;
2608    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
2609
2610    // Check if the course exists in the partners_blocks table
2611    let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
2612
2613    let partner_block = if course_exists {
2614        // If the course exists, fetch the partner block
2615        models::partner_block::get_partner_block(&mut conn, course_id).await?
2616    } else {
2617        // If the course does not exist, create a new partner block with an empty content array
2618        let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
2619
2620        // Upsert the partner block with the empty content
2621        models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
2622    };
2623
2624    token.authorized_ok(web::Json(partner_block))
2625}
2626
2627/**
2628DELETE `/api/v0/main-frontend/courses/:course_id` - Delete a partners block in a course.
2629*/
2630#[utoipa::path(
2631    delete,
2632    path = "/{course_id}/partners-block",
2633    operation_id = "deleteCoursePartnersBlock",
2634    tag = "courses",
2635    params(
2636        ("course_id" = Uuid, Path, description = "Course id")
2637    ),
2638    responses(
2639        (status = 200, description = "Deleted partners block", body = serde_json::Value)
2640    )
2641)]
2642#[instrument(skip(pool))]
2643async fn delete_partners_block(
2644    path: web::Path<Uuid>,
2645    pool: web::Data<PgPool>,
2646    user: AuthUser,
2647) -> ControllerResult<web::Json<PartnersBlock>> {
2648    let course_id = path.into_inner();
2649    let mut conn = pool.acquire().await?;
2650    let token = authorize(
2651        &mut conn,
2652        Act::UsuallyUnacceptableDeletion,
2653        Some(user.id),
2654        Res::Course(course_id),
2655    )
2656    .await?;
2657    let deleted_partners_block =
2658        models::partner_block::delete_partner_block(&mut conn, course_id).await?;
2659
2660    token.authorized_ok(web::Json(deleted_partners_block))
2661}
2662
2663/**
2664GET `/api/v0/main-frontend/courses/:course_id/sisu-course-llm-descriptions` - Get Sisu descriptions summarised by LLM
2665
2666Returns LLM generated descriptions for a course based on information from Sisu API.
2667*/
2668#[utoipa::path(
2669    get,
2670    path = "/{course_id}/sisu-course-llm-descriptions",
2671    operation_id = "getSisuCourseLlmDescriptions",
2672    tag = "courses",
2673    params(
2674        ("course_id" = Uuid, Path, description = "Course id")
2675    ),
2676    responses(
2677        (status = 200, description = "Sisu course LLM descriptions", body = SisuDescriptionResponse)
2678    )
2679)]
2680#[instrument(skip(pool, app_conf))]
2681async fn get_sisu_course_llm_descriptions(
2682    course_id: web::Path<Uuid>,
2683    pool: web::Data<PgPool>,
2684    app_conf: web::Data<ApplicationConfiguration>,
2685    user: AuthUser,
2686    sisu_client: web::Data<SisuClient>,
2687) -> ControllerResult<web::Json<SisuDescriptionResponse>> {
2688    let (course_modules, course_lang, message_suggest_llm, token) = {
2689        let mut conn = pool.acquire().await?;
2690
2691        let token =
2692            authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
2693        let course_modules =
2694            models::course_modules::get_by_course_id(&mut conn, *course_id).await?;
2695        let course_lang = models::courses::get_course(&mut conn, *course_id)
2696            .await?
2697            .language_code;
2698        let message_suggest_llm = models::application_task_default_language_models::get_for_task(
2699            &mut conn,
2700            ApplicationTask::SisuDescriptionSummary,
2701        )
2702        .await?;
2703        (course_modules, course_lang, message_suggest_llm, token)
2704    };
2705
2706    let uh_course_codes: Vec<String> = course_modules
2707        .into_iter()
2708        .filter_map(|course_module| course_module.uh_course_code)
2709        .collect::<Vec<String>>();
2710    let course_ids = SisuClient::get_course_ids(&sisu_client, uh_course_codes).await?;
2711    let course_info = SisuClient::get_course_info(&sisu_client, course_ids).await?;
2712
2713    let parsed_course_info = SisuClient::parse_course_info(course_info, course_lang);
2714
2715    let llm_descriptions = headless_lms_chatbot::course_description_summary::generate_description(
2716        &app_conf,
2717        message_suggest_llm,
2718        parsed_course_info,
2719    )
2720    .await?;
2721    token.authorized_ok(web::Json(llm_descriptions))
2722}
2723
2724/**
2725POST `/api/v0/main-frontend/courses/:course_id/update-metadata` - Update metadata.
2726
2727*/
2728#[utoipa::path(
2729    post,
2730    path = "/{course_id}/update-metadata",
2731    operation_id = "updateMetadata",
2732    tag = "courses",
2733    params(
2734        ("course_id" = Uuid, Path, description = "Course id")
2735    ),
2736    request_body = CourseMetadataUpdate,
2737    responses(
2738        (status = 200, description = "Updated metadata", body = CourseMetadata)
2739    )
2740)]
2741#[instrument(skip(pool, app_conf))]
2742async fn update_metadata(
2743    payload: web::Json<CourseMetadataUpdate>,
2744    app_conf: web::Data<ApplicationConfiguration>,
2745    course_id: web::Path<Uuid>,
2746    pool: web::Data<PgPool>,
2747    user: AuthUser,
2748) -> ControllerResult<web::Json<CourseMetadata>> {
2749    let mut conn = pool.acquire().await?;
2750    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2751    let metadata_update = payload.0;
2752    let course =
2753        models::courses::set_metadata(&mut conn, app_conf.as_ref(), *course_id, metadata_update)
2754            .await?;
2755
2756    token.authorized_ok(web::Json(course))
2757}
2758
2759/**
2760get `/api/v0/main-frontend/courses/:course_id/get-course-prerequisites` - Get course prerequisites.
2761
2762*/
2763#[utoipa::path(
2764    get,
2765    path = "/{course_id}/get-course-prerequisites",
2766    operation_id= "getCoursePrerequisites",
2767    tag = "courses",
2768    params(
2769        ("course_id" = Uuid, Path, description = "Course id")
2770    ),
2771    responses(
2772        (status = 200, description = "Course prerequisites", body = Vec<CoursePrerequisite>)
2773    )
2774)]
2775#[instrument(skip(pool))]
2776async fn get_course_prerequisites(
2777    course_id: web::Path<Uuid>,
2778    pool: web::Data<PgPool>,
2779    user: AuthUser,
2780) -> ControllerResult<web::Json<Vec<CoursePrerequisite>>> {
2781    let mut conn = pool.acquire().await?;
2782    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2783    let prerequisites =
2784        models::course_prerequisites::get_by_course_id(&mut conn, *course_id).await?;
2785
2786    token.authorized_ok(web::Json(prerequisites))
2787}
2788
2789/**
2790get `/api/v0/main-frontend/courses/:course_id/get-course-audiences` - Get course audiences.
2791
2792*/
2793#[utoipa::path(
2794    get,
2795    path = "/{course_id}/get-course-audiences",
2796    operation_id= "getCourseAudiences",
2797    tag = "courses",
2798    params(
2799        ("course_id" = Uuid, Path, description = "Course id")
2800    ),
2801    responses(
2802        (status = 200, description = "Course audiences", body = Vec<CourseAudience>)
2803    )
2804)]
2805#[instrument(skip(pool))]
2806async fn get_course_audiences(
2807    course_id: web::Path<Uuid>,
2808    pool: web::Data<PgPool>,
2809    user: AuthUser,
2810) -> ControllerResult<web::Json<Vec<CourseAudience>>> {
2811    let mut conn = pool.acquire().await?;
2812    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2813    let audiences = models::course_audiences::get_by_course_id(&mut conn, *course_id).await?;
2814
2815    token.authorized_ok(web::Json(audiences))
2816}
2817
2818/**
2819get `/api/v0/main-frontend/courses/:course_id/get-course-metadata` - Get course metadata.
2820
2821*/
2822#[utoipa::path(
2823    get,
2824    path = "/{course_id}/get-course-metadata",
2825    operation_id= "getCourseMetadata",
2826    tag = "courses",
2827    params(
2828        ("course_id" = Uuid, Path, description = "Course id")
2829    ),
2830    responses(
2831        (status = 200, description = "Course metadata", body = CompleteCourseMetadata)
2832    )
2833)]
2834#[instrument(skip(pool))]
2835async fn get_course_metadata(
2836    course_id: web::Path<Uuid>,
2837    pool: web::Data<PgPool>,
2838    user: AuthUser,
2839) -> ControllerResult<web::Json<CompleteCourseMetadata>> {
2840    let mut conn: sqlx::pool::PoolConnection<Postgres> = pool.acquire().await?;
2841    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
2842    let metadata = models::courses::get_metadata(&mut conn, *course_id).await?;
2843
2844    token.authorized_ok(web::Json(metadata))
2845}
2846/**
2847GET `/api/v0/main-frontend/courses` - Get all courses
2848
2849Returns all courses.
2850*/
2851#[utoipa::path(
2852    get,
2853    path = "/",
2854    operation_id = "getAllCourses",
2855    tag = "courses",
2856    responses(
2857        (status = 200, description = "All courses", body = Vec<Course>)
2858    )
2859)]
2860#[instrument(skip(pool))]
2861async fn get_all_courses(
2862    pool: web::Data<PgPool>,
2863    user: AuthUser,
2864) -> ControllerResult<web::Json<Vec<Course>>> {
2865    let mut conn = pool.acquire().await?;
2866    let all_courses = models::courses::all_courses(&mut conn).await?;
2867    let token = authorize(&mut conn, Act::View, Some(user.id), Res::GlobalPermissions).await?;
2868    token.authorized_ok(web::Json(all_courses))
2869}
2870
2871/**
2872Add a route for each controller in this module.
2873
2874The name starts with an underline in order to appear before other functions in the module documentation.
2875
2876We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
2877*/
2878pub fn _add_routes(cfg: &mut ServiceConfig) {
2879    cfg.service(web::scope("/{course_id}/stats").configure(stats::_add_routes))
2880        .service(web::scope("/{course_id}/chatbots").configure(chatbots::_add_routes))
2881        .service(web::scope("/{course_id}/students").configure(students::_add_routes))
2882        .route("/{course_id}", web::get().to(get_course))
2883        .route("", web::post().to(post_new_course))
2884        .route("/{course_id}", web::put().to(update_course))
2885        .route("/{course_id}", web::delete().to(delete_course))
2886        .route(
2887            "/{course_id}/status-for-all-exercises/{user_id}",
2888            web::get().to(get_all_exercise_statuses_by_course_id),
2889        )
2890        .route(
2891            "/{course_id}/course-module-completions/{user_id}",
2892            web::get().to(get_all_course_module_completions_for_user_by_course_id),
2893        )
2894        .route(
2895            "/{course_id}/daily-submission-counts",
2896            web::get().to(get_daily_submission_counts),
2897        )
2898        .route(
2899            "/{course_id}/daily-users-who-have-submitted-something",
2900            web::get().to(get_daily_user_counts_with_submissions),
2901        )
2902        .route("/{course_id}/exercises", web::get().to(get_all_exercises))
2903        .route(
2904            "/{course_id}/exercises-and-count-of-answers-requiring-attention",
2905            web::get().to(get_all_exercises_and_count_of_answers_requiring_attention),
2906        )
2907        .route(
2908            "/{course_id}/structure",
2909            web::get().to(get_course_structure),
2910        )
2911        .route(
2912            "/{course_id}/language-versions",
2913            web::get().to(get_all_course_language_versions),
2914        )
2915        .route(
2916            "/{course_id}/create-copy",
2917            web::post().to(create_course_copy),
2918        )
2919        .route("/{course_id}/upload", web::post().to(add_media_for_course))
2920        .route(
2921            "/{course_id}/weekday-hour-submission-counts",
2922            web::get().to(get_weekday_hour_submission_counts),
2923        )
2924        .route(
2925            "/{course_id}/submission-counts-by-exercise",
2926            web::get().to(get_submission_counts_by_exercise),
2927        )
2928        .route(
2929            "/{course_id}/course-instances",
2930            web::get().to(get_course_instances),
2931        )
2932        .route("/{course_id}/feedback", web::get().to(get_feedback))
2933        .route(
2934            "/{course_id}/feedback-count",
2935            web::get().to(get_feedback_count),
2936        )
2937        .route(
2938            "/{course_id}/new-course-instance",
2939            web::post().to(new_course_instance),
2940        )
2941        .route("/{course_id}/glossary", web::get().to(glossary))
2942        .route("/{course_id}/glossary", web::post().to(new_glossary_term))
2943        .route(
2944            "/{course_id}/course-users-counts-by-exercise",
2945            web::get().to(get_course_users_counts_by_exercise),
2946        )
2947        .route(
2948            "/{course_id}/new-page-ordering",
2949            web::post().to(post_new_page_ordering),
2950        )
2951        .route(
2952            "/{course_id}/new-chapter-ordering",
2953            web::post().to(post_new_chapter_ordering),
2954        )
2955        .route(
2956            "/{course_id}/references",
2957            web::get().to(get_material_references_by_course_id),
2958        )
2959        .route(
2960            "/{course_id}/references",
2961            web::post().to(insert_material_references),
2962        )
2963        .route(
2964            "/{course_id}/references/{reference_id}",
2965            web::post().to(update_material_reference),
2966        )
2967        .route(
2968            "/{course_id}/references/{reference_id}",
2969            web::delete().to(delete_material_reference_by_id),
2970        )
2971        .route(
2972            "/{course_id}/course-modules",
2973            web::post().to(update_modules),
2974        )
2975        .route(
2976            "/{course_id}/default-peer-review",
2977            web::get().to(get_course_default_peer_review),
2978        )
2979        .route(
2980            "/{course_id}/update-peer-review-queue-reviews-received",
2981            web::post().to(post_update_peer_review_queue_reviews_received),
2982        )
2983        .route(
2984            "/{course_id}/breadcrumb-info",
2985            web::get().to(get_course_breadcrumb_info),
2986        )
2987        .route(
2988            "/{course_id}/progress/{user_id}",
2989            web::get().to(get_user_progress_for_course),
2990        )
2991        .route(
2992            "/{course_id}/user-settings/{user_id}",
2993            web::get().to(get_user_course_settings),
2994        )
2995        .route(
2996            "/{course_id}/export-submissions",
2997            web::get().to(submission_export),
2998        )
2999        .route(
3000            "/{course_id}/export-user-details",
3001            web::get().to(user_details_export),
3002        )
3003        .route(
3004            "/{course_id}/export-exercise-tasks",
3005            web::get().to(exercise_tasks_export),
3006        )
3007        .route(
3008            "/{course_id}/export-course-instances",
3009            web::get().to(course_instances_export),
3010        )
3011        .route(
3012            "/{course_id}/export-course-user-consents",
3013            web::get().to(course_consent_form_answers_export),
3014        )
3015        .route(
3016            "/{course_id}/export-user-exercise-states",
3017            web::get().to(user_exercise_states_export),
3018        )
3019        .route(
3020            "/{course_id}/page-visit-datum-summary",
3021            web::get().to(get_page_visit_datum_summary),
3022        )
3023        .route(
3024            "/{course_id}/page-visit-datum-summary-by-pages",
3025            web::get().to(get_page_visit_datum_summary_by_pages),
3026        )
3027        .route(
3028            "/{course_id}/page-visit-datum-summary-by-device-types",
3029            web::get().to(get_page_visit_datum_summary_by_device_types),
3030        )
3031        .route(
3032            "/{course_id}/page-visit-datum-summary-by-countries",
3033            web::get().to(get_page_visit_datum_summary_by_countries),
3034        )
3035        .route(
3036            "/{course_id}/teacher-reset-course-progress-for-themselves",
3037            web::delete().to(teacher_reset_course_progress_for_themselves),
3038        )
3039        .route("/{course_id}/thresholds", web::get().to(get_all_thresholds))
3040        .route(
3041            "/{course_id}/suspected-cheaters",
3042            web::get().to(get_all_suspected_cheaters),
3043        )
3044        .route(
3045            "/{course_id}/suspected-cheaters/flagged-count",
3046            web::get().to(get_flagged_suspected_cheaters_count),
3047        )
3048        .route(
3049            "/{course_id}/suspected-cheaters/dismiss/{user_id}",
3050            web::post().to(teacher_dismiss_suspected_cheater),
3051        )
3052        .route(
3053            "/{course_id}/suspected-cheaters/confirm/{user_id}",
3054            web::post().to(teacher_confirm_suspected_cheater),
3055        )
3056        .route(
3057            "/{course_id}/teacher-reset-course-progress-for-everyone",
3058            web::delete().to(teacher_reset_course_progress_for_everyone),
3059        )
3060        .route(
3061            "/{course_id}/join-course-with-join-code",
3062            web::post().to(add_user_to_course_with_join_code),
3063        )
3064        .route(
3065            "/{course_id}/partners-block",
3066            web::post().to(post_partners_block),
3067        )
3068        .route(
3069            "/{course_id}/partners-block",
3070            web::get().to(get_partners_block),
3071        )
3072        .route(
3073            "/{course_id}/partners-block",
3074            web::delete().to(delete_partners_block),
3075        )
3076        .route(
3077            "/{course_id}/set-join-code",
3078            web::post().to(set_join_code_for_course),
3079        )
3080        .route(
3081            "/{course_id}/reprocess-completions",
3082            web::post().to(post_reprocess_module_completions),
3083        )
3084        .route(
3085            "/join/{join_code}",
3086            web::get().to(get_course_with_join_code),
3087        )
3088        .route(
3089            "/{course_id}/sisu-course-llm-descriptions",
3090            web::get().to(get_sisu_course_llm_descriptions),
3091        )
3092        .route(
3093            "/{course_id}/update-metadata",
3094            web::post().to(update_metadata),
3095        )
3096        .route(
3097            "/{course_id}/get-course-prerequisites",
3098            web::get().to(get_course_prerequisites),
3099        )
3100        .route(
3101            "/{course_id}/get-course-audiences",
3102            web::get().to(get_course_audiences),
3103        )
3104        .route(
3105            "/{course_id}/get-course-metadata",
3106            web::get().to(get_course_metadata),
3107        )
3108        .route("/", web::get().to(get_all_courses));
3109}