headless_lms_server/controllers/main_frontend/courses/
mod.rs

1//! Controllers for requests starting with `/api/v0/main-frontend/courses`.
2
3pub mod chatbots;
4pub mod stats;
5pub mod students;
6
7use chrono::Utc;
8use domain::csv_export::user_exercise_states_export::UserExerciseStatesExportOperation;
9use headless_lms_models::{
10    partner_block::PartnersBlock,
11    suspected_cheaters::{SuspectedCheaters, Threshold},
12};
13use rand::Rng;
14use std::sync::Arc;
15
16use headless_lms_utils::strings::is_ietf_language_code_like;
17use models::{
18    chapters::Chapter,
19    course_instances::{CourseInstance, CourseInstanceForm, NewCourseInstance},
20    course_module_completions::CourseModuleCompletion,
21    course_modules::ModuleUpdates,
22    courses::{Course, CourseBreadcrumbInfo, CourseStructure, CourseUpdate, NewCourse},
23    exercise_slide_submissions::{
24        self, ExerciseAnswersInCourseRequiringAttentionCount, ExerciseSlideSubmissionCount,
25        ExerciseSlideSubmissionCountByExercise, ExerciseSlideSubmissionCountByWeekAndHour,
26    },
27    exercises::{Exercise, ExerciseStatusSummaryForUser},
28    feedback::{self, Feedback, FeedbackCount},
29    glossary::{Term, TermUpdate},
30    library,
31    material_references::{MaterialReference, NewMaterialReference},
32    page_visit_datum_summary_by_courses::PageVisitDatumSummaryByCourse,
33    page_visit_datum_summary_by_courses_countries::PageVisitDatumSummaryByCoursesCountries,
34    page_visit_datum_summary_by_courses_device_types::PageVisitDatumSummaryByCourseDeviceTypes,
35    page_visit_datum_summary_by_pages::PageVisitDatumSummaryByPages,
36    pages::Page,
37    peer_or_self_review_configs::PeerOrSelfReviewConfig,
38    peer_or_self_review_questions::PeerOrSelfReviewQuestion,
39    user_course_settings::UserCourseSettings,
40    user_exercise_states::{ExerciseUserCounts, UserCourseProgress},
41};
42
43use crate::{
44    domain::models_requests::{self, JwtKey},
45    prelude::*,
46};
47
48use headless_lms_models::course_language_groups;
49
50use crate::domain::csv_export::course_instance_export::CourseInstancesExportOperation;
51use crate::domain::csv_export::course_research_form_questions_answers_export::CourseResearchFormExportOperation;
52use crate::domain::csv_export::exercise_tasks_export::CourseExerciseTasksExportOperation;
53use crate::domain::csv_export::general_export;
54use crate::domain::csv_export::submissions::CourseSubmissionExportOperation;
55use crate::domain::csv_export::users_export::UsersExportOperation;
56
57/**
58GET `/api/v0/main-frontend/courses/:course_id` - Get course.
59*/
60#[instrument(skip(pool))]
61async fn get_course(
62    course_id: web::Path<Uuid>,
63    pool: web::Data<PgPool>,
64    user: AuthUser,
65) -> ControllerResult<web::Json<Course>> {
66    let mut conn = pool.acquire().await?;
67    let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
68    let course = models::courses::get_course(&mut conn, *course_id).await?;
69    token.authorized_ok(web::Json(course))
70}
71
72/**
73GET `/api/v0/main-frontend/courses/:course_id/breadcrumb-info` - Get information to display breadcrumbs on the manage course pages.
74*/
75#[instrument(skip(pool))]
76async fn get_course_breadcrumb_info(
77    course_id: web::Path<Uuid>,
78    pool: web::Data<PgPool>,
79    user: AuthUser,
80) -> ControllerResult<web::Json<CourseBreadcrumbInfo>> {
81    let mut conn = pool.acquire().await?;
82    let user_id = Some(user.id);
83    let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
84    let info = models::courses::get_course_breadcrumb_info(&mut conn, *course_id).await?;
85    token.authorized_ok(web::Json(info))
86}
87
88/**
89GET `/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.
90*/
91#[instrument(skip(pool))]
92async fn get_all_exercise_statuses_by_course_id(
93    params: web::Path<(Uuid, Uuid)>,
94    pool: web::Data<PgPool>,
95    user: AuthUser,
96) -> ControllerResult<web::Json<Vec<ExerciseStatusSummaryForUser>>> {
97    let (course_id, user_id) = params.into_inner();
98    let mut conn = pool.acquire().await?;
99    let token = authorize(
100        &mut conn,
101        Act::ViewUserProgressOrDetails,
102        Some(user.id),
103        Res::Course(course_id),
104    )
105    .await?;
106    let res = models::exercises::get_all_exercise_statuses_by_user_id_and_course_id(
107        &mut conn, course_id, user_id,
108    )
109    .await?;
110    token.authorized_ok(web::Json(res))
111}
112
113/**
114GET `/api/v0/main-frontend/courses/:course_id/course-module-completions/:user_id` - Returns all course module completions for a given user for this course.
115*/
116#[instrument(skip(pool))]
117async fn get_all_course_module_completions_for_user_by_course_id(
118    params: web::Path<(Uuid, Uuid)>,
119    pool: web::Data<PgPool>,
120    user: AuthUser,
121) -> ControllerResult<web::Json<Vec<CourseModuleCompletion>>> {
122    let (course_id, user_id) = params.into_inner();
123    let mut conn = pool.acquire().await?;
124    let token = authorize(
125        &mut conn,
126        Act::ViewUserProgressOrDetails,
127        Some(user.id),
128        Res::Course(course_id),
129    )
130    .await?;
131    let res = models::course_module_completions::get_all_by_course_id_and_user_id(
132        &mut conn, course_id, user_id,
133    )
134    .await?;
135    token.authorized_ok(web::Json(res))
136}
137
138/**
139GET `/api/v0/main-frontend/courses/:course_id/progress/:user_id` - Returns user progress for the course.
140*/
141#[instrument(skip(pool))]
142async fn get_user_progress_for_course(
143    path: web::Path<(Uuid, Uuid)>,
144    pool: web::Data<PgPool>,
145    user: AuthUser,
146) -> ControllerResult<web::Json<Vec<UserCourseProgress>>> {
147    let (course_id, target_user_id) = path.into_inner();
148    let mut conn = pool.acquire().await?;
149    let token = authorize(
150        &mut conn,
151        Act::ViewUserProgressOrDetails,
152        Some(user.id),
153        Res::Course(course_id),
154    )
155    .await?;
156    let user_course_progress = models::user_exercise_states::get_user_course_progress(
157        &mut conn,
158        course_id,
159        target_user_id,
160        false,
161    )
162    .await?;
163    token.authorized_ok(web::Json(user_course_progress))
164}
165
166/**
167GET `/api/v0/main-frontend/courses/:course_id/user-settings/:user_id` - Get current course settings for a specific user.
168*/
169#[instrument(skip(pool))]
170async fn get_user_course_settings(
171    path: web::Path<(Uuid, Uuid)>,
172    pool: web::Data<PgPool>,
173    user: AuthUser,
174) -> ControllerResult<web::Json<Option<UserCourseSettings>>> {
175    let (course_id, target_user_id) = path.into_inner();
176    let mut conn = pool.acquire().await?;
177    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
178    let settings = models::user_course_settings::get_user_course_settings_by_course_id(
179        &mut conn,
180        target_user_id,
181        course_id,
182    )
183    .await?;
184    token.authorized_ok(web::Json(settings))
185}
186
187/**
188POST `/api/v0/main-frontend/courses/{course_id}/reprocess-completions`
189
190Reprocesses all module completions for the given course instance. Only available to admins.
191*/
192
193#[instrument(skip(pool, user))]
194async fn post_reprocess_module_completions(
195    pool: web::Data<PgPool>,
196    user: AuthUser,
197    course_id: web::Path<Uuid>,
198) -> ControllerResult<web::Json<bool>> {
199    let mut conn = pool.acquire().await?;
200    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
201    models::library::progressing::process_all_course_completions(&mut conn, *course_id).await?;
202    token.authorized_ok(web::Json(true))
203}
204
205/**
206POST `/api/v0/main-frontend/courses` - Create a new course.
207# Example
208
209Request:
210```http
211POST /api/v0/main-frontend/courses HTTP/1.1
212Content-Type: application/json
213
214{
215  "name": "Introduction to introduction",
216  "slug": "introduction-to-introduction",
217  "organization_id": "1b89e57e-8b57-42f2-9fed-c7a6736e3eec"
218}
219```
220*/
221
222#[instrument(skip(pool, app_conf))]
223async fn post_new_course(
224    request_id: RequestId,
225    pool: web::Data<PgPool>,
226    payload: web::Json<NewCourse>,
227    user: AuthUser,
228    app_conf: web::Data<ApplicationConfiguration>,
229    jwt_key: web::Data<JwtKey>,
230) -> ControllerResult<web::Json<Course>> {
231    let mut conn = pool.acquire().await?;
232    let new_course = payload.0;
233    if !is_ietf_language_code_like(&new_course.language_code) {
234        return Err(ControllerError::new(
235            ControllerErrorType::BadRequest,
236            "Malformed language code.".to_string(),
237            None,
238        ));
239    }
240    let token = authorize(
241        &mut conn,
242        Act::CreateCoursesOrExams,
243        Some(user.id),
244        Res::Organization(new_course.organization_id),
245    )
246    .await?;
247
248    let mut tx = conn.begin().await?;
249    let (course, ..) = library::content_management::create_new_course(
250        &mut tx,
251        PKeyPolicy::Generate,
252        new_course,
253        user.id,
254        models_requests::make_spec_fetcher(
255            app_conf.base_url.clone(),
256            request_id.0,
257            Arc::clone(&jwt_key),
258        ),
259        models_requests::fetch_service_info,
260    )
261    .await?;
262    models::roles::insert(
263        &mut tx,
264        user.id,
265        models::roles::UserRole::Teacher,
266        models::roles::RoleDomain::Course(course.id),
267    )
268    .await?;
269    tx.commit().await?;
270
271    token.authorized_ok(web::Json(course))
272}
273
274/**
275POST `/api/v0/main-frontend/courses/:course_id` - Update course.
276# Example
277
278Request:
279```http
280PUT /api/v0/main-frontend/courses/ab4541d8-6db4-4561-bdb2-45f35b2544a1 HTTP/1.1
281Content-Type: application/json
282
283{
284  "name": "Introduction to Introduction"
285}
286
287```
288*/
289#[instrument(skip(pool))]
290async fn update_course(
291    payload: web::Json<CourseUpdate>,
292    course_id: web::Path<Uuid>,
293    pool: web::Data<PgPool>,
294    user: AuthUser,
295) -> ControllerResult<web::Json<Course>> {
296    let mut conn = pool.acquire().await?;
297    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
298    let course_update = payload.0;
299    let course_before_update = models::courses::get_course(&mut conn, *course_id).await?;
300    if course_update.can_add_chatbot != course_before_update.can_add_chatbot {
301        // Only global admins can change the chatbot status
302        let _token2 =
303            authorize(&mut conn, Act::Teach, Some(user.id), Res::GlobalPermissions).await?;
304    }
305
306    let locking_just_enabled =
307        !course_before_update.chapter_locking_enabled && course_update.chapter_locking_enabled;
308
309    let course = models::courses::update_course(&mut conn, *course_id, course_update).await?;
310
311    if locking_just_enabled {
312        use models::{chapters, user_chapter_locking_statuses, user_course_settings};
313
314        let all_user_settings =
315            user_course_settings::get_all_by_course_id(&mut conn, *course_id).await?;
316
317        for settings in all_user_settings {
318            let existing_statuses = user_chapter_locking_statuses::get_by_user_and_course(
319                &mut conn,
320                settings.user_id,
321                *course_id,
322            )
323            .await?;
324
325            if existing_statuses.is_empty() {
326                chapters::unlock_first_chapters_for_user(&mut conn, settings.user_id, *course_id)
327                    .await?;
328            }
329        }
330    }
331
332    token.authorized_ok(web::Json(course))
333}
334
335/**
336DELETE `/api/v0/main-frontend/courses/:course_id` - Delete a course.
337*/
338#[instrument(skip(pool))]
339async fn delete_course(
340    course_id: web::Path<Uuid>,
341    pool: web::Data<PgPool>,
342    user: AuthUser,
343) -> ControllerResult<web::Json<Course>> {
344    let mut conn = pool.acquire().await?;
345    let token = authorize(
346        &mut conn,
347        Act::UsuallyUnacceptableDeletion,
348        Some(user.id),
349        Res::Course(*course_id),
350    )
351    .await?;
352    let course = models::courses::delete_course(&mut conn, *course_id).await?;
353
354    token.authorized_ok(web::Json(course))
355}
356
357/**
358GET `/api/v0/main-frontend/courses/:course_id/structure` - Returns the structure of a course.
359# Example
360```json
361{
362  "course": {
363    "id": "d86cf910-4d26-40e9-8c9c-1cc35294fdbb",
364    "slug": "introduction-to-everything",
365    "created_at": "2021-04-28T10:40:54.503917",
366    "updated_at": "2021-04-28T10:40:54.503917",
367    "name": "Introduction to everything",
368    "organization_id": "1b89e57e-8b57-42f2-9fed-c7a6736e3eec",
369    "deleted_at": null,
370    "language_code": "en-US",
371    "copied_from": null,
372    "language_version_of_course_id": null
373  },
374  "pages": [
375    {
376      "id": "f3b0d699-c9be-4d56-bd0a-9d40e5547e4d",
377      "created_at": "2021-04-28T13:51:51.024118",
378      "updated_at": "2021-04-28T14:36:18.179490",
379      "course_id": "d86cf910-4d26-40e9-8c9c-1cc35294fdbb",
380      "content": [],
381      "url_path": "/",
382      "title": "Welcome to Introduction to Everything",
383      "deleted_at": null,
384      "chapter_id": "d332f3d9-39a5-4a18-80f4-251727693c37"
385    }
386  ],
387  "chapters": [
388    {
389      "id": "d332f3d9-39a5-4a18-80f4-251727693c37",
390      "created_at": "2021-04-28T16:11:47.477850",
391      "updated_at": "2021-04-28T16:11:47.477850",
392      "name": "The Basics",
393      "course_id": "d86cf910-4d26-40e9-8c9c-1cc35294fdbb",
394      "deleted_at": null,
395      "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",
396      "chapter_number": 1,
397      "front_page_id": null
398    }
399  ]
400}
401```
402*/
403
404#[instrument(skip(pool, file_store, app_conf))]
405async fn get_course_structure(
406    course_id: web::Path<Uuid>,
407    pool: web::Data<PgPool>,
408    user: AuthUser,
409    file_store: web::Data<dyn FileStore>,
410    app_conf: web::Data<ApplicationConfiguration>,
411) -> ControllerResult<web::Json<CourseStructure>> {
412    let mut conn = pool.acquire().await?;
413    let token = authorize(
414        &mut conn,
415        Act::ViewInternalCourseStructure,
416        Some(user.id),
417        Res::Course(*course_id),
418    )
419    .await?;
420    let course_structure = models::courses::get_course_structure(
421        &mut conn,
422        *course_id,
423        file_store.as_ref(),
424        app_conf.as_ref(),
425    )
426    .await?;
427
428    token.authorized_ok(web::Json(course_structure))
429}
430
431/**
432POST `/api/v0/main-frontend/courses/:course_id/upload` - Uploads a media (image, audio, file) for the course from Gutenberg page edit.
433
434Put the the contents of the media in a form and add a content type header multipart/form-data.
435# Example
436
437Request:
438```http
439POST /api/v0/main-frontend/pages/d86cf910-4d26-40e9-8c9c-1cc35294fdbb/upload HTTP/1.1
440Content-Type: multipart/form-data
441
442BINARY_DATA
443```
444*/
445
446#[instrument(skip(payload, request, pool, file_store, app_conf))]
447async fn add_media_for_course(
448    course_id: web::Path<Uuid>,
449    payload: Multipart,
450    request: HttpRequest,
451    pool: web::Data<PgPool>,
452    user: AuthUser,
453    file_store: web::Data<dyn FileStore>,
454    app_conf: web::Data<ApplicationConfiguration>,
455) -> ControllerResult<web::Json<UploadResult>> {
456    let mut conn = pool.acquire().await?;
457    let course = models::courses::get_course(&mut conn, *course_id).await?;
458    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
459    let media_path = upload_file_from_cms(
460        request.headers(),
461        payload,
462        StoreKind::Course(course.id),
463        file_store.as_ref(),
464        &mut conn,
465        user,
466    )
467    .await?;
468    let download_url = file_store.get_download_url(media_path.as_path(), app_conf.as_ref());
469
470    token.authorized_ok(web::Json(UploadResult { url: download_url }))
471}
472
473/**
474GET `/api/v0/main-frontend/courses/:id/exercises` - Returns all exercises for the course.
475*/
476#[instrument(skip(pool))]
477async fn get_all_exercises(
478    pool: web::Data<PgPool>,
479    course_id: web::Path<Uuid>,
480    user: AuthUser,
481) -> ControllerResult<web::Json<Vec<Exercise>>> {
482    let mut conn = pool.acquire().await?;
483    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
484    let exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
485
486    token.authorized_ok(web::Json(exercises))
487}
488
489/**
490GET `/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.
491*/
492#[instrument(skip(pool))]
493async fn get_all_exercises_and_count_of_answers_requiring_attention(
494    pool: web::Data<PgPool>,
495    course_id: web::Path<Uuid>,
496    user: AuthUser,
497) -> ControllerResult<web::Json<Vec<ExerciseAnswersInCourseRequiringAttentionCount>>> {
498    let mut conn = pool.acquire().await?;
499    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
500    let _exercises = models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
501    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?;
502    token.authorized_ok(web::Json(count_of_answers_requiring_attention))
503}
504
505/**
506GET `/api/v0/main-frontend/courses/:id/language-versions` - Returns all language versions of the same course.
507
508# Example
509
510Request:
511```http
512GET /api/v0/main-frontend/courses/fd484707-25b6-4c51-a4ff-32d8259e3e47/language-versions HTTP/1.1
513Content-Type: application/json
514```
515*/
516#[instrument(skip(pool))]
517async fn get_all_course_language_versions(
518    pool: web::Data<PgPool>,
519    course_id: web::Path<Uuid>,
520    user: AuthUser,
521) -> ControllerResult<web::Json<Vec<Course>>> {
522    let mut conn = pool.acquire().await?;
523    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
524    let course = models::courses::get_course(&mut conn, *course_id).await?;
525    let language_versions =
526        models::courses::get_all_language_versions_of_course(&mut conn, &course).await?;
527
528    token.authorized_ok(web::Json(language_versions))
529}
530
531#[derive(Deserialize, Debug)]
532#[serde(tag = "mode", rename_all = "snake_case")]
533#[cfg_attr(feature = "ts_rs", derive(TS))]
534pub enum CopyCourseMode {
535    /// Create a completely separate copy with a new course language group
536    Duplicate,
537    /// Create a new language version within the same language group as the source
538    SameLanguageGroup,
539    /// Create a new language version in a specified language group
540    ExistingLanguageGroup { target_course_id: Uuid },
541    /// Create a new language version in a new language group
542    NewLanguageGroup,
543}
544
545#[derive(Deserialize, Debug)]
546#[cfg_attr(feature = "ts_rs", derive(TS))]
547pub struct CopyCourseRequest {
548    #[serde(flatten)]
549    pub new_course: NewCourse,
550    pub mode: CopyCourseMode,
551}
552
553/**
554POST `/api/v0/main-frontend/courses/:id/create-copy` - Create a copy of a course with specified mode.
555
556Different copy modes:
557- `duplicate`: Creates a completely separate copy with new language group
558- `same_language_group`: Creates a new language version within the same language group
559- `existing_language_group`: Creates a new language version in the specified language group
560- `new_language_group`: Creates a new language version in a new language group
561
562# Example
563
564Request:
565```http
566POST /api/v0/main-frontend/courses/fd484707-25b6-4c51-a4ff-32d8259e3e47/create-copy HTTP/1.1
567Content-Type: application/json
568
569{
570  "name": "Johdatus kaikkeen",
571  "slug": "johdatus-kaikkeen",
572  "organization_id": "1b89e57e-8b57-42f2-9fed-c7a6736e3eec",
573  "language_code": "fi-FI",
574  "mode": "duplicate"
575}
576```
577
578Or with an existing language group:
579```http
580POST /api/v0/main-frontend/courses/fd484707-25b6-4c51-a4ff-32d8259e3e47/create-copy HTTP/1.1
581Content-Type: application/json
582
583{
584  "name": "Johdatus kaikkeen",
585  "slug": "johdatus-kaikkeen",
586  "organization_id": "1b89e57e-8b57-42f2-9fed-c7a6736e3eec",
587  "language_code": "fi-FI",
588  "mode": {
589    "existing_language_group": {
590      "target_course_id": "1b89e57e-8b57-42f2-9fed-c7a6736e3eec"
591    }
592  }
593}
594```
595*/
596#[instrument(skip(pool))]
597pub async fn create_course_copy(
598    pool: web::Data<PgPool>,
599    course_id: web::Path<Uuid>,
600    payload: web::Json<CopyCourseRequest>,
601    user: AuthUser,
602) -> ControllerResult<web::Json<Course>> {
603    let mut conn = pool.acquire().await?;
604    let token = authorize(
605        &mut conn,
606        Act::Duplicate,
607        Some(user.id),
608        Res::Course(*course_id),
609    )
610    .await?;
611
612    let mut tx = conn.begin().await?;
613
614    let new_course = payload.new_course.clone();
615
616    let copied_course = match &payload.mode {
617        CopyCourseMode::Duplicate => {
618            models::library::copying::copy_course(&mut tx, *course_id, &new_course, false, user.id)
619                .await?
620        }
621        CopyCourseMode::SameLanguageGroup => {
622            models::library::copying::copy_course(&mut tx, *course_id, &new_course, true, user.id)
623                .await?
624        }
625        CopyCourseMode::ExistingLanguageGroup { target_course_id } => {
626            let target_course = models::courses::get_course(&mut tx, *target_course_id).await?;
627            // Verify that the user has permissions also to the course of the custom language group
628            authorize(
629                &mut tx,
630                Act::Duplicate,
631                Some(user.id),
632                Res::Course(*target_course_id),
633            )
634            .await?;
635            models::library::copying::copy_course_with_language_group(
636                &mut tx,
637                *course_id,
638                target_course.course_language_group_id,
639                &new_course,
640                user.id,
641            )
642            .await?
643        }
644        CopyCourseMode::NewLanguageGroup => {
645            let new_clg_id = course_language_groups::insert(
646                &mut tx,
647                PKeyPolicy::Generate,
648                new_course.slug.as_str(),
649            )
650            .await?;
651            models::library::copying::copy_course_with_language_group(
652                &mut tx,
653                *course_id,
654                new_clg_id,
655                &new_course,
656                user.id,
657            )
658            .await?
659        }
660    };
661
662    models::roles::insert(
663        &mut tx,
664        user.id,
665        models::roles::UserRole::Teacher,
666        models::roles::RoleDomain::Course(copied_course.id),
667    )
668    .await?;
669
670    tx.commit().await?;
671
672    token.authorized_ok(web::Json(copied_course))
673}
674
675/**
676GET `/api/v0/main-frontend/courses/:id/daily-submission-counts` - Returns submission counts grouped by day.
677*/
678#[instrument(skip(pool))]
679async fn get_daily_submission_counts(
680    pool: web::Data<PgPool>,
681    course_id: web::Path<Uuid>,
682    user: AuthUser,
683) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
684    let mut conn = pool.acquire().await?;
685    let token = authorize(
686        &mut conn,
687        Act::ViewStats,
688        Some(user.id),
689        Res::Course(*course_id),
690    )
691    .await?;
692    let course = models::courses::get_course(&mut conn, *course_id).await?;
693    let res =
694        exercise_slide_submissions::get_course_daily_slide_submission_counts(&mut conn, &course)
695            .await?;
696
697    token.authorized_ok(web::Json(res))
698}
699
700/**
701GET `/api/v0/main-frontend/courses/:id/daily-users-who-have-submitted-something` - Returns a count of users who have submitted something grouped by day.
702*/
703#[instrument(skip(pool))]
704async fn get_daily_user_counts_with_submissions(
705    pool: web::Data<PgPool>,
706    course_id: web::Path<Uuid>,
707    user: AuthUser,
708) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCount>>> {
709    let mut conn = pool.acquire().await?;
710    let token = authorize(
711        &mut conn,
712        Act::ViewStats,
713        Some(user.id),
714        Res::Course(*course_id),
715    )
716    .await?;
717    let course = models::courses::get_course(&mut conn, *course_id).await?;
718    let res = exercise_slide_submissions::get_course_daily_user_counts_with_submissions(
719        &mut conn, &course,
720    )
721    .await?;
722
723    token.authorized_ok(web::Json(res))
724}
725
726/**
727GET `/api/v0/main-frontend/courses/:id/weekday-hour-submission-counts` - Returns submission counts grouped by weekday and hour.
728*/
729#[instrument(skip(pool))]
730async fn get_weekday_hour_submission_counts(
731    pool: web::Data<PgPool>,
732    course_id: web::Path<Uuid>,
733    user: AuthUser,
734) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByWeekAndHour>>> {
735    let mut conn = pool.acquire().await?;
736    let token = authorize(
737        &mut conn,
738        Act::ViewStats,
739        Some(user.id),
740        Res::Course(*course_id),
741    )
742    .await?;
743    let course = models::courses::get_course(&mut conn, *course_id).await?;
744    let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_weekday_and_hour(
745        &mut conn, &course,
746    )
747    .await?;
748
749    token.authorized_ok(web::Json(res))
750}
751
752/**
753GET `/api/v0/main-frontend/courses/:id/submission-counts-by-exercise` - Returns submission counts grouped by weekday and hour.
754*/
755#[instrument(skip(pool))]
756async fn get_submission_counts_by_exercise(
757    pool: web::Data<PgPool>,
758    course_id: web::Path<Uuid>,
759    user: AuthUser,
760) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionCountByExercise>>> {
761    let mut conn = pool.acquire().await?;
762    let token = authorize(
763        &mut conn,
764        Act::ViewStats,
765        Some(user.id),
766        Res::Course(*course_id),
767    )
768    .await?;
769    let course = models::courses::get_course(&mut conn, *course_id).await?;
770    let res = exercise_slide_submissions::get_course_exercise_slide_submission_counts_by_exercise(
771        &mut conn, &course,
772    )
773    .await?;
774
775    token.authorized_ok(web::Json(res))
776}
777
778/**
779GET `/api/v0/main-frontend/courses/:id/course-instances` - Returns all course instances for given course id.
780*/
781#[instrument(skip(pool))]
782async fn get_course_instances(
783    pool: web::Data<PgPool>,
784    course_id: web::Path<Uuid>,
785    user: AuthUser,
786) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
787    let mut conn = pool.acquire().await?;
788    let token = authorize(
789        &mut conn,
790        Act::Teach,
791        Some(user.id),
792        Res::Course(*course_id),
793    )
794    .await?;
795    let course_instances =
796        models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
797
798    token.authorized_ok(web::Json(course_instances))
799}
800
801#[derive(Debug, Deserialize)]
802#[cfg_attr(feature = "ts_rs", derive(TS))]
803pub struct GetFeedbackQuery {
804    read: bool,
805    #[serde(flatten)]
806    pagination: Pagination,
807}
808
809/**
810GET `/api/v0/main-frontend/courses/:id/feedback?read=true` - Returns feedback for the given course.
811*/
812#[instrument(skip(pool))]
813pub async fn get_feedback(
814    course_id: web::Path<Uuid>,
815    pool: web::Data<PgPool>,
816    read: web::Query<GetFeedbackQuery>,
817    user: AuthUser,
818) -> ControllerResult<web::Json<Vec<Feedback>>> {
819    let mut conn = pool.acquire().await?;
820    let token = authorize(
821        &mut conn,
822        Act::Teach,
823        Some(user.id),
824        Res::Course(*course_id),
825    )
826    .await?;
827    let feedback =
828        feedback::get_feedback_for_course(&mut conn, *course_id, read.read, read.pagination)
829            .await?;
830
831    token.authorized_ok(web::Json(feedback))
832}
833
834/**
835GET `/api/v0/main-frontend/courses/:id/feedback-count` - Returns the amount of feedback for the given course.
836*/
837#[instrument(skip(pool))]
838pub async fn get_feedback_count(
839    course_id: web::Path<Uuid>,
840    pool: web::Data<PgPool>,
841    user: AuthUser,
842) -> ControllerResult<web::Json<FeedbackCount>> {
843    let mut conn = pool.acquire().await?;
844    let token = authorize(
845        &mut conn,
846        Act::Teach,
847        Some(user.id),
848        Res::Course(*course_id),
849    )
850    .await?;
851
852    let feedback_count = feedback::get_feedback_count_for_course(&mut conn, *course_id).await?;
853
854    token.authorized_ok(web::Json(feedback_count))
855}
856
857/**
858POST `/api/v0/main-frontend/courses/:id/new-course-instance`
859*/
860#[instrument(skip(pool))]
861async fn new_course_instance(
862    form: web::Json<CourseInstanceForm>,
863    course_id: web::Path<Uuid>,
864    pool: web::Data<PgPool>,
865    user: AuthUser,
866) -> ControllerResult<web::Json<Uuid>> {
867    let mut conn = pool.acquire().await?;
868    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
869    let form = form.into_inner();
870    let new = NewCourseInstance {
871        course_id: *course_id,
872        name: form.name.as_deref(),
873        description: form.description.as_deref(),
874        support_email: form.support_email.as_deref(),
875        teacher_in_charge_name: &form.teacher_in_charge_name,
876        teacher_in_charge_email: &form.teacher_in_charge_email,
877        opening_time: form.opening_time,
878        closing_time: form.closing_time,
879    };
880    let ci = models::course_instances::insert(&mut conn, PKeyPolicy::Generate, new).await?;
881
882    token.authorized_ok(web::Json(ci.id))
883}
884
885#[instrument(skip(pool))]
886async fn glossary(
887    pool: web::Data<PgPool>,
888    course_id: web::Path<Uuid>,
889    user: AuthUser,
890) -> ControllerResult<web::Json<Vec<Term>>> {
891    let mut conn = pool.acquire().await?;
892    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
893    let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
894
895    token.authorized_ok(web::Json(glossary))
896}
897
898// unused?
899
900#[instrument(skip(pool))]
901async fn _new_term(
902    pool: web::Data<PgPool>,
903    course_id: web::Path<Uuid>,
904    user: AuthUser,
905) -> ControllerResult<web::Json<Vec<Term>>> {
906    let mut conn = pool.acquire().await?;
907    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
908    let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
909
910    token.authorized_ok(web::Json(glossary))
911}
912
913#[instrument(skip(pool))]
914async fn new_glossary_term(
915    pool: web::Data<PgPool>,
916    course_id: web::Path<Uuid>,
917    new_term: web::Json<TermUpdate>,
918    user: AuthUser,
919) -> ControllerResult<web::Json<Uuid>> {
920    let mut conn = pool.acquire().await?;
921    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
922    let TermUpdate { term, definition } = new_term.into_inner();
923    let term = models::glossary::insert(&mut conn, &term, &definition, *course_id).await?;
924
925    token.authorized_ok(web::Json(term))
926}
927
928/**
929GET `/api/v0/main-frontend/courses/:id/course-users-counts-by-exercise` - Returns the amount of users for each exercise.
930*/
931#[instrument(skip(pool))]
932pub async fn get_course_users_counts_by_exercise(
933    course_id: web::Path<Uuid>,
934    pool: web::Data<PgPool>,
935    user: AuthUser,
936) -> ControllerResult<web::Json<Vec<ExerciseUserCounts>>> {
937    let mut conn = pool.acquire().await?;
938    let course_id = course_id.into_inner();
939    let token = authorize(
940        &mut conn,
941        Act::ViewStats,
942        Some(user.id),
943        Res::Course(course_id),
944    )
945    .await?;
946
947    let res =
948        models::user_exercise_states::get_course_users_counts_by_exercise(&mut conn, course_id)
949            .await?;
950
951    token.authorized_ok(web::Json(res))
952}
953
954/**
955POST `/api/v0/main-frontend/courses/:id/new-page-ordering` - Reorders pages to the given order numbers and given chapters.
956
957Note 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.
958
959Creates redirects if url_path changes.
960*/
961#[instrument(skip(pool))]
962pub async fn post_new_page_ordering(
963    course_id: web::Path<Uuid>,
964    pool: web::Data<PgPool>,
965    user: AuthUser,
966    payload: web::Json<Vec<Page>>,
967) -> ControllerResult<web::Json<()>> {
968    let mut conn = pool.acquire().await?;
969    let course_id = course_id.into_inner();
970    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
971
972    models::pages::reorder_pages(&mut conn, &payload, course_id).await?;
973
974    token.authorized_ok(web::Json(()))
975}
976
977/**
978POST `/api/v0/main-frontend/courses/:id/new-chapter-ordering` - Reorders chapters based on modified chapter number.#
979
980Creates redirects if url_path changes.
981*/
982#[instrument(skip(pool))]
983pub async fn post_new_chapter_ordering(
984    course_id: web::Path<Uuid>,
985    pool: web::Data<PgPool>,
986    user: AuthUser,
987    payload: web::Json<Vec<Chapter>>,
988) -> ControllerResult<web::Json<()>> {
989    let mut conn = pool.acquire().await?;
990    let course_id = course_id.into_inner();
991    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
992
993    models::pages::reorder_chapters(&mut conn, &payload, course_id).await?;
994
995    token.authorized_ok(web::Json(()))
996}
997
998#[instrument(skip(pool))]
999async fn get_material_references_by_course_id(
1000    course_id: web::Path<Uuid>,
1001    pool: web::Data<PgPool>,
1002    user: AuthUser,
1003) -> ControllerResult<web::Json<Vec<MaterialReference>>> {
1004    let mut conn = pool.acquire().await?;
1005    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1006
1007    let res =
1008        models::material_references::get_references_by_course_id(&mut conn, *course_id).await?;
1009    token.authorized_ok(web::Json(res))
1010}
1011
1012#[instrument(skip(pool))]
1013async fn insert_material_references(
1014    course_id: web::Path<Uuid>,
1015    payload: web::Json<Vec<NewMaterialReference>>,
1016    pool: web::Data<PgPool>,
1017    user: AuthUser,
1018) -> ControllerResult<web::Json<()>> {
1019    let mut conn = pool.acquire().await?;
1020    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1021
1022    models::material_references::insert_reference(&mut conn, *course_id, payload.0).await?;
1023
1024    token.authorized_ok(web::Json(()))
1025}
1026
1027#[instrument(skip(pool))]
1028async fn update_material_reference(
1029    path: web::Path<(Uuid, Uuid)>,
1030    pool: web::Data<PgPool>,
1031    user: AuthUser,
1032    payload: web::Json<NewMaterialReference>,
1033) -> ControllerResult<web::Json<()>> {
1034    let (course_id, reference_id) = path.into_inner();
1035    let mut conn = pool.acquire().await?;
1036    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1037
1038    models::material_references::update_material_reference_by_id(
1039        &mut conn,
1040        reference_id,
1041        payload.0,
1042    )
1043    .await?;
1044    token.authorized_ok(web::Json(()))
1045}
1046
1047#[instrument(skip(pool))]
1048async fn delete_material_reference_by_id(
1049    path: web::Path<(Uuid, Uuid)>,
1050    pool: web::Data<PgPool>,
1051    user: AuthUser,
1052) -> ControllerResult<web::Json<()>> {
1053    let (course_id, reference_id) = path.into_inner();
1054    let mut conn = pool.acquire().await?;
1055    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
1056
1057    models::material_references::delete_reference(&mut conn, reference_id).await?;
1058    token.authorized_ok(web::Json(()))
1059}
1060
1061#[instrument(skip(pool))]
1062pub async fn update_modules(
1063    course_id: web::Path<Uuid>,
1064    pool: web::Data<PgPool>,
1065    user: AuthUser,
1066    payload: web::Json<ModuleUpdates>,
1067) -> ControllerResult<web::Json<()>> {
1068    let mut conn = pool.acquire().await?;
1069    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
1070
1071    models::course_modules::update_modules(&mut conn, *course_id, payload.into_inner()).await?;
1072    token.authorized_ok(web::Json(()))
1073}
1074
1075async fn get_course_default_peer_review(
1076    course_id: web::Path<Uuid>,
1077    pool: web::Data<PgPool>,
1078    user: AuthUser,
1079) -> ControllerResult<web::Json<(PeerOrSelfReviewConfig, Vec<PeerOrSelfReviewQuestion>)>> {
1080    let mut conn = pool.acquire().await?;
1081    let token = authorize(
1082        &mut conn,
1083        Act::Teach,
1084        Some(user.id),
1085        Res::Course(*course_id),
1086    )
1087    .await?;
1088
1089    let peer_review = models::peer_or_self_review_configs::get_default_for_course_by_course_id(
1090        &mut conn, *course_id,
1091    )
1092    .await?;
1093    let peer_or_self_review_questions =
1094        models::peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
1095            &mut conn,
1096            peer_review.id,
1097        )
1098        .await?;
1099    token.authorized_ok(web::Json((peer_review, peer_or_self_review_questions)))
1100}
1101
1102/**
1103POST `/api/v0/main-frontend/courses/${course_id}/update-peer-review-queue-reviews-received`
1104
1105Updates 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.
1106*/
1107
1108#[instrument(skip(pool, user))]
1109async fn post_update_peer_review_queue_reviews_received(
1110    pool: web::Data<PgPool>,
1111    user: AuthUser,
1112    course_id: web::Path<Uuid>,
1113) -> ControllerResult<web::Json<bool>> {
1114    let mut conn = pool.acquire().await?;
1115    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
1116    models::library::peer_or_self_reviewing::update_peer_review_queue_reviews_received(
1117        &mut conn, *course_id,
1118    )
1119    .await?;
1120    token.authorized_ok(web::Json(true))
1121}
1122
1123/**
1124GET `/api/v0/main-frontend/courses/${courseId}/export-submissions`
1125
1126gets SCV of course exercise submissions
1127*/
1128#[instrument(skip(pool))]
1129pub async fn submission_export(
1130    course_id: web::Path<Uuid>,
1131    pool: web::Data<PgPool>,
1132    user: AuthUser,
1133) -> ControllerResult<HttpResponse> {
1134    let mut conn = pool.acquire().await?;
1135
1136    let token = authorize(
1137        &mut conn,
1138        Act::Teach,
1139        Some(user.id),
1140        Res::Course(*course_id),
1141    )
1142    .await?;
1143
1144    let course = models::courses::get_course(&mut conn, *course_id).await?;
1145
1146    general_export(
1147        pool,
1148        &format!(
1149            "attachment; filename=\"Course: {} - Submissions (exercise tasks) {}.csv\"",
1150            course.name,
1151            Utc::now().format("%Y-%m-%d")
1152        ),
1153        CourseSubmissionExportOperation {
1154            course_id: *course_id,
1155        },
1156        token,
1157    )
1158    .await
1159}
1160
1161/**
1162GET `/api/v0/main-frontend/courses/${course.id}/export-user-details`
1163
1164gets SCV of user details for all users having submitted an exercise in the course
1165*/
1166#[instrument(skip(pool))]
1167pub async fn user_details_export(
1168    course_id: web::Path<Uuid>,
1169    pool: web::Data<PgPool>,
1170    user: AuthUser,
1171) -> ControllerResult<HttpResponse> {
1172    let mut conn = pool.acquire().await?;
1173
1174    let token = authorize(
1175        &mut conn,
1176        Act::Teach,
1177        Some(user.id),
1178        Res::Course(*course_id),
1179    )
1180    .await?;
1181
1182    let course = models::courses::get_course(&mut conn, *course_id).await?;
1183
1184    general_export(
1185        pool,
1186        &format!(
1187            "attachment; filename=\"Course: {} - User Details {}.csv\"",
1188            course.name,
1189            Utc::now().format("%Y-%m-%d")
1190        ),
1191        UsersExportOperation {
1192            course_id: *course_id,
1193        },
1194        token,
1195    )
1196    .await
1197}
1198
1199/**
1200GET `/api/v0/main-frontend/courses/${course.id}/export-exercise-tasks`
1201
1202gets SCV all exercise-tasks' private specs in course
1203*/
1204#[instrument(skip(pool))]
1205pub async fn exercise_tasks_export(
1206    course_id: web::Path<Uuid>,
1207    pool: web::Data<PgPool>,
1208    user: AuthUser,
1209) -> ControllerResult<HttpResponse> {
1210    let mut conn = pool.acquire().await?;
1211
1212    let token = authorize(
1213        &mut conn,
1214        Act::Teach,
1215        Some(user.id),
1216        Res::Course(*course_id),
1217    )
1218    .await?;
1219
1220    let course = models::courses::get_course(&mut conn, *course_id).await?;
1221
1222    general_export(
1223        pool,
1224        &format!(
1225            "attachment; filename=\"Course: {} - Exercise tasks {}.csv\"",
1226            course.name,
1227            Utc::now().format("%Y-%m-%d")
1228        ),
1229        CourseExerciseTasksExportOperation {
1230            course_id: *course_id,
1231        },
1232        token,
1233    )
1234    .await
1235}
1236
1237/**
1238GET `/api/v0/main-frontend/courses/${course.id}/export-course-instances`
1239
1240gets SCV course instances for course
1241*/
1242#[instrument(skip(pool))]
1243pub async fn course_instances_export(
1244    course_id: web::Path<Uuid>,
1245    pool: web::Data<PgPool>,
1246    user: AuthUser,
1247) -> ControllerResult<HttpResponse> {
1248    let mut conn = pool.acquire().await?;
1249
1250    let token = authorize(
1251        &mut conn,
1252        Act::Teach,
1253        Some(user.id),
1254        Res::Course(*course_id),
1255    )
1256    .await?;
1257
1258    let course = models::courses::get_course(&mut conn, *course_id).await?;
1259
1260    general_export(
1261        pool,
1262        &format!(
1263            "attachment; filename=\"Course: {} - Instances {}.csv\"",
1264            course.name,
1265            Utc::now().format("%Y-%m-%d")
1266        ),
1267        CourseInstancesExportOperation {
1268            course_id: *course_id,
1269        },
1270        token,
1271    )
1272    .await
1273}
1274
1275/**
1276GET `/api/v0/main-frontend/courses/${course.id}/export-course-user-consents`
1277
1278gets SCV course specific research form questions and user answers for course
1279*/
1280#[instrument(skip(pool))]
1281pub async fn course_consent_form_answers_export(
1282    course_id: web::Path<Uuid>,
1283    pool: web::Data<PgPool>,
1284    user: AuthUser,
1285) -> ControllerResult<HttpResponse> {
1286    let mut conn = pool.acquire().await?;
1287
1288    let token = authorize(
1289        &mut conn,
1290        Act::Teach,
1291        Some(user.id),
1292        Res::Course(*course_id),
1293    )
1294    .await?;
1295
1296    let course = models::courses::get_course(&mut conn, *course_id).await?;
1297
1298    general_export(
1299        pool,
1300        &format!(
1301            "attachment; filename=\"Course: {} - User Consents {}.csv\"",
1302            course.name,
1303            Utc::now().format("%Y-%m-%d")
1304        ),
1305        CourseResearchFormExportOperation {
1306            course_id: *course_id,
1307        },
1308        token,
1309    )
1310    .await
1311}
1312
1313/**
1314GET `/api/v0/main-frontend/courses/${course.id}/export-user-exercise-states`
1315
1316gets CSV for course specific user exercise states
1317*/
1318#[instrument(skip(pool))]
1319pub async fn user_exercise_states_export(
1320    course_id: web::Path<Uuid>,
1321    pool: web::Data<PgPool>,
1322    user: AuthUser,
1323) -> ControllerResult<HttpResponse> {
1324    let mut conn = pool.acquire().await?;
1325
1326    let token = authorize(
1327        &mut conn,
1328        Act::Teach,
1329        Some(user.id),
1330        Res::Course(*course_id),
1331    )
1332    .await?;
1333
1334    let course = models::courses::get_course(&mut conn, *course_id).await?;
1335
1336    general_export(
1337        pool,
1338        &format!(
1339            "attachment; filename=\"Course: {} - User exercise states {}.csv\"",
1340            course.name,
1341            Utc::now().format("%Y-%m-%d")
1342        ),
1343        UserExerciseStatesExportOperation {
1344            course_id: *course_id,
1345        },
1346        token,
1347    )
1348    .await
1349}
1350
1351/**
1352GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary` - Gets aggregated statistics for page visits for the course.
1353*/
1354pub async fn get_page_visit_datum_summary(
1355    course_id: web::Path<Uuid>,
1356    pool: web::Data<PgPool>,
1357    user: AuthUser,
1358) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourse>>> {
1359    let mut conn = pool.acquire().await?;
1360    let course_id = course_id.into_inner();
1361    let token = authorize(
1362        &mut conn,
1363        Act::ViewStats,
1364        Some(user.id),
1365        Res::Course(course_id),
1366    )
1367    .await?;
1368
1369    let res = models::page_visit_datum_summary_by_courses::get_all_for_course(&mut conn, course_id)
1370        .await?;
1371
1372    token.authorized_ok(web::Json(res))
1373}
1374
1375/**
1376GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary-by-pages` - Gets aggregated statistics for page visits for the course.
1377*/
1378pub async fn get_page_visit_datum_summary_by_pages(
1379    course_id: web::Path<Uuid>,
1380    pool: web::Data<PgPool>,
1381    user: AuthUser,
1382) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByPages>>> {
1383    let mut conn = pool.acquire().await?;
1384    let course_id = course_id.into_inner();
1385    let token = authorize(
1386        &mut conn,
1387        Act::ViewStats,
1388        Some(user.id),
1389        Res::Course(course_id),
1390    )
1391    .await?;
1392
1393    let res =
1394        models::page_visit_datum_summary_by_pages::get_all_for_course(&mut conn, course_id).await?;
1395
1396    token.authorized_ok(web::Json(res))
1397}
1398
1399/**
1400GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary-by-device-types` - Gets aggregated statistics for page visits for the course.
1401*/
1402pub async fn get_page_visit_datum_summary_by_device_types(
1403    course_id: web::Path<Uuid>,
1404    pool: web::Data<PgPool>,
1405    user: AuthUser,
1406) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCourseDeviceTypes>>> {
1407    let mut conn = pool.acquire().await?;
1408    let course_id = course_id.into_inner();
1409    let token = authorize(
1410        &mut conn,
1411        Act::ViewStats,
1412        Some(user.id),
1413        Res::Course(course_id),
1414    )
1415    .await?;
1416
1417    let res = models::page_visit_datum_summary_by_courses_device_types::get_all_for_course(
1418        &mut conn, course_id,
1419    )
1420    .await?;
1421
1422    token.authorized_ok(web::Json(res))
1423}
1424
1425/**
1426GET `/api/v0/main-frontend/courses/${course.id}/page-visit-datum-summary-by-countries` - Gets aggregated statistics for page visits for the course.
1427*/
1428pub async fn get_page_visit_datum_summary_by_countries(
1429    course_id: web::Path<Uuid>,
1430    pool: web::Data<PgPool>,
1431    user: AuthUser,
1432) -> ControllerResult<web::Json<Vec<PageVisitDatumSummaryByCoursesCountries>>> {
1433    let mut conn = pool.acquire().await?;
1434    let course_id = course_id.into_inner();
1435    let token = authorize(
1436        &mut conn,
1437        Act::ViewStats,
1438        Some(user.id),
1439        Res::Course(course_id),
1440    )
1441    .await?;
1442
1443    let res = models::page_visit_datum_summary_by_courses_countries::get_all_for_course(
1444        &mut conn, course_id,
1445    )
1446    .await?;
1447
1448    token.authorized_ok(web::Json(res))
1449}
1450
1451/**
1452DELETE `/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.
1453
1454Deletes submissions, user exercise states, and peer reviews etc. for all the course instances of this course.
1455*/
1456pub async fn teacher_reset_course_progress_for_themselves(
1457    course_id: web::Path<Uuid>,
1458    pool: web::Data<PgPool>,
1459    user: AuthUser,
1460) -> ControllerResult<web::Json<bool>> {
1461    let mut conn = pool.acquire().await?;
1462    let course_id = course_id.into_inner();
1463    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1464
1465    let mut tx = conn.begin().await?;
1466    let course_instances =
1467        models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
1468    for course_instance in course_instances {
1469        models::course_instances::reset_progress_on_course_instance_for_user(
1470            &mut tx,
1471            user.id,
1472            course_instance.course_id,
1473        )
1474        .await?;
1475    }
1476
1477    tx.commit().await?;
1478    token.authorized_ok(web::Json(true))
1479}
1480
1481/**
1482DELETE `/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.
1483
1484Deletes submissions, user exercise states, and peer reviews etc. for all the course instances of this course.
1485*/
1486pub async fn teacher_reset_course_progress_for_everyone(
1487    course_id: web::Path<Uuid>,
1488    pool: web::Data<PgPool>,
1489    user: AuthUser,
1490) -> ControllerResult<web::Json<bool>> {
1491    let mut conn = pool.acquire().await?;
1492    let course_id = course_id.into_inner();
1493    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1494    let course = models::courses::get_course(&mut conn, course_id).await?;
1495    if !course.is_draft {
1496        return Err(ControllerError::new(
1497            ControllerErrorType::BadRequest,
1498            "Can only reset progress for a draft course.".to_string(),
1499            None,
1500        ));
1501    }
1502    // 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.
1503    let n_course_module_completions =
1504        models::course_module_completions::get_count_of_distinct_completors_by_course_id(
1505            &mut conn, course_id,
1506        )
1507        .await?;
1508    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(
1509        &mut conn, course_id,
1510    ).await?;
1511    if n_course_module_completions > 200 {
1512        return Err(ControllerError::new(
1513            ControllerErrorType::BadRequest,
1514            "Too many students have completed the course.".to_string(),
1515            None,
1516        ));
1517    }
1518    if n_completions_registered_to_study_registry > 2 {
1519        return Err(ControllerError::new(
1520            ControllerErrorType::BadRequest,
1521            "Too many students have registered their completion to a study registry".to_string(),
1522            None,
1523        ));
1524    }
1525
1526    let mut tx = conn.begin().await?;
1527    let course_instances =
1528        models::course_instances::get_course_instances_for_course(&mut tx, course_id).await?;
1529
1530    // Looping though the data since this is only for draft courses and the amount of data is not expected to be large.
1531    for course_instance in course_instances {
1532        let users_in_course_instance =
1533            models::users::get_users_by_course_instance_enrollment(&mut tx, course_instance.id)
1534                .await?;
1535        for user_in_course_instance in users_in_course_instance {
1536            models::course_instances::reset_progress_on_course_instance_for_user(
1537                &mut tx,
1538                user_in_course_instance.id,
1539                course_instance.course_id,
1540            )
1541            .await?;
1542        }
1543    }
1544
1545    tx.commit().await?;
1546    token.authorized_ok(web::Json(true))
1547}
1548
1549#[derive(Debug, Deserialize)]
1550#[cfg_attr(feature = "ts_rs", derive(TS))]
1551pub struct GetSuspectedCheatersQuery {
1552    archive: bool,
1553}
1554
1555/**
1556 GET /api/v0/main-frontend/courses/${course.id}/suspected-cheaters?archive=true - returns all suspected cheaters related to a course instance.
1557*/
1558#[instrument(skip(pool))]
1559async fn get_all_suspected_cheaters(
1560    user: AuthUser,
1561    params: web::Path<Uuid>,
1562    query: web::Query<GetSuspectedCheatersQuery>,
1563    pool: web::Data<PgPool>,
1564) -> ControllerResult<web::Json<Vec<SuspectedCheaters>>> {
1565    let course_id = params.into_inner();
1566
1567    let mut conn = pool.acquire().await?;
1568    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1569
1570    let course_cheaters = models::suspected_cheaters::get_all_suspected_cheaters_in_course(
1571        &mut conn,
1572        course_id,
1573        query.archive,
1574    )
1575    .await?;
1576
1577    token.authorized_ok(web::Json(course_cheaters))
1578}
1579
1580/**
1581 GET /api/v0/main-frontend/courses/${course.id}/thresholds - get all thresholds for all modules in a course.
1582*/
1583#[instrument(skip(pool))]
1584async fn get_all_thresholds(
1585    user: AuthUser,
1586    params: web::Path<Uuid>,
1587    pool: web::Data<PgPool>,
1588) -> ControllerResult<web::Json<Vec<Threshold>>> {
1589    let mut conn = pool.acquire().await?;
1590    let course_id = params.into_inner();
1591
1592    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1593
1594    let thresholds =
1595        models::suspected_cheaters::get_all_thresholds_for_course(&mut conn, course_id).await?;
1596
1597    token.authorized_ok(web::Json(thresholds))
1598}
1599
1600/**
1601 POST /api/v0/main-frontend/courses/${course.id}/suspected-cheaters/archive/:id - UPDATE is_archived to TRUE.
1602*/
1603#[instrument(skip(pool))]
1604async fn teacher_archive_suspected_cheater(
1605    user: AuthUser,
1606    path: web::Path<(Uuid, Uuid)>,
1607    pool: web::Data<PgPool>,
1608) -> ControllerResult<web::Json<()>> {
1609    let (course_id, user_id) = path.into_inner();
1610
1611    let mut conn = pool.acquire().await?;
1612    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1613
1614    models::suspected_cheaters::archive_suspected_cheater(&mut conn, user_id).await?;
1615
1616    token.authorized_ok(web::Json(()))
1617}
1618
1619/**
1620 POST /api/v0/main-frontend/courses/${course.id}/suspected-cheaters/approve/:id - UPDATE is_archived to FALSE.
1621*/
1622#[instrument(skip(pool))]
1623async fn teacher_approve_suspected_cheater(
1624    user: AuthUser,
1625    path: web::Path<(Uuid, Uuid)>,
1626    pool: web::Data<PgPool>,
1627) -> ControllerResult<web::Json<()>> {
1628    let (course_id, user_id) = path.into_inner();
1629
1630    let mut conn = pool.acquire().await?;
1631    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1632
1633    models::suspected_cheaters::approve_suspected_cheater(&mut conn, user_id).await?;
1634
1635    // Fail student
1636    //find by user_id and course_id
1637    models::course_module_completions::update_passed_and_grade_status(
1638        &mut conn, course_id, user_id, false, 0,
1639    )
1640    .await?;
1641
1642    token.authorized_ok(web::Json(()))
1643}
1644
1645/**
1646POST /courses/:course_id/join-course-with-join-code - Adds the user to join_code_uses so the user gets access to the course
1647*/
1648#[instrument(skip(pool))]
1649async fn add_user_to_course_with_join_code(
1650    course_id: web::Path<Uuid>,
1651    user: AuthUser,
1652    pool: web::Data<PgPool>,
1653) -> ControllerResult<web::Json<Uuid>> {
1654    let mut conn = pool.acquire().await?;
1655    let token = skip_authorize();
1656
1657    let joined =
1658        models::join_code_uses::insert(&mut conn, PKeyPolicy::Generate, user.id, *course_id)
1659            .await?;
1660    token.authorized_ok(web::Json(joined))
1661}
1662
1663/**
1664 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
1665*/
1666#[instrument(skip(pool))]
1667async fn set_join_code_for_course(
1668    id: web::Path<Uuid>,
1669    pool: web::Data<PgPool>,
1670    user: AuthUser,
1671) -> ControllerResult<HttpResponse> {
1672    let mut conn = pool.acquire().await?;
1673    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*id)).await?;
1674
1675    const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
1676                            abcdefghjkmnpqrstuvwxyz";
1677    const PASSWORD_LEN: usize = 64;
1678    let mut rng = rand::rng();
1679
1680    let code: String = (0..PASSWORD_LEN)
1681        .map(|_| {
1682            let idx = rng.random_range(0..CHARSET.len());
1683            CHARSET[idx] as char
1684        })
1685        .collect();
1686
1687    models::courses::set_join_code_for_course(&mut conn, *id, code).await?;
1688    token.authorized_ok(HttpResponse::Ok().finish())
1689}
1690
1691/**
1692GET /courses/join/:join_code - Gets the course related to join code
1693*/
1694#[instrument(skip(pool))]
1695async fn get_course_with_join_code(
1696    join_code: web::Path<String>,
1697    user: AuthUser,
1698    pool: web::Data<PgPool>,
1699) -> ControllerResult<web::Json<Course>> {
1700    let mut conn = pool.acquire().await?;
1701    let token = skip_authorize();
1702    let course =
1703        models::courses::get_course_with_join_code(&mut conn, join_code.to_string()).await?;
1704
1705    token.authorized_ok(web::Json(course))
1706}
1707
1708/**
1709 POST /api/v0/main-frontend/courses/:course_id/partners_block - Create or updates a partners block for a course
1710*/
1711#[instrument(skip(payload, pool))]
1712async fn post_partners_block(
1713    path: web::Path<Uuid>,
1714    payload: web::Json<Option<serde_json::Value>>,
1715    pool: web::Data<PgPool>,
1716    user: AuthUser,
1717) -> ControllerResult<web::Json<PartnersBlock>> {
1718    let course_id = path.into_inner();
1719
1720    let content = payload.into_inner();
1721    let mut conn = pool.acquire().await?;
1722    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1723
1724    let upserted_partner_block =
1725        models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
1726
1727    token.authorized_ok(web::Json(upserted_partner_block))
1728}
1729
1730/**
1731GET /courses/:course_id/partners_blocks - Gets a partners block related to a course
1732*/
1733#[instrument(skip(pool))]
1734async fn get_partners_block(
1735    path: web::Path<Uuid>,
1736    user: AuthUser,
1737    pool: web::Data<PgPool>,
1738) -> ControllerResult<web::Json<PartnersBlock>> {
1739    let course_id = path.into_inner();
1740    let mut conn = pool.acquire().await?;
1741    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
1742
1743    // Check if the course exists in the partners_blocks table
1744    let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
1745
1746    let partner_block = if course_exists {
1747        // If the course exists, fetch the partner block
1748        models::partner_block::get_partner_block(&mut conn, course_id).await?
1749    } else {
1750        // If the course does not exist, create a new partner block with an empty content array
1751        let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
1752
1753        // Upsert the partner block with the empty content
1754        models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
1755    };
1756
1757    token.authorized_ok(web::Json(partner_block))
1758}
1759
1760/**
1761DELETE `/api/v0/main-frontend/courses/:course_id` - Delete a partners block in a course.
1762*/
1763#[instrument(skip(pool))]
1764async fn delete_partners_block(
1765    path: web::Path<Uuid>,
1766    pool: web::Data<PgPool>,
1767    user: AuthUser,
1768) -> ControllerResult<web::Json<PartnersBlock>> {
1769    let course_id = path.into_inner();
1770    let mut conn = pool.acquire().await?;
1771    let token = authorize(
1772        &mut conn,
1773        Act::UsuallyUnacceptableDeletion,
1774        Some(user.id),
1775        Res::Course(course_id),
1776    )
1777    .await?;
1778    let deleted_partners_block =
1779        models::partner_block::delete_partner_block(&mut conn, course_id).await?;
1780
1781    token.authorized_ok(web::Json(deleted_partners_block))
1782}
1783
1784/**
1785Add a route for each controller in this module.
1786
1787The name starts with an underline in order to appear before other functions in the module documentation.
1788
1789We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
1790*/
1791pub fn _add_routes(cfg: &mut ServiceConfig) {
1792    cfg.service(web::scope("/{course_id}/stats").configure(stats::_add_routes))
1793        .service(web::scope("/{course_id}/chatbots").configure(chatbots::_add_routes))
1794        .service(web::scope("/{course_id}/students").configure(students::_add_routes))
1795        .route("/{course_id}", web::get().to(get_course))
1796        .route("", web::post().to(post_new_course))
1797        .route("/{course_id}", web::put().to(update_course))
1798        .route("/{course_id}", web::delete().to(delete_course))
1799        .route(
1800            "/{course_id}/status-for-all-exercises/{user_id}",
1801            web::get().to(get_all_exercise_statuses_by_course_id),
1802        )
1803        .route(
1804            "/{course_id}/course-module-completions/{user_id}",
1805            web::get().to(get_all_course_module_completions_for_user_by_course_id),
1806        )
1807        .route(
1808            "/{course_id}/daily-submission-counts",
1809            web::get().to(get_daily_submission_counts),
1810        )
1811        .route(
1812            "/{course_id}/daily-users-who-have-submitted-something",
1813            web::get().to(get_daily_user_counts_with_submissions),
1814        )
1815        .route("/{course_id}/exercises", web::get().to(get_all_exercises))
1816        .route(
1817            "/{course_id}/exercises-and-count-of-answers-requiring-attention",
1818            web::get().to(get_all_exercises_and_count_of_answers_requiring_attention),
1819        )
1820        .route(
1821            "/{course_id}/structure",
1822            web::get().to(get_course_structure),
1823        )
1824        .route(
1825            "/{course_id}/language-versions",
1826            web::get().to(get_all_course_language_versions),
1827        )
1828        .route(
1829            "/{course_id}/create-copy",
1830            web::post().to(create_course_copy),
1831        )
1832        .route("/{course_id}/upload", web::post().to(add_media_for_course))
1833        .route(
1834            "/{course_id}/weekday-hour-submission-counts",
1835            web::get().to(get_weekday_hour_submission_counts),
1836        )
1837        .route(
1838            "/{course_id}/submission-counts-by-exercise",
1839            web::get().to(get_submission_counts_by_exercise),
1840        )
1841        .route(
1842            "/{course_id}/course-instances",
1843            web::get().to(get_course_instances),
1844        )
1845        .route("/{course_id}/feedback", web::get().to(get_feedback))
1846        .route(
1847            "/{course_id}/feedback-count",
1848            web::get().to(get_feedback_count),
1849        )
1850        .route(
1851            "/{course_id}/new-course-instance",
1852            web::post().to(new_course_instance),
1853        )
1854        .route("/{course_id}/glossary", web::get().to(glossary))
1855        .route("/{course_id}/glossary", web::post().to(new_glossary_term))
1856        .route(
1857            "/{course_id}/course-users-counts-by-exercise",
1858            web::get().to(get_course_users_counts_by_exercise),
1859        )
1860        .route(
1861            "/{course_id}/new-page-ordering",
1862            web::post().to(post_new_page_ordering),
1863        )
1864        .route(
1865            "/{course_id}/new-chapter-ordering",
1866            web::post().to(post_new_chapter_ordering),
1867        )
1868        .route(
1869            "/{course_id}/references",
1870            web::get().to(get_material_references_by_course_id),
1871        )
1872        .route(
1873            "/{course_id}/references",
1874            web::post().to(insert_material_references),
1875        )
1876        .route(
1877            "/{course_id}/references/{reference_id}",
1878            web::post().to(update_material_reference),
1879        )
1880        .route(
1881            "/{course_id}/references/{reference_id}",
1882            web::delete().to(delete_material_reference_by_id),
1883        )
1884        .route(
1885            "/{course_id}/course-modules",
1886            web::post().to(update_modules),
1887        )
1888        .route(
1889            "/{course_id}/default-peer-review",
1890            web::get().to(get_course_default_peer_review),
1891        )
1892        .route(
1893            "/{course_id}/update-peer-review-queue-reviews-received",
1894            web::post().to(post_update_peer_review_queue_reviews_received),
1895        )
1896        .route(
1897            "/{course_id}/breadcrumb-info",
1898            web::get().to(get_course_breadcrumb_info),
1899        )
1900        .route(
1901            "/{course_id}/progress/{user_id}",
1902            web::get().to(get_user_progress_for_course),
1903        )
1904        .route(
1905            "/{course_id}/user-settings/{user_id}",
1906            web::get().to(get_user_course_settings),
1907        )
1908        .route(
1909            "/{course_id}/export-submissions",
1910            web::get().to(submission_export),
1911        )
1912        .route(
1913            "/{course_id}/export-user-details",
1914            web::get().to(user_details_export),
1915        )
1916        .route(
1917            "/{course_id}/export-exercise-tasks",
1918            web::get().to(exercise_tasks_export),
1919        )
1920        .route(
1921            "/{course_id}/export-course-instances",
1922            web::get().to(course_instances_export),
1923        )
1924        .route(
1925            "/{course_id}/export-course-user-consents",
1926            web::get().to(course_consent_form_answers_export),
1927        )
1928        .route(
1929            "/{course_id}/export-user-exercise-states",
1930            web::get().to(user_exercise_states_export),
1931        )
1932        .route(
1933            "/{course_id}/page-visit-datum-summary",
1934            web::get().to(get_page_visit_datum_summary),
1935        )
1936        .route(
1937            "/{course_id}/page-visit-datum-summary-by-pages",
1938            web::get().to(get_page_visit_datum_summary_by_pages),
1939        )
1940        .route(
1941            "/{course_id}/page-visit-datum-summary-by-device-types",
1942            web::get().to(get_page_visit_datum_summary_by_device_types),
1943        )
1944        .route(
1945            "/{course_id}/page-visit-datum-summary-by-countries",
1946            web::get().to(get_page_visit_datum_summary_by_countries),
1947        )
1948        .route(
1949            "/{course_id}/teacher-reset-course-progress-for-themselves",
1950            web::delete().to(teacher_reset_course_progress_for_themselves),
1951        )
1952        .route("/{course_id}/thresholds", web::get().to(get_all_thresholds))
1953        .route(
1954            "/{course_id}/suspected-cheaters",
1955            web::get().to(get_all_suspected_cheaters),
1956        )
1957        .route(
1958            "/{course_id}/suspected-cheaters/archive/{id}",
1959            web::post().to(teacher_archive_suspected_cheater),
1960        )
1961        .route(
1962            "/{course_id}/suspected-cheaters/approve/{id}",
1963            web::post().to(teacher_approve_suspected_cheater),
1964        )
1965        .route(
1966            "/{course_id}/teacher-reset-course-progress-for-everyone",
1967            web::delete().to(teacher_reset_course_progress_for_everyone),
1968        )
1969        .route(
1970            "/{course_id}/join-course-with-join-code",
1971            web::post().to(add_user_to_course_with_join_code),
1972        )
1973        .route(
1974            "/{course_id}/partners-block",
1975            web::post().to(post_partners_block),
1976        )
1977        .route(
1978            "/{course_id}/partners-block",
1979            web::get().to(get_partners_block),
1980        )
1981        .route(
1982            "/{course_id}/partners-block",
1983            web::delete().to(delete_partners_block),
1984        )
1985        .route(
1986            "/{course_id}/set-join-code",
1987            web::post().to(set_join_code_for_course),
1988        )
1989        .route(
1990            "/{course_id}/reprocess-completions",
1991            web::post().to(post_reprocess_module_completions),
1992        )
1993        .route(
1994            "/join/{join_code}",
1995            web::get().to(get_course_with_join_code),
1996        );
1997}