api/routes/modules/assignments/tasks/post.rs
1use axum::{
2 extract::{State, Path},
3 http::StatusCode,
4 response::IntoResponse,
5 Json,
6};
7use chrono::Utc;
8use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter};
9use serde::Deserialize;
10use util::state::AppState;
11use crate::response::ApiResponse;
12use db::models::assignment_task::{ActiveModel, Column, Entity};
13use crate::routes::modules::assignments::tasks::common::TaskResponse;
14
15#[derive(Debug, Deserialize)]
16pub struct CreateTaskRequest {
17 task_number: i64,
18 name: String,
19 command: String,
20}
21
22/// POST /api/modules/{module_id}/assignments/{assignment_id}/tasks
23///
24/// Create a new task for a given assignment. Accessible to users with Lecturer or Admin roles
25/// assigned to the module.
26///
27/// Each task must have a unique `task_number` within the assignment. The `name` field defines a short,
28/// human-readable title for the task, while the `command` field defines how the task will be executed
29/// during evaluation (e.g., test commands, build commands).
30///
31/// ### Path Parameters
32/// - `module_id` (i64): The ID of the module containing the assignment
33/// - `assignment_id` (i64): The ID of the assignment to add the task to
34///
35/// ### Request Body
36/// ```json
37/// {
38/// "task_number": 1,
39/// "name": "Unit Tests",
40/// "command": "cargo test --lib"
41/// }
42/// ```
43///
44/// ### Request Body Fields
45/// - `task_number` (i64, required): Unique sequential number for the task within the assignment
46/// - `name` (string, required): Short descriptive name for the task (e.g., "Compile", "Unit Tests")
47/// - `command` (string, required): Command to execute for this task (e.g., test commands, build scripts)
48///
49/// ### Example Request
50/// ```bash
51/// curl -X POST http://localhost:3000/api/modules/1/assignments/2/tasks \
52/// -H "Authorization: Bearer <token>" \
53/// -H "Content-Type: application/json" \
54/// -d '{
55/// "task_number": 1,
56/// "name": "Unit Tests",
57/// "command": "cargo test --lib"
58/// }'
59/// ```
60///
61/// ### Success Response (201 Created)
62/// ```json
63/// {
64/// "success": true,
65/// "message": "Task created successfully",
66/// "data": {
67/// "id": 123,
68/// "task_number": 1,
69/// "name": "Unit Tests",
70/// "command": "cargo test --lib",
71/// "created_at": "2024-01-01T00:00:00Z",
72/// "updated_at": "2024-01-01T00:00:00Z"
73/// }
74/// }
75/// ```
76///
77/// ### Error Responses
78///
79/// **400 Bad Request** - Invalid JSON body
80/// ```json
81/// {
82/// "success": false,
83/// "message": "Invalid JSON body"
84/// }
85/// ```
86///
87/// **403 Forbidden** - Insufficient permissions
88/// ```json
89/// {
90/// "success": false,
91/// "message": "Access denied"
92/// }
93/// ```
94///
95/// **404 Not Found** - Assignment or module not found
96/// ```json
97/// {
98/// "success": false,
99/// "message": "Assignment or module not found"
100/// }
101/// ```
102///
103/// **422 Unprocessable Entity** - Validation errors
104/// ```json
105/// {
106/// "success": false,
107/// "message": "Invalid task_number, name, or command"
108/// }
109/// ```
110/// or
111/// ```json
112/// {
113/// "success": false,
114/// "message": "task_number must be unique"
115/// }
116/// ```
117///
118/// **500 Internal Server Error** - Database or server error
119/// ```json
120/// {
121/// "success": false,
122/// "message": "Failed to create task"
123/// }
124/// ```
125///
126/// ### Validation Rules
127/// - `task_number` must be greater than 0
128/// - `name` must not be empty or whitespace-only
129/// - `command` must not be empty or whitespace-only
130/// - `task_number` must be unique within the assignment
131/// - Assignment must exist and belong to the specified module
132///
133/// ### Notes
134/// - Tasks are executed in order of their `task_number` during assignment evaluation
135/// - The `command` field supports any shell command that can be executed in the evaluation environment
136/// - Task creation is restricted to users with appropriate module permissions
137pub async fn create_task(
138 State(app_state): State<AppState>,
139 Path((_, assignment_id)): Path<(i64, i64)>,
140 Json(payload): Json<CreateTaskRequest>,
141) -> impl IntoResponse {
142 let db = app_state.db();
143
144 // Validation: task_number > 0, name non-empty, command non-empty
145 if payload.task_number <= 0
146 || payload.name.trim().is_empty()
147 || payload.command.trim().is_empty()
148 {
149 return (
150 StatusCode::UNPROCESSABLE_ENTITY,
151 Json(ApiResponse::<()>::error("Invalid task_number, name, or command")),
152 )
153 .into_response();
154 }
155
156 // Ensure task_number uniqueness
157 let exists = Entity::find()
158 .filter(Column::AssignmentId.eq(assignment_id))
159 .filter(Column::TaskNumber.eq(payload.task_number))
160 .one(db)
161 .await;
162
163 if let Ok(Some(_)) = exists {
164 return (
165 StatusCode::UNPROCESSABLE_ENTITY,
166 Json(ApiResponse::<()>::error("task_number must be unique")),
167 )
168 .into_response();
169 }
170
171 let now = Utc::now();
172 let new_task = ActiveModel {
173 assignment_id: sea_orm::ActiveValue::Set(assignment_id),
174 task_number: sea_orm::ActiveValue::Set(payload.task_number),
175 name: sea_orm::ActiveValue::Set(payload.name.clone()),
176 command: sea_orm::ActiveValue::Set(payload.command.clone()),
177 created_at: sea_orm::ActiveValue::Set(now.clone()),
178 updated_at: sea_orm::ActiveValue::Set(now.clone()),
179 ..Default::default()
180 };
181
182 match new_task.insert(db).await {
183 Ok(task) => {
184 let response = TaskResponse {
185 id: task.id,
186 task_number: task.task_number,
187 name: task.name,
188 command: task.command,
189 created_at: task.created_at.to_rfc3339(),
190 updated_at: task.updated_at.to_rfc3339(),
191 };
192
193 (
194 StatusCode::CREATED,
195 Json(ApiResponse::success(response, "Task created successfully")),
196 )
197 .into_response()
198 }
199 Err(_) => (
200 StatusCode::INTERNAL_SERVER_ERROR,
201 Json(ApiResponse::<()>::error("Failed to create task")),
202 )
203 .into_response(),
204 }
205}