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