api/routes/modules/assignments/plagiarism/delete.rs
1use axum::{
2 extract::{Path, State},
3 http::StatusCode,
4 response::IntoResponse,
5 Json,
6};
7use db::models::{plagiarism_case::{Entity as PlagiarismEntity, Column as PlagiarismColumn}};
8use sea_orm::{TransactionTrait, EntityTrait, QueryFilter, ColumnTrait};
9use serde::Deserialize;
10use util::state::AppState;
11use crate::response::ApiResponse;
12
13/// DELETE /api/modules/{module_id}/assignments/{assignment_id}/plagiarism/{case_id}
14///
15/// Permanently deletes a plagiarism case from the system.
16/// Accessible only to lecturers and assistant lecturers assigned to the module.
17///
18/// # Path Parameters
19///
20/// - `module_id`: The ID of the parent module
21/// - `assignment_id`: The ID of the assignment containing the plagiarism case
22/// - `case_id`: The ID of the plagiarism case to delete
23///
24/// # Returns
25///
26/// Returns an HTTP response indicating the result:
27/// - `200 OK` with success message when deletion is successful
28/// - `403 FORBIDDEN` if user lacks required permissions
29/// - `404 NOT FOUND` if specified plagiarism case doesn't exist
30/// - `500 INTERNAL SERVER ERROR` for database errors or deletion failures
31///
32/// The response body follows a standardized JSON format with a success message.
33///
34/// # Example Response (200 OK)
35///
36/// ```json
37/// {
38/// "success": true,
39/// "message": "Plagiarism case deleted successfully"
40/// }
41/// ```
42///
43/// # Example Responses
44///
45/// - `404 Not Found`
46/// ```json
47/// {
48/// "success": false,
49/// "message": "Plagiarism case not found"
50/// }
51/// ```
52///
53/// - `500 Internal Server Error`
54/// ```json
55/// {
56/// "success": false,
57/// "message": "Failed to delete plagiarism case: [error details]"
58/// }
59/// ```
60///
61/// # Notes
62///
63/// This operation is irreversible and permanently removes the plagiarism case record.
64/// Only users with lecturer or assistant lecturer roles assigned to the module can perform this action.
65pub async fn delete_plagiarism_case(
66 State(app_state): State<AppState>,
67 Path((_, _, case_id)): Path<(i64, i64, i64)>,
68) -> impl IntoResponse {
69 match PlagiarismEntity::delete_by_id(case_id)
70 .exec(app_state.db())
71 .await
72 {
73 Ok(result) => {
74 if result.rows_affected == 0 {
75 return (
76 StatusCode::NOT_FOUND,
77 Json(ApiResponse::<()>::error("Plagiarism case not found")),
78 );
79 }
80
81 (
82 StatusCode::OK,
83 Json(ApiResponse::success_without_data("Plagiarism case deleted successfully")),
84 )
85 }
86 Err(e) => (
87 StatusCode::INTERNAL_SERVER_ERROR,
88 Json(ApiResponse::<()>::error(format!(
89 "Failed to delete plagiarism case: {}",
90 e
91 ))),
92 ),
93 }
94}
95
96#[derive(Debug, Deserialize)]
97pub struct BulkDeletePayload {
98 case_ids: Vec<i64>,
99}
100
101/// DELETE /api/modules/{module_id}/assignments/{assignment_id}/plagiarism/bulk
102///
103/// Deletes multiple plagiarism cases in bulk for a specific assignment.
104/// Accessible only to lecturers and assistant lecturers assigned to the module.
105///
106/// # Path Parameters
107///
108/// - `module_id`: The ID of the parent module
109/// - `assignment_id`: The ID of the assignment containing the plagiarism cases
110///
111/// # Request Body
112///
113/// Requires a JSON payload with the following field:
114/// - `case_ids`: Array of plagiarism case IDs to delete (must not be empty)
115///
116/// # Returns
117///
118/// Returns an HTTP response indicating the result:
119/// - `200 OK` with success message and count of deleted cases
120/// - `400 BAD REQUEST` for invalid payload or missing cases
121/// - `403 FORBIDDEN` if user lacks required permissions
122/// - `500 INTERNAL SERVER ERROR` for database errors or deletion failures
123///
124/// The response body follows a standardized JSON format with a success message.
125///
126/// # Example Request
127///
128/// ```json
129/// {
130/// "case_ids": [12, 13, 17]
131/// }
132/// ```
133///
134/// # Example Response (200 OK)
135///
136/// ```json
137/// {
138/// "success": true,
139/// "message": "3 plagiarism cases deleted successfully"
140/// }
141/// ```
142///
143/// # Example Responses
144///
145/// - `400 Bad Request` (empty case_ids)
146/// ```json
147/// {
148/// "success": false,
149/// "message": "case_ids cannot be empty"
150/// }
151/// ```
152///
153/// - `400 Bad Request` (missing cases)
154/// ```json
155/// {
156/// "success": false,
157/// "message": "Some plagiarism cases not found or not in assignment: [17, 25]"
158/// }
159/// ```
160///
161/// - `500 Internal Server Error` (transaction failure)
162/// ```json
163/// {
164/// "success": false,
165/// "message": "Transaction commit failed: [error details]"
166/// }
167/// ```
168///
169/// # Notes
170///
171/// - This operation is atomic - either all cases are deleted or none
172/// - Returns an error if any specified case doesn't exist or belongs to a different assignment
173/// - Only users with lecturer or assistant lecturer roles assigned to the module can perform this action
174pub async fn bulk_delete_plagiarism_cases(
175 State(app_state): State<AppState>,
176 Path((_, assignment_id)): Path<(i64, i64)>,
177 Json(payload): Json<BulkDeletePayload>,
178) -> impl IntoResponse {
179 if payload.case_ids.is_empty() {
180 return (
181 StatusCode::BAD_REQUEST,
182 Json(ApiResponse::<()>::error("case_ids cannot be empty")),
183 );
184 }
185
186 let txn = match app_state.db().begin().await {
187 Ok(txn) => txn,
188 Err(e) => {
189 return (
190 StatusCode::INTERNAL_SERVER_ERROR,
191 Json(ApiResponse::<()>::error(format!(
192 "Failed to start transaction: {}",
193 e
194 ))),
195 )
196 }
197 };
198
199 let existing_cases = match PlagiarismEntity::find()
200 .filter(PlagiarismColumn::Id.is_in(payload.case_ids.clone()))
201 .filter(PlagiarismColumn::AssignmentId.eq(assignment_id))
202 .all(&txn)
203 .await
204 {
205 Ok(cases) => cases,
206 Err(e) => {
207 return (
208 StatusCode::INTERNAL_SERVER_ERROR,
209 Json(ApiResponse::<()>::error(format!(
210 "Database error: {}",
211 e
212 ))),
213 )
214 }
215 };
216
217 if existing_cases.len() != payload.case_ids.len() {
218 let existing_ids: Vec<i64> = existing_cases.iter().map(|c| c.id).collect();
219 let missing_ids: Vec<i64> = payload.case_ids
220 .iter()
221 .filter(|id| !existing_ids.contains(id))
222 .map(|&id| id as i64)
223 .collect();
224
225 return (
226 StatusCode::BAD_REQUEST,
227 Json(ApiResponse::<()>::error(format!(
228 "Some plagiarism cases not found or not in assignment: {:?}",
229 missing_ids
230 ))),
231 );
232 }
233
234 let delete_result = match PlagiarismEntity::delete_many()
235 .filter(PlagiarismColumn::Id.is_in(payload.case_ids))
236 .exec(&txn)
237 .await
238 {
239 Ok(result) => result,
240 Err(e) => {
241 return (
242 StatusCode::INTERNAL_SERVER_ERROR,
243 Json(ApiResponse::<()>::error(format!(
244 "Failed to delete plagiarism cases: {}",
245 e
246 ))),
247 )
248 }
249 };
250
251 if let Err(e) = txn.commit().await {
252 return (
253 StatusCode::INTERNAL_SERVER_ERROR,
254 Json(ApiResponse::<()>::error(format!(
255 "Transaction commit failed: {}",
256 e
257 ))),
258 );
259 }
260
261 (
262 StatusCode::OK,
263 Json(ApiResponse::success_without_data(&format!("{} plagiarism case{} deleted successfully", delete_result.rows_affected, if delete_result.rows_affected == 1 { "" } else { "s" }))),
264 )
265}