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