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