headless_lms_server/controllers/main_frontend/
exercises.rs

1//! Controllers for requests starting with `/api/v0/main-frontend/exercises`.
2
3use futures::future;
4
5use headless_lms_models::exercises::Exercise;
6use models::{
7    CourseOrExamId, exercise_slide_submissions::ExerciseSlideSubmission,
8    library::grading::AnswersRequiringAttention,
9};
10
11use crate::{domain::models_requests, prelude::*};
12
13#[derive(Debug, Serialize)]
14#[cfg_attr(feature = "ts_rs", derive(TS))]
15pub struct ExerciseSubmissions {
16    pub data: Vec<ExerciseSlideSubmission>,
17    pub total_pages: u32,
18}
19
20/**
21GET `/api/v0/main-frontend/exercises/:exercise_id` - Returns a single exercise.
22 */
23#[instrument(skip(pool))]
24async fn get_exercise(
25    pool: web::Data<PgPool>,
26    exercise_id: web::Path<Uuid>,
27    user: AuthUser,
28) -> ControllerResult<web::Json<Exercise>> {
29    let mut conn = pool.acquire().await?;
30
31    let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
32
33    let token = if let Some(course_id) = exercise.course_id {
34        authorize(&mut conn, Act::View, Some(user.id), Res::Course(course_id)).await?
35    } else if let Some(exam_id) = exercise.exam_id {
36        authorize(&mut conn, Act::View, Some(user.id), Res::Exam(exam_id)).await?
37    } else {
38        return Err(ControllerError::new(
39            ControllerErrorType::BadRequest,
40            "Exercise is not associated with a course or exam".to_string(),
41            None,
42        ));
43    };
44
45    token.authorized_ok(web::Json(exercise))
46}
47
48/**
49GET `/api/v0/main-frontend/exercises/:exercise_id/submissions` - Returns an exercise's submissions.
50 */
51#[instrument(skip(pool))]
52async fn get_exercise_submissions(
53    pool: web::Data<PgPool>,
54    exercise_id: web::Path<Uuid>,
55    pagination: web::Query<Pagination>,
56    user: AuthUser,
57) -> ControllerResult<web::Json<ExerciseSubmissions>> {
58    let mut conn = pool.acquire().await?;
59
60    let token = match models::exercises::get_course_or_exam_id(&mut conn, *exercise_id).await? {
61        CourseOrExamId::Course(id) => {
62            authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(id)).await?
63        }
64        CourseOrExamId::Exam(id) => {
65            authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(id)).await?
66        }
67    };
68
69    let submission_count = models::exercise_slide_submissions::exercise_slide_submission_count(
70        &mut conn,
71        *exercise_id,
72    );
73    let mut conn = pool.acquire().await?;
74    let submissions = models::exercise_slide_submissions::exercise_slide_submissions(
75        &mut conn,
76        *exercise_id,
77        *pagination,
78    );
79    let (submission_count, submissions) = future::try_join(submission_count, submissions).await?;
80
81    let total_pages = pagination.total_pages(submission_count);
82
83    token.authorized_ok(web::Json(ExerciseSubmissions {
84        data: submissions,
85        total_pages,
86    }))
87}
88
89/**
90GET `/api/v0/main-frontend/exercises/:exercise_id/answers-requiring-attention` - Returns an exercise's answers requiring attention.
91 */
92#[instrument(skip(pool))]
93async fn get_exercise_answers_requiring_attention(
94    pool: web::Data<PgPool>,
95    exercise_id: web::Path<Uuid>,
96    pagination: web::Query<Pagination>,
97    user: AuthUser,
98) -> ControllerResult<web::Json<AnswersRequiringAttention>> {
99    let mut conn = pool.acquire().await?;
100    let token = match models::exercises::get_course_or_exam_id(&mut conn, *exercise_id).await? {
101        CourseOrExamId::Course(id) => {
102            authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(id)).await?
103        }
104        CourseOrExamId::Exam(id) => {
105            authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(id)).await?
106        }
107    };
108    let res = models::library::grading::get_paginated_answers_requiring_attention_for_exercise(
109        &mut conn,
110        *exercise_id,
111        *pagination,
112        user.id,
113        models_requests::fetch_service_info,
114    )
115    .await?;
116    token.authorized_ok(web::Json(res))
117}
118
119/**
120GET `/api/v0/main-frontend/exercises/:course_id/exercises-by-course-id` - Returns all exercises for a course with course_id
121 */
122pub async fn get_exercises_by_course_id(
123    course_id: web::Path<Uuid>,
124    pool: web::Data<PgPool>,
125    user: AuthUser,
126) -> ControllerResult<web::Json<Vec<Exercise>>> {
127    let mut conn = pool.acquire().await?;
128
129    let token = authorize(
130        &mut conn,
131        Act::ViewUserProgressOrDetails,
132        Some(user.id),
133        Res::Course(*course_id),
134    )
135    .await?;
136
137    let mut exercises =
138        models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
139
140    exercises.sort_by_key(|e| (e.chapter_id, e.page_id, e.order_number));
141
142    token.authorized_ok(web::Json(exercises))
143}
144
145#[derive(Deserialize)]
146pub struct ResetExercisesPayload {
147    pub user_ids: Vec<Uuid>,
148    pub exercise_ids: Vec<Uuid>,
149    pub threshold: Option<f64>,
150    pub reset_all_below_max_points: bool,
151    pub reset_only_locked_peer_reviews: bool,
152}
153
154/**
155POST `/api/v0/main-frontend/exercises/:course_id/reset-exercises-for-selected-users` - Resets all selected exercises for selected users and then logs the resets to exercise_reset_logs table
156 */
157pub async fn reset_exercises_for_selected_users(
158    course_id: web::Path<Uuid>,
159    pool: web::Data<PgPool>,
160    user: AuthUser,
161    payload: web::Json<ResetExercisesPayload>,
162) -> ControllerResult<web::Json<i32>> {
163    let mut conn = pool.acquire().await?;
164
165    let token = authorize(
166        &mut conn,
167        Act::Teach,
168        Some(user.id),
169        Res::Course(*course_id),
170    )
171    .await?;
172
173    // Gets all valid users and their related exercises using the given filters
174    let users_and_exercises = models::exercises::collect_user_ids_and_exercise_ids_for_reset(
175        &mut conn,
176        &payload.user_ids,
177        &payload.exercise_ids,
178        payload.threshold,
179        payload.reset_all_below_max_points,
180        payload.reset_only_locked_peer_reviews,
181    )
182    .await?;
183
184    // Resets exercises for selected users and add the resets to a log
185    let reset_results = models::exercises::reset_exercises_for_selected_users(
186        &mut conn,
187        &users_and_exercises,
188        user.id,
189        *course_id,
190    )
191    .await?;
192
193    let successful_resets_count = reset_results.len();
194
195    token.authorized_ok(web::Json(successful_resets_count as i32))
196}
197
198/**
199Add a route for each controller in this module.
200
201The name starts with an underline in order to appear before other functions in the module documentation.
202
203We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
204*/
205pub fn _add_routes(cfg: &mut ServiceConfig) {
206    cfg.route(
207        "/{exercise_id}/submissions",
208        web::get().to(get_exercise_submissions),
209    )
210    .route(
211        "/{exercise_id}/answers-requiring-attention",
212        web::get().to(get_exercise_answers_requiring_attention),
213    )
214    .route(
215        "/{course_id}/exercises-by-course-id",
216        web::get().to(get_exercises_by_course_id),
217    )
218    .route(
219        "/{course_id}/reset-exercises-for-selected-users",
220        web::post().to(reset_exercises_for_selected_users),
221    );
222    cfg.route("/{exercise_id}", web::get().to(get_exercise))
223        .route(
224            "/{exercise_id}/submissions",
225            web::get().to(get_exercise_submissions),
226        )
227        .route(
228            "/{exercise_id}/answers-requiring-attention",
229            web::get().to(get_exercise_answers_requiring_attention),
230        );
231}