headless_lms_server/controllers/main_frontend/courses/
mod.rs

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