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}