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

1use crate::response::ApiResponse;
2use axum::{
3    extract::{Path, State},
4    http::StatusCode,
5    response::IntoResponse,
6    Json,
7};
8use db::models::assignment::{Column as AssignmentColumn, Entity as AssignmentEntity};
9use db::models::assignment_file::{Entity as AssignmentFile, Column as AssignmentFileColumn, FileType, Model as AssignmentFileModel};
10use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
11use serde_json::to_value;
12use util::{execution_config::ExecutionConfig, state::AppState};
13
14/// GET /api/modules/{module_id}/assignments/{assignment_id}/config
15///
16/// Retrieve the JSON configuration object associated with a specific assignment. Accessible to users
17/// assigned to the module with appropriate permissions.
18///
19/// The configuration object is loaded from disk using the [`ExecutionConfig`] schema. If no configuration
20/// file is present on disk, an empty config is returned instead.
21///
22/// ### Path Parameters
23/// - `module_id` (i64): The ID of the module containing the assignment
24/// - `assignment_id` (i64): The ID of the assignment to retrieve configuration for
25///
26/// ### Example Request
27/// ```bash
28/// curl -X GET http://localhost:3000/api/modules/1/assignments/2/config \
29///   -H "Authorization: Bearer <token>"
30/// ```
31///
32/// ### Success Response (200 OK) - With Configuration
33/// ```json
34/// {
35///   "success": true,
36///   "message": "Assignment configuration retrieved successfully",
37///   "data": {
38///     "execution": {
39///       "timeout_secs": 10,
40///       "max_memory": 8589934592,
41///       "max_cpus": 2,
42///       "max_uncompressed_size": 100000000,
43///       "max_processes": 256
44///     },
45///     "marking": {
46///       "marking_scheme": "exact",
47///       "feedback_scheme": "auto",
48///       "deliminator": "&-=-&"
49///     }
50///   }
51/// }
52/// ```
53///
54/// ### Success Response (200 OK) - No Configuration File
55/// ```json
56/// {
57///   "success": true,
58///   "message": "No configuration set for this assignment",
59///   "data": {}
60/// }
61/// ```
62///
63/// ### Error Responses
64/// - **404** – Assignment not found
65/// - **500** – Failed to load configuration from disk
66///
67/// ### Notes
68/// - Configurations are stored on disk under `ASSIGNMENT_STORAGE_ROOT/module_{id}/assignment_{id}/config/config.json`
69/// - Config format uses [`ExecutionConfig`] as the schema
70/// - This is an example schema and will evolve over time
71pub async fn get_assignment_config(
72    State(app_state): State<AppState>,
73    Path((module_id, assignment_id)): Path<(i64, i64)>,
74) -> impl IntoResponse {
75    let db = app_state.db();
76
77    // Verify the assignment exists
78    match AssignmentEntity::find()
79        .filter(AssignmentColumn::Id.eq(assignment_id as i32))
80        .filter(AssignmentColumn::ModuleId.eq(module_id as i32))
81        .one(db)
82        .await
83    {
84        Ok(Some(_)) => {}
85        Ok(None) => {
86            return (
87                StatusCode::NOT_FOUND,
88                Json(ApiResponse::<()>::error("Assignment or module not found")),
89            )
90                .into_response();
91        }
92        Err(e) => {
93            eprintln!("DB error: {:?}", e);
94            return (
95                StatusCode::INTERNAL_SERVER_ERROR,
96                Json(ApiResponse::<()>::error("Database error")),
97            )
98                .into_response();
99        }
100    }
101
102    // Look up the latest config assignment file
103    let config_file: Option<AssignmentFileModel> = match AssignmentFile::find()
104        .filter(AssignmentFileColumn::AssignmentId.eq(assignment_id))
105        .filter(AssignmentFileColumn::FileType.eq(FileType::Config))
106        .order_by_desc(AssignmentFileColumn::UpdatedAt)
107        .one(db)
108        .await
109    {
110        Ok(opt) => opt,
111        Err(e) => {
112            eprintln!("DB error while fetching config file: {:?}", e);
113            return (
114                StatusCode::INTERNAL_SERVER_ERROR,
115                Json(ApiResponse::<()>::error("Database error")),
116            )
117                .into_response();
118        }
119    };
120
121    // Load the config from the file model
122    match config_file {
123        Some(file_model) => match file_model.load_execution_config(module_id) {
124            Ok(cfg) => {
125                let json = to_value(cfg).unwrap_or_else(|_| serde_json::json!({}));
126                (
127                    StatusCode::OK,
128                    Json(ApiResponse::success(json, "Assignment configuration retrieved successfully")),
129                )
130                    .into_response()
131            }
132            Err(err) => {
133                eprintln!("Failed to load config from disk: {}", err);
134                (
135                    StatusCode::OK,
136                    Json(ApiResponse::success(
137                        serde_json::json!({}),
138                        "No configuration set for this assignment",
139                    )),
140                )
141                    .into_response()
142            }
143        },
144        None => (
145            StatusCode::OK,
146            Json(ApiResponse::success(
147                serde_json::json!({}),
148                "No configuration set for this assignment",
149            )),
150        )
151            .into_response(),
152    }
153}
154
155/// GET /api/modules/{module_id}/assignments/{assignment_id}/config/default
156///
157/// Returns the default execution configuration used when no custom config file is present.
158/// This helps clients pre-fill configuration forms or understand system defaults.
159///
160/// ### Success Response (200 OK)
161/// ```json
162/// {
163///   "success": true,
164///   "message": "Default execution config retrieved successfully",
165///   "data":
166/// {
167//   "execution": {
168//     "timeout_secs": 10,
169//     "max_memory": 1000000,
170//     "max_cpus": 2,
171//     "max_uncompressed_size": 1000000,
172//     "max_processes": 256
173//   },
174//   "marking": {
175//     "marking_scheme": "exact",
176//     "feedback_scheme": "auto",
177//     "deliminator": "&-=-&"
178//   }
179// }
180
181/// }
182/// ```
183pub async fn get_default_assignment_config(
184    Path((_module_id, _assignment_id)): Path<(i64, i64)>,
185) -> impl IntoResponse {
186    let default_config = ExecutionConfig::default_config();
187    (
188        StatusCode::OK,
189        Json(ApiResponse::success(
190            default_config,
191            "Default execution config retrieved successfully",
192        )),
193    )
194}