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

1//! Assignment Submission Handlers
2//!
3//! Provides endpoints to manage and retrieve assignment submissions.
4//!
5//! Users can retrieve their own submissions or, if authorized (lecturers, tutors, admins), 
6//! retrieve all submissions for a given assignment. The endpoints support filtering, sorting, 
7//! and pagination. Submission details include marks, late status, practice status, tasks, 
8//! code coverage, and code complexity analysis.
9
10use super::common::{
11    ListSubmissionsQuery, SubmissionListItem, SubmissionsListResponse, UserResponse,
12};
13use crate::{auth::AuthUser, response::ApiResponse};
14use axum::{
15    Extension, Json,
16    extract::{Path, Query, State},
17    http::StatusCode,
18    response::IntoResponse,
19};
20use chrono::{DateTime, Utc};
21use db::models::{
22    assignment::{Column as AssignmentColumn, Entity as AssignmentEntity}, assignment_submission::{self, Entity as SubmissionEntity}, assignment_submission_output::Model as SubmissionOutput, assignment_task, user, user_module_role::{self, Role}
23};
24use sea_orm::{
25    ColumnTrait, Condition, DatabaseConnection, EntityTrait, JoinType, PaginatorTrait, QueryFilter,
26    QueryOrder, QuerySelect, RelationTrait,
27};
28use serde::Serialize;
29use serde_json::Value;
30use util::state::AppState;
31use std::{fs, path::PathBuf};
32
33fn is_late(submission: DateTime<Utc>, due_date: DateTime<Utc>) -> bool {
34    submission > due_date
35}
36
37/// GET /api/modules/:module_id/assignments/:assignment_id/submissions
38///
39/// Retrieve the list of submissions for a specific assignment **belonging to the authenticated student only**.
40/// This endpoint enforces access control such that the user only sees their own submissions, regardless of query params.
41///
42/// ### When to use
43/// - Called when a student views their own submissions for an assignment.
44/// - Similar to `get_list_submissions` but limited to `user_id`.
45///
46/// ### Path Parameters
47/// - `module_id` (i64): ID of the module.
48/// - `assignment_id` (i64): ID of the assignment.
49/// - `user_id` (i64): ID of the student (extracted from JWT claims).
50///
51/// ### Query Parameters
52/// - `page` (optional): Page number (default 1, min 1).
53/// - `per_page` (optional): Items per page (default 20, max 100).
54/// - `query` (optional): Case-insensitive partial match on filename.
55/// - `sort` (optional): Comma-separated sort fields (`created_at`, `filename`, `attempt`, `mark`).
56///
57/// ### Sorting
58/// - DB-backed sorting is available on `created_at`, `filename`, `attempt`.
59/// - In-memory sorting is applied for `mark` after file is read.
60///
61/// ### Output
62/// - Returns a paginated `SubmissionsListResponse` for this student, including `is_late`, `mark`, `is_practice`.
63/// - Late status is computed relative to assignment `due_date`.
64///
65/// ### Errors
66/// - `404`: Assignment not found.
67/// - `500`: DB error.
68///
69/// ### Notes
70/// - No filtering on other students or usernames is possible in this endpoint.
71async fn get_user_submissions(
72    db: &DatabaseConnection,
73    module_id: i64,
74    assignment_id: i64,
75    user_id: i64,
76    Query(params): Query<ListSubmissionsQuery>,
77) -> impl IntoResponse {
78    let assignment = 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        .unwrap()
84        .unwrap();
85
86    let page = params.page.unwrap_or(1).max(1);
87    let per_page = params.per_page.unwrap_or(20).clamp(1, 100);
88
89    let mut condition = Condition::all()
90        .add(assignment_submission::Column::AssignmentId.eq(assignment_id as i32))
91        .add(assignment_submission::Column::UserId.eq(user_id));
92
93    if let Some(query) = &params.query {
94        let pattern = format!("%{}%", query.to_lowercase());
95        condition = condition.add(assignment_submission::Column::Filename.contains(&pattern));
96    }
97
98    if let Some(late_status) = params.late {
99        condition = if late_status {
100            condition.add(assignment_submission::Column::CreatedAt.gt(assignment.due_date))
101        } else {
102            condition.add(assignment_submission::Column::CreatedAt.lte(assignment.due_date))
103        };
104    }
105
106    let mut query = assignment_submission::Entity::find().filter(condition);
107
108    if let Some(ref sort) = params.sort {
109        for field in sort.split(',') {
110            let (field, dir) = if field.starts_with('-') {
111                (&field[1..], sea_orm::Order::Desc)
112            } else {
113                (field, sea_orm::Order::Asc)
114            };
115
116            match field {
117                "created_at" => {
118                    query = query.order_by(assignment_submission::Column::CreatedAt, dir)
119                }
120                "filename" => query = query.order_by(assignment_submission::Column::Filename, dir),
121                "attempt" => query = query.order_by(assignment_submission::Column::Attempt, dir),
122                _ => {}
123            }
124        }
125    } else {
126        query = query.order_by(
127            assignment_submission::Column::CreatedAt,
128            sea_orm::Order::Desc,
129        );
130    }
131
132    let paginator = query.paginate(db, per_page.into());
133    let total = paginator.num_items().await.unwrap_or(0);
134    let rows = paginator
135        .fetch_page((page - 1) as u64)
136        .await
137        .unwrap_or_default();
138
139    let base =
140        std::env::var("ASSIGNMENT_STORAGE_ROOT").unwrap_or_else(|_| "data/assignment_files".into());
141
142    let user_resp = {
143        let u = user::Entity::find_by_id(user_id)
144            .one(db)
145            .await
146            .ok()
147            .flatten();
148        if let Some(u) = u {
149            UserResponse {
150                id: u.id,
151                username: u.username,
152                email: u.email,
153            }
154        } else {
155            UserResponse {
156                id: user_id,
157                username: "unknown".to_string(),
158                email: "unknown".to_string(),
159            }
160        }
161    };
162
163    let mut items: Vec<SubmissionListItem> = rows
164        .into_iter()
165        .map(|s| {
166            let report_path = PathBuf::from(&base)
167                .join(format!("module_{module_id}"))
168                .join(format!("assignment_{assignment_id}"))
169                .join("assignment_submissions")
170                .join(format!("user_{}", s.user_id))
171                .join(format!("attempt_{}", s.attempt))
172                .join("submission_report.json");
173
174            let (mark, is_practice) = match fs::read_to_string(&report_path) {
175                Ok(content) => {
176                    if let Ok(json) = serde_json::from_str::<Value>(&content) {
177                        let mark = json
178                            .get("mark")
179                            .and_then(|m| serde_json::from_value(m.clone()).ok());
180                        let is_practice = json
181                            .get("is_practice")
182                            .and_then(|p| p.as_bool())
183                            .unwrap_or(false);
184                        (mark, is_practice)
185                    } else {
186                        (None, false)
187                    }
188                }
189                Err(_) => (None, false),
190            };
191
192            SubmissionListItem {
193                id: s.id,
194                user: user_resp.clone(),
195                filename: s.filename,
196                attempt: s.attempt,
197                created_at: s.created_at.to_rfc3339(),
198                updated_at: s.updated_at.to_rfc3339(),
199                is_practice,
200                is_late: is_late(s.created_at, assignment.due_date),
201                mark,
202            }
203        })
204        .collect();
205
206    if let Some(ref sort) = params.sort {
207        for field in sort.split(',').rev() {
208            let (field, desc) = if field.starts_with('-') {
209                (&field[1..], true)
210            } else {
211                (field, false)
212            };
213
214            match field {
215                "mark" => {
216                    items.sort_by(|a, b| {
217                        let a_mark = a.mark.as_ref().map(|m| m.earned).unwrap_or(0);
218                        let b_mark = b.mark.as_ref().map(|m| m.earned).unwrap_or(0);
219                        let ord = a_mark.cmp(&b_mark);
220                        if desc { ord.reverse() } else { ord }
221                    });
222                }
223                _ => {}
224            }
225        }
226    }
227
228    (
229        StatusCode::OK,
230        Json(ApiResponse::success(
231            SubmissionsListResponse {
232                submissions: items,
233                page,
234                per_page,
235                total,
236            },
237            "Submissions retrieved successfully",
238        )),
239    )
240        .into_response()
241}
242
243/// GET /api/modules/:module_id/assignments/:assignment_id/submissions
244///
245/// Retrieve the full list of submissions for a specific assignment.  
246/// Accessible to lecturers, tutors, and admins who have permission on the module.  
247/// Supports advanced filtering, sorting, and pagination over all students.
248///
249/// ### When to use
250/// - Called when a lecturer or tutor wants to review or grade submissions from all students.
251///
252/// ### Path Parameters
253/// - `module_id` (i64): ID of the module.
254/// - `assignment_id` (i64): ID of the assignment.
255///
256/// ### Query Parameters
257/// - `page` (optional): Page number (default 1, min 1).
258/// - `per_page` (optional): Items per page (default 20, max 100).
259/// - `query` (optional): Case-insensitive partial match on filename or username.
260/// - `username` (optional): Filter submissions of a specific student.
261/// - `sort` (optional): Comma-separated sort fields. Prefix with `-` for descending. Allowed fields:
262///   - `username`
263///   - `attempt`
264///   - `filename`
265///   - `created_at`
266///   - `mark`
267///
268/// ### Sorting
269/// - DB-backed sorting for `created_at`, `filename`, `attempt`.
270/// - In-memory sorting for `username` and `mark`.
271///
272/// ### Filtering
273/// - If `query` is provided, applies to both `filename` and `username`.
274/// - If `username` is provided, restricts to that user.
275///
276/// ### Output
277/// - Returns paginated `SubmissionsListResponse` for all students, including `is_late`, `mark`, `is_practice`.
278///
279/// ### Errors
280/// - `404`: Assignment not found.
281/// - `500`: DB error.
282///
283/// ### Notes
284/// - Late submissions are computed relative to assignment `due_date`.
285/// - Defaults to sorting by `created_at DESC`.
286async fn get_list_submissions(
287    db: &DatabaseConnection,
288    module_id: i64,
289    assignment_id: i64,
290    params: ListSubmissionsQuery,
291) -> impl IntoResponse {
292    let assignment = match AssignmentEntity::find()
293        .filter(AssignmentColumn::Id.eq(assignment_id as i32))
294        .filter(AssignmentColumn::ModuleId.eq(module_id as i32))
295        .one(db)
296        .await
297    {
298        Ok(Some(a)) => a,
299        Ok(None) => {
300            return (
301                StatusCode::NOT_FOUND,
302                Json(ApiResponse::<SubmissionsListResponse>::error(
303                    "Assignment not found",
304                )),
305            )
306                .into_response();
307        }
308        Err(_) => {
309            return (
310                StatusCode::INTERNAL_SERVER_ERROR,
311                Json(ApiResponse::<SubmissionsListResponse>::error(
312                    "Database error",
313                )),
314            )
315                .into_response();
316        }
317    };
318
319    let page = params.page.unwrap_or(1).max(1);
320    let per_page = params.per_page.unwrap_or(20).clamp(1, 100);
321
322    let mut condition =
323        Condition::all().add(assignment_submission::Column::AssignmentId.eq(assignment_id as i32));
324
325    if let Some(query) = &params.query {
326        let pattern = format!("%{}%", query.to_lowercase());
327
328        // Build OR condition across multiple fields
329        let mut or_condition =
330            Condition::any().add(assignment_submission::Column::Filename.contains(&pattern));
331
332        if let Ok(Some(user)) = user::Entity::find()
333            .filter(user::Column::Username.contains(&pattern))
334            .one(db)
335            .await
336        {
337            or_condition = or_condition.add(assignment_submission::Column::UserId.eq(user.id));
338        }
339
340        condition = condition.add(or_condition);
341    }
342
343    if let Some(ref username) = params.username {
344        match user::Entity::find()
345            .filter(user::Column::Username.eq(username.clone()))
346            .one(db)
347            .await
348        {
349            Ok(Some(user)) => {
350                condition = condition.add(assignment_submission::Column::UserId.eq(user.id));
351            }
352            Ok(None) => {
353                // No such user => return empty list
354                return (
355                    StatusCode::OK,
356                    Json(ApiResponse::success(
357                        SubmissionsListResponse {
358                            submissions: vec![],
359                            page: 1,
360                            per_page,
361                            total: 0,
362                        },
363                        "No submissions found for the specified username",
364                    )),
365                )
366                    .into_response();
367            }
368            Err(_) => {
369                return (
370                    StatusCode::INTERNAL_SERVER_ERROR,
371                    Json(ApiResponse::<SubmissionsListResponse>::error(
372                        "Database error",
373                    )),
374                )
375                    .into_response();
376            }
377        }
378    }
379
380    if let Some(late_status) = params.late {
381        condition = if late_status {
382            condition.add(assignment_submission::Column::CreatedAt.gt(assignment.due_date))
383        } else {
384            condition.add(assignment_submission::Column::CreatedAt.lte(assignment.due_date))
385        };
386    }
387
388    let mut query = assignment_submission::Entity::find()
389        .filter(condition)
390        .find_also_related(user::Entity);
391
392    if let Some(ref sort) = params.sort {
393        for field in sort.split(',') {
394            let (field, dir) = if field.starts_with('-') {
395                (&field[1..], sea_orm::Order::Desc)
396            } else {
397                (field, sea_orm::Order::Asc)
398            };
399
400            match field {
401                "created_at" => {
402                    query = query.order_by(assignment_submission::Column::CreatedAt, dir)
403                }
404                "filename" => query = query.order_by(assignment_submission::Column::Filename, dir),
405                "attempt" => query = query.order_by(assignment_submission::Column::Attempt, dir),
406                _ => {} // mark/status handled in-memory
407            }
408        }
409    } else {
410        query = query.order_by(
411            assignment_submission::Column::CreatedAt,
412            sea_orm::Order::Desc,
413        );
414    }
415
416    let paginator = query.paginate(db, per_page.into());
417    let total = paginator.num_items().await.unwrap_or(0);
418    let rows = paginator
419        .fetch_page((page - 1) as u64)
420        .await
421        .unwrap_or_default();
422
423    let base =
424        std::env::var("ASSIGNMENT_STORAGE_ROOT").unwrap_or_else(|_| "data/assignment_files".into());
425
426    let mut items: Vec<SubmissionListItem> = rows
427        .into_iter()
428        .map(|(s, u)| {
429            let user_resp = if let Some(u) = u {
430                UserResponse {
431                    id: u.id,
432                    username: u.username,
433                    email: u.email,
434                }
435            } else {
436                UserResponse {
437                    id: s.user_id,
438                    username: "unknown".to_string(),
439                    email: "unknown".to_string(),
440                }
441            };
442
443            let report_path = PathBuf::from(&base)
444                .join(format!("module_{module_id}"))
445                .join(format!("assignment_{assignment_id}"))
446                .join("assignment_submissions")
447                .join(format!("user_{}", s.user_id))
448                .join(format!("attempt_{}", s.attempt))
449                .join("submission_report.json");
450
451            let (mark, is_practice) = match fs::read_to_string(&report_path) {
452                Ok(content) => {
453                    if let Ok(json) = serde_json::from_str::<Value>(&content) {
454                        let mark = json
455                            .get("mark")
456                            .and_then(|m| serde_json::from_value(m.clone()).ok());
457                        let is_practice = json
458                            .get("is_practice")
459                            .and_then(|p| p.as_bool())
460                            .unwrap_or(false);
461                        (mark, is_practice)
462                    } else {
463                        (None, false)
464                    }
465                }
466                Err(_) => (None, false),
467            };
468
469            SubmissionListItem {
470                id: s.id,
471                user: user_resp,
472                filename: s.filename,
473                attempt: s.attempt,
474                created_at: s.created_at.to_rfc3339(),
475                updated_at: s.updated_at.to_rfc3339(),
476                is_practice,
477                is_late: is_late(s.created_at, assignment.due_date),
478                mark,
479            }
480        })
481        .collect();
482
483    if let Some(ref sort) = params.sort {
484        for field in sort.split(',').rev() {
485            let (field, desc) = if field.starts_with('-') {
486                (&field[1..], true)
487            } else {
488                (field, false)
489            };
490
491            match field {
492                "username" => {
493                    items.sort_by(|a, b| {
494                        let ord = a.user.username.cmp(&b.user.username);
495                        if desc { ord.reverse() } else { ord }
496                    });
497                }
498                "mark" => {
499                    items.sort_by(|a, b| {
500                        let a_mark = a.mark.as_ref().map(|m| m.earned).unwrap_or(0);
501                        let b_mark = b.mark.as_ref().map(|m| m.earned).unwrap_or(0);
502                        let ord = a_mark.cmp(&b_mark);
503                        if desc { ord.reverse() } else { ord }
504                    });
505                }
506                _ => {}
507            }
508        }
509    }
510
511    (
512        StatusCode::OK,
513        Json(ApiResponse::success(
514            SubmissionsListResponse {
515                submissions: items,
516                page,
517                per_page,
518                total,
519            },
520            "Submissions retrieved successfully",
521        )),
522    )
523        .into_response()
524}
525
526async fn is_student(module_id: i64, user_id: i64, db: &DatabaseConnection) -> bool {
527    user_module_role::Entity::find()
528        .filter(user_module_role::Column::UserId.eq(user_id))
529        .filter(user_module_role::Column::ModuleId.eq(module_id))
530        .filter(user_module_role::Column::Role.eq(Role::Student))
531        .join(JoinType::InnerJoin, user_module_role::Relation::User.def())
532        .filter(user::Column::Admin.eq(false))
533        .one(db)
534        .await
535        .map(|opt| opt.is_some())
536        .unwrap_or(false)
537}
538
539/// GET /api/modules/{module_id}/assignments/{assignment_id}/submissions
540///
541/// List submissions for a specific assignment.  
542/// - **Students**: Can only view their own submissions, with optional query, pagination, and sort.  
543/// - **Lecturers/Tutors/Admins**: Can view all submissions with full filters, pagination, sorting.
544///
545/// ### Path Parameters
546/// - `module_id` (i64): The ID of the module
547/// - `assignment_id` (i64): The ID of the assignment
548///
549/// ### Query Parameters
550/// - `page`, `per_page`, `query`, `username`, `sort` (see API docs above for details)
551///
552/// ### Notes
553/// - Students: `username` is ignored, only their own submissions returned.
554/// - Late submissions are calculated based on `due_date`.
555pub async fn list_submissions(
556    State(app_state): State<AppState>,
557    Path((module_id, assignment_id)): Path<(i64, i64)>,
558    Extension(AuthUser(claims)): Extension<AuthUser>,
559    Query(params): Query<ListSubmissionsQuery>,
560) -> axum::response::Response {
561    let db = app_state.db();
562
563    let user_id = claims.sub;
564    if is_student(module_id, user_id, db).await {
565        return get_user_submissions(db, module_id, assignment_id, user_id, Query(params))
566            .await
567            .into_response();
568    }
569
570    get_list_submissions(db, module_id, assignment_id, params)
571        .await
572        .into_response()
573}
574
575/// GET /api/modules/{module_id}/assignments/{assignment_id}/submissions/{submission_id}
576///
577/// Retrieve a specific submission report for a given assignment. Accessible to users assigned to
578/// the module with appropriate permissions.
579///
580/// This endpoint validates that the submission belongs to the specified assignment and module, then
581/// reads the `submission_report.json` file associated with it. If the requesting user is not a
582/// student, user metadata is included in the response.
583///
584/// ### Path Parameters
585/// - `module_id` (i64): The ID of the module containing the assignment
586/// - `assignment_id` (i64): The ID of the assignment containing the submission
587/// - `submission_id` (i64): The ID of the submission to retrieve
588///
589/// ### Example Request
590/// ```bash
591/// curl -X GET "http://localhost:3000/api/modules/1/assignments/2/submissions/123" \
592///   -H "Authorization: Bearer <token>"
593/// ```
594///
595/// ### Success Response (200 OK)
596/// ```json
597/// {
598///   "success": true,
599///   "message": "Submission details retrieved successfully",
600///   "data": {
601///     "id": 123,
602///     "attempt": 1,
603///     "filename": "assignment1.java",
604///     "created_at": "2024-01-15T10:30:00Z",
605///     "updated_at": "2024-01-15T10:30:00Z",
606///     "mark": {
607///       "earned": 85,
608///       "total": 100
609///     },
610///     "is_practice": false,
611///     "is_late": false,
612///     "tasks": [...],
613///     "code_coverage": [...],
614///     "code_complexity": {...},
615///     "user": {
616///       "user_id": 456,
617///       "username": "student1",
618///       "email": "[email protected]"
619///     }
620///   }
621/// }
622/// ```
623///
624/// ### Error Responses
625///
626/// **404 Not Found** - Submission, assignment, or module not found
627/// ```json
628/// {
629///   "success": false,
630///   "message": "Submission not found"
631/// }
632/// ```
633/// or
634/// ```json
635/// {
636///   "success": false,
637///   "message": "Assignment not found"
638/// }
639/// ```
640/// or
641/// ```json
642/// {
643///   "success": false,
644///   "message": "Assignment does not belong to the specified module"
645/// }
646/// ```
647/// or
648/// ```json
649/// {
650///   "success": false,
651///   "message": "Submission report not found"
652/// }
653/// ```
654///
655/// **500 Internal Server Error** - Database or file read error
656/// ```json
657/// {
658///   "success": false,
659///   "message": "Database error"
660/// }
661/// ```
662/// or
663/// ```json
664/// {
665///   "success": false,
666///   "message": "Failed to parse submission report"
667/// }
668/// ```
669/// or
670/// ```json
671/// {
672///   "success": false,
673///   "message": "ASSIGNMENT_STORAGE_ROOT not set"
674/// }
675/// ```
676///
677/// ### Notes
678/// - The submission report is read from the filesystem at:
679///   `ASSIGNMENT_STORAGE_ROOT/module_{module_id}/assignment_{assignment_id}/assignment_submissions/user_{user_id}/attempt_{attempt}/submission_report.json`
680/// - User metadata is only included for non-student users (lecturers, tutors, admins)
681/// - The response contains the complete grading report including marks, tasks, and optional
682///   code coverage/complexity analysis
683/// - Access is restricted to users with appropriate permissions for the module
684pub async fn get_submission(
685    State(app_state): State<AppState>,
686    Path((module_id, assignment_id, submission_id)): Path<(i64, i64, i64)>,
687    Extension(AuthUser(claims)): Extension<AuthUser>,
688) -> impl IntoResponse {
689    let db = app_state.db();
690
691    let submission = SubmissionEntity::find_by_id(submission_id)
692        .one(db)
693        .await
694        .unwrap()
695        .unwrap();
696
697    if submission.assignment_id != assignment_id {
698        return (
699            StatusCode::NOT_FOUND,
700            Json(ApiResponse::<()>::error(
701                "Submission does not belong to the specified assignment",
702            )),
703        )
704            .into_response();
705    }
706
707    let assignment = match AssignmentEntity::find_by_id(assignment_id).one(db).await {
708        Ok(Some(assignment)) => assignment,
709        Ok(None) => {
710            return (
711                StatusCode::NOT_FOUND,
712                Json(ApiResponse::<()>::error("Assignment not found")),
713            )
714                .into_response();
715        }
716        Err(err) => {
717            eprintln!("DB error checking assignment: {:?}", err);
718            return (
719                StatusCode::INTERNAL_SERVER_ERROR,
720                Json(ApiResponse::<()>::error("Database error")),
721            )
722                .into_response();
723        }
724    };
725
726    if assignment.module_id != module_id {
727        return (
728            StatusCode::NOT_FOUND,
729            Json(ApiResponse::<()>::error(
730                "Assignment does not belong to the specified module",
731            )),
732        )
733            .into_response();
734    }
735
736    let user_id = submission.user_id;
737    let attempt = submission.attempt;
738
739    let base = match std::env::var("ASSIGNMENT_STORAGE_ROOT") {
740        Ok(val) => val,
741        Err(_) => {
742            return (
743                StatusCode::INTERNAL_SERVER_ERROR,
744                Json(ApiResponse::<()>::error("ASSIGNMENT_STORAGE_ROOT not set")),
745            )
746                .into_response();
747        }
748    };
749
750    let path = PathBuf::from(&base)
751        .join(format!("module_{}", module_id))
752        .join(format!("assignment_{}", assignment_id))
753        .join("assignment_submissions")
754        .join(format!("user_{}", user_id))
755        .join(format!("attempt_{}", attempt))
756        .join("submission_report.json");
757
758    let content = match fs::read_to_string(&path) {
759        Ok(c) => c,
760        Err(_) => {
761            return (
762                StatusCode::NOT_FOUND,
763                Json(ApiResponse::<()>::error("Submission report not found")),
764            )
765                .into_response();
766        }
767    };
768
769    let mut parsed: Value = match serde_json::from_str(&content) {
770        Ok(val) => val,
771        Err(_) => {
772            return (
773                StatusCode::INTERNAL_SERVER_ERROR,
774                Json(ApiResponse::<()>::error(
775                    "Failed to parse submission report",
776                )),
777            )
778                .into_response();
779        }
780    };
781
782    if !is_student(module_id, claims.sub, db).await {
783        if let Ok(Some(u)) = user::Entity::find_by_id(user_id).one(db).await {
784            let user_value = serde_json::to_value(UserResponse {
785                id: u.id,
786                username: u.username,
787                email: u.email,
788            })
789            .unwrap(); // safe since UserResponse is serializable
790
791            if let Some(obj) = parsed.as_object_mut() {
792                obj.insert("user".to_string(), user_value);
793            }
794        }
795    }
796
797    (
798        StatusCode::OK,
799        Json(ApiResponse::success(
800            parsed,
801            "Submission details retrieved successfully",
802        )),
803    )
804        .into_response()
805}
806
807#[derive(Serialize)]
808struct MemoResponse {
809    task_number: i64,
810    raw: String,
811}
812
813pub async fn get_submission_output(
814    State(app_state): State<AppState>,
815    Path((module_id, assignment_id, submission_id)): Path<(i64, i64, i64)>,
816) -> impl IntoResponse {
817    let db = app_state.db();
818
819    let output = match SubmissionOutput::get_output(db, module_id, assignment_id, submission_id).await {
820        Ok(output) => output,
821        Err(_) => {
822            return (
823                StatusCode::INTERNAL_SERVER_ERROR,
824                Json(ApiResponse::<()>::error("Failed to retrieve submission output")),
825            )
826            .into_response();
827        }
828    };
829
830    if output.is_empty() {
831        return (
832            StatusCode::NOT_FOUND,
833            Json(ApiResponse::<()>::error("Submission output not found")),
834        )
835        .into_response();
836    }
837
838    let mut memo_data = Vec::new();
839
840    for (task_id, content) in output {
841        let task = assignment_task::Entity::find_by_id(task_id)
842            .filter(assignment_task::Column::AssignmentId.eq(assignment_id))
843            .one(db)
844            .await;
845
846        match task {
847            Ok(Some(task)) => {
848                memo_data.push(MemoResponse {
849                    task_number: task.task_number,
850                    raw: content,
851                });
852            }
853            Ok(None) => {
854                return (
855                    StatusCode::NOT_FOUND,
856                    Json(ApiResponse::<()>::error(&format!(
857                        "Task with ID {} not found for assignment {}",
858                        task_id, assignment_id
859                    ))),
860                )
861                .into_response();
862            }
863            Err(_) => {
864                return (
865                    StatusCode::INTERNAL_SERVER_ERROR,
866                    Json(ApiResponse::<()>::error("Database error while fetching task info")),
867                )
868                .into_response();
869            }
870        }
871    }
872
873    (
874        StatusCode::OK,
875        Json(ApiResponse::success(
876            memo_data,
877            "Fetched memo output successfully",
878        )),
879    )
880    .into_response()
881}