api/routes/modules/assignments/mark_allocator/
put.rs1use axum::{extract::Path, http::StatusCode, response::IntoResponse, Json};
2use crate::response::ApiResponse;
3use serde_json::Value;
4use util::mark_allocator::mark_allocator::{save_allocator, SaveError};
5
6pub async fn save(
76 Path((module_id, assignment_id)): Path<(i64, i64)>,
77 Json(req): Json<Value>,
78) -> impl IntoResponse {
79 let bad = |msg: &str| {
80 (
81 StatusCode::BAD_REQUEST,
82 Json(ApiResponse::<()>::error(msg)),
83 )
84 .into_response()
85 };
86
87 let root = match req.as_object() {
89 Some(o) => o,
90 None => return bad("Body must be a JSON object"),
91 };
92
93 let tasks = match root.get("tasks").and_then(|t| t.as_array()) {
94 Some(a) if !a.is_empty() => a,
95 _ => return bad("\"tasks\" must be a non-empty array"),
96 };
97
98 let total_value = match root.get("total_value").and_then(|v| v.as_i64()) {
99 Some(v) if v >= 0 => v,
100 _ => return bad("\"total_value\" must be an integer >= 0"),
101 };
102
103 let mut sum_task_values: i64 = 0;
104
105 for (idx, entry) in tasks.iter().enumerate() {
106 let entry_obj = match entry.as_object() {
108 Some(m) if m.len() == 1 => m,
109 Some(_) => {
110 return bad(&format!(
111 "tasks[{}] must be an object with exactly one key (e.g., \"task1\")",
112 idx
113 ))
114 }
115 None => return bad(&format!("tasks[{}] must be an object", idx)),
116 };
117
118 let (task_key, task_val) = entry_obj.iter().next().unwrap();
119
120 if !task_key.starts_with("task") {
122 return bad(&format!(
123 "tasks[{}] invalid key '{}': expected key like \"task1\"",
124 idx, task_key
125 ));
126 }
127 let key_num_part = &task_key[4..];
128 let key_task_num: i64 = match key_num_part.parse::<i64>() {
129 Ok(n) if n > 0 => n,
130 _ => {
131 return bad(&format!(
132 "tasks[{}] invalid key '{}': expected positive integer after 'task'",
133 idx, task_key
134 ))
135 }
136 };
137
138 let task_obj = match task_val.as_object() {
140 Some(o) => o,
141 None => {
142 return bad(&format!(
143 "tasks[{}].{} value must be an object",
144 idx, task_key
145 ))
146 }
147 };
148
149 let name = match task_obj.get("name").and_then(|v| v.as_str()) {
151 Some(s) if !s.trim().is_empty() => s,
152 _ => {
153 return bad(&format!(
154 "tasks[{}].{}.name must be a non-empty string",
155 idx, task_key
156 ))
157 }
158 };
159
160 let task_number = match task_obj.get("task_number").and_then(|v| v.as_i64()) {
162 Some(n) if n > 0 => n,
163 _ => {
164 return bad(&format!(
165 "tasks[{}].{}.task_number must be a positive integer",
166 idx, task_key
167 ))
168 }
169 };
170 if task_number != key_task_num {
171 return bad(&format!(
172 "tasks[{}]: key '{}' does not match inner task_number {}",
173 idx, task_key, task_number
174 ));
175 }
176
177 let task_value = match task_obj.get("value").and_then(|v| v.as_i64()) {
179 Some(v) if v >= 0 => v,
180 _ => {
181 return bad(&format!(
182 "tasks[{}].{}.value must be an integer >= 0",
183 idx, task_key
184 ))
185 }
186 };
187
188 let subsections = match task_obj.get("subsections").and_then(|v| v.as_array()) {
190 Some(a) => a,
191 None => {
192 return bad(&format!(
193 "tasks[{}].{}.subsections must be an array (can be empty)",
194 idx, task_key
195 ))
196 }
197 };
198
199 let mut sum_sub_values: i64 = 0;
201 for (sidx, s) in subsections.iter().enumerate() {
202 let s_obj = match s.as_object() {
203 Some(o) => o,
204 None => {
205 return bad(&format!(
206 "tasks[{}].{}.subsections[{}] must be an object",
207 idx, task_key, sidx
208 ))
209 }
210 };
211
212 match s_obj.get("name").and_then(|v| v.as_str()) {
213 Some(n) if !n.trim().is_empty() => {}
214 _ => {
215 return bad(&format!(
216 "tasks[{}].{}.subsections[{}].name must be a non-empty string",
217 idx, task_key, sidx
218 ))
219 }
220 }
221
222 let sub_value = match s_obj.get("value").and_then(|v| v.as_i64()) {
223 Some(v) if v >= 0 => v,
224 _ => {
225 return bad(&format!(
226 "tasks[{}].{}.subsections[{}].value must be an integer >= 0",
227 idx, task_key, sidx
228 ))
229 }
230 };
231
232 sum_sub_values += sub_value;
233 }
234
235 if sum_sub_values != task_value {
236 return bad(&format!(
237 "tasks[{}] ('{}' / {}): sum of subsection values ({}) must equal task value ({})",
238 idx, name, task_key, sum_sub_values, task_value
239 ));
240 }
241
242 sum_task_values += task_value;
243 }
244
245 if sum_task_values != total_value {
246 return bad(&format!(
247 "sum of task values must equal total_value ({}), got {}",
248 total_value, sum_task_values
249 ));
250 }
251
252 match save_allocator(module_id, assignment_id, req).await {
253 Ok(_) => (
254 StatusCode::OK,
255 Json(ApiResponse::success("{}", "Mark allocator successfully saved.")),
256 )
257 .into_response(),
258 Err(SaveError::DirectoryNotFound) => (
259 StatusCode::NOT_FOUND,
260 Json::<ApiResponse<()>>(ApiResponse::error(
261 "Module or assignment directory does not exist",
262 )),
263 )
264 .into_response(),
265 Err(SaveError::JsonError(_)) => (
266 StatusCode::BAD_REQUEST,
267 Json::<ApiResponse<()>>(ApiResponse::error("Invalid JSON")),
268 )
269 .into_response(),
270 Err(_) => (
271 StatusCode::INTERNAL_SERVER_ERROR,
272 Json::<ApiResponse<()>>(ApiResponse::error("Could not save file")),
273 )
274 .into_response(),
275 }
276}