api/routes/modules/assignments/plagiarism/patch.rs
1use axum::{
2 extract::{Path, State},
3 http::StatusCode,
4 response::IntoResponse,
5 Json,
6};
7use chrono::{DateTime, Utc};
8use db::models::plagiarism_case::{Entity as PlagiarismEntity, Status, Column as PlagiarismColumn};
9use sea_orm::{ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, ActiveModelTrait, Set};
10use serde::Serialize;
11use util::state::AppState;
12use crate::response::ApiResponse;
13
14#[derive(Debug, Serialize)]
15pub struct FlaggedCaseResponse {
16 id: i64,
17 status: String,
18 updated_at: DateTime<Utc>,
19}
20
21/// PATCH /api/modules/{module_id}/assignments/{assignment_id}/plagiarism/{case_id}/flag
22///
23/// Flags a plagiarism case after manual review, indicating confirmed plagiarism.
24/// Accessible only to lecturers and assistant lecturers assigned to the module.
25///
26/// # Path Parameters
27///
28/// - `module_id`: The ID of the parent module
29/// - `assignment_id`: The ID of the assignment containing the plagiarism case
30/// - `case_id`: The ID of the plagiarism case to flag
31///
32/// # Returns
33///
34/// Returns an HTTP response indicating the result:
35/// - `200 OK` with minimal case information on success
36/// - `403 FORBIDDEN` if user lacks required permissions
37/// - `404 NOT FOUND` if specified plagiarism case doesn't exist
38/// - `500 INTERNAL SERVER ERROR` for database errors or update failures
39///
40/// The response body includes only essential fields after the status change.
41///
42/// # Example Response (200 OK)
43///
44/// ```json
45/// {
46/// "success": true,
47/// "message": "Plagiarism case flagged",
48/// "data": {
49/// "id": 17,
50/// "status": "flagged",
51/// "updated_at": "2024-05-20T16:30:00Z"
52/// }
53/// }
54/// ```
55///
56/// # Example Responses
57///
58/// - `404 Not Found`
59/// ```json
60/// {
61/// "success": false,
62/// "message": "Plagiarism case not found"
63/// }
64/// ```
65///
66/// - `500 Internal Server Error`
67/// ```json
68/// {
69/// "success": false,
70/// "message": "Failed to update plagiarism case: [error details]"
71/// }
72/// ```
73///
74/// # Notes
75///
76/// - This operation updates the case status to "flagged" and sets the current timestamp to `updated_at`
77/// - Only users with lecturer or assistant lecturer roles assigned to the module can perform this action
78/// - Considered an irreversible action indicating confirmed plagiarism
79pub async fn patch_plagiarism_flag(
80 State(app_state): State<AppState>,
81 Path((_, assignment_id, case_id)): Path<(i64, i64, i64)>,
82) -> impl IntoResponse {
83 let case = match PlagiarismEntity::find()
84 .filter(PlagiarismColumn::Id.eq(case_id))
85 .filter(PlagiarismColumn::AssignmentId.eq(assignment_id))
86 .one(app_state.db())
87 .await
88 {
89 Ok(Some(case)) => case,
90 Ok(None) => {
91 return (
92 StatusCode::NOT_FOUND,
93 Json(ApiResponse::error("Plagiarism case not found")),
94 )
95 }
96 Err(e) => {
97 return (
98 StatusCode::INTERNAL_SERVER_ERROR,
99 Json(ApiResponse::error(format!("Database error: {}", e))),
100 )
101 }
102 };
103
104 let mut active_case = case.into_active_model();
105 active_case.status = Set(Status::Flagged);
106 active_case.updated_at = Set(Utc::now());
107
108 let updated_case = match active_case.update(app_state.db()).await {
109 Ok(case) => case,
110 Err(e) => {
111 return (
112 StatusCode::INTERNAL_SERVER_ERROR,
113 Json(ApiResponse::error(format!("Failed to update plagiarism case: {}", e))),
114 )
115 }
116 };
117
118 let response_data = FlaggedCaseResponse {
119 id: updated_case.id as i64,
120 status: updated_case.status.to_string(),
121 updated_at: updated_case.updated_at,
122 };
123
124 (
125 StatusCode::OK,
126 Json(ApiResponse::success(response_data, "Plagiarism case flagged")),
127 )
128}
129
130#[derive(Debug, Serialize)]
131pub struct ReviewedCaseResponse {
132 id: i64,
133 status: String,
134 updated_at: DateTime<Utc>,
135}
136
137/// PATCH /api/modules/{module_id}/assignments/{assignment_id}/plagiarism/{case_id}/review
138///
139/// Marks a plagiarism case as reviewed after manual inspection, indicating it's been cleared of plagiarism concerns.
140/// Accessible only to lecturers and assistant lecturers assigned to the module.
141///
142/// # Path Parameters
143///
144/// - `module_id`: The ID of the parent module
145/// - `assignment_id`: The ID of the assignment containing the plagiarism case
146/// - `case_id`: The ID of the plagiarism case to mark as reviewed
147///
148/// # Returns
149///
150/// Returns an HTTP response indicating the result:
151/// - `200 OK` with minimal case information on success
152/// - `403 FORBIDDEN` if user lacks required permissions
153/// - `404 NOT FOUND` if specified plagiarism case doesn't exist
154/// - `500 INTERNAL SERVER ERROR` for database errors or update failures
155///
156/// The response body includes only essential fields after the status change.
157///
158/// # Example Response (200 OK)
159///
160/// ```json
161/// {
162/// "success": true,
163/// "message": "Plagiarism case marked as reviewed",
164/// "data": {
165/// "id": 17,
166/// "status": "reviewed",
167/// "updated_at": "2024-05-20T17:45:00Z"
168/// }
169/// }
170/// ```
171///
172/// # Example Responses
173///
174/// - `404 Not Found`
175/// ```json
176/// {
177/// "success": false,
178/// "message": "Plagiarism case not found"
179/// }
180/// ```
181///
182/// - `500 Internal Server Error`
183/// ```json
184/// {
185/// "success": false,
186/// "message": "Failed to update plagiarism case: [error details]"
187/// }
188/// ```
189///
190/// # Notes
191///
192/// - This operation updates the case status to "reviewed" and sets the current timestamp to `updated_at`
193/// - Only users with lecturer or assistant lecturer roles assigned to the module can perform this action
194/// - Typically indicates the case was investigated and determined not to be plagiarism
195pub async fn patch_plagiarism_review(
196 State(app_state): State<AppState>,
197 Path((_, assignment_id, case_id)): Path<(i64, i64, i64)>,
198) -> impl IntoResponse {
199 let case = match PlagiarismEntity::find()
200 .filter(PlagiarismColumn::Id.eq(case_id))
201 .filter(PlagiarismColumn::AssignmentId.eq(assignment_id))
202 .one(app_state.db())
203 .await
204 {
205 Ok(Some(case)) => case,
206 Ok(None) => {
207 return (
208 StatusCode::NOT_FOUND,
209 Json(ApiResponse::error("Plagiarism case not found")),
210 )
211 }
212 Err(e) => {
213 return (
214 StatusCode::INTERNAL_SERVER_ERROR,
215 Json(ApiResponse::error(format!("Database error: {}", e))),
216 )
217 }
218 };
219
220 let mut active_case = case.into_active_model();
221 active_case.status = Set(Status::Reviewed);
222 active_case.updated_at = Set(Utc::now());
223
224 let updated_case = match active_case.update(app_state.db()).await {
225 Ok(case) => case,
226 Err(e) => {
227 return (
228 StatusCode::INTERNAL_SERVER_ERROR,
229 Json(ApiResponse::error(format!("Failed to update plagiarism case: {}", e))),
230 )
231 }
232 };
233
234 let response_data = ReviewedCaseResponse {
235 id: updated_case.id as i64,
236 status: updated_case.status.to_string(),
237 updated_at: updated_case.updated_at,
238 };
239
240 (
241 StatusCode::OK,
242 Json(ApiResponse::success(response_data, "Plagiarism case marked as reviewed")),
243 )
244}