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