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}