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