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 && !hidden
205 {
206 for grading in teacher_grading_decisions.into_iter() {
208 let user_exercise_state = user_exercise_states
209 .iter()
210 .find(|state| state.id == grading.user_exercise_state_id)
211 .ok_or_else(|| {
212 ModelError::new(
213 ModelErrorType::Generic,
214 "User_exercise_state not found",
215 None,
216 )
217 })?;
218
219 let exercise = exam_exercises
220 .iter()
221 .find(|exercise| exercise.id == user_exercise_state.exercise_id)
222 .ok_or_else(|| {
223 ModelError::new(
224 ModelErrorType::Generic,
225 "Exercise not found",
226 None,
227 )
228 })?;
229
230 grading_decision_and_exercise_list.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 if enrollment.ended_at.is_some() {
254 let token: domain::authorization::AuthorizationToken =
255 authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
256 return token.authorized_ok(web::Json(ExamData {
257 id: exam.id,
258 name: exam.name,
259 instructions: exam.instructions,
260 starts_at,
261 ends_at,
262 ended,
263 time_minutes: exam.time_minutes,
264 enrollment_data: ExamEnrollmentData::StudentTimeUp,
265 language: exam.language,
266 }));
267 }
268 }
269
270 if Utc::now() < ends_at
272 && (Utc::now()
273 > enrollment.started_at + Duration::minutes(exam.time_minutes.into())
274 || enrollment.ended_at.is_some())
275 {
276 if enrollment.ended_at.is_none() {
278 exams::update_exam_ended_at(&mut conn, *exam_id, user.id, Utc::now()).await?;
279 }
280 let token: domain::authorization::AuthorizationToken =
281 authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
282 return token.authorized_ok(web::Json(ExamData {
283 id: exam.id,
284 name: exam.name,
285 instructions: exam.instructions,
286 starts_at,
287 ends_at,
288 ended,
289 time_minutes: exam.time_minutes,
290 enrollment_data: ExamEnrollmentData::StudentTimeUp,
291 language: exam.language,
292 }));
293 }
294 enrollment
295 }
296 _ => {
297 let token = authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
299 let can_enroll =
300 models::library::progressing::user_can_take_exam(&mut conn, *exam_id, user.id)
301 .await?;
302 return token.authorized_ok(web::Json(ExamData {
303 id: exam.id,
304 name: exam.name,
305 instructions: exam.instructions,
306 starts_at,
307 ends_at,
308 ended,
309 time_minutes: exam.time_minutes,
310 enrollment_data: ExamEnrollmentData::NotEnrolled { can_enroll },
311 language: exam.language,
312 }));
313 }
314 };
315
316 let page = pages::get_page(&mut conn, exam.page_id).await?;
317
318 let token = authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
319 token.authorized_ok(web::Json(ExamData {
320 id: exam.id,
321 name: exam.name,
322 instructions: exam.instructions,
323 starts_at,
324 ends_at,
325 ended,
326 time_minutes: exam.time_minutes,
327 enrollment_data: ExamEnrollmentData::EnrolledAndStarted {
328 page_id: exam.page_id,
329 page: Box::new(page),
330 enrollment,
331 },
332 language: exam.language,
333 }))
334}
335
336#[instrument(skip(pool))]
342pub async fn fetch_exam_for_testing(
343 pool: web::Data<PgPool>,
344 exam_id: web::Path<Uuid>,
345 user: AuthUser,
346) -> ControllerResult<web::Json<ExamData>> {
347 let mut conn = pool.acquire().await?;
348 let exam = exams::get(&mut conn, *exam_id).await?;
349
350 let starts_at = Utc::now();
351 let ends_at = if let Some(ends_at) = exam.ends_at {
352 ends_at
353 } else {
354 return Err(ControllerError::new(
355 ControllerErrorType::Forbidden,
356 "Cannot fetch exam that has no end time".to_string(),
357 None,
358 ));
359 };
360 let ended = ends_at < Utc::now();
361
362 let enrollment = match exams::get_enrollment(&mut conn, *exam_id, user.id).await? {
363 Some(enrollment) => enrollment,
364 _ => {
365 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Exam(*exam_id)).await?;
367 let can_enroll =
368 models::library::progressing::user_can_take_exam(&mut conn, *exam_id, user.id)
369 .await?;
370 return token.authorized_ok(web::Json(ExamData {
371 id: exam.id,
372 name: exam.name,
373 instructions: exam.instructions,
374 starts_at,
375 ends_at,
376 ended,
377 time_minutes: exam.time_minutes,
378 enrollment_data: ExamEnrollmentData::NotEnrolled { can_enroll },
379 language: exam.language,
380 }));
381 }
382 };
383
384 let page = pages::get_page(&mut conn, exam.page_id).await?;
385
386 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Exam(*exam_id)).await?;
387 token.authorized_ok(web::Json(ExamData {
388 id: exam.id,
389 name: exam.name,
390 instructions: exam.instructions,
391 starts_at,
392 ends_at,
393 ended,
394 time_minutes: exam.time_minutes,
395 enrollment_data: ExamEnrollmentData::EnrolledAndStarted {
396 page_id: exam.page_id,
397 page: Box::new(page),
398 enrollment,
399 },
400 language: exam.language,
401 }))
402}
403
404#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
405#[cfg_attr(feature = "ts_rs", derive(TS))]
406pub struct ShowExerciseAnswers {
407 pub show_exercise_answers: bool,
408}
409#[instrument(skip(pool))]
415pub async fn update_show_exercise_answers(
416 pool: web::Data<PgPool>,
417 exam_id: web::Path<Uuid>,
418 user: AuthUser,
419 payload: web::Json<ShowExerciseAnswers>,
420) -> ControllerResult<web::Json<()>> {
421 let mut conn = pool.acquire().await?;
422 let show_answers = payload.show_exercise_answers;
423 exams::update_show_exercise_answers(&mut conn, *exam_id, user.id, show_answers).await?;
424 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
425 token.authorized_ok(web::Json(()))
426}
427
428#[instrument(skip(pool))]
434pub async fn reset_exam_progress(
435 pool: web::Data<PgPool>,
436 exam_id: web::Path<Uuid>,
437 user: AuthUser,
438) -> ControllerResult<web::Json<()>> {
439 let mut conn = pool.acquire().await?;
440
441 let started_at = Utc::now();
442 exams::update_exam_start_time(&mut conn, *exam_id, user.id, started_at).await?;
443
444 models::exercise_slide_submissions::delete_exercise_submissions_with_exam_id_and_user_id(
445 &mut conn, *exam_id, user.id,
446 )
447 .await?;
448
449 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
450 token.authorized_ok(web::Json(()))
451}
452
453#[instrument(skip(pool))]
459pub async fn end_exam_time(
460 pool: web::Data<PgPool>,
461 exam_id: web::Path<Uuid>,
462 user: AuthUser,
463) -> ControllerResult<web::Json<()>> {
464 let mut conn = pool.acquire().await?;
465
466 let ended_at = Utc::now();
467 models::exams::update_exam_ended_at(&mut conn, *exam_id, user.id, ended_at).await?;
468
469 let token = authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
470 token.authorized_ok(web::Json(()))
471}
472
473pub fn _add_routes(cfg: &mut ServiceConfig) {
481 cfg.route("/{id}/enrollment", web::get().to(enrollment))
482 .route("/{id}/enroll", web::post().to(enroll))
483 .route("/{id}", web::get().to(fetch_exam_for_user))
484 .route(
485 "/testexam/{id}/fetch-exam-for-testing",
486 web::get().to(fetch_exam_for_testing),
487 )
488 .route(
489 "/testexam/{id}/update-show-exercise-answers",
490 web::post().to(update_show_exercise_answers),
491 )
492 .route(
493 "/testexam/{id}/reset-exam-progress",
494 web::post().to(reset_exam_progress),
495 )
496 .route("/{id}/end-exam-time", web::post().to(end_exam_time));
497}