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}