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}