headless_lms_server/controllers/main_frontend/courses/
mod.rs

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