api/routes/modules/assignments/memo_output/post.rs
1use crate::response::ApiResponse;
2use axum::{
3 Json,
4 extract::{Path, State},
5 http::StatusCode,
6};
7use code_runner::create_memo_outputs_for_all_tasks;
8use util::state::AppState;
9use std::{env, fs, path::PathBuf};
10use tracing::{error, info};
11
12/// POST /api/modules/{module_id}/assignments/{assignment_id}/memo_output/generate
13///
14/// Start asynchronous generation of memo outputs for all tasks in the specified assignment. Accessible
15/// to users with Lecturer or Admin roles assigned to the module.
16///
17/// This endpoint validates the presence of required directories and starts a background task to
18/// generate memo outputs for all tasks in the assignment. The memo outputs are used by the grading
19/// system to evaluate student submissions against expected results.
20///
21/// ### Path Parameters
22/// - `module_id` (i64): The ID of the module containing the assignment
23/// - `assignment_id` (i64): The ID of the assignment to generate memo outputs for
24///
25/// ### Example Request
26/// ```bash
27/// curl -X POST http://localhost:3000/api/modules/1/assignments/2/memo_output/generate \
28/// -H "Authorization: Bearer <token>"
29/// ```
30///
31/// ### Success Response (202 Accepted)
32/// ```json
33/// {
34/// "success": true,
35/// "message": "Task started",
36/// "data": null
37/// }
38/// ```
39///
40/// ### Error Responses
41///
42/// **422 Unprocessable Entity** - Required directories missing or empty
43/// ```json
44/// {
45/// "success": false,
46/// "message": "Required memo directory is missing or empty"
47/// }
48/// ```
49/// or
50/// ```json
51/// {
52/// "success": false,
53/// "message": "Config file not valid"
54/// }
55/// ```
56///
57/// **403 Forbidden** - Insufficient permissions
58/// ```json
59/// {
60/// "success": false,
61/// "message": "Access denied"
62/// }
63/// ```
64///
65/// **404 Not Found** - Assignment or module not found
66/// ```json
67/// {
68/// "success": false,
69/// "message": "Assignment or module not found"
70/// }
71/// ```
72///
73/// ### Directory Requirements
74/// The endpoint validates the presence of required directories under:
75/// `ASSIGNMENT_STORAGE_ROOT/module_{module_id}/assignment_{assignment_id}/`
76///
77/// - `memo/` directory: Must exist and contain at least one file
78/// - Contains memo files that define expected outputs for each task
79/// - Used as reference for evaluating student submissions
80/// - `config/` directory: Must exist and contain at least one file
81/// - Contains configuration files for task execution
82/// - Defines test parameters and evaluation criteria
83///
84/// ### Background Processing
85/// - Memo generation is performed asynchronously in a background task
86/// - The response is returned immediately after validation
87/// - Processing continues even if the client disconnects
88/// - Task completion is logged with success/failure status
89/// - Generated memo outputs are stored in the assignment directory
90///
91/// ### Notes
92/// - This endpoint only starts the generation process; it does not wait for completion
93/// - Memo outputs are essential for the grading system to function properly
94/// - The background task processes all tasks defined in the assignment
95/// - Generation is restricted to users with appropriate module permissions
96/// - Check server logs for detailed progress and error information
97pub async fn generate_memo_output(
98 State(app_state): State<AppState>,
99 Path((module_id, assignment_id)): Path<(i64, i64)>,
100) -> (StatusCode, Json<ApiResponse<()>>) {
101 let db = app_state.db();
102
103 let base_path =
104 env::var("ASSIGNMENT_STORAGE_ROOT").unwrap_or_else(|_| "data/assignment_files".into());
105
106 let assignment_root = PathBuf::from(&base_path)
107 .join(format!("module_{}", module_id))
108 .join(format!("assignment_{}", assignment_id));
109
110 let memo_dir = assignment_root.join("memo");
111 let memo_valid = memo_dir.is_dir()
112 && fs::read_dir(&memo_dir)
113 .map(|entries| {
114 entries
115 .filter_map(Result::ok)
116 .any(|entry| entry.path().is_file())
117 })
118 .unwrap_or(false);
119
120 if !memo_valid {
121 return (
122 StatusCode::UNPROCESSABLE_ENTITY,
123 Json(ApiResponse::<()>::error(
124 "Required memo directory is missing or empty",
125 )),
126 );
127 }
128
129 let config_dir = assignment_root.join("config");
130 let config_valid = config_dir.is_dir()
131 && fs::read_dir(&config_dir)
132 .map(|entries| {
133 entries
134 .filter_map(Result::ok)
135 .any(|entry| entry.path().is_file())
136 })
137 .unwrap_or(false);
138
139 if !config_valid {
140 return (
141 StatusCode::UNPROCESSABLE_ENTITY,
142 Json(ApiResponse::<()>::error("Config file not valid")),
143 );
144 }
145
146 match create_memo_outputs_for_all_tasks(db, assignment_id).await {
147 Ok(_) => {
148 info!(
149 "Memo output generation complete for assignment {}",
150 assignment_id
151 );
152 (
153 StatusCode::OK,
154 Json(ApiResponse::<()>::success(
155 (),
156 "Memo output generation complete",
157 )),
158 )
159 }
160 Err(e) => {
161 println!("{}", e);
162 error!(
163 "Memo output generation failed for assignment {}: {:?}",
164 assignment_id, e
165 );
166 (
167 StatusCode::INTERNAL_SERVER_ERROR,
168 Json(ApiResponse::<()>::error("Failed to generate memo output")),
169 )
170 }
171 }
172}