headless_lms_server/controllers/main_frontend/
teacher_grading_decisions.rs1use crate::prelude::*;
2use headless_lms_models::{
3 teacher_grading_decisions::{NewTeacherGradingDecision, TeacherDecisionType},
4 user_exercise_states::UserExerciseState,
5};
6use utoipa::OpenApi;
7
8#[derive(OpenApi)]
9#[openapi(paths(create_teacher_grading_decision))]
10pub(crate) struct MainFrontendTeacherGradingDecisionsApiDoc;
11
12#[utoipa::path(
16 post,
17 path = "",
18 operation_id = "createTeacherGradingDecision",
19 tag = "teacher_grading_decisions",
20 request_body = NewTeacherGradingDecision,
21 responses(
22 (status = 200, description = "Teacher grading decision created", body = Option<UserExerciseState>)
23 )
24)]
25#[instrument(skip(pool))]
26async fn create_teacher_grading_decision(
27 payload: web::Json<NewTeacherGradingDecision>,
28 pool: web::Data<PgPool>,
29 user: AuthUser,
30) -> ControllerResult<web::Json<Option<UserExerciseState>>> {
31 let action = &payload.action;
32 let exercise_id = payload.exercise_id;
33 let user_exercise_state_id = payload.user_exercise_state_id;
34 let manual_points = payload.manual_points;
35 let justification = &payload.justification;
36 let hidden = payload.hidden;
37 let mut conn = pool.acquire().await?;
38
39 let student_state =
40 models::user_exercise_states::get_by_id(&mut conn, user_exercise_state_id).await?;
41 if student_state.exercise_id != exercise_id {
42 return Err(controller_err!(
43 Forbidden,
44 "User exercise state does not belong to the requested exercise".to_string()
45 ));
46 }
47 let exercise =
48 models::exercises::get_non_deleted_by_id(&mut conn, student_state.exercise_id).await?;
49 if exercise.course_id != student_state.course_id || exercise.exam_id != student_state.exam_id {
50 return Err(controller_err!(
51 Forbidden,
52 "User exercise state does not match the requested exercise context".to_string()
53 ));
54 }
55
56 let token = authorize(
57 &mut conn,
58 Act::Edit,
59 Some(user.id),
60 Res::Exercise(student_state.exercise_id),
61 )
62 .await?;
63 let points_given = match *action {
66 TeacherDecisionType::FullPoints => exercise.score_maximum as f32,
67 TeacherDecisionType::ZeroPoints
68 | TeacherDecisionType::SuspectedPlagiarism
69 | TeacherDecisionType::UnauthorizedAiUse
70 | TeacherDecisionType::RejectAndReset
71 | TeacherDecisionType::BadAnswer
72 | TeacherDecisionType::Other => 0.0,
73 TeacherDecisionType::CustomPoints => {
74 let points = manual_points.unwrap_or(0.0);
75 if points < 0.0 || points > exercise.score_maximum as f32 {
76 return Err(controller_err!(
77 BadRequest,
78 "manual_points must be between 0 and the exercise's maximum points".to_string()
79 ));
80 }
81 points
82 }
83 };
84
85 info!(
86 "Teacher took the following action: {:?}. Points given: {:?}.",
87 &action, points_given
88 );
89
90 if payload.reset_exercise || *action == TeacherDecisionType::RejectAndReset {
92 let course_id = student_state.course_id.ok_or_else(|| {
93 ControllerError::new(
94 ControllerErrorType::BadRequest,
95 "Resetting the exercise requires it to belong to a course".to_string(),
96 None,
97 )
98 })?;
99
100 let _reset = models::exercises::reset_progress_by_course_id_user_ids_and_exercise_ids(
101 &mut conn,
102 course_id,
103 &[student_state.user_id],
104 &[student_state.exercise_id],
105 Some(user.id),
106 Some("reset-by-staff".to_string()),
107 )
108 .await?;
109
110 let _res = models::teacher_grading_decisions::add_teacher_grading_decision(
113 &mut conn,
114 user_exercise_state_id,
115 *action,
116 points_given,
117 Some(user.id),
118 justification.clone(),
119 hidden,
120 )
121 .await?;
122
123 return token.authorized_ok(web::Json(None));
124 }
125
126 let _res = models::teacher_grading_decisions::upsert_by_state_id_and_exercise_id(
127 &mut conn,
128 user_exercise_state_id,
129 student_state.exercise_id,
130 *action,
131 points_given,
132 Some(user.id),
133 justification.clone(),
134 hidden,
135 )
136 .await?;
137
138 let new_user_exercise_state = models::user_exercise_states::recalculate_by_id_and_exercise_id(
139 &mut conn,
140 user_exercise_state_id,
141 student_state.exercise_id,
142 )
143 .await?;
144
145 if let Some(course_id) = new_user_exercise_state.course_id {
146 models::peer_review_queue_entries::remove_queue_entries_for_unusual_reason(
148 &mut conn,
149 new_user_exercise_state.user_id,
150 new_user_exercise_state.exercise_id,
151 course_id,
152 )
153 .await?;
154 }
155
156 token.authorized_ok(web::Json(Some(new_user_exercise_state)))
157}
158
159pub fn _add_routes(cfg: &mut ServiceConfig) {
160 cfg.route("", web::post().to(create_teacher_grading_decision));
161}