1use std::collections::HashMap;
2
3use futures::future;
4
5use chrono::Utc;
6use headless_lms_models::user_exercise_states::UserExerciseState;
7use models::{
8 course_exams,
9 exams::{self, Exam, NewExam},
10 exercise_slide_submissions::{
11 ExerciseSlideSubmissionAndUserExerciseState,
12 ExerciseSlideSubmissionAndUserExerciseStateList,
13 },
14 exercises::Exercise,
15 library::user_exercise_state_updater,
16 teacher_grading_decisions,
17};
18
19use crate::{
20 domain::csv_export::{
21 general_export, points::ExamPointExportOperation,
22 submissions::ExamSubmissionExportOperation,
23 },
24 prelude::*,
25};
26
27#[instrument(skip(pool))]
31pub async fn get_exam(
32 pool: web::Data<PgPool>,
33 exam_id: web::Path<Uuid>,
34 user: AuthUser,
35) -> ControllerResult<web::Json<Exam>> {
36 let mut conn = pool.acquire().await?;
37 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
38
39 let exam = exams::get(&mut conn, *exam_id).await?;
40
41 token.authorized_ok(web::Json(exam))
42}
43
44#[derive(Debug, Deserialize)]
45#[cfg_attr(feature = "ts_rs", derive(TS))]
46pub struct ExamCourseInfo {
47 course_id: Uuid,
48}
49
50#[instrument(skip(pool))]
54pub async fn set_course(
55 pool: web::Data<PgPool>,
56 exam_id: web::Path<Uuid>,
57 exam: web::Json<ExamCourseInfo>,
58 user: AuthUser,
59) -> ControllerResult<web::Json<()>> {
60 let mut conn = pool.acquire().await?;
61 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Exam(*exam_id)).await?;
62
63 course_exams::upsert(&mut conn, *exam_id, exam.course_id).await?;
64
65 token.authorized_ok(web::Json(()))
66}
67
68#[instrument(skip(pool))]
72pub async fn unset_course(
73 pool: web::Data<PgPool>,
74 exam_id: web::Path<Uuid>,
75 exam: web::Json<ExamCourseInfo>,
76 user: AuthUser,
77) -> ControllerResult<web::Json<()>> {
78 let mut conn = pool.acquire().await?;
79 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Exam(*exam_id)).await?;
80
81 course_exams::delete(&mut conn, *exam_id, exam.course_id).await?;
82
83 token.authorized_ok(web::Json(()))
84}
85
86#[instrument(skip(pool))]
90pub async fn export_points(
91 exam_id: web::Path<Uuid>,
92 pool: web::Data<PgPool>,
93 user: AuthUser,
94) -> ControllerResult<HttpResponse> {
95 let mut conn = pool.acquire().await?;
96 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
97
98 let exam = exams::get(&mut conn, *exam_id).await?;
99
100 general_export(
101 pool,
102 &format!(
103 "attachment; filename=\"Exam: {} - Point export {}.csv\"",
104 exam.name,
105 Utc::now().format("%Y-%m-%d")
106 ),
107 ExamPointExportOperation { exam_id: *exam_id },
108 token,
109 )
110 .await
111}
112
113#[instrument(skip(pool))]
117pub async fn export_submissions(
118 exam_id: web::Path<Uuid>,
119 pool: web::Data<PgPool>,
120 user: AuthUser,
121) -> ControllerResult<HttpResponse> {
122 let mut conn = pool.acquire().await?;
123 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
124
125 let exam = exams::get(&mut conn, *exam_id).await?;
126
127 general_export(
128 pool,
129 &format!(
130 "attachment; filename=\"Exam: {} - Submissions {}.csv\"",
131 exam.name,
132 Utc::now().format("%Y-%m-%d")
133 ),
134 ExamSubmissionExportOperation { exam_id: *exam_id },
135 token,
136 )
137 .await
138}
139
140#[instrument(skip(pool))]
144async fn duplicate_exam(
145 pool: web::Data<PgPool>,
146 exam_id: web::Path<Uuid>,
147 new_exam: web::Json<NewExam>,
148 user: AuthUser,
149) -> ControllerResult<web::Json<bool>> {
150 let mut conn = pool.acquire().await?;
151 let organization_id = models::exams::get_organization_id(&mut conn, *exam_id).await?;
152 let token = authorize(
153 &mut conn,
154 Act::CreateCoursesOrExams,
155 Some(user.id),
156 Res::Organization(organization_id),
157 )
158 .await?;
159
160 let mut tx = conn.begin().await?;
161 let new_exam = models::library::copying::copy_exam(&mut tx, &exam_id, &new_exam).await?;
162
163 models::roles::insert(
164 &mut tx,
165 user.id,
166 models::roles::UserRole::Teacher,
167 models::roles::RoleDomain::Exam(new_exam.id),
168 )
169 .await?;
170 tx.commit().await?;
171
172 token.authorized_ok(web::Json(true))
173}
174
175#[instrument(skip(pool))]
179async fn edit_exam(
180 pool: web::Data<PgPool>,
181 exam_id: web::Path<Uuid>,
182 payload: web::Json<NewExam>,
183 user: AuthUser,
184) -> ControllerResult<web::Json<()>> {
185 let mut conn = pool.acquire().await?;
186 let mut tx = conn.begin().await?;
187
188 let exam = payload.0;
189 let token = authorize(&mut tx, Act::Edit, Some(user.id), Res::Exam(*exam_id)).await?;
190
191 models::exams::edit(&mut tx, *exam_id, exam).await?;
192
193 tx.commit().await?;
194
195 token.authorized_ok(web::Json(()))
196}
197
198#[instrument(skip(pool))]
202async fn get_exercise_slide_submissions_and_user_exercise_states_with_exercise_id(
203 pool: web::Data<PgPool>,
204 exercise_id: web::Path<Uuid>,
205 pagination: web::Query<Pagination>,
206 user: AuthUser,
207) -> ControllerResult<web::Json<ExerciseSlideSubmissionAndUserExerciseStateList>> {
208 let mut conn = pool.acquire().await?;
209
210 let token = authorize(
211 &mut conn,
212 Act::Teach,
213 Some(user.id),
214 Res::Exercise(*exercise_id),
215 )
216 .await?;
217
218 let submission_count =
219 models::exercise_slide_submissions::exercise_slide_submission_count_with_exercise_id(
220 &mut conn,
221 *exercise_id,
222 );
223 let mut conn = pool.acquire().await?;
224 let submissions = models::exercise_slide_submissions::get_latest_exercise_slide_submissions_and_user_exercise_state_list_with_exercise_id(
225 &mut conn,
226 *exercise_id,
227 *pagination,
228 );
229 let (submission_count, submissions) = future::try_join(submission_count, submissions).await?;
230 let total_pages = pagination.total_pages(submission_count);
231
232 token.authorized_ok(web::Json(ExerciseSlideSubmissionAndUserExerciseStateList {
233 data: submissions,
234 total_pages,
235 }))
236}
237
238#[instrument(skip(pool))]
242async fn get_exercise_slide_submissions_and_user_exercise_states_with_exam_id(
243 pool: web::Data<PgPool>,
244 exam_id: web::Path<Uuid>,
245 pagination: web::Query<Pagination>,
246 user: AuthUser,
247) -> ControllerResult<web::Json<Vec<Vec<ExerciseSlideSubmissionAndUserExerciseState>>>> {
248 let mut conn = pool.acquire().await?;
249
250 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
251
252 let mut submissions_and_user_exercise_states: Vec<
253 Vec<ExerciseSlideSubmissionAndUserExerciseState>,
254 > = Vec::new();
255
256 let exercises = models::exercises::get_exercises_by_exam_id(&mut conn, *exam_id).await?;
257
258 let mut conn = pool.acquire().await?;
259 for exercise in exercises.iter() {
260 let submissions = models::exercise_slide_submissions::get_latest_exercise_slide_submissions_and_user_exercise_state_list_with_exercise_id(
261 &mut conn,
262 exercise.id,
263 *pagination,
264 ).await?;
265 submissions_and_user_exercise_states.push(submissions)
266 }
267
268 token.authorized_ok(web::Json(submissions_and_user_exercise_states))
269}
270
271#[instrument(skip(pool))]
275async fn get_exercises_with_exam_id(
276 pool: web::Data<PgPool>,
277 exam_id: web::Path<Uuid>,
278 user: AuthUser,
279) -> ControllerResult<web::Json<Vec<Exercise>>> {
280 let mut conn = pool.acquire().await?;
281 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
282
283 let exercises = models::exercises::get_exercises_by_exam_id(&mut conn, *exam_id).await?;
284
285 token.authorized_ok(web::Json(exercises))
286}
287
288#[instrument(skip(pool))]
292async fn release_grades(
293 pool: web::Data<PgPool>,
294 exam_id: web::Path<Uuid>,
295 user: AuthUser,
296 payload: web::Json<Vec<Uuid>>,
297) -> ControllerResult<web::Json<()>> {
298 let mut conn = pool.acquire().await?;
299 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
300
301 let teacher_grading_decision_ids = payload.0;
302
303 let teacher_grading_decisions =
304 models::teacher_grading_decisions::get_by_ids(&mut conn, &teacher_grading_decision_ids)
305 .await?;
306
307 let user_exercise_state_mapping = models::user_exercise_states::get_by_ids(
308 &mut conn,
309 &teacher_grading_decisions
310 .iter()
311 .map(|x| x.user_exercise_state_id)
312 .collect::<Vec<Uuid>>(),
313 )
314 .await?
315 .into_iter()
316 .map(|x| (x.id, x))
317 .collect::<HashMap<Uuid, UserExerciseState>>();
318
319 let mut tx = conn.begin().await?;
320 for teacher_grading_decision in teacher_grading_decisions.iter() {
321 let user_exercise_state = user_exercise_state_mapping
322 .get(&teacher_grading_decision.user_exercise_state_id)
323 .ok_or_else(|| {
324 ControllerError::new(
325 ControllerErrorType::InternalServerError,
326 "User exercise state not found for a teacher grading decision",
327 None,
328 )
329 })?;
330
331 if user_exercise_state.exam_id != Some(*exam_id) {
332 return Err(ControllerError::new(
333 ControllerErrorType::BadRequest,
334 "Teacher grading decision does not belong to the specified exam.",
335 None,
336 ));
337 }
338
339 teacher_grading_decisions::update_teacher_grading_decision_hidden_field(
340 &mut tx,
341 teacher_grading_decision.id,
342 false,
343 )
344 .await?;
345 user_exercise_state_updater::update_user_exercise_state(&mut tx, user_exercise_state.id)
346 .await?;
347 }
348
349 tx.commit().await?;
350
351 token.authorized_ok(web::Json(()))
352}
353
354pub fn _add_routes(cfg: &mut ServiceConfig) {
362 cfg.route("/{id}", web::get().to(get_exam))
363 .route("/{id}/set", web::post().to(set_course))
364 .route("/{id}/unset", web::post().to(unset_course))
365 .route("/{id}/export-points", web::get().to(export_points))
366 .route(
367 "/{id}/export-submissions",
368 web::get().to(export_submissions),
369 )
370 .route("/{id}/edit-exam", web::post().to(edit_exam))
371 .route("/{id}/duplicate", web::post().to(duplicate_exam))
372 .route(
373 "/{exercise_id}/submissions-with-exercise-id",
374 web::get().to(get_exercise_slide_submissions_and_user_exercise_states_with_exercise_id),
375 )
376 .route(
377 "/{exam_id}/submissions-with-exam-id",
378 web::get().to(get_exercise_slide_submissions_and_user_exercise_states_with_exam_id),
379 )
380 .route("/{exam_id}/release-grades", web::post().to(release_grades))
381 .route(
382 "/{exam_id}/exam-exercises",
383 web::get().to(get_exercises_with_exam_id),
384 );
385}