1use axum::{
26 extract::{State, Path, Query},
27 http::StatusCode,
28 response::{IntoResponse, Json},
29};
30use chrono::{DateTime, Utc};
31use serde::{Deserialize, Serialize};
32use sea_orm::{
33 ColumnTrait, Condition, EntityTrait, PaginatorTrait, QueryFilter,
34 QueryOrder, sea_query::Expr,
35};
36use util::state::AppState;
37use crate::response::ApiResponse;
38use crate::routes::modules::assignments::common::{File, AssignmentResponse};
39use db::{
40 models::{
41 assignment::{
42 self, AssignmentType, Column as AssignmentColumn, Entity as AssignmentEntity, Model as AssignmentModel
43 },
44 assignment_file,
45 assignment_submission,
46 user
47 },
48};
49
50#[derive(Debug, Serialize, Deserialize)]
51pub struct AssignmentFileResponse {
52 pub assignment: AssignmentResponse,
53 pub files: Vec<File>,
54}
55
56impl From<AssignmentModel> for AssignmentFileResponse {
57 fn from(assignment: AssignmentModel) -> Self {
58 Self {
59 assignment: AssignmentResponse {
60 id: assignment.id,
61 module_id: assignment.module_id as i64,
62 name: assignment.name,
63 description: assignment.description,
64 status: assignment.status.to_string(),
65 assignment_type: assignment.assignment_type.to_string(),
66 available_from: assignment.available_from.to_rfc3339(),
67 due_date: assignment.due_date.to_rfc3339(),
68 created_at: assignment.created_at.to_rfc3339(),
69 updated_at: assignment.updated_at.to_rfc3339(),
70 },
71 files: Vec::new(),
72 }
73 }
74}
75
76pub async fn get_assignment(
132 State(app_state): State<AppState>,
133 Path((module_id, assignment_id)): Path<(i64, i64)>,
134) -> impl IntoResponse {
135 let db = app_state.db();
136
137 let assignment_res = assignment::Entity::find()
138 .filter(assignment::Column::Id.eq(assignment_id as i32))
139 .filter(assignment::Column::ModuleId.eq(module_id as i32))
140 .one(db)
141 .await;
142
143 match assignment_res {
144 Ok(Some(a)) => {
145 let files_res = assignment_file::Entity::find()
146 .filter(assignment_file::Column::AssignmentId.eq(a.id))
147 .all(db)
148 .await;
149
150 match files_res {
151 Ok(files) => {
152 let converted_files: Vec<File> = files
153 .into_iter()
154 .map(|f| File {
155 id: f.id.to_string(),
156 filename: f.filename,
157 path: f.path,
158 file_type: f.file_type.to_string(),
159 created_at: f.created_at.to_rfc3339(),
160 updated_at: f.updated_at.to_rfc3339(),
161 })
162 .collect();
163
164 let response = AssignmentFileResponse {
165 assignment: AssignmentResponse::from(a),
166 files: converted_files,
167 };
168
169 (
170 StatusCode::OK,
171 Json(ApiResponse::success(
172 response,
173 "Assignment retrieved successfully",
174 )),
175 )
176 }
177 Err(e) => (
178 StatusCode::INTERNAL_SERVER_ERROR,
179 Json(ApiResponse::<AssignmentFileResponse>::error(&format!(
180 "Failed to retrieve files: {}",
181 e
182 ))),
183 ),
184 }
185 }
186 Ok(None) => (
187 StatusCode::NOT_FOUND,
188 Json(ApiResponse::<AssignmentFileResponse>::error(
189 "Assignment not found",
190 )),
191 ),
192 Err(e) => (
193 StatusCode::INTERNAL_SERVER_ERROR,
194 Json(ApiResponse::<AssignmentFileResponse>::error(&format!(
195 "An error occurred: {}",
196 e
197 ))),
198 ),
199 }
200}
201
202#[derive(Debug, Deserialize)]
203pub struct FilterReq {
204 pub page: Option<i32>,
205 pub per_page: Option<i32>,
206 pub sort: Option<String>,
207 pub query: Option<String>,
208 pub name: Option<String>,
209 pub assignment_type: Option<String>,
210 pub available_before: Option<String>,
211 pub available_after: Option<String>,
212 pub due_before: Option<String>,
213 pub due_after: Option<String>,
214}
215
216#[derive(Serialize)]
217pub struct FilterResponse {
218 pub assignments: Vec<AssignmentResponse>,
219 pub page: i32,
220 pub per_page: i32,
221 pub total: i32,
222}
223
224impl FilterResponse {
225 fn new(
226 assignments: Vec<AssignmentResponse>,
227 page: i32,
228 per_page: i32,
229 total: i32,
230 ) -> Self {
231 Self {
232 assignments,
233 page,
234 per_page,
235 total,
236 }
237 }
238}
239
240pub async fn get_assignments(
305 State(app_state): State<AppState>,
306 Path(module_id): Path<i64>,
307 Query(params): Query<FilterReq>,
308) -> impl IntoResponse {
309 let db = app_state.db();
310
311 let page = params.page.unwrap_or(1).max(1);
312 let per_page = params.per_page.unwrap_or(20).min(100).max(1);
313
314 if let Some(sort_field) = ¶ms.sort {
315 let valid_fields = [
316 "name",
317 "description",
318 "due_date",
319 "available_from",
320 "assignment_type",
321 "created_at",
322 "updated_at",
323 ];
324 for field in sort_field.split(',') {
325 let field = field.trim().trim_start_matches('-');
326 if !valid_fields.contains(&field) {
327 return (
328 StatusCode::BAD_REQUEST,
329 Json(ApiResponse::<FilterResponse>::error("Invalid field used")),
330 );
331 }
332 }
333 }
334
335 let mut condition = Condition::all().add(AssignmentColumn::ModuleId.eq(module_id as i32));
336
337 if let Some(ref query) = params.query {
338 let pattern = format!("%{}%", query.to_lowercase());
339 condition = condition.add(
340 Condition::any()
341 .add(Expr::cust("LOWER(name)").like(&pattern))
342 .add(Expr::cust("LOWER(description)").like(&pattern)),
343 );
344 }
345
346 if let Some(ref name) = params.name {
347 let pattern = format!("%{}%", name.to_lowercase());
348 condition = condition.add(Expr::cust("LOWER(name)").like(&pattern));
349 }
350
351 if let Some(ref assignment_type) = params.assignment_type {
352 match assignment_type.parse::<AssignmentType>() {
353 Ok(atype_enum) => {
354 condition = condition.add(AssignmentColumn::AssignmentType.eq(atype_enum));
355 }
356 Err(_) => {
357 return (
358 StatusCode::BAD_REQUEST,
359 Json(ApiResponse::<FilterResponse>::error(
360 "Invalid assignment_type",
361 )),
362 );
363 }
364 }
365 }
366
367 if let Some(ref before) = params.available_before {
368 if let Ok(date) = DateTime::parse_from_rfc3339(before) {
369 condition = condition.add(AssignmentColumn::AvailableFrom.lt(date.with_timezone(&Utc)));
370 }
371 }
372
373 if let Some(ref after) = params.available_after {
374 if let Ok(date) = DateTime::parse_from_rfc3339(after) {
375 condition = condition.add(AssignmentColumn::AvailableFrom.gt(date.with_timezone(&Utc)));
376 }
377 }
378
379 if let Some(ref before) = params.due_before {
380 if let Ok(date) = DateTime::parse_from_rfc3339(before) {
381 condition = condition.add(AssignmentColumn::DueDate.lt(date.with_timezone(&Utc)));
382 }
383 }
384
385 if let Some(ref after) = params.due_after {
386 if let Ok(date) = DateTime::parse_from_rfc3339(after) {
387 condition = condition.add(AssignmentColumn::DueDate.gt(date.with_timezone(&Utc)));
388 }
389 }
390
391 let mut query = AssignmentEntity::find().filter(condition);
392
393 if let Some(sort_param) = ¶ms.sort {
394 for sort in sort_param.split(',') {
395 let (field, asc) = if sort.starts_with('-') {
396 (&sort[1..], false)
397 } else {
398 (sort, true)
399 };
400
401 query = match field {
402 "name" => {
403 if asc {
404 query.order_by_asc(AssignmentColumn::Name)
405 } else {
406 query.order_by_desc(AssignmentColumn::Name)
407 }
408 }
409 "description" => {
410 if asc {
411 query.order_by_asc(AssignmentColumn::Description)
412 } else {
413 query.order_by_desc(AssignmentColumn::Description)
414 }
415 }
416 "due_date" => {
417 if asc {
418 query.order_by_asc(AssignmentColumn::DueDate)
419 } else {
420 query.order_by_desc(AssignmentColumn::DueDate)
421 }
422 }
423 "available_from" => {
424 if asc {
425 query.order_by_asc(AssignmentColumn::AvailableFrom)
426 } else {
427 query.order_by_desc(AssignmentColumn::AvailableFrom)
428 }
429 }
430 "assignment_type" => {
431 if asc {
432 query.order_by_asc(AssignmentColumn::AssignmentType)
433 } else {
434 query.order_by_desc(AssignmentColumn::AssignmentType)
435 }
436 }
437 "created_at" => {
438 if asc {
439 query.order_by_asc(AssignmentColumn::CreatedAt)
440 } else {
441 query.order_by_desc(AssignmentColumn::CreatedAt)
442 }
443 }
444 "updated_at" => {
445 if asc {
446 query.order_by_asc(AssignmentColumn::UpdatedAt)
447 } else {
448 query.order_by_desc(AssignmentColumn::UpdatedAt)
449 }
450 }
451 _ => query,
452 };
453 }
454 }
455
456 let paginator = query.clone().paginate(db, per_page as u64);
457 let total = match paginator.num_items().await {
458 Ok(n) => n as i32,
459 Err(e) => {
460 eprintln!("Error counting items: {:?}", e);
461 return (
462 StatusCode::INTERNAL_SERVER_ERROR,
463 Json(ApiResponse::<FilterResponse>::error("Error counting items")),
464 );
465 }
466 };
467
468 match paginator.fetch_page((page - 1) as u64).await {
469 Ok(results) => {
470 let assignments: Vec<AssignmentResponse> = results
471 .into_iter()
472 .map(AssignmentResponse::from)
473 .collect();
474
475 let response = FilterResponse::new(assignments, page, per_page, total);
476 (
477 StatusCode::OK,
478 Json(ApiResponse::success(
479 response,
480 "Assignments retrieved successfully",
481 )),
482 )
483 }
484 Err(err) => {
485 eprintln!("DB error: {:?}", err);
486 (
487 StatusCode::INTERNAL_SERVER_ERROR,
488 Json(ApiResponse::<FilterResponse>::error(
489 "Failed to retrieve assignments",
490 )),
491 )
492 }
493 }
494}
495
496#[derive(Debug, Serialize)]
497pub struct PerStudentSubmission {
498 pub user_id: i64,
499 pub username: String,
500 pub count: i8,
501 pub latest_at: DateTime<Utc>,
502 pub latest_late: bool
503}
504
505#[derive(Debug, Serialize)]
506pub struct StatResponse {
507 pub assignment_id: i64,
508 pub total_submissions: i8,
509 pub unique_submitters: i8,
510 pub late_submissions: i8,
511 pub per_student_submission_count: Vec<PerStudentSubmission>
512}
513
514pub fn is_late(submission: DateTime<Utc>, due_date: DateTime<Utc>) -> bool {
515 submission > due_date
516}
517
518pub async fn get_assignment_stats(
574 State(app_state): State<AppState>,
575 Path((module_id, assignment_id)): Path<(i64, i64)>
576) -> impl IntoResponse {
577 let db = app_state.db();
578
579 let assignment = match AssignmentEntity::find()
580 .filter(AssignmentColumn::Id.eq(assignment_id as i32))
581 .filter(AssignmentColumn::ModuleId.eq(module_id as i32))
582 .one(db)
583 .await
584 {
585 Ok(Some(a)) => a,
586 Ok(None) => {
587 return (
588 StatusCode::NOT_FOUND,
589 Json(ApiResponse::<StatResponse>::error("Assignment not found")),
590 )
591 .into_response();
592 }
593 Err(err) => {
594 eprintln!("DB error fetching assignment: {:?}", err);
595 return (
596 StatusCode::INTERNAL_SERVER_ERROR,
597 Json(ApiResponse::<StatResponse>::error("Database error")),
598 )
599 .into_response();
600 }
601 };
602
603 match assignment_submission::Entity::find()
604 .filter(assignment_submission::Column::AssignmentId.eq(assignment_id as i32))
605 .order_by_desc(assignment_submission::Column::CreatedAt)
606 .all(db)
607 .await
608 {
609 Ok(submissions) => {
610 use std::collections::HashMap;
611
612 let mut total_submissions = 0;
613 let mut late_submissions = 0;
614 let mut unique_users: HashMap<i64, Vec<DateTime<Utc>>> = HashMap::new(); for sub in &submissions {
617 total_submissions += 1;
618 if is_late(sub.created_at, assignment.due_date) {
619 late_submissions += 1;
620 }
621
622 unique_users
623 .entry(sub.user_id)
624 .or_insert_with(Vec::new)
625 .push(sub.created_at);
626 }
627
628 let user_ids: Vec<i64> = unique_users.keys().copied().collect();
629
630 let user_models = user::Entity::find()
631 .filter(user::Column::Id.is_in(user_ids.clone()))
632 .all(db)
633 .await;
634
635 let mut user_id_to_username = HashMap::new();
636 match user_models {
637 Ok(users) => {
638 for user in users {
639 user_id_to_username.insert(user.id, user.username);
640 }
641 }
642 Err(err) => {
643 eprintln!("DB error fetching student numbers: {:?}", err);
644 return (
645 StatusCode::INTERNAL_SERVER_ERROR,
646 Json(ApiResponse::<StatResponse>::error("Failed to fetch student numbers")),
647 )
648 .into_response();
649 }
650 }
651
652 let mut per_student_submission_count = Vec::new();
653
654 for (user_id, created_times) in unique_users.iter() {
655 let latest_at = *created_times.iter().max().unwrap();
656 let latest_late = is_late(latest_at, assignment.due_date);
657 let username = user_id_to_username
658 .get(user_id)
659 .cloned()
660 .unwrap_or_else(|| "UNKNOWN".to_string());
661
662 per_student_submission_count.push(PerStudentSubmission {
663 user_id: *user_id,
664 username,
665 count: created_times.len() as i8,
666 latest_at,
667 latest_late,
668 });
669 }
670
671 let response = StatResponse {
672 assignment_id,
673 total_submissions,
674 unique_submitters: unique_users.len() as i8,
675 late_submissions,
676 per_student_submission_count,
677 };
678
679 (
680 StatusCode::OK,
681 Json(ApiResponse::success(response, "Stats retrieved successfully")),
682 )
683 .into_response()
684 }
685 Err(err) => {
686 eprintln!("DB error fetching submissions for stats: {:?}", err);
687 (
688 StatusCode::INTERNAL_SERVER_ERROR,
689 Json(ApiResponse::<StatResponse>::error("Database error")),
690 )
691 .into_response()
692 }
693 }
694}
695
696#[derive(Debug, Serialize)]
697pub struct AssignmentReadiness {
698 pub config_present: bool,
699 pub tasks_present: bool,
700 pub main_present: bool,
701 pub memo_present: bool,
702 pub makefile_present: bool,
703 pub memo_output_present: bool,
704 pub mark_allocator_present: bool,
705 pub is_ready: bool,
706}
707
708pub async fn get_assignment_readiness(
750 State(app_state): State<AppState>,
751 Path((module_id, assignment_id)): Path<(i64, i64)>,
752) -> (StatusCode, Json<ApiResponse<AssignmentReadiness>>) {
753 let db = app_state.db();
754
755 match AssignmentModel::compute_readiness_report(db, module_id, assignment_id).await {
756 Ok(report) => {
757 if report.is_ready() {
758 if let Err(e) =
759 AssignmentModel::try_transition_to_ready(db, module_id, assignment_id).await
760 {
761 tracing::warn!(
762 "Failed to transition assignment {} to Ready: {:?}",
763 assignment_id,
764 e
765 );
766 }
767 }
768
769 let response = AssignmentReadiness {
770 config_present: report.config_present,
771 tasks_present: report.tasks_present,
772 main_present: report.main_present,
773 memo_present: report.memo_present,
774 makefile_present: report.makefile_present,
775 memo_output_present: report.memo_output_present,
776 mark_allocator_present: report.mark_allocator_present,
777 is_ready: report.is_ready(),
778 };
779
780 (
781 StatusCode::OK,
782 Json(ApiResponse::success(
783 response,
784 "Assignment readiness checked successfully",
785 )),
786 )
787 }
788 Err(e) => (
789 StatusCode::INTERNAL_SERVER_ERROR,
790 Json(ApiResponse::<AssignmentReadiness>::error(&format!(
791 "Failed to compute readiness: {}",
792 e
793 ))),
794 ),
795 }
796}