api/routes/modules/assignments/
put.rs1use axum::{extract::{State, Path}, http::StatusCode, response::IntoResponse, Json};
17use chrono::{DateTime, Utc};
18use util::state::AppState;
19use crate::response::ApiResponse;
20use db::models::assignment::{self, AssignmentType, Status};
21use sea_orm::{ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter, IntoActiveModel, DbErr};
22use super::common::{AssignmentRequest, AssignmentResponse, BulkUpdateRequest, BulkUpdateResult};
23
24pub async fn edit_assignment(
80 State(app_state): State<AppState>,
81 Path((module_id, assignment_id)): Path<(i64, i64)>,
82 Json(req): Json<AssignmentRequest>,
83) -> impl IntoResponse {
84 let db = app_state.db();
85
86 let available_from = match DateTime::parse_from_rfc3339(&req.available_from)
87 .map(|dt| dt.with_timezone(&Utc))
88 {
89 Ok(dt) => dt,
90 Err(_) => {
91 return (
92 StatusCode::BAD_REQUEST,
93 Json(ApiResponse::<AssignmentResponse>::error(
94 "Invalid available_from datetime format",
95 )),
96 );
97 }
98 };
99
100 let due_date = match DateTime::parse_from_rfc3339(&req.due_date)
101 .map(|dt| dt.with_timezone(&Utc))
102 {
103 Ok(dt) => dt,
104 Err(_) => {
105 return (
106 StatusCode::BAD_REQUEST,
107 Json(ApiResponse::<AssignmentResponse>::error(
108 "Invalid due_date datetime format",
109 )),
110 );
111 }
112 };
113
114 let assignment_type = match req.assignment_type.parse::<AssignmentType>() {
115 Ok(t) => t,
116 Err(_) => {
117 return (
118 StatusCode::BAD_REQUEST,
119 Json(ApiResponse::<AssignmentResponse>::error(
120 "assignment_type must be 'assignment' or 'practical'",
121 )),
122 );
123 }
124 };
125
126 match assignment::Model::edit(
127 db,
128 assignment_id,
129 module_id,
130 &req.name,
131 req.description.as_deref(),
132 assignment_type,
133 available_from,
134 due_date,
135 )
136 .await
137 {
138 Ok(updated) => {
139 let response = AssignmentResponse::from(updated);
140 (
141 StatusCode::OK,
142 Json(ApiResponse::success(
143 response,
144 "Assignment updated successfully",
145 )),
146 )
147 }
148 Err(DbErr::RecordNotFound(_)) => (
149 StatusCode::NOT_FOUND,
150 Json(ApiResponse::<AssignmentResponse>::error("Assignment not found")),
151 ),
152 Err(DbErr::Custom(msg)) => (
153 StatusCode::BAD_REQUEST,
154 Json(ApiResponse::<AssignmentResponse>::error(&msg)),
155 ),
156 Err(_) => (
157 StatusCode::INTERNAL_SERVER_ERROR,
158 Json(ApiResponse::<AssignmentResponse>::error(
159 "Failed to update assignment",
160 )),
161 ),
162 }
163}
164
165pub async fn bulk_update_assignments(
225 State(app_state): State<AppState>,
226 Path(module_id): Path<i64>,
227 Json(req): Json<BulkUpdateRequest>,
228) -> impl IntoResponse {
229 let db = app_state.db();
230
231 if req.assignment_ids.is_empty() {
232 return (
233 StatusCode::BAD_REQUEST,
234 Json(ApiResponse::error("No assignment IDs provided")),
235 );
236 }
237
238 let mut updated = 0;
239 let mut failed = Vec::new();
240
241 for id in &req.assignment_ids {
242 let res = assignment::Entity::find()
243 .filter(assignment::Column::Id.eq(*id))
244 .filter(assignment::Column::ModuleId.eq(module_id))
245 .one(db)
246 .await;
247
248 match res {
249 Ok(Some(model)) => {
250 let mut active = model.into_active_model();
251
252 if let Some(available_from) = &req.available_from {
253 if let Ok(dt) = DateTime::parse_from_rfc3339(available_from) {
254 active.available_from = Set(dt.with_timezone(&Utc));
255 }
256 }
257
258 if let Some(due_date) = &req.due_date {
259 if let Ok(dt) = DateTime::parse_from_rfc3339(due_date) {
260 active.due_date = Set(dt.with_timezone(&Utc));
261 }
262 }
263
264 active.updated_at = Set(Utc::now());
265
266 if active.update(db).await.is_ok() {
267 updated += 1;
268 } else {
269 failed.push(crate::routes::modules::assignments::common::FailedUpdate {
270 id: *id,
271 error: "Failed to save updated assignment".into(),
272 });
273 }
274 }
275 Ok(None) => failed.push(crate::routes::modules::assignments::common::FailedUpdate {
276 id: *id,
277 error: "Assignment not found".into(),
278 }),
279 Err(e) => failed.push(crate::routes::modules::assignments::common::FailedUpdate {
280 id: *id,
281 error: e.to_string(),
282 }),
283 }
284 }
285
286 let result = BulkUpdateResult { updated, failed };
287
288 let message = format!("Updated {}/{} assignments", updated, req.assignment_ids.len());
289
290 (
291 StatusCode::OK,
292 Json(ApiResponse::success(result, message)),
293 )
294}
295
296pub async fn open_assignment(
302 State(app_state): State<AppState>,
303 Path((module_id, assignment_id)): Path<(i64, i64)>,
304) -> impl IntoResponse {
305 let db = app_state.db();
306
307 let assignment = assignment::Entity::find()
308 .filter(assignment::Column::Id.eq(assignment_id))
309 .filter(assignment::Column::ModuleId.eq(module_id))
310 .one(db)
311 .await;
312
313 match assignment {
314 Ok(Some(model)) => {
315 if !matches!(
316 model.status,
317 Status::Ready | Status::Closed | Status::Archived
318 ) {
319 return (
320 StatusCode::BAD_REQUEST,
321 Json(ApiResponse::<()>::error(
322 "Assignment can only be opened if it is in Ready, Closed, or Archived state",
323 )),
324 );
325 }
326
327 let mut active = model.into_active_model();
328 active.status = Set(Status::Open);
329 active.updated_at = Set(Utc::now());
330
331 if active.update(db).await.is_ok() {
332 (
333 StatusCode::OK,
334 Json(ApiResponse::<()>::success(
335 (),
336 "Assignment successfully opened",
337 )),
338 )
339 } else {
340 (
341 StatusCode::INTERNAL_SERVER_ERROR,
342 Json(ApiResponse::<()>::error("Failed to update assignment")),
343 )
344 }
345 }
346 Ok(None) => (
347 StatusCode::NOT_FOUND,
348 Json(ApiResponse::<()>::error("Assignment not found")),
349 ),
350 Err(e) => (
351 StatusCode::INTERNAL_SERVER_ERROR,
352 Json(ApiResponse::<()>::error(&format!(
353 "Database error: {}",
354 e
355 ))),
356 ),
357 }
358}
359
360pub async fn close_assignment(
366 State(app_state): State<AppState>,
367 Path((module_id, assignment_id)): Path<(i64, i64)>,
368) -> impl IntoResponse {
369 let db = app_state.db();
370 let assignment = assignment::Entity::find()
371 .filter(assignment::Column::Id.eq(assignment_id))
372 .filter(assignment::Column::ModuleId.eq(module_id))
373 .one(db)
374 .await;
375
376 match assignment {
377 Ok(Some(model)) => {
378 if model.status != Status::Open {
379 return (
380 StatusCode::BAD_REQUEST,
381 Json(ApiResponse::<()>::error(
382 "Assignment can only be closed if it is in Open state",
383 )),
384 );
385 }
386
387 let mut active = model.into_active_model();
388 active.status = Set(Status::Closed);
389 active.updated_at = Set(Utc::now());
390
391 if active.update(db).await.is_ok() {
392 (
393 StatusCode::OK,
394 Json(ApiResponse::<()>::success(
395 (),
396 "Assignment successfully closed",
397 )),
398 )
399 } else {
400 (
401 StatusCode::INTERNAL_SERVER_ERROR,
402 Json(ApiResponse::<()>::error("Failed to update assignment")),
403 )
404 }
405 }
406 Ok(None) => (
407 StatusCode::NOT_FOUND,
408 Json(ApiResponse::<()>::error("Assignment not found")),
409 ),
410 Err(e) => (
411 StatusCode::INTERNAL_SERVER_ERROR,
412 Json(ApiResponse::<()>::error(&format!(
413 "Database error: {}",
414 e
415 ))),
416 ),
417 }
418}