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