headless_lms_server/controllers/main_frontend/
exams.rs

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/**
28GET `/api/v0/main-frontend/exams/:id
29*/
30#[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/**
51POST `/api/v0/main-frontend/exams/:id/set`
52*/
53#[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/**
69POST `/api/v0/main-frontend/exams/:id/unset`
70*/
71#[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/**
87GET `/api/v0/main-frontend/exams/:id/export-points`
88*/
89#[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/**
114GET `/api/v0/main-frontend/exams/:id/export-submissions`
115*/
116#[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/**
141 * POST `/api/v0/cms/exams/:exam_id/duplicate` - duplicates existing exam.
142 */
143#[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/**
176POST `/api/v0/main-frontend/organizations/{organization_id}/edit-exam` - edits an exam.
177*/
178#[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/**
199GET `/api/v0/main-frontend/exam/:exercise_id/submissions-with-exercise_id` - Returns all the exercise submissions and user exercise states with exercise_id.
200 */
201#[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/**
239GET `/api/v0/main-frontend/exam/:exam_id/submissions-with-exam-id` - Returns all the exercise submissions and user exercise states with exam_id.
240 */
241#[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/**
272GET `/api/v0/main-frontend/exam/:exam_id/exam-exercises` - Returns all the exercises with exam_id.
273 */
274#[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/**
289POST `/api/v0/main-frontend/exam/:exam_id/release-grades` - Publishes grading results of an exam by updating user_exercise_states according to teacher_grading_decisons and changes teacher_grading_decisions hidden field to false. Takes teacher grading decision ids as input.
290 */
291#[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
354/**
355Add a route for each controller in this module.
356
357The name starts with an underline in order to appear before other functions in the module documentation.
358
359We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
360*/
361pub 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}