api/routes/modules/assignments/config/
post.rs

1use db::models::assignment_file::{FileType, Model as AssignmentFile};
2use axum::{
3    extract::{State, Json, Path},
4    http::StatusCode,
5    response::IntoResponse,
6};
7use sea_orm::{EntityTrait, ColumnTrait, QueryFilter};
8use serde_json::Value;
9use crate::response::ApiResponse;
10use db::models::assignment::{Column as AssignmentColumn, Entity as AssignmentEntity};
11use util::{execution_config::ExecutionConfig, state::AppState};
12
13
14/// POST /api/modules/{module_id}/assignments/{assignment_id}/config
15///
16/// Save or replace the JSON execution configuration object for a specific assignment.
17///
18/// Accessible to users with Lecturer or Admin roles assigned to the module. The config is persisted
19/// to disk as a JSON file under the module's assignment directory. This currently uses the
20/// [`ExecutionConfig`] structure, which will be expanded in the future.
21///
22/// ### Path Parameters
23/// - `module_id` (i64): The ID of the module
24/// - `assignment_id` (i64): The ID of the assignment
25///
26/// ### Request Body
27/// A JSON object matching the shape of `ExecutionConfig`:
28/// ```json
29/// {
30///   "execution": {
31///     "timeout_secs": 10,
32///     "max_cpus": 2,
33///     "max_processes": 256
34///   },
35///   "marking": {
36///     "marking_scheme": "exact",
37///     "feedback_scheme": "auto",
38///     "deliminator": "&-=-&"
39///   }
40/// }
41/// ```
42///
43/// ### Success Response (200 OK)
44/// ```json
45/// {
46///   "success": true,
47///   "message": "Assignment configuration saved",
48///   "data": null
49/// }
50/// ```
51///
52/// ### Error Responses
53/// - **400** – Invalid JSON structure
54/// - **404** – Assignment not found
55/// - **500** – Internal error saving the file
56///
57/// ### Notes
58/// - Configuration is saved to disk under `ASSIGNMENT_STORAGE_ROOT/module_{id}/assignment_{id}/config/config.json`.
59/// - Only valid `ExecutionConfig` objects are accepted.
60pub async fn set_assignment_config(
61    State(app_state): State<AppState>,
62    Path((module_id, assignment_id)): Path<(i64, i64)>,
63    Json(config_json): Json<Value>,
64) -> impl IntoResponse {
65    let db = app_state.db();
66
67    if !config_json.is_object() {
68        return (
69            StatusCode::BAD_REQUEST,
70            Json(ApiResponse::<()>::error("Configuration must be a JSON object")),
71        );
72    }
73
74    let config: ExecutionConfig = match serde_json::from_value(config_json) {
75        Ok(cfg) => cfg,
76        Err(e) => {
77            return (
78                StatusCode::BAD_REQUEST,
79                Json(ApiResponse::<()>::error(format!("Invalid config format: {}", e))),
80            );
81        }
82    };
83
84    // Check assignment existence
85    let assignment = match AssignmentEntity::find()
86        .filter(AssignmentColumn::Id.eq(assignment_id as i32))
87        .filter(AssignmentColumn::ModuleId.eq(module_id as i32))
88        .one(db)
89        .await
90    {
91        Ok(Some(a)) => a,
92        Ok(None) => {
93            return (
94                StatusCode::NOT_FOUND,
95                Json(ApiResponse::<()>::error("Assignment or module not found")),
96            );
97        }
98        Err(e) => {
99            eprintln!("DB error: {:?}", e);
100            return (
101                StatusCode::INTERNAL_SERVER_ERROR,
102                Json(ApiResponse::<()>::error("Database error")),
103            );
104        }
105    };
106
107    // Save as assignment file using `save_file`
108    let bytes = match serde_json::to_vec_pretty(&config) {
109        Ok(b) => b,
110        Err(e) => {
111            eprintln!("Serialization error: {:?}", e);
112            return (
113                StatusCode::INTERNAL_SERVER_ERROR,
114                Json(ApiResponse::<()>::error("Failed to serialize config")),
115            );
116        }
117    };
118
119    match AssignmentFile::save_file(
120        &db,
121        assignment.id.into(),
122        module_id,
123        FileType::Config,
124        "config.json",
125        &bytes,
126    )
127    .await
128    {
129        Ok(_) => (
130            StatusCode::OK,
131            Json(ApiResponse::success((), "Assignment configuration saved")),
132        ),
133        Err(e) => {
134            eprintln!("File save error: {:?}", e);
135            (
136                StatusCode::INTERNAL_SERVER_ERROR,
137                Json(ApiResponse::<()>::error("Failed to save config as assignment file")),
138            )
139        }
140    }
141}
142
143
144/// POST /api/modules/{module_id}/assignments/{assignment_id}/config/reset
145///
146/// Overwrite the assignment's config on disk with the system defaults (`ExecutionConfig::default_config()`).
147/// Returns the default config that was saved.
148///
149/// ### Success Response (200 OK)
150/// ```json
151/// {
152///   "success": true,
153///   "message": "Assignment configuration reset to defaults",
154///   "data": { ... full ExecutionConfig ... }
155/// }
156/// ```
157pub async fn reset_assignment_config(
158    State(app_state): State<AppState>,
159    Path((module_id, assignment_id)): Path<(i64, i64)>,
160) -> impl IntoResponse {
161    let db = app_state.db();
162
163    // Ensure the assignment exists and belongs to the module
164    let assignment = match AssignmentEntity::find()
165        .filter(AssignmentColumn::Id.eq(assignment_id as i32))
166        .filter(AssignmentColumn::ModuleId.eq(module_id as i32))
167        .one(db)
168        .await
169    {
170        Ok(Some(a)) => a,
171        Ok(None) => {
172            return (
173                StatusCode::NOT_FOUND,
174                Json(ApiResponse::<ExecutionConfig>::error("Assignment or module not found")),
175            );
176        }
177        Err(e) => {
178            eprintln!("DB error: {:?}", e);
179            return (
180                StatusCode::INTERNAL_SERVER_ERROR,
181                Json(ApiResponse::<ExecutionConfig>::error("Database error")),
182            );
183        }
184    };
185
186    // Build defaults
187    let default_cfg = ExecutionConfig::default_config();
188
189    // Persist file
190    let bytes = match serde_json::to_vec_pretty(&default_cfg) {
191        Ok(b) => b,
192        Err(e) => {
193            eprintln!("Serialization error: {:?}", e);
194            return (
195                StatusCode::INTERNAL_SERVER_ERROR,
196                Json(ApiResponse::<ExecutionConfig>::error("Failed to serialize default config")),
197            );
198        }
199    };
200
201    match AssignmentFile::save_file(
202        &db,
203        assignment.id.into(),
204        module_id,
205        FileType::Config,
206        "config.json",
207        &bytes,
208    )
209    .await
210    {
211        Ok(_) => (
212            StatusCode::OK,
213            Json(ApiResponse::<ExecutionConfig>::success(
214                default_cfg,
215                "Assignment configuration reset to defaults",
216            )),
217        ),
218        Err(e) => {
219            eprintln!("File save error: {:?}", e);
220            (
221                StatusCode::INTERNAL_SERVER_ERROR,
222                Json(ApiResponse::<ExecutionConfig>::error(
223                    "Failed to save default config as assignment file",
224                )),
225            )
226        }
227    }
228}