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