1use super::common::{CodeComplexity, CodeComplexitySummary, MarkSummary, SubmissionDetailResponse};
2use crate::{auth::AuthUser, response::ApiResponse, routes::modules::assignments::get::is_late};
3use axum::{
4 Json,
5 extract::{Extension, Multipart, Path, State},
6 http::StatusCode,
7 response::IntoResponse,
8};
9use chrono::Utc;
10use code_runner;
11use db::models::{
12 assignment::{Column as AssignmentColumn, Entity as AssignmentEntity},
13 assignment_submission::{self, Model as AssignmentSubmissionModel},
14};
15use marker::MarkingJob;
16use md5;
17use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
18use serde::{Deserialize, Serialize};
19use tokio_util::bytes;
20use util::{
21 execution_config::{ExecutionConfig, execution_config::SubmissionMode},
22 mark_allocator::mark_allocator::generate_allocator,
23 mark_allocator::mark_allocator::load_allocator,
24 state::AppState,
25};
26
27#[derive(Debug, Deserialize)]
28pub struct RemarkRequest {
29 #[serde(default)]
30 submission_ids: Option<Vec<i64>>,
31 #[serde(default)]
32 all: Option<bool>,
33}
34
35#[derive(Debug, Deserialize)]
36pub struct ResubmitRequest {
37 #[serde(default)]
38 submission_ids: Option<Vec<i64>>,
39 #[serde(default)]
40 all: Option<bool>,
41}
42
43#[derive(Debug, Serialize)]
44pub struct BulkOperationResponse {
45 processed: usize,
46 failed: Vec<FailedOperation>,
47}
48
49#[derive(Debug, Serialize)]
50pub struct RemarkResponse {
51 regraded: usize,
52 failed: Vec<FailedOperation>,
53}
54
55#[derive(Debug, Serialize)]
56pub struct ResubmitResponse {
57 resubmitted: usize,
58 failed: Vec<FailedOperation>,
59}
60
61#[derive(Debug, Serialize)]
62pub struct FailedOperation {
63 id: Option<i64>,
64 error: String,
65}
66
67fn validate_bulk_request(
73 submission_ids: &Option<Vec<i64>>,
74 all: &Option<bool>,
75) -> Result<(), &'static str> {
76 match (submission_ids, all) {
77 (Some(ids), None) if !ids.is_empty() => Ok(()),
78 (None, Some(true)) => Ok(()),
79 (Some(_), None) => Err("submission_ids cannot be empty"),
80 _ => Err("Must provide exactly one of submission_ids or all=true"),
81 }
82}
83
84async fn resolve_submission_ids(
86 submission_ids: Option<Vec<i64>>,
87 all: Option<bool>,
88 assignment_id: i64,
89 db: &sea_orm::DatabaseConnection,
90) -> Result<Vec<i64>, String> {
91 match (submission_ids, all) {
92 (Some(ids), _) if !ids.is_empty() => Ok(ids),
93 (_, Some(true)) => AssignmentSubmissionModel::find_by_assignment(assignment_id, db)
94 .await
95 .map_err(|e| format!("Failed to fetch submissions: {}", e)),
96 _ => Err("Must provide either submission_ids or all=true".to_string()),
97 }
98}
99
100async fn load_assignment(
102 module_id: i64,
103 assignment_id: i64,
104 db: &sea_orm::DatabaseConnection,
105) -> Result<db::models::assignment::Model, String> {
106 AssignmentEntity::find()
107 .filter(AssignmentColumn::Id.eq(assignment_id))
108 .filter(AssignmentColumn::ModuleId.eq(module_id))
109 .one(db)
110 .await
111 .map_err(|e| format!("Database error: {}", e))?
112 .ok_or_else(|| "Assignment not found".to_string())
113}
114
115async fn load_assignment_allocator(module_id: i64, assignment_id: i64) -> Result<(), String> {
117 load_allocator(module_id, assignment_id)
118 .await
119 .map(|_| ())
120 .map_err(|_| "Failed to load mark allocator".to_string())
121}
122
123fn get_assignment_paths(
125 module_id: i64,
126 assignment_id: i64,
127) -> Result<
128 (
129 std::path::PathBuf,
130 std::path::PathBuf,
131 Vec<std::path::PathBuf>,
132 ),
133 String,
134> {
135 let assignment_storage_root = std::env::var("ASSIGNMENT_STORAGE_ROOT")
136 .unwrap_or_else(|_| "data/assignment_files".to_string());
137
138 let base_path = std::path::PathBuf::from(&assignment_storage_root)
139 .join(format!("module_{}", module_id))
140 .join(format!("assignment_{}", assignment_id));
141
142 let mark_allocator_path = base_path.join("mark_allocator/allocator.json");
143 let memo_output_dir = base_path.join("memo_output");
144
145 let mut memo_outputs: Vec<_> = match std::fs::read_dir(&memo_output_dir) {
146 Ok(rd) => rd
147 .filter_map(|e| e.ok().map(|e| e.path()))
148 .filter(|p| p.is_file())
149 .collect(),
150 Err(_) => Vec::new(),
151 };
152 memo_outputs.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
153
154 Ok((base_path, mark_allocator_path, memo_outputs))
155}
156
157fn get_execution_config(module_id: i64, assignment_id: i64) -> Result<ExecutionConfig, String> {
159 ExecutionConfig::get_execution_config(module_id, assignment_id)
160 .map_err(|_| "Failed to load execution config".to_string())
161}
162
163fn validate_file_upload(
165 file_name: &Option<String>,
166 file_bytes: &Option<bytes::Bytes>,
167) -> Result<(String, bytes::Bytes), (StatusCode, Json<ApiResponse<SubmissionDetailResponse>>)> {
168 let file_name = match file_name {
169 Some(name) => name.clone(),
170 None => {
171 return Err((
172 StatusCode::UNPROCESSABLE_ENTITY,
173 Json(ApiResponse::<SubmissionDetailResponse>::error(
174 "No file provided",
175 )),
176 ));
177 }
178 };
179
180 let file_bytes = match file_bytes {
181 Some(bytes) => bytes.clone(),
182 None => {
183 return Err((
184 StatusCode::UNPROCESSABLE_ENTITY,
185 Json(ApiResponse::<SubmissionDetailResponse>::error(
186 "No file provided",
187 )),
188 ));
189 }
190 };
191
192 let allowed_extensions = [".tgz", ".gz", ".tar", ".zip"];
193 let file_extension = std::path::Path::new(&file_name)
194 .extension()
195 .and_then(|ext| ext.to_str())
196 .map(|ext| format!(".{}", ext.to_lowercase()));
197
198 if !file_extension
199 .as_ref()
200 .map_or(false, |ext| allowed_extensions.contains(&ext.as_str()))
201 {
202 return Err((
203 StatusCode::UNPROCESSABLE_ENTITY,
204 Json(ApiResponse::<SubmissionDetailResponse>::error(
205 "Only .tgz, .gz, .tar, and .zip files are allowed",
206 )),
207 ));
208 }
209
210 if file_bytes.is_empty() {
211 return Err((
212 StatusCode::UNPROCESSABLE_ENTITY,
213 Json(ApiResponse::<SubmissionDetailResponse>::error(
214 "Empty file provided",
215 )),
216 ));
217 }
218
219 Ok((file_name, file_bytes))
220}
221
222async fn get_next_attempt(
224 assignment_id: i64,
225 user_id: i64,
226 db: &sea_orm::DatabaseConnection,
227) -> Result<i64, String> {
228 let prev_attempt = assignment_submission::Entity::find()
229 .filter(assignment_submission::Column::AssignmentId.eq(assignment_id))
230 .filter(assignment_submission::Column::UserId.eq(user_id))
231 .order_by_desc(assignment_submission::Column::Attempt)
232 .one(db)
233 .await
234 .map_err(|e| format!("Database error: {}", e))?
235 .map(|s| s.attempt)
236 .unwrap_or(0);
237
238 Ok(prev_attempt + 1)
239}
240
241async fn grade_submission(
243 submission: AssignmentSubmissionModel,
244 assignment: &db::models::assignment::Model,
245 base_path: &std::path::Path,
246 memo_outputs: &[std::path::PathBuf],
247 mark_allocator_path: &std::path::Path,
248 config: &util::execution_config::ExecutionConfig,
249 db: &sea_orm::DatabaseConnection,
250) -> Result<SubmissionDetailResponse, String> {
251 let student_output_dir = base_path
252 .join("assignment_submissions")
253 .join(format!("user_{}", submission.user_id))
254 .join(format!("attempt_{}", submission.attempt))
255 .join("submission_output");
256
257 let mut student_outputs = Vec::new();
258 if let Ok(entries) = std::fs::read_dir(&student_output_dir) {
259 for entry in entries.flatten() {
260 let file_path = entry.path();
261 if file_path.is_file() {
262 if let Some(ext) = file_path.extension().and_then(|e| e.to_str()) {
263 if ext.eq_ignore_ascii_case("txt") {
264 student_outputs.push(file_path);
265 }
266 }
267 }
268 }
269 }
270 student_outputs.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
271
272 let marking_job = MarkingJob::new(
273 memo_outputs.to_vec(),
274 student_outputs,
275 mark_allocator_path.to_path_buf(),
276 config.clone(),
277 );
278
279 let mark_report = marking_job.mark().await.map_err(|e| {
280 eprintln!("MARKING ERROR: {:#?}", e);
281 format!("{:?}", e)
282 })?;
283
284 let mark = MarkSummary {
285 earned: mark_report.data.mark.earned,
286 total: mark_report.data.mark.total,
287 };
288 let tasks = serde_json::to_value(&mark_report.data.tasks)
289 .unwrap_or_default()
290 .as_array()
291 .cloned()
292 .unwrap_or_default();
293 let code_coverage = match &mark_report.data.code_coverage {
294 Some(cov) => {
295 let arr = serde_json::to_value(cov)
296 .unwrap_or_default()
297 .as_array()
298 .cloned()
299 .unwrap_or_default();
300 if !arr.is_empty() { Some(arr) } else { None }
301 }
302 None => None,
303 };
304 let code_complexity = match &mark_report.data.code_complexity {
305 Some(c) => {
306 let metrics = serde_json::to_value(&c.metrics)
307 .unwrap_or_default()
308 .as_array()
309 .cloned()
310 .unwrap_or_default();
311 let summary = CodeComplexitySummary {
312 earned: c.summary.as_ref().map(|s| s.earned).unwrap_or(0),
313 total: c.summary.as_ref().map(|s| s.total).unwrap_or(0),
314 };
315 if !metrics.is_empty() || summary.earned != 0 || summary.total != 0 {
316 Some(CodeComplexity { summary, metrics })
317 } else {
318 None
319 }
320 }
321 None => None,
322 };
323
324 let mut active_model: assignment_submission::ActiveModel = submission.clone().into();
325 active_model.earned = sea_orm::ActiveValue::Set(mark.earned);
326 active_model.total = sea_orm::ActiveValue::Set(mark.total);
327 assignment_submission::Entity::update(active_model)
328 .exec(db)
329 .await
330 .map_err(|e| e.to_string())?;
331
332 let now = Utc::now();
333 let resp = SubmissionDetailResponse {
334 id: submission.id,
335 attempt: submission.attempt,
336 filename: submission.filename.clone(),
337 hash: submission.file_hash.clone(),
338 created_at: now.to_rfc3339(),
339 updated_at: now.to_rfc3339(),
340 mark,
341 is_practice: submission.is_practice,
342 is_late: is_late(submission.created_at, assignment.due_date),
343 tasks,
344 code_coverage,
345 code_complexity,
346 };
347
348 let attempt_dir = base_path
349 .join("assignment_submissions")
350 .join(format!("user_{}", submission.user_id))
351 .join(format!("attempt_{}", submission.attempt));
352 let report_path = attempt_dir.join("submission_report.json");
353 if let Ok(json) = serde_json::to_string_pretty(&resp) {
354 std::fs::write(&report_path, json).map_err(|e| e.to_string())?;
355 } else {
356 return Err("Failed to serialize submission report".to_string());
357 }
358
359 Ok(resp)
360}
361
362async fn process_submission_code(
364 db: &sea_orm::DatabaseConnection,
365 submission_id: i64,
366 config: ExecutionConfig,
367 module_id: i64,
368 assignment_id: i64,
369) -> Result<(), String> {
370 if config.project.submission_mode == SubmissionMode::Manual.clone() {
371 code_runner::create_submission_outputs_for_all_tasks(db, submission_id)
372 .await
373 .map_err(|e| format!("Code runner failed: {}", e))
374 } else {
375 ai::run_ga_job(db, submission_id, config, module_id, assignment_id)
376 .await
377 .map_err(|e| format!("GATLAM failed: {}", e))
378 }
379}
380
381fn clear_submission_output(
383 submission: &AssignmentSubmissionModel,
384 base_path: &std::path::Path,
385) -> Result<(), String> {
386 let attempt_dir = base_path
387 .join("assignment_submissions")
388 .join(format!("user_{}", submission.user_id))
389 .join(format!("attempt_{}", submission.attempt));
390
391 let output_dir = attempt_dir.join("submission_output");
392
393 if output_dir.exists() {
394 std::fs::remove_dir_all(&output_dir)
395 .map_err(|e| format!("Failed to clear output directory: {}", e))?;
396 }
397
398 let report_path = attempt_dir.join("submission_report.json");
399 if report_path.exists() {
400 std::fs::remove_file(&report_path)
401 .map_err(|e| format!("Failed to remove existing report: {}", e))?;
402 }
403
404 Ok(())
405}
406
407async fn update_submission_report_marks(
409 base_path: &std::path::Path,
410 submission: &AssignmentSubmissionModel,
411 new_mark: &MarkSummary,
412) -> Result<(), String> {
413 let attempt_dir = base_path
414 .join("assignment_submissions")
415 .join(format!("user_{}", submission.user_id))
416 .join(format!("attempt_{}", submission.attempt));
417 let report_path = attempt_dir.join("submission_report.json");
418
419 let content = std::fs::read_to_string(&report_path)
420 .map_err(|e| format!("Failed to read existing report: {}", e))?;
421
422 let mut resp: SubmissionDetailResponse = serde_json::from_str(&content).map_err(|e| {
423 format!(
424 "Failed to deserialize report into SubmissionDetailResponse: {}",
425 e
426 )
427 })?;
428
429 resp.mark = MarkSummary {
430 earned: new_mark.earned,
431 total: new_mark.total,
432 };
433
434 resp.updated_at = Utc::now().to_rfc3339();
435
436 let output = serde_json::to_string_pretty(&resp)
437 .map_err(|e| format!("Failed to serialize updated report: {}", e))?;
438 std::fs::write(&report_path, output)
439 .map_err(|e| format!("Failed to write temp report: {}", e))?;
440
441 Ok(())
442}
443
444async fn execute_bulk_operation<F, Fut>(
446 submission_ids: Vec<i64>,
447 assignment_id: i64,
448 db: &sea_orm::DatabaseConnection,
449 operation: F,
450) -> (usize, Vec<FailedOperation>)
451where
452 F: Fn(AssignmentSubmissionModel) -> Fut,
453 Fut: std::future::Future<Output = Result<(), String>>,
454{
455 let mut processed = 0;
456 let mut failed = Vec::new();
457
458 for submission_id in submission_ids {
459 let submission = match assignment_submission::Entity::find_by_id(submission_id)
460 .one(db)
461 .await
462 {
463 Ok(Some(sub)) => sub,
464 Ok(None) => {
465 failed.push(FailedOperation {
466 id: Some(submission_id),
467 error: "Submission not found".to_string(),
468 });
469 continue;
470 }
471 Err(e) => {
472 failed.push(FailedOperation {
473 id: Some(submission_id),
474 error: format!("Database error: {}", e),
475 });
476 continue;
477 }
478 };
479
480 if submission.assignment_id != assignment_id {
481 failed.push(FailedOperation {
482 id: Some(submission_id),
483 error: "Submission does not belong to this assignment".to_string(),
484 });
485 continue;
486 }
487
488 match operation(submission).await {
489 Ok(_) => processed += 1,
490 Err(e) => failed.push(FailedOperation {
491 id: Some(submission_id),
492 error: e,
493 }),
494 }
495 }
496
497 (processed, failed)
498}
499
500pub async fn submit_assignment(
607 State(app_state): State<AppState>,
608 Path((module_id, assignment_id)): Path<(i64, i64)>,
609 Extension(AuthUser(claims)): Extension<AuthUser>,
610 mut multipart: Multipart,
611) -> impl IntoResponse {
612 let db = app_state.db();
613
614 let assignment = match load_assignment(module_id, assignment_id, db).await {
615 Ok(assignment) => assignment,
616 Err(_) => {
617 return (
618 StatusCode::NOT_FOUND,
619 Json(ApiResponse::<SubmissionDetailResponse>::error(
620 "Assignment not found",
621 )),
622 );
623 }
624 };
625
626 let mut is_practice: bool = false;
627 let mut file_name: Option<String> = None;
628 let mut file_bytes: Option<bytes::Bytes> = None;
629
630 while let Some(field) = multipart.next_field().await.unwrap_or(None) {
631 match field.name() {
632 Some("file") => {
633 file_name = field.file_name().map(|s| s.to_string());
634 file_bytes = Some(field.bytes().await.unwrap_or_default());
635 }
636 Some("is_practice") => {
637 let val = field.text().await.unwrap_or_default();
638 is_practice = val == "true" || val == "1";
639 }
640 _ => {}
641 }
642 }
643
644 let (file_name, file_bytes) = match validate_file_upload(&file_name, &file_bytes) {
645 Ok((name, bytes)) => (name, bytes),
646 Err(response) => return response,
647 };
648
649 let file_hash = format!("{:x}", md5::compute(&file_bytes));
650
651 let attempt = match get_next_attempt(assignment_id, claims.sub, db).await {
652 Ok(attempt) => attempt,
653 Err(e) => {
654 eprintln!("Error getting next attempt: {}", e);
655 return (
656 StatusCode::INTERNAL_SERVER_ERROR,
657 Json(ApiResponse::<SubmissionDetailResponse>::error(
658 "Failed to determine attempt number",
659 )),
660 );
661 }
662 };
663
664 let submission = match AssignmentSubmissionModel::save_file(
665 db,
666 assignment_id,
667 claims.sub,
668 attempt,
669 0,
670 0,
671 is_practice,
672 &file_name,
673 &file_hash,
674 &file_bytes,
675 )
676 .await
677 {
678 Ok(model) => model,
679 Err(e) => {
680 eprintln!("Error saving submission: {:?}", e);
681 return (
682 StatusCode::INTERNAL_SERVER_ERROR,
683 Json(ApiResponse::<SubmissionDetailResponse>::error(
684 "Failed to save submission",
685 )),
686 );
687 }
688 };
689
690 let config = match get_execution_config(module_id, assignment_id) {
691 Ok(config) => config,
692 Err(e) => {
693 return (
694 StatusCode::INTERNAL_SERVER_ERROR,
695 Json(ApiResponse::<SubmissionDetailResponse>::error(&e)),
696 );
697 }
698 };
699
700 if let Err(e) =
701 process_submission_code(db, submission.id, config.clone(), module_id, assignment_id).await
702 {
703 eprintln!("Code execution failed: {}", e);
704 return (
705 StatusCode::INTERNAL_SERVER_ERROR,
706 Json(ApiResponse::<SubmissionDetailResponse>::error(
707 "Failed to run code for submission",
708 )),
709 );
710 }
711
712 if config.project.submission_mode != SubmissionMode::Manual {
713 if let Err(_) = generate_allocator(module_id, assignment_id).await {
714 return (
715 StatusCode::INTERNAL_SERVER_ERROR,
716 Json(ApiResponse::<SubmissionDetailResponse>::error(
717 "Failed to generate allocator",
718 )),
719 );
720 }
721 }
722
723 if let Err(e) = load_assignment_allocator(assignment.module_id, assignment.id).await {
724 return (
725 StatusCode::INTERNAL_SERVER_ERROR,
726 Json(ApiResponse::<SubmissionDetailResponse>::error(&e)),
727 );
728 }
729
730 let (base_path, mark_allocator_path, memo_outputs) =
731 match get_assignment_paths(assignment.module_id, assignment.id) {
732 Ok(paths) => paths,
733 Err(e) => {
734 return (
735 StatusCode::INTERNAL_SERVER_ERROR,
736 Json(ApiResponse::<SubmissionDetailResponse>::error(&e)),
737 );
738 }
739 };
740
741 match grade_submission(
742 submission,
743 &assignment,
744 &base_path,
745 &memo_outputs,
746 &mark_allocator_path,
747 &config,
748 db,
749 )
750 .await
751 {
752 Ok(resp) => (
753 StatusCode::OK,
754 Json(ApiResponse::success(resp, "Submission received and graded")),
755 ),
756 Err(e) => (
757 StatusCode::INTERNAL_SERVER_ERROR,
758 Json(ApiResponse::<SubmissionDetailResponse>::error(e)),
759 ),
760 }
761}
762
763pub async fn remark_submissions(
821 State(app_state): State<AppState>,
822 Path((module_id, assignment_id)): Path<(i64, i64)>,
823 Json(req): Json<RemarkRequest>,
824) -> impl IntoResponse {
825 let db = app_state.db();
826
827 if let Err(e) = validate_bulk_request(&req.submission_ids, &req.all) {
828 return (
829 StatusCode::BAD_REQUEST,
830 Json(ApiResponse::<RemarkResponse>::error(e)),
831 );
832 }
833
834 let assignment = match load_assignment(module_id, assignment_id, db).await {
835 Ok(assignment) => assignment,
836 Err(_) => {
837 return (
838 StatusCode::NOT_FOUND,
839 Json(ApiResponse::<RemarkResponse>::error("Assignment not found")),
840 );
841 }
842 };
843
844 let submission_ids =
845 match resolve_submission_ids(req.submission_ids, req.all, assignment_id, db).await {
846 Ok(ids) => ids,
847 Err(e) => {
848 return (
849 StatusCode::INTERNAL_SERVER_ERROR,
850 Json(ApiResponse::<RemarkResponse>::error(e)),
851 );
852 }
853 };
854
855 if let Err(e) = load_assignment_allocator(assignment.module_id, assignment.id).await {
856 return (
857 StatusCode::INTERNAL_SERVER_ERROR,
858 Json(ApiResponse::<RemarkResponse>::error(e)),
859 );
860 }
861
862 let (base_path, mark_allocator_path, memo_outputs) =
863 match get_assignment_paths(assignment.module_id, assignment.id) {
864 Ok(paths) => paths,
865 Err(e) => {
866 return (
867 StatusCode::INTERNAL_SERVER_ERROR,
868 Json(ApiResponse::<RemarkResponse>::error(e)),
869 );
870 }
871 };
872
873 let config = match get_execution_config(module_id, assignment_id) {
874 Ok(config) => config,
875 Err(e) => {
876 return (
877 StatusCode::INTERNAL_SERVER_ERROR,
878 Json(ApiResponse::<RemarkResponse>::error(e)),
879 );
880 }
881 };
882
883 let (regraded, failed) =
884 execute_bulk_operation(submission_ids.clone(), assignment_id, db, |submission| {
885 let assignment = assignment.clone();
886 let base_path = base_path.clone();
887 let memo_outputs = memo_outputs.clone();
888 let mark_allocator_path = mark_allocator_path.clone();
889 let config = config.clone();
890 async move {
891 let student_output_dir = base_path
892 .join("assignment_submissions")
893 .join(format!("user_{}", submission.user_id))
894 .join(format!("attempt_{}", submission.attempt))
895 .join("submission_output");
896
897 let mut student_outputs = Vec::new();
898 if let Ok(entries) = std::fs::read_dir(&student_output_dir) {
899 for entry in entries.flatten() {
900 let file_path = entry.path();
901 if file_path.is_file() {
902 if let Some(ext) = file_path.extension().and_then(|e| e.to_str()) {
903 if ext.eq_ignore_ascii_case("txt") {
904 student_outputs.push(file_path);
905 }
906 }
907 }
908 }
909 }
910 student_outputs.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
911
912 let marking_job = MarkingJob::new(
913 memo_outputs.to_vec(),
914 student_outputs,
915 mark_allocator_path.to_path_buf(),
916 config.clone(),
917 );
918
919 let mark_report = marking_job
920 .mark()
921 .await
922 .map_err(|e| format!("Marking error: {:?}", e))?;
923
924 let mark = MarkSummary {
925 earned: mark_report.data.mark.earned,
926 total: mark_report.data.mark.total,
927 };
928
929 match update_submission_report_marks(&base_path, &submission, &mark).await {
930 Ok(_) => Ok(()),
931 Err(_err) => grade_submission(
932 submission,
933 &assignment,
934 &base_path,
935 &memo_outputs,
936 &mark_allocator_path,
937 &config,
938 db,
939 )
940 .await
941 .map(|_| ()),
942 }
943 }
944 })
945 .await;
946
947 let response = RemarkResponse { regraded, failed };
948 let message = format!("Regraded {}/{} submissions", regraded, submission_ids.len());
949
950 (
951 StatusCode::OK,
952 Json(ApiResponse::success(response, &message)),
953 )
954}
955
956pub async fn resubmit_submissions(
1033 State(app_state): State<AppState>,
1034 Path((module_id, assignment_id)): Path<(i64, i64)>,
1035 Extension(AuthUser(_claims)): Extension<AuthUser>,
1036 Json(req): Json<ResubmitRequest>,
1037) -> impl IntoResponse {
1038 let db = app_state.db();
1039
1040 if let Err(e) = validate_bulk_request(&req.submission_ids, &req.all) {
1041 return (
1042 StatusCode::BAD_REQUEST,
1043 Json(ApiResponse::<ResubmitResponse>::error(e)),
1044 );
1045 }
1046
1047 let assignment = match load_assignment(module_id, assignment_id, db).await {
1048 Ok(assignment) => assignment,
1049 Err(_) => {
1050 return (
1051 StatusCode::NOT_FOUND,
1052 Json(ApiResponse::<ResubmitResponse>::error(
1053 "Assignment not found",
1054 )),
1055 );
1056 }
1057 };
1058
1059 let submission_ids =
1060 match resolve_submission_ids(req.submission_ids, req.all, assignment_id, db).await {
1061 Ok(ids) => ids,
1062 Err(e) => {
1063 return (
1064 StatusCode::INTERNAL_SERVER_ERROR,
1065 Json(ApiResponse::<ResubmitResponse>::error(e)),
1066 );
1067 }
1068 };
1069
1070 if let Err(e) = load_assignment_allocator(assignment.module_id, assignment.id).await {
1071 return (
1072 StatusCode::INTERNAL_SERVER_ERROR,
1073 Json(ApiResponse::<ResubmitResponse>::error(e)),
1074 );
1075 }
1076
1077 let (base_path, mark_allocator_path, memo_outputs) =
1078 match get_assignment_paths(assignment.module_id, assignment.id) {
1079 Ok(paths) => paths,
1080 Err(e) => {
1081 return (
1082 StatusCode::INTERNAL_SERVER_ERROR,
1083 Json(ApiResponse::<ResubmitResponse>::error(e)),
1084 );
1085 }
1086 };
1087
1088 let config = match get_execution_config(module_id, assignment_id) {
1089 Ok(config) => config,
1090 Err(e) => {
1091 return (
1092 StatusCode::INTERNAL_SERVER_ERROR,
1093 Json(ApiResponse::<ResubmitResponse>::error(e)),
1094 );
1095 }
1096 };
1097
1098 let (resubmitted, failed) =
1099 execute_bulk_operation(submission_ids.clone(), assignment_id, db, |submission| {
1100 let db = db.clone();
1101 let assignment = assignment.clone();
1102 let base_path = base_path.clone();
1103 let memo_outputs = memo_outputs.clone();
1104 let mark_allocator_path = mark_allocator_path.clone();
1105 let config = config.clone();
1106 async move {
1107 if let Err(e) = clear_submission_output(&submission, &base_path) {
1108 return Err(e);
1109 }
1110 if let Err(e) = process_submission_code(
1111 &db,
1112 submission.id,
1113 config.clone(),
1114 module_id,
1115 assignment_id,
1116 )
1117 .await
1118 {
1119 return Err(format!("Failed to run code for submission: {}", e));
1120 }
1121
1122 grade_submission(
1123 submission,
1124 &assignment,
1125 &base_path,
1126 &memo_outputs,
1127 &mark_allocator_path,
1128 &config,
1129 &db,
1130 )
1131 .await
1132 .map(|_| ())
1133 }
1134 })
1135 .await;
1136
1137 let response = ResubmitResponse {
1138 resubmitted,
1139 failed,
1140 };
1141 let message = format!(
1142 "Resubmitted {}/{} submissions",
1143 resubmitted,
1144 submission_ids.len()
1145 );
1146
1147 (
1148 StatusCode::OK,
1149 Json(ApiResponse::success(response, &message)),
1150 )
1151}