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

1use crate::response::ApiResponse;
2use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, Json};
3use serde::Serialize;
4use tokio::fs as tokio_fs;
5use util::{execution_config::ExecutionConfig, state::AppState};
6use db::models::{assignment_memo_output, assignment_task};
7use sea_orm::{EntityTrait, ColumnTrait, QueryFilter};
8
9#[derive(Serialize)]
10struct MemoSubsection {
11    label: String,
12    output: String,
13}
14
15#[derive(Serialize)]
16struct MemoTaskOutput {
17    task_number: i64,
18    name: String,
19    subsections: Vec<MemoSubsection>,
20    raw: String,
21}
22
23/// GET /api/modules/{module_id}/assignments/{assignment_id}/memo_output
24///
25/// Retrieve all memo output files for a given assignment, parsed into structured format.
26///
27/// Scans the `memo_output` directory for the specified assignment and parses each file into a
28/// `MemoTaskOutput` object, which contains labeled subsections and the raw file content.
29///
30/// **Path Parameters**
31/// - `module_id` (i64): The ID of the module
32/// - `assignment_id` (i64): The ID of the assignment
33///
34/// **Success Response (200 OK)**
35/// ```json
36/// [
37///   {
38///     "task_number": 1,
39///     "name": "Task 1",
40///     "subsections": [
41///       { "label": "Section A", "output": "..." }
42///     ],
43///     "raw": "..."
44///   }
45/// ]
46/// ```
47///
48/// **Error Responses**
49/// - `404 Not Found` if the memo output directory does not exist or contains no valid files
50/// - `500 Internal Server Error` if reading the directory fails
51///
52/// **Example Request**
53/// ```bash
54/// curl http://localhost:3000/api/modules/1/assignments/2/memo_output
55/// ```
56pub async fn get_all_memo_outputs(
57    State(app_state): State<AppState>,
58    Path((module_id, assignment_id)): Path<(i64, i64)>,
59) -> impl IntoResponse {
60    let db = app_state.db();
61
62    // Fetch all memo output models for the given assignment
63    let memo_outputs = match assignment_memo_output::Entity::find()
64        .filter(assignment_memo_output::Column::AssignmentId.eq(assignment_id))
65        .all(db)
66        .await
67    {
68        Ok(models) if !models.is_empty() => models,
69        Ok(_) => {
70            return (
71                StatusCode::NOT_FOUND,
72                Json(ApiResponse::<Vec<MemoTaskOutput>>::error("No memo output records found")),
73            );
74        }
75        Err(_) => {
76            return (
77                StatusCode::INTERNAL_SERVER_ERROR,
78                Json(ApiResponse::<Vec<MemoTaskOutput>>::error("Failed to query memo outputs")),
79            );
80        }
81    };
82
83    // Load separator from execution config (once)
84    let separator = match ExecutionConfig::get_execution_config(module_id, assignment_id) {
85        Ok(config) => config.marking.deliminator,
86        Err(_) => "&-=-&".to_string(),
87    };
88
89    let mut results = Vec::new();
90
91    for memo in memo_outputs {
92        let full_path = memo.full_path();
93        if !full_path.is_file() {
94            continue;
95        }
96
97        let raw_content = match tokio_fs::read_to_string(&full_path).await {
98            Ok(c) => c,
99            Err(_) => continue,
100        };
101
102        // Parse output
103        let sections = raw_content
104            .split(&separator)
105            .filter(|s| !s.trim().is_empty());
106
107        let subsections = sections
108            .map(|s| {
109                let mut lines = s.lines();
110                let label = lines.next().unwrap_or("").trim().to_string();
111                let output = lines.collect::<Vec<_>>().join("\n");
112                MemoSubsection { label, output }
113            })
114            .collect::<Vec<_>>();
115
116        // Lookup the task number and name
117        let Some(task) = assignment_task::Entity::find_by_id(memo.task_id)
118            .filter(assignment_task::Column::AssignmentId.eq(assignment_id))
119            .one(db)
120            .await
121            .ok()
122            .flatten()
123        else {
124            continue;
125        };
126
127        results.push(MemoTaskOutput {
128            task_number: task.task_number,
129            name: task.name,
130            subsections,
131            raw: raw_content,
132        });
133    }
134
135    if results.is_empty() {
136        return (
137            StatusCode::NOT_FOUND,
138            Json(ApiResponse::<Vec<MemoTaskOutput>>::error(
139                "No valid memo output files found",
140            )),
141        );
142    }
143
144    (
145        StatusCode::OK,
146        Json(ApiResponse::success(results, "Fetched memo output successfully")),
147    )
148}