1use chrono::{DateTime, Duration, Utc};
2use headless_lms_models::{CourseOrExamId, ModelError, ModelErrorType, exercises::Exercise};
3use models::{
4 exams::{self, ExamEnrollment},
5 exercises,
6 pages::{self, Page},
7 teacher_grading_decisions::{self, TeacherGradingDecision},
8 user_exercise_states,
9};
10
11use crate::prelude::*;
12
13#[instrument(skip(pool))]
17pub async fn enrollment(
18 pool: web::Data<PgPool>,
19 exam_id: web::Path<Uuid>,
20 user: AuthUser,
21) -> ControllerResult<web::Json<Option<ExamEnrollment>>> {
22 let mut conn = pool.acquire().await?;
23 let enrollment = exams::get_enrollment(&mut conn, *exam_id, user.id).await?;
24 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
25 token.authorized_ok(web::Json(enrollment))
26}
27
28#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
29#[cfg_attr(feature = "ts_rs", derive(TS))]
30pub struct IsTeacherTesting {
31 pub is_teacher_testing: bool,
32}
33#[instrument(skip(pool))]
37pub async fn enroll(
38 pool: web::Data<PgPool>,
39 exam_id: web::Path<Uuid>,
40 user: AuthUser,
41 payload: web::Json<IsTeacherTesting>,
42) -> ControllerResult<web::Json<()>> {
43 let mut conn = pool.acquire().await?;
44 let exam = exams::get(&mut conn, *exam_id).await?;
45
46 if payload.is_teacher_testing {
48 exams::enroll(&mut conn, *exam_id, user.id, payload.is_teacher_testing).await?;
49 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Exam(*exam_id)).await?;
50 return token.authorized_ok(web::Json(()));
51 }
52
53 let now = Utc::now();
55 if exam.ended_at_or(now, false) {
56 return Err(ControllerError::new(
57 ControllerErrorType::Forbidden,
58 "Exam is over".to_string(),
59 None,
60 ));
61 }
62
63 if exam.started_at_or(now, false) {
64 let can_start =
67 models::library::progressing::user_can_take_exam(&mut conn, *exam_id, user.id).await?;
68 if !can_start {
69 return Err(ControllerError::new(
70 ControllerErrorType::Forbidden,
71 "User is not allowed to enroll to the exam.".to_string(),
72 None,
73 ));
74 }
75 exams::enroll(&mut conn, *exam_id, user.id, payload.is_teacher_testing).await?;
76 let token = skip_authorize();
77 return token.authorized_ok(web::Json(()));
78 }
79
80 Err(ControllerError::new(
82 ControllerErrorType::Forbidden,
83 "Exam has not started yet".to_string(),
84 None,
85 ))
86}
87
88#[derive(Debug, Serialize)]
89#[cfg_attr(feature = "ts_rs", derive(TS))]
90pub struct ExamData {
91 pub id: Uuid,
92 pub name: String,
93 pub instructions: serde_json::Value,
94 pub starts_at: DateTime<Utc>,
95 pub ends_at: DateTime<Utc>,
96 pub ended: bool,
97 pub time_minutes: i32,
98 pub enrollment_data: ExamEnrollmentData,
99 pub language: String,
100}
101
102#[derive(Debug, Serialize)]
103#[cfg_attr(feature = "ts_rs", derive(TS))]
104#[serde(tag = "tag")]
105pub enum ExamEnrollmentData {
106 EnrolledAndStarted {
108 page_id: Uuid,
109 page: Box<Page>,
110 enrollment: ExamEnrollment,
111 },
112 NotEnrolled { can_enroll: bool },
114 NotYetStarted,
116 StudentTimeUp,
118 StudentCanViewGrading {
120 gradings: Vec<(TeacherGradingDecision, Exercise)>,
121 enrollment: ExamEnrollment,
122 },
123}
124
125#[instrument(skip(pool))]
129pub async fn fetch_exam_for_user(
130 pool: web::Data<PgPool>,
131 exam_id: web::Path<Uuid>,
132 user: AuthUser,
133) -> ControllerResult<web::Json<ExamData>> {
134 let mut conn = pool.acquire().await?;
135 let exam = exams::get(&mut conn, *exam_id).await?;
136
137 let starts_at = if let Some(starts_at) = exam.starts_at {
138 starts_at
139 } else {
140 return Err(ControllerError::new(
141 ControllerErrorType::Forbidden,
142 "Cannot fetch exam that has no start time".to_string(),
143 None,
144 ));
145 };
146 let ends_at = if let Some(ends_at) = exam.ends_at {
147 ends_at
148 } else {
149 return Err(ControllerError::new(
150 ControllerErrorType::Forbidden,
151 "Cannot fetch exam that has no end time".to_string(),
152 None,
153 ));
154 };
155
156 let ended = ends_at < Utc::now();
157
158 if starts_at > Utc::now() {
159 let token = authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
161 return token.authorized_ok(web::Json(ExamData {
162 id: exam.id,
163 name: exam.name,
164 instructions: exam.instructions,
165 starts_at,
166 ends_at,
167 ended,
168 time_minutes: exam.time_minutes,
169 enrollment_data: ExamEnrollmentData::NotYetStarted,
170 language: exam.language,
171 }));
172 }
173
174 let enrollment = match exams::get_enrollment(&mut conn, *exam_id, user.id).await? {
175 Some(enrollment) => {
176 if exam.grade_manually {
177 let teachers_grading_decisions_list =
179 teacher_grading_decisions::get_all_latest_grading_decisions_by_user_id_and_exam_id(
180 &mut conn, user.id, *exam_id,
181 )
182 .await?;
183 let teacher_grading_decisions = teachers_grading_decisions_list.clone();
184
185 let exam_exercises =
186 exercises::get_exercises_by_exam_id(&mut conn, *exam_id).await?;
187
188 let user_exercise_states =
189 user_exercise_states::get_all_for_user_and_course_or_exam(
190 &mut conn,
191 user.id,
192 CourseOrExamId::Exam(*exam_id),
193 )
194 .await?;
195
196 let mut grading_decision_and_exercise_list: Vec<(
197 TeacherGradingDecision,
198 Exercise,
199 )> = Vec::new();
200
201 for grading_decision in teachers_grading_decisions_list.into_iter() {
203 if let Some(hidden) = grading_decision.hidden {
204 if !hidden {
205 for grading in teacher_grading_decisions.into_iter() {
207 let user_exercise_state = user_exercise_states
208 .iter()
209 .find(|state| state.id == grading.user_exercise_state_id)
210 .ok_or_else(|| {
211 ModelError::new(
212 ModelErrorType::Generic,
213 "User_exercise_state not found",
214 None,
215 )
216 })?;
217
218 let exercise = exam_exercises
219 .iter()
220 .find(|exercise| exercise.id == user_exercise_state.exercise_id)
221 .ok_or_else(|| {
222 ModelError::new(
223 ModelErrorType::Generic,
224 "Exercise not found",
225 None,
226 )
227 })?;
228
229 grading_decision_and_exercise_list
230 .push((grading, exercise.clone()));
231 }
232
233 let token =
234 authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id))
235 .await?;
236 return token.authorized_ok(web::Json(ExamData {
237 id: exam.id,
238 name: exam.name,
239 instructions: exam.instructions,
240 starts_at,
241 ends_at,
242 ended,
243 time_minutes: exam.time_minutes,
244 enrollment_data: ExamEnrollmentData::StudentCanViewGrading {
245 gradings: grading_decision_and_exercise_list,
246 enrollment,
247 },
248 language: exam.language,
249 }));
250 }
251 }
252 }
253 if enrollment.ended_at.is_some() {
255 let token: domain::authorization::AuthorizationToken =
256 authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
257 return token.authorized_ok(web::Json(ExamData {
258 id: exam.id,
259 name: exam.name,
260 instructions: exam.instructions,
261 starts_at,
262 ends_at,
263 ended,
264 time_minutes: exam.time_minutes,
265 enrollment_data: ExamEnrollmentData::StudentTimeUp,
266 language: exam.language,
267 }));
268 }
269 }
270
271 if Utc::now() < ends_at
273 && (Utc::now()
274 > enrollment.started_at + Duration::minutes(exam.time_minutes.into())
275 || enrollment.ended_at.is_some())
276 {
277 if enrollment.ended_at.is_none() {
279 exams::update_exam_ended_at(&mut conn, *exam_id, user.id, Utc::now()).await?;
280 }
281 let token: domain::authorization::AuthorizationToken =
282 authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
283 return token.authorized_ok(web::Json(ExamData {
284 id: exam.id,
285 name: exam.name,
286 instructions: exam.instructions,
287 starts_at,
288 ends_at,
289 ended,
290 time_minutes: exam.time_minutes,
291 enrollment_data: ExamEnrollmentData::StudentTimeUp,
292 language: exam.language,
293 }));
294 }
295 enrollment
296 }
297 _ => {
298 let token = authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
300 let can_enroll =
301 models::library::progressing::user_can_take_exam(&mut conn, *exam_id, user.id)
302 .await?;
303 return token.authorized_ok(web::Json(ExamData {
304 id: exam.id,
305 name: exam.name,
306 instructions: exam.instructions,
307 starts_at,
308 ends_at,
309 ended,
310 time_minutes: exam.time_minutes,
311 enrollment_data: ExamEnrollmentData::NotEnrolled { can_enroll },
312 language: exam.language,
313 }));
314 }
315 };
316
317 let page = pages::get_page(&mut conn, exam.page_id).await?;
318
319 let token = authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
320 token.authorized_ok(web::Json(ExamData {
321 id: exam.id,
322 name: exam.name,
323 instructions: exam.instructions,
324 starts_at,
325 ends_at,
326 ended,
327 time_minutes: exam.time_minutes,
328 enrollment_data: ExamEnrollmentData::EnrolledAndStarted {
329 page_id: exam.page_id,
330 page: Box::new(page),
331 enrollment,
332 },
333 language: exam.language,
334 }))
335}
336
337#[instrument(skip(pool))]
343pub async fn fetch_exam_for_testing(
344 pool: web::Data<PgPool>,
345 exam_id: web::Path<Uuid>,
346 user: AuthUser,
347) -> ControllerResult<web::Json<ExamData>> {
348 let mut conn = pool.acquire().await?;
349 let exam = exams::get(&mut conn, *exam_id).await?;
350
351 let starts_at = Utc::now();
352 let ends_at = if let Some(ends_at) = exam.ends_at {
353 ends_at
354 } else {
355 return Err(ControllerError::new(
356 ControllerErrorType::Forbidden,
357 "Cannot fetch exam that has no end time".to_string(),
358 None,
359 ));
360 };
361 let ended = ends_at < Utc::now();
362
363 let enrollment = match exams::get_enrollment(&mut conn, *exam_id, user.id).await? {
364 Some(enrollment) => enrollment,
365 _ => {
366 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Exam(*exam_id)).await?;
368 let can_enroll =
369 models::library::progressing::user_can_take_exam(&mut conn, *exam_id, user.id)
370 .await?;
371 return token.authorized_ok(web::Json(ExamData {
372 id: exam.id,
373 name: exam.name,
374 instructions: exam.instructions,
375 starts_at,
376 ends_at,
377 ended,
378 time_minutes: exam.time_minutes,
379 enrollment_data: ExamEnrollmentData::NotEnrolled { can_enroll },
380 language: exam.language,
381 }));
382 }
383 };
384
385 let page = pages::get_page(&mut conn, exam.page_id).await?;
386
387 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Exam(*exam_id)).await?;
388 token.authorized_ok(web::Json(ExamData {
389 id: exam.id,
390 name: exam.name,
391 instructions: exam.instructions,
392 starts_at,
393 ends_at,
394 ended,
395 time_minutes: exam.time_minutes,
396 enrollment_data: ExamEnrollmentData::EnrolledAndStarted {
397 page_id: exam.page_id,
398 page: Box::new(page),
399 enrollment,
400 },
401 language: exam.language,
402 }))
403}
404
405#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
406#[cfg_attr(feature = "ts_rs", derive(TS))]
407pub struct ShowExerciseAnswers {
408 pub show_exercise_answers: bool,
409}
410#[instrument(skip(pool))]
416pub async fn update_show_exercise_answers(
417 pool: web::Data<PgPool>,
418 exam_id: web::Path<Uuid>,
419 user: AuthUser,
420 payload: web::Json<ShowExerciseAnswers>,
421) -> ControllerResult<web::Json<()>> {
422 let mut conn = pool.acquire().await?;
423 let show_answers = payload.show_exercise_answers;
424 exams::update_show_exercise_answers(&mut conn, *exam_id, user.id, show_answers).await?;
425 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
426 token.authorized_ok(web::Json(()))
427}
428
429#[instrument(skip(pool))]
435pub async fn reset_exam_progress(
436 pool: web::Data<PgPool>,
437 exam_id: web::Path<Uuid>,
438 user: AuthUser,
439) -> ControllerResult<web::Json<()>> {
440 let mut conn = pool.acquire().await?;
441
442 let started_at = Utc::now();
443 exams::update_exam_start_time(&mut conn, *exam_id, user.id, started_at).await?;
444
445 models::exercise_slide_submissions::delete_exercise_submissions_with_exam_id_and_user_id(
446 &mut conn, *exam_id, user.id,
447 )
448 .await?;
449
450 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
451 token.authorized_ok(web::Json(()))
452}
453
454#[instrument(skip(pool))]
460pub async fn end_exam_time(
461 pool: web::Data<PgPool>,
462 exam_id: web::Path<Uuid>,
463 user: AuthUser,
464) -> ControllerResult<web::Json<()>> {
465 let mut conn = pool.acquire().await?;
466
467 let ended_at = Utc::now();
468 models::exams::update_exam_ended_at(&mut conn, *exam_id, user.id, ended_at).await?;
469
470 let token = authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
471 token.authorized_ok(web::Json(()))
472}
473
474pub fn _add_routes(cfg: &mut ServiceConfig) {
482 cfg.route("/{id}/enrollment", web::get().to(enrollment))
483 .route("/{id}/enroll", web::post().to(enroll))
484 .route("/{id}", web::get().to(fetch_exam_for_user))
485 .route(
486 "/testexam/{id}/fetch-exam-for-testing",
487 web::get().to(fetch_exam_for_testing),
488 )
489 .route(
490 "/testexam/{id}/update-show-exercise-answers",
491 web::post().to(update_show_exercise_answers),
492 )
493 .route(
494 "/testexam/{id}/reset-exam-progress",
495 web::post().to(reset_exam_progress),
496 )
497 .route("/{id}/end-exam-time", web::post().to(end_exam_time));
498}