1use axum::{
2 extract::{Query, State},
3 http::StatusCode,
4 response::IntoResponse,
5 Extension, Json,
6};
7use common::format_validation_errors;
8use sea_orm::{
9 ColumnTrait, Condition, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, RelationTrait, QuerySelect, FromQueryResult, prelude::Expr,
10};
11use sea_orm_migration::prelude::Alias;
12use serde::{Deserialize, Serialize};
13use validator::Validate;
14
15use crate::{auth::claims::AuthUser, response::ApiResponse};
16use db::models::{
17 assignment,
18 assignment_submission::{self, Column as SubmissionColumn, Entity as SubmissionEntity},
19 module,
20 user,
21 user_module_role::{self, Column as RoleColumn, Role},
22};
23use util::state::AppState;
24
25#[derive(Debug, Deserialize, Validate)]
26pub struct GetSubmissionsQuery {
27 #[validate(range(min = 1))]
28 pub page: Option<u64>,
29 #[validate(range(min = 1, max = 100))]
30 pub per_page: Option<u64>,
31 pub query: Option<String>,
32 pub role: Option<Role>,
33 pub year: Option<i32>,
34 pub is_late: Option<bool>,
35 pub sort: Option<String>,
36}
37
38#[derive(Debug, Serialize)]
39pub struct SubmissionItem {
40 pub id: i64,
41 pub status: String,
42 pub score: Score,
43 pub created_at: String,
44 pub updated_at: String,
45 pub is_late: bool,
46 pub module: ModuleInfo,
47 pub assignment: AssignmentInfo,
48 pub user: UserInfo,
49}
50
51#[derive(Debug, Serialize)]
52pub struct Score {
53 pub earned: i64,
54 pub total: i64,
55}
56
57#[derive(Debug, Serialize)]
58pub struct ModuleInfo {
59 pub id: i64,
60 pub code: String,
61}
62
63#[derive(Debug, Serialize)]
64pub struct AssignmentInfo {
65 pub id: i64,
66 pub name: String,
67 pub description: Option<String>,
68}
69
70#[derive(Debug, Serialize)]
71pub struct UserInfo {
72 pub id: i64,
73 pub username: String,
74}
75
76#[derive(Debug, Serialize)]
77pub struct GetSubmissionsResponse {
78 pub submissions: Vec<SubmissionItem>,
79 pub page: u64,
80 pub per_page: u64,
81 pub total: u64,
82}
83
84#[derive(Debug, FromQueryResult)]
85pub struct SubmissionWithRelations {
86 pub id: i64,
87 pub earned: i64,
88 pub total: i64,
89 pub created_at: chrono::NaiveDateTime,
90 pub updated_at: chrono::NaiveDateTime,
91 pub user_id: i64,
92 pub assignment_id: i64,
93 pub assignment_name: String,
94 pub assignment_description: Option<String>,
95 pub assignment_due_date: chrono::NaiveDateTime,
96 pub module_id: i64,
97 pub module_code: String,
98 pub username: String,
99}
100
101pub async fn get_my_submissions(
153 State(app_state): State<AppState>,
154 Extension(user): Extension<AuthUser>,
155 Query(query): Query<GetSubmissionsQuery>,
156) -> impl IntoResponse {
157 if let Err(e) = query.validate() {
158 return (
159 StatusCode::BAD_REQUEST,
160 Json(ApiResponse::<()>::error(format_validation_errors(&e))),
161 )
162 .into_response();
163 }
164
165 let db = app_state.db();
166 let caller_id = user.0.sub;
167
168 let page = query.page.unwrap_or(1);
169 let per_page = query.per_page.unwrap_or(20);
170
171 let mut query_builder = SubmissionEntity::find()
172 .column_as(SubmissionColumn::Id, "id")
173 .column_as(SubmissionColumn::Earned, "earned")
174 .column_as(SubmissionColumn::Total, "total")
175 .column_as(SubmissionColumn::CreatedAt, "created_at")
176 .column_as(SubmissionColumn::UpdatedAt, "updated_at")
177 .column_as(SubmissionColumn::UserId, "user_id")
178 .column_as(SubmissionColumn::AssignmentId, "assignment_id")
179 .column_as(assignment::Column::Name, "assignment_name")
180 .column_as(assignment::Column::Description, "assignment_description")
181 .column_as(assignment::Column::DueDate, "assignment_due_date")
182 .column_as(module::Column::Id, "module_id")
183 .column_as(module::Column::Code, "module_code")
184 .column_as(user::Column::Username, "username")
185 .join(
186 sea_orm::JoinType::InnerJoin,
187 assignment_submission::Relation::Assignment.def(),
188 )
189 .join(
190 sea_orm::JoinType::InnerJoin,
191 assignment::Relation::Module.def(),
192 )
193 .join(
194 sea_orm::JoinType::InnerJoin,
195 assignment_submission::Relation::User.def(),
196 )
197 .join(
198 sea_orm::JoinType::InnerJoin,
199 module::Relation::UserModuleRole.def(),
200 )
201 .filter(user_module_role::Column::UserId.eq(caller_id));
202
203 let role_to_check = query.role.unwrap_or(Role::Student);
204
205 match role_to_check {
206 Role::Student => {
207 query_builder = query_builder.filter(SubmissionColumn::UserId.eq(caller_id));
208 }
209 Role::Lecturer | Role::AssistantLecturer | Role::Tutor => {
210 query_builder = query_builder.filter(RoleColumn::Role.eq(role_to_check));
211 }
212 }
213
214 let mut condition = Condition::all();
215
216 if let Some(q) = &query.query {
217 let pattern = format!("%{}%", q.to_lowercase());
218 condition = condition.add(
219 Condition::any()
220 .add(module::Column::Code.like(&pattern))
221 .add(user::Column::Username.like(&pattern))
222 .add(assignment::Column::Name.like(&pattern)),
223 );
224 }
225
226 if let Some(year) = query.year {
227 condition = condition.add(module::Column::Year.eq(year));
228 }
229
230 if let Some(is_late) = query.is_late {
231 let submission_alias = Alias::new("assignment_submissions");
232 let assignment_alias = Alias::new("assignments");
233
234 let created_at = Expr::col((submission_alias.clone(), SubmissionColumn::CreatedAt));
235 let due_date = Expr::col((assignment_alias.clone(), assignment::Column::DueDate));
236
237 if is_late {
238 condition = condition.add(created_at.gt(due_date));
239 } else {
240 condition = condition.add(created_at.lte(due_date));
241 }
242 }
243
244 query_builder = query_builder.filter(condition);
245
246 if let Some(sort) = &query.sort {
247 for s in sort.split(',') {
248 let (field, order) = if s.starts_with('-') {
249 (&s[1..], sea_orm::Order::Desc)
250 } else {
251 (s, sea_orm::Order::Asc)
252 };
253
254 match field {
255 "score" => {
256 query_builder = query_builder.order_by(
257 sea_orm::prelude::Expr::cust(
258 "COALESCE((earned * 1.0) / NULLIF(total, 0), 0)",
259 ),
260 order,
261 );
262 }
263 "created_at" => {
264 query_builder = query_builder.order_by(SubmissionColumn::CreatedAt, order);
265 }
266 _ => {}
267 }
268 }
269 } else {
270 query_builder = query_builder.order_by(SubmissionColumn::CreatedAt, sea_orm::Order::Desc);
271 }
272
273 query_builder = query_builder.order_by(SubmissionColumn::Id, sea_orm::Order::Asc);
274
275 let paginator = query_builder
276 .into_model::<SubmissionWithRelations>()
277 .paginate(db, per_page);
278
279 let total = paginator.num_items().await.unwrap_or(0);
280 let submissions_db: Vec<SubmissionWithRelations> = paginator
281 .fetch_page(page - 1)
282 .await
283 .unwrap_or_default();
284
285 let submissions: Vec<SubmissionItem> = submissions_db
286 .into_iter()
287 .map(|s| {
288 let is_late = s.created_at > s.assignment_due_date;
289
290 SubmissionItem {
291 id: s.id,
292 status: "submitted".to_string(),
293 score: Score {
294 earned: s.earned,
295 total: s.total,
296 },
297 created_at: s.created_at.to_string(),
298 updated_at: s.updated_at.to_string(),
299 is_late,
300 module: ModuleInfo {
301 id: s.module_id,
302 code: s.module_code,
303 },
304 assignment: AssignmentInfo {
305 id: s.assignment_id,
306 name: s.assignment_name,
307 description: s.assignment_description,
308 },
309 user: UserInfo {
310 id: s.user_id,
311 username: s.username,
312 },
313 }
314 })
315 .collect();
316
317 if submissions.is_empty() {
318 return (
319 StatusCode::OK,
320 Json(ApiResponse::success(
321 GetSubmissionsResponse {
322 submissions: vec![],
323 page,
324 per_page,
325 total: 0,
326 },
327 "No submissions found",
328 )),
329 ).into_response();
330 }
331
332 (
333 StatusCode::OK,
334 Json(ApiResponse::success(
335 GetSubmissionsResponse {
336 submissions,
337 page,
338 per_page,
339 total,
340 },
341 "Submissions retrieved",
342 )),
343 )
344 .into_response()
345}