headless_lms_server/controllers/main_frontend/
exercises.rs1use 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#[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#[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#[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
119pub 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
154pub 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 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 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
198pub 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}