1use 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
37async 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) = ¶ms.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
243async 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) = ¶ms.query {
326 let pattern = format!("%{}%", query.to_lowercase());
327
328 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 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 _ => {} }
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
539pub 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
575pub 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(); 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}