api/routes/modules/assignments/tasks/put.rs
1//! Task Edit Endpoint
2//!
3//! This module provides the endpoint handler for editing the command of a specific assignment task within a module. It validates the existence and relationships of the module, assignment, and task, and updates the task's command in the database. The endpoint returns detailed information about the updated task or appropriate error responses.
4
5use axum::{extract::{State, Path, Json}, http::StatusCode, response::IntoResponse};
6use db::models::{assignment_task};
7use serde::Deserialize;
8use util::state::AppState;
9use crate::response::ApiResponse;
10use crate::routes::modules::assignments::tasks::common::TaskResponse;
11
12/// The request payload for editing a task's command.
13#[derive(Deserialize)]
14pub struct EditTaskRequest {
15 /// The new command string for the task. Must be non-empty.
16 command: String,
17 /// The new name for the task. Must be non-empty.
18 name: String,
19}
20
21/// PUT /api/modules/{module_id}/assignments/{assignment_id}/tasks/{task_id}
22///
23/// Edit the command of a specific task within an assignment. Accessible to users with Lecturer or Admin roles
24/// assigned to the module.
25///
26/// This endpoint allows updating the command that will be executed during task evaluation. The command
27/// can be any shell command that can be executed in the evaluation environment.
28///
29/// ### Path Parameters
30/// - `module_id` (i64): The ID of the module containing the assignment
31/// - `assignment_id` (i64): The ID of the assignment containing the task
32/// - `task_id` (i64): The ID of the task to edit
33///
34/// ### Request Body
35/// ```json
36/// {
37/// "command": "cargo test --lib --release"
38/// }
39/// ```
40///
41/// ### Request Body Fields
42/// - `command` (string, required): The new command to execute for this task (e.g., test commands, build scripts)
43///
44/// ### Example Request
45/// ```bash
46/// curl -X PUT http://localhost:3000/api/modules/1/assignments/2/tasks/3 \
47/// -H "Authorization: Bearer <token>" \
48/// -H "Content-Type: application/json" \
49/// -d '{
50/// "command": "cargo test --lib --release"
51/// }'
52/// ```
53///
54/// ### Success Response (200 OK)
55/// ```json
56/// {
57/// "success": true,
58/// "message": "Task updated successfully",
59/// "data": {
60/// "id": 3,
61/// "task_number": 1,
62/// "command": "cargo test --lib --release",
63/// "created_at": "2024-01-01T00:00:00Z",
64/// "updated_at": "2024-01-01T12:30:00Z"
65/// }
66/// }
67/// ```
68///
69/// ### Error Responses
70///
71/// **400 Bad Request** - Invalid JSON body
72/// ```json
73/// {
74/// "success": false,
75/// "message": "Invalid JSON body"
76/// }
77/// ```
78///
79/// **403 Forbidden** - Insufficient permissions
80/// ```json
81/// {
82/// "success": false,
83/// "message": "Access denied"
84/// }
85/// ```
86///
87/// **404 Not Found** - Resource not found
88/// ```json
89/// {
90/// "success": false,
91/// "message": "Module not found"
92/// }
93/// ```
94/// or
95/// ```json
96/// {
97/// "success": false,
98/// "message": "Assignment not found"
99/// }
100/// ```
101/// or
102/// ```json
103/// {
104/// "success": false,
105/// "message": "Task not found"
106/// }
107/// ```
108/// or
109/// ```json
110/// {
111/// "success": false,
112/// "message": "Assignment does not belong to this module"
113/// }
114/// ```
115/// or
116/// ```json
117/// {
118/// "success": false,
119/// "message": "Task does not belong to this assignment"
120/// }
121/// ```
122///
123/// **422 Unprocessable Entity** - Validation error
124/// ```json
125/// {
126/// "success": false,
127/// "message": "'command' must be a non-empty string"
128/// }
129/// ```
130///
131/// **500 Internal Server Error** - Database or server error
132/// ```json
133/// {
134/// "success": false,
135/// "message": "Database error retrieving module"
136/// }
137/// ```
138/// or
139/// ```json
140/// {
141/// "success": false,
142/// "message": "Failed to update task"
143/// }
144/// ```
145///
146/// ### Validation Rules
147/// - `command` must not be empty or whitespace-only
148/// - Module must exist
149/// - Assignment must exist and belong to the specified module
150/// - Task must exist and belong to the specified assignment
151///
152/// ### Notes
153/// - Only the command field can be updated; task_number and other fields remain unchanged
154/// - The updated task will be used in future assignment evaluations
155/// - Task editing is restricted to users with appropriate module permissions
156/// - The `updated_at` timestamp is automatically set when the task is modified
157pub async fn edit_task(
158 State(app_state): State<AppState>,
159 Path((_, _, task_id)): Path<(i64, i64, i64)>,
160 Json(payload): Json<EditTaskRequest>,
161) -> impl IntoResponse {
162 let db = app_state.db();
163
164 if payload.command.trim().is_empty() || payload.name.trim().is_empty() {
165 return (
166 StatusCode::UNPROCESSABLE_ENTITY,
167 Json(ApiResponse::<()>::error("'name' and 'command' must be non-empty strings")),
168 ).into_response();
169 }
170
171 let updated = match assignment_task::Model::edit_command_and_name(db, task_id, &payload.name, &payload.command).await {
172 Ok(t) => t,
173 Err(_) => {
174 return (
175 StatusCode::INTERNAL_SERVER_ERROR,
176 Json(ApiResponse::<()>::error("Failed to update task")),
177 ).into_response();
178 }
179 };
180
181 let resp = TaskResponse {
182 id: updated.id,
183 task_number: updated.task_number,
184 name: updated.name,
185 command: updated.command,
186 created_at: updated.created_at.to_rfc3339(),
187 updated_at: updated.updated_at.to_rfc3339(),
188 };
189
190 (
191 StatusCode::OK,
192 Json(ApiResponse::success(resp, "Task updated successfully")),
193 ).into_response()
194}