use chrono::{DateTime, Duration, Utc};
use headless_lms_models::{
exercises::Exercise, user_exercise_states::CourseInstanceOrExamId, ModelError, ModelErrorType,
};
use models::{
exams::{self, ExamEnrollment},
exercises,
pages::{self, Page},
teacher_grading_decisions::{self, TeacherGradingDecision},
user_exercise_states,
};
use crate::prelude::*;
#[instrument(skip(pool))]
pub async fn enrollment(
pool: web::Data<PgPool>,
exam_id: web::Path<Uuid>,
user: AuthUser,
) -> ControllerResult<web::Json<Option<ExamEnrollment>>> {
let mut conn = pool.acquire().await?;
let enrollment = exams::get_enrollment(&mut conn, *exam_id, user.id).await?;
let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
token.authorized_ok(web::Json(enrollment))
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "ts_rs", derive(TS))]
pub struct IsTeacherTesting {
pub is_teacher_testing: bool,
}
#[instrument(skip(pool))]
pub async fn enroll(
pool: web::Data<PgPool>,
exam_id: web::Path<Uuid>,
user: AuthUser,
payload: web::Json<IsTeacherTesting>,
) -> ControllerResult<web::Json<()>> {
let mut conn = pool.acquire().await?;
let exam = exams::get(&mut conn, *exam_id).await?;
if payload.is_teacher_testing {
exams::enroll(&mut conn, *exam_id, user.id, payload.is_teacher_testing).await?;
let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Exam(*exam_id)).await?;
return token.authorized_ok(web::Json(()));
}
let now = Utc::now();
if exam.ended_at_or(now, false) {
return Err(ControllerError::new(
ControllerErrorType::Forbidden,
"Exam is over".to_string(),
None,
));
}
if exam.started_at_or(now, false) {
let can_start =
models::library::progressing::user_can_take_exam(&mut conn, *exam_id, user.id).await?;
if !can_start {
return Err(ControllerError::new(
ControllerErrorType::Forbidden,
"User is not allowed to enroll to the exam.".to_string(),
None,
));
}
exams::enroll(&mut conn, *exam_id, user.id, payload.is_teacher_testing).await?;
let token = skip_authorize();
return token.authorized_ok(web::Json(()));
}
Err(ControllerError::new(
ControllerErrorType::Forbidden,
"Exam has not started yet".to_string(),
None,
))
}
#[derive(Debug, Serialize)]
#[cfg_attr(feature = "ts_rs", derive(TS))]
pub struct ExamData {
pub id: Uuid,
pub name: String,
pub instructions: serde_json::Value,
pub starts_at: DateTime<Utc>,
pub ends_at: DateTime<Utc>,
pub ended: bool,
pub time_minutes: i32,
pub enrollment_data: ExamEnrollmentData,
pub language: String,
}
#[derive(Debug, Serialize)]
#[cfg_attr(feature = "ts_rs", derive(TS))]
#[serde(tag = "tag")]
pub enum ExamEnrollmentData {
EnrolledAndStarted {
page_id: Uuid,
page: Box<Page>,
enrollment: ExamEnrollment,
},
NotEnrolled { can_enroll: bool },
NotYetStarted,
StudentTimeUp,
StudentCanViewGrading {
gradings: Vec<(TeacherGradingDecision, Exercise)>,
enrollment: ExamEnrollment,
},
}
#[instrument(skip(pool))]
pub async fn fetch_exam_for_user(
pool: web::Data<PgPool>,
exam_id: web::Path<Uuid>,
user: AuthUser,
) -> ControllerResult<web::Json<ExamData>> {
let mut conn = pool.acquire().await?;
let exam = exams::get(&mut conn, *exam_id).await?;
let starts_at = if let Some(starts_at) = exam.starts_at {
starts_at
} else {
return Err(ControllerError::new(
ControllerErrorType::Forbidden,
"Cannot fetch exam that has no start time".to_string(),
None,
));
};
let ends_at = if let Some(ends_at) = exam.ends_at {
ends_at
} else {
return Err(ControllerError::new(
ControllerErrorType::Forbidden,
"Cannot fetch exam that has no end time".to_string(),
None,
));
};
let ended = ends_at < Utc::now();
if starts_at > Utc::now() {
let token = authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
return token.authorized_ok(web::Json(ExamData {
id: exam.id,
name: exam.name,
instructions: exam.instructions,
starts_at,
ends_at,
ended,
time_minutes: exam.time_minutes,
enrollment_data: ExamEnrollmentData::NotYetStarted,
language: exam.language,
}));
}
let enrollment = if let Some(enrollment) =
exams::get_enrollment(&mut conn, *exam_id, user.id).await?
{
if exam.grade_manually {
let teachers_grading_decisions_list =
teacher_grading_decisions::get_all_latest_grading_decisions_by_user_id_and_exam_id(
&mut conn, user.id, *exam_id,
)
.await?;
let teacher_grading_decisions = teachers_grading_decisions_list.clone();
let exam_exercises = exercises::get_exercises_by_exam_id(&mut conn, *exam_id).await?;
let user_exercise_states =
user_exercise_states::get_all_for_user_and_course_instance_or_exam(
&mut conn,
user.id,
CourseInstanceOrExamId::Exam(*exam_id),
)
.await?;
let mut grading_decision_and_exercise_list: Vec<(TeacherGradingDecision, Exercise)> =
Vec::new();
for grading_decision in teachers_grading_decisions_list.into_iter() {
if let Some(hidden) = grading_decision.hidden {
if !hidden {
for grading in teacher_grading_decisions.into_iter() {
let user_exercise_state = user_exercise_states
.iter()
.find(|state| state.id == grading.user_exercise_state_id)
.ok_or_else(|| {
ModelError::new(
ModelErrorType::Generic,
"User_exercise_state not found",
None,
)
})?;
let exercise = exam_exercises
.iter()
.find(|exercise| exercise.id == user_exercise_state.exercise_id)
.ok_or_else(|| {
ModelError::new(
ModelErrorType::Generic,
"Exercise not found",
None,
)
})?;
grading_decision_and_exercise_list.push((grading, exercise.clone()));
}
let token =
authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id))
.await?;
return token.authorized_ok(web::Json(ExamData {
id: exam.id,
name: exam.name,
instructions: exam.instructions,
starts_at,
ends_at,
ended,
time_minutes: exam.time_minutes,
enrollment_data: ExamEnrollmentData::StudentCanViewGrading {
gradings: grading_decision_and_exercise_list,
enrollment,
},
language: exam.language,
}));
}
}
}
if enrollment.ended_at.is_some() {
let token: domain::authorization::AuthorizationToken =
authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
return token.authorized_ok(web::Json(ExamData {
id: exam.id,
name: exam.name,
instructions: exam.instructions,
starts_at,
ends_at,
ended,
time_minutes: exam.time_minutes,
enrollment_data: ExamEnrollmentData::StudentTimeUp,
language: exam.language,
}));
}
}
if Utc::now() < ends_at
&& (Utc::now() > enrollment.started_at + Duration::minutes(exam.time_minutes.into())
|| enrollment.ended_at.is_some())
{
if enrollment.ended_at.is_none() {
exams::update_exam_ended_at(&mut conn, *exam_id, user.id, Utc::now()).await?;
}
let token: domain::authorization::AuthorizationToken =
authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
return token.authorized_ok(web::Json(ExamData {
id: exam.id,
name: exam.name,
instructions: exam.instructions,
starts_at,
ends_at,
ended,
time_minutes: exam.time_minutes,
enrollment_data: ExamEnrollmentData::StudentTimeUp,
language: exam.language,
}));
}
enrollment
} else {
let token = authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
let can_enroll =
models::library::progressing::user_can_take_exam(&mut conn, *exam_id, user.id).await?;
return token.authorized_ok(web::Json(ExamData {
id: exam.id,
name: exam.name,
instructions: exam.instructions,
starts_at,
ends_at,
ended,
time_minutes: exam.time_minutes,
enrollment_data: ExamEnrollmentData::NotEnrolled { can_enroll },
language: exam.language,
}));
};
let page = pages::get_page(&mut conn, exam.page_id).await?;
let token = authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
token.authorized_ok(web::Json(ExamData {
id: exam.id,
name: exam.name,
instructions: exam.instructions,
starts_at,
ends_at,
ended,
time_minutes: exam.time_minutes,
enrollment_data: ExamEnrollmentData::EnrolledAndStarted {
page_id: exam.page_id,
page: Box::new(page),
enrollment,
},
language: exam.language,
}))
}
#[instrument(skip(pool))]
pub async fn fetch_exam_for_testing(
pool: web::Data<PgPool>,
exam_id: web::Path<Uuid>,
user: AuthUser,
) -> ControllerResult<web::Json<ExamData>> {
let mut conn = pool.acquire().await?;
let exam = exams::get(&mut conn, *exam_id).await?;
let starts_at = Utc::now();
let ends_at = if let Some(ends_at) = exam.ends_at {
ends_at
} else {
return Err(ControllerError::new(
ControllerErrorType::Forbidden,
"Cannot fetch exam that has no end time".to_string(),
None,
));
};
let ended = ends_at < Utc::now();
let enrollment = if let Some(enrollment) =
exams::get_enrollment(&mut conn, *exam_id, user.id).await?
{
enrollment
} else {
let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Exam(*exam_id)).await?;
let can_enroll =
models::library::progressing::user_can_take_exam(&mut conn, *exam_id, user.id).await?;
return token.authorized_ok(web::Json(ExamData {
id: exam.id,
name: exam.name,
instructions: exam.instructions,
starts_at,
ends_at,
ended,
time_minutes: exam.time_minutes,
enrollment_data: ExamEnrollmentData::NotEnrolled { can_enroll },
language: exam.language,
}));
};
let page = pages::get_page(&mut conn, exam.page_id).await?;
let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Exam(*exam_id)).await?;
token.authorized_ok(web::Json(ExamData {
id: exam.id,
name: exam.name,
instructions: exam.instructions,
starts_at,
ends_at,
ended,
time_minutes: exam.time_minutes,
enrollment_data: ExamEnrollmentData::EnrolledAndStarted {
page_id: exam.page_id,
page: Box::new(page),
enrollment,
},
language: exam.language,
}))
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "ts_rs", derive(TS))]
pub struct ShowExerciseAnswers {
pub show_exercise_answers: bool,
}
#[instrument(skip(pool))]
pub async fn update_show_exercise_answers(
pool: web::Data<PgPool>,
exam_id: web::Path<Uuid>,
user: AuthUser,
payload: web::Json<ShowExerciseAnswers>,
) -> ControllerResult<web::Json<()>> {
let mut conn = pool.acquire().await?;
let show_answers = payload.show_exercise_answers;
exams::update_show_exercise_answers(&mut conn, *exam_id, user.id, show_answers).await?;
let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
token.authorized_ok(web::Json(()))
}
#[instrument(skip(pool))]
pub async fn reset_exam_progress(
pool: web::Data<PgPool>,
exam_id: web::Path<Uuid>,
user: AuthUser,
) -> ControllerResult<web::Json<()>> {
let mut conn = pool.acquire().await?;
let started_at = Utc::now();
exams::update_exam_start_time(&mut conn, *exam_id, user.id, started_at).await?;
models::exercise_slide_submissions::delete_exercise_submissions_with_exam_id_and_user_id(
&mut conn, *exam_id, user.id,
)
.await?;
let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
token.authorized_ok(web::Json(()))
}
#[instrument(skip(pool))]
pub async fn end_exam_time(
pool: web::Data<PgPool>,
exam_id: web::Path<Uuid>,
user: AuthUser,
) -> ControllerResult<web::Json<()>> {
let mut conn = pool.acquire().await?;
let ended_at = Utc::now();
models::exams::update_exam_ended_at(&mut conn, *exam_id, user.id, ended_at).await?;
let token = authorize(&mut conn, Act::View, Some(user.id), Res::Exam(*exam_id)).await?;
token.authorized_ok(web::Json(()))
}
pub fn _add_routes(cfg: &mut ServiceConfig) {
cfg.route("/{id}/enrollment", web::get().to(enrollment))
.route("/{id}/enroll", web::post().to(enroll))
.route("/{id}", web::get().to(fetch_exam_for_user))
.route(
"/testexam/{id}/fetch-exam-for-testing",
web::get().to(fetch_exam_for_testing),
)
.route(
"/testexam/{id}/update-show-exercise-answers",
web::post().to(update_show_exercise_answers),
)
.route(
"/testexam/{id}/reset-exam-progress",
web::post().to(reset_exam_progress),
)
.route("/{id}/end-exam-time", web::post().to(end_exam_time));
}