api/routes/modules/
put.rs1use axum::{
8 extract::{State, Path},
9 http::StatusCode,
10 response::IntoResponse,
11 Json,
12};
13use chrono::Utc;
14use util::state::AppState;
15use validator::Validate;
16use sea_orm::{
17 IntoActiveModel,
18 ActiveModelTrait,
19 ColumnTrait,
20 Condition,
21 EntityTrait,
22 QueryFilter,
23 Set,
24};
25use db::models::module::{
26 self,
27 ActiveModel as ModuleActiveModel,
28 Column as ModuleCol,
29 Entity as ModuleEntity,
30};
31use crate::response::ApiResponse;
32use crate::routes::modules::common::{ModuleRequest, ModuleResponse};
33use serde::{Deserialize, Serialize};
34use serde_json::Value;
35
36pub async fn edit_module(
112 State(state): State<AppState>,
113 Path(module_id): Path<i64>,
114 Json(req): Json<ModuleRequest>,
115) -> impl IntoResponse {
116 let db = state.db();
117
118 if let Err(validation_errors) = req.validate() {
119 let error_message = common::format_validation_errors(&validation_errors);
120 return (
121 StatusCode::BAD_REQUEST,
122 Json(ApiResponse::<ModuleResponse>::error(error_message)),
123 );
124 }
125
126 let duplicate = ModuleEntity::find()
127 .filter(
128 Condition::all()
129 .add(ModuleCol::Code.eq(req.code.clone()))
130 .add(ModuleCol::Id.ne(module_id)),
131 )
132 .one(db)
133 .await;
134
135 if let Ok(Some(_)) = duplicate {
136 return (
137 StatusCode::CONFLICT,
138 Json(ApiResponse::<ModuleResponse>::error("Module code already exists")),
139 );
140 }
141
142 let updated_module = ModuleActiveModel {
143 id: Set(module_id),
144 code: Set(req.code.clone()),
145 year: Set(req.year),
146 description: Set(req.description.clone()),
147 credits: Set(req.credits),
148 updated_at: Set(Utc::now()),
149 ..Default::default()
150 };
151
152 match updated_module.update(db).await {
153 Ok(module) => (
154 StatusCode::OK,
155 Json(ApiResponse::success(ModuleResponse::from(module), "Module updated successfully")),
156 ),
157 Err(_) => (
158 StatusCode::INTERNAL_SERVER_ERROR,
159 Json(ApiResponse::<ModuleResponse>::error("Failed to update module")),
160 ),
161 }
162}
163
164
165#[derive(Debug, Deserialize, Validate)]
166pub struct BulkUpdateRequest {
167 #[validate(length(min = 1, message = "At least one module ID is required"))]
168 pub module_ids: Vec<i64>,
169
170 #[validate(range(min = 2024, message = "Year must be at least 2024"))]
171 pub year: Option<i32>,
172
173 #[validate(length(max = 1000, message = "Description must be at most 1000 characters"))]
174 pub description: Option<String>,
175
176 #[validate(range(min = 1, message = "Credits must be positive"))]
177 pub credits: Option<i32>,
178}
179
180#[derive(Serialize)]
181pub struct BulkUpdateResult {
182 pub updated: usize,
183 pub failed: Vec<FailedUpdate>,
184}
185
186#[derive(Serialize)]
187pub struct FailedUpdate {
188 pub id: i64,
189 pub error: String,
190}
191
192pub async fn bulk_edit_modules(
237 State(app_state): State<AppState>,
238 Json(raw_value): Json<Value>,
239) -> impl IntoResponse {
240 let db = app_state.db();
241
242 if let Some(obj) = raw_value.as_object() {
244 if obj.keys().any(|k| k.to_lowercase() == "code") {
245 return (
246 StatusCode::BAD_REQUEST,
247 Json(ApiResponse::<BulkUpdateResult>::error("Bulk update cannot change module code")),
248 );
249 }
250 }
251
252 let req: BulkUpdateRequest = match serde_json::from_value(raw_value) {
254 Ok(req) => req,
255 Err(e) => {
256 return (
257 StatusCode::BAD_REQUEST,
258 Json(ApiResponse::<BulkUpdateResult>::error(format!("Invalid request body: {}", e))),
259 );
260 }
261 };
262
263 if let Err(validation_errors) = req.validate() {
265 let error_message = common::format_validation_errors(&validation_errors);
266 return (
267 StatusCode::BAD_REQUEST,
268 Json(ApiResponse::<BulkUpdateResult>::error(error_message)),
269 );
270 }
271
272 let mut updated = 0;
273 let mut failed = Vec::new();
274
275 for id in &req.module_ids {
276 let res = module::Entity::find()
277 .filter(module::Column::Id.eq(*id))
278 .one(db)
279 .await;
280
281 match res {
282 Ok(Some(model)) => {
283 let mut active = model.into_active_model();
284 let mut has_changes = false;
285
286 if let Some(year) = req.year {
288 active.year = Set(year);
289 has_changes = true;
290 }
291
292 if let Some(ref description) = req.description {
293 active.description = Set(Some(description.clone()));
294 has_changes = true;
295 }
296
297 if let Some(credits) = req.credits {
298 active.credits = Set(credits);
299 has_changes = true;
300 }
301
302 if has_changes {
303 active.updated_at = Set(Utc::now());
304
305 if active.update(db).await.is_ok() {
306 updated += 1;
307 } else {
308 failed.push(FailedUpdate {
309 id: *id,
310 error: "Failed to save updated module".into(),
311 });
312 }
313 } else {
314 updated += 1;
316 }
317 }
318 Ok(None) => failed.push(FailedUpdate {
319 id: *id,
320 error: "Module not found".into(),
321 }),
322 Err(e) => failed.push(FailedUpdate {
323 id: *id,
324 error: e.to_string(),
325 }),
326 }
327 }
328
329 let result = BulkUpdateResult { updated, failed };
330 let message = format!("Updated {}/{} modules", updated, req.module_ids.len());
331
332 (
333 StatusCode::OK,
334 Json(ApiResponse::success(result, message)),
335 )
336}