Skip to main content

headless_lms_server/controllers/main_frontend/
teacher_grading_decisions.rs

1use 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/**
13POST `/api/v0/main-frontend/teacher-grading-decisions` - Creates a new teacher grading decision, overriding the points a user has received from an exercise.
14*/
15#[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;
64    if *action == TeacherDecisionType::FullPoints {
65        points_given = exercise.score_maximum as f32;
66    } else if *action == TeacherDecisionType::ZeroPoints {
67        points_given = 0.0;
68    } else if *action == TeacherDecisionType::CustomPoints {
69        points_given = manual_points.unwrap_or(0.0);
70    } else if *action == TeacherDecisionType::SuspectedPlagiarism {
71        points_given = 0.0;
72    } else if *action == TeacherDecisionType::RejectAndReset {
73        points_given = 0.0;
74
75        models::teacher_grading_decisions::upsert_by_state_id_and_exercise_id(
76            &mut conn,
77            user_exercise_state_id,
78            student_state.exercise_id,
79            *action,
80            points_given,
81            Some(user.id),
82            justification.clone(),
83            hidden,
84        )
85        .await?;
86
87        let course_id = student_state.course_id.ok_or_else(|| {
88            ControllerError::new(
89                ControllerErrorType::BadRequest,
90                "RejectAndReset requires course_id".to_string(),
91                None,
92            )
93        })?;
94
95        let _reset = models::exercises::reset_progress_by_course_id_user_ids_and_exercise_ids(
96            &mut conn,
97            course_id,
98            &[student_state.user_id],
99            &[student_state.exercise_id],
100            Some(user.id),
101            Some("reset-by-staff".to_string()),
102        )
103        .await?;
104
105        info!("Teacher took the following action: RejectAndReset.",);
106
107        return token.authorized_ok(web::Json(None));
108    } else {
109        return Err(ControllerError::new(
110            ControllerErrorType::BadRequest,
111            "Invalid query".to_string(),
112            None,
113        ));
114    }
115
116    info!(
117        "Teacher took the following action: {:?}. Points given: {:?}.",
118        &action, points_given
119    );
120
121    let _res = models::teacher_grading_decisions::upsert_by_state_id_and_exercise_id(
122        &mut conn,
123        user_exercise_state_id,
124        student_state.exercise_id,
125        *action,
126        points_given,
127        Some(user.id),
128        justification.clone(),
129        hidden,
130    )
131    .await?;
132
133    let new_user_exercise_state = models::user_exercise_states::recalculate_by_id_and_exercise_id(
134        &mut conn,
135        user_exercise_state_id,
136        student_state.exercise_id,
137    )
138    .await?;
139
140    if let Some(course_id) = new_user_exercise_state.course_id {
141        // Since the teacher just reviewed the submission we should mark possible peer review queue entries so that they won't be given to others to review. Receiving peer reviews for this answer now would not make much sense.
142        models::peer_review_queue_entries::remove_queue_entries_for_unusual_reason(
143            &mut conn,
144            new_user_exercise_state.user_id,
145            new_user_exercise_state.exercise_id,
146            course_id,
147        )
148        .await?;
149    }
150
151    token.authorized_ok(web::Json(Some(new_user_exercise_state)))
152}
153
154pub fn _add_routes(cfg: &mut ServiceConfig) {
155    cfg.route("", web::post().to(create_teacher_grading_decision));
156}