headless_lms_server/controllers/main_frontend/courses/
mod.rs

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