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    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/submissions/user/:user_id` - Returns an exercise's submissions for a user.
91 */
92#[instrument(skip(pool, user))]
93async fn get_exercise_submissions_for_user(
94    pool: web::Data<PgPool>,
95    ids: web::Path<(Uuid, Uuid)>,
96    user: AuthUser,
97) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmission>>> {
98    let (exercise_id, user_id) = ids.into_inner();
99    let mut conn = pool.acquire().await?;
100
101    let target_user = models::users::get_by_id(&mut conn, user_id).await?;
102
103    let course_or_exam_id =
104        models::exercises::get_course_or_exam_id(&mut conn, exercise_id).await?;
105
106    let token = match course_or_exam_id {
107        CourseOrExamId::Course(id) => {
108            authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(id)).await?
109        }
110        CourseOrExamId::Exam(id) => {
111            authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(id)).await?
112        }
113    };
114
115    let submissions = models::exercise_slide_submissions::get_users_submissions_for_exercise(
116        &mut conn,
117        target_user.id,
118        exercise_id,
119    )
120    .await?;
121
122    token.authorized_ok(web::Json(submissions))
123}
124
125/**
126GET `/api/v0/main-frontend/exercises/:exercise_id/answers-requiring-attention` - Returns an exercise's answers requiring attention.
127 */
128#[instrument(skip(pool))]
129async fn get_exercise_answers_requiring_attention(
130    pool: web::Data<PgPool>,
131    exercise_id: web::Path<Uuid>,
132    pagination: web::Query<Pagination>,
133    user: AuthUser,
134) -> ControllerResult<web::Json<AnswersRequiringAttention>> {
135    let mut conn = pool.acquire().await?;
136    let token = match models::exercises::get_course_or_exam_id(&mut conn, *exercise_id).await? {
137        CourseOrExamId::Course(id) => {
138            authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(id)).await?
139        }
140        CourseOrExamId::Exam(id) => {
141            authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(id)).await?
142        }
143    };
144    let res = models::library::grading::get_paginated_answers_requiring_attention_for_exercise(
145        &mut conn,
146        *exercise_id,
147        *pagination,
148        user.id,
149        models_requests::fetch_service_info,
150    )
151    .await?;
152    token.authorized_ok(web::Json(res))
153}
154
155/**
156GET `/api/v0/main-frontend/exercises/:course_id/exercises-by-course-id` - Returns all exercises for a course with course_id
157 */
158pub async fn get_exercises_by_course_id(
159    course_id: web::Path<Uuid>,
160    pool: web::Data<PgPool>,
161    user: AuthUser,
162) -> ControllerResult<web::Json<Vec<Exercise>>> {
163    let mut conn = pool.acquire().await?;
164
165    let token = authorize(
166        &mut conn,
167        Act::ViewUserProgressOrDetails,
168        Some(user.id),
169        Res::Course(*course_id),
170    )
171    .await?;
172
173    let mut exercises =
174        models::exercises::get_exercises_by_course_id(&mut conn, *course_id).await?;
175
176    exercises.sort_by_key(|e| (e.chapter_id, e.page_id, e.order_number));
177
178    token.authorized_ok(web::Json(exercises))
179}
180
181#[derive(Deserialize)]
182pub struct ResetExercisesPayload {
183    pub user_ids: Vec<Uuid>,
184    pub exercise_ids: Vec<Uuid>,
185    pub threshold: Option<f64>,
186    pub reset_all_below_max_points: bool,
187    pub reset_only_locked_peer_reviews: bool,
188}
189
190/**
191POST `/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
192 */
193pub async fn reset_exercises_for_selected_users(
194    course_id: web::Path<Uuid>,
195    pool: web::Data<PgPool>,
196    user: AuthUser,
197    payload: web::Json<ResetExercisesPayload>,
198) -> ControllerResult<web::Json<i32>> {
199    let mut conn = pool.acquire().await?;
200
201    let token = authorize(
202        &mut conn,
203        Act::Teach,
204        Some(user.id),
205        Res::Course(*course_id),
206    )
207    .await?;
208
209    // Gets all valid users and their related exercises using the given filters
210    let users_and_exercises = models::exercises::collect_user_ids_and_exercise_ids_for_reset(
211        &mut conn,
212        &payload.user_ids,
213        &payload.exercise_ids,
214        payload.threshold,
215        payload.reset_all_below_max_points,
216        payload.reset_only_locked_peer_reviews,
217    )
218    .await?;
219
220    // Resets exercises for selected users and add the resets to a log
221    let reset_results = models::exercises::reset_exercises_for_selected_users(
222        &mut conn,
223        &users_and_exercises,
224        user.id,
225        *course_id,
226    )
227    .await?;
228
229    let successful_resets_count = reset_results.len();
230
231    token.authorized_ok(web::Json(successful_resets_count as i32))
232}
233
234/**
235Add a route for each controller in this module.
236
237The name starts with an underline in order to appear before other functions in the module documentation.
238
239We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
240*/
241pub fn _add_routes(cfg: &mut ServiceConfig) {
242    cfg.route(
243        "/{exercise_id}/submissions",
244        web::get().to(get_exercise_submissions),
245    )
246    .route(
247        "/{exercise_id}/answers-requiring-attention",
248        web::get().to(get_exercise_answers_requiring_attention),
249    )
250    .route(
251        "/{course_id}/exercises-by-course-id",
252        web::get().to(get_exercises_by_course_id),
253    )
254    .route(
255        "/{course_id}/reset-exercises-for-selected-users",
256        web::post().to(reset_exercises_for_selected_users),
257    )
258    .route("/{exercise_id}", web::get().to(get_exercise))
259    .route(
260        "/{exercise_id}/submissions/user/{user_id}",
261        web::get().to(get_exercise_submissions_for_user),
262    );
263}