api/routes/me/
assignments.rs

1//! # My Assignments Handlers
2//!
3//! Provides endpoints to fetch assignments for the currently authenticated user.
4//!
5//! Users can retrieve a paginated list of assignments filtered by role, year, status,
6//! search query, and sorted by various fields. Only assignments in modules the user
7//! is associated with are returned.
8
9use axum::{
10    Extension, Json,
11    extract::{Query, State},
12    http::StatusCode,
13    response::IntoResponse,
14};
15use db::models::{assignment, module, user_module_role};
16use migration::Expr;
17use sea_orm::{
18    ColumnTrait, Condition, EntityTrait, JoinType, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, RelationTrait
19};
20use serde::{Deserialize, Serialize};
21use util::state::AppState;
22use crate::{auth::AuthUser, response::ApiResponse};
23
24/// Query parameters for filtering, sorting, and pagination of assignments
25#[derive(Debug, Deserialize)]
26pub struct AssignmentFilterReq {
27    /// Page number (default: 1)
28    pub page: Option<i32>,
29    /// Items per page (default: 20)
30    pub per_page: Option<i32>,
31    /// Search query (matches assignment title or module code)
32    pub query: Option<String>,
33    /// Filter assignments by role (lecturer, assistant_lecturer, tutor, student)
34    pub role: Option<String>,
35    /// Filter by module year
36    pub year: Option<i32>,
37    /// Filter by assignment status
38    pub status: Option<String>,
39    /// Sort fields (comma-separated, prefix with `-` for descending)
40    pub sort: Option<String>,
41}
42
43/// Response object for a module
44#[derive(Serialize)]
45pub struct ModuleResponse {
46    pub id: i64,
47    pub code: String,
48}
49
50/// Response object for an assignment
51#[derive(Serialize)]
52pub struct AssignmentResponse {
53    pub id: i64,
54    pub title: String,
55    pub status: String,
56    pub available_from: String,
57    pub due_date: String,
58    pub created_at: String,
59    pub updated_at: String,
60    pub module: ModuleResponse,
61}
62
63/// Response for a paginated list of assignments
64#[derive(Serialize)]
65pub struct FilterAssignmentResponse {
66    pub assignments: Vec<AssignmentResponse>,
67    pub page: i32,
68    pub per_page: i32,
69    pub total: i32,
70}
71
72impl FilterAssignmentResponse {
73    fn new(assignments: Vec<AssignmentResponse>, page: i32, per_page: i32, total: i32) -> Self {
74        Self { assignments, page, per_page, total }
75    }
76}
77
78/// Retrieves assignments for the currently authenticated user.
79///
80/// **Endpoint:** `GET /my/assignments`  
81/// **Permissions:** User must be associated with at least one module (student, tutor, lecturer, assistant)
82///
83/// ### Query parameters
84/// - `page` → Page number (default: 1)
85/// - `per_page` → Number of items per page (default: 20, max: 100)
86/// - `query` → Search query in assignment title or module code
87/// - `role` → Filter assignments by user role
88/// - `year` → Filter assignments by module year
89/// - `status` → Filter assignments by assignment status
90/// - `sort` → Sort assignments by fields (e.g., `due_date,-available_from`)
91///
92/// ### Responses
93/// - `200 OK` → Assignments retrieved successfully
94/// ```json
95/// {
96///   "success": true,
97///   "data": {
98///     "assignments": [ /* Assignment objects */ ],
99///     "page": 1,
100///     "per_page": 20,
101///     "total": 42
102///   },
103///   "message": "Assignments retrieved"
104/// }
105/// ```
106/// - `500 Internal Server Error` → Failed to retrieve assignments
107/// ```json
108/// {
109///   "success": false,
110///   "data": null,
111///   "message": "Failed to retrieve assignments"
112/// }
113/// ```
114pub async fn get_my_assignments(
115    State(state): State<AppState>,
116    Extension(AuthUser(claims)): Extension<AuthUser>,
117    Query(params): Query<AssignmentFilterReq>,
118) -> impl IntoResponse {
119    let db = state.db();
120    let user_id = claims.sub;
121    let page = params.page.unwrap_or(1).max(1);
122    let per_page = params.per_page.unwrap_or(20).min(100);
123
124    let allowed_roles = vec!["lecturer", "assistant_lecturer", "tutor", "student"];
125    let requested_role = params.role.clone().filter(|r| allowed_roles.contains(&r.as_str()));
126
127    let memberships = user_module_role::Entity::find()
128        .filter(user_module_role::Column::UserId.eq(user_id))
129        .filter(user_module_role::Column::Role.is_in(allowed_roles.clone()))
130        .all(db)
131        .await
132        .unwrap_or_default();
133
134    if memberships.is_empty() {
135        let response = FilterAssignmentResponse::new(vec![], page, per_page, 0);
136        return (StatusCode::OK, Json(ApiResponse::success(response, "Assignments retrieved"))).into_response();
137    }
138
139    let module_ids: Vec<i64> = memberships.iter()
140        .filter(|m| requested_role.as_ref().map_or(true, |r| &m.role.to_string() == r))
141        .map(|m| m.module_id)
142        .collect();
143
144    if module_ids.is_empty() {
145        let response = FilterAssignmentResponse::new(vec![], page, per_page, 0);
146        return (StatusCode::OK, Json(ApiResponse::success(response, "Assignments retrieved"))).into_response();
147    }
148
149    let mut condition = Condition::all().add(assignment::Column::ModuleId.is_in(module_ids));
150
151    if let Some(year) = params.year {
152        condition = condition.add(Expr::col((module::Entity, module::Column::Year)).eq(year));
153    }
154
155    if let Some(ref status) = params.status {
156        condition = condition.add(assignment::Column::Status.eq(status));
157    }
158
159    if let Some(ref q) = params.query {
160        let pattern = format!("%{}%", q.to_lowercase());
161        condition = condition.add(
162            Condition::any()
163                .add(Expr::cust("LOWER(assignment.title)").like(&pattern))
164                .add(Expr::cust("LOWER(module.code)").like(&pattern))
165        );
166    }
167
168    let mut query = assignment::Entity::find()
169        .join(JoinType::InnerJoin, assignment::Relation::Module.def())
170        .filter(condition);
171
172    if let Some(sort_param) = &params.sort {
173        for sort in sort_param.split(',') {
174            let (field, asc) = if sort.starts_with('-') { (&sort[1..], false) } else { (sort, true) };
175            query = match field {
176                "due_date" => if asc { query.order_by_asc(assignment::Column::DueDate) } else { query.order_by_desc(assignment::Column::DueDate) },
177                "available_from" => if asc { query.order_by_asc(assignment::Column::AvailableFrom) } else { query.order_by_desc(assignment::Column::AvailableFrom) },
178                _ => query,
179            };
180        }
181    } else {
182        query = query.order_by_asc(assignment::Column::DueDate).order_by_asc(assignment::Column::Id);
183    }
184
185    let paginator = query.clone().paginate(db, per_page as u64);
186    let total = match paginator.num_items().await {
187        Ok(n) => n as i32,
188        Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<FilterAssignmentResponse>::error("Error counting assignments"))).into_response(),
189    };
190
191    match paginator.fetch_page((page - 1) as u64).await {
192        Ok(results) => {
193            let mut assignments_vec = Vec::new();
194            for a in results {
195                let m = module::Entity::find_by_id(a.module_id).one(db).await.unwrap_or(None);
196                if m.is_none() { continue; }
197                let m = m.unwrap();
198
199                assignments_vec.push(AssignmentResponse {
200                    id: a.id,
201                    title: a.name,
202                    status: a.status.to_string(),
203                    available_from: a.available_from.to_string(),
204                    due_date: a.due_date.to_string(),
205                    created_at: a.created_at.to_string(),
206                    updated_at: a.updated_at.to_string(),
207                    module: ModuleResponse { id: m.id, code: m.code },
208                });
209            }
210
211            let response = FilterAssignmentResponse::new(assignments_vec, page, per_page, total);
212            (StatusCode::OK, Json(ApiResponse::success(response, "Assignments retrieved"))).into_response()
213        }
214        Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<FilterAssignmentResponse>::error("Failed to retrieve assignments"))).into_response(),
215    }
216}