api/routes/modules/assignments/
post.rs

1//! Assignment creation route.
2//!
3//! Provides an endpoint for creating a new assignment in a module:
4//! - `POST /api/modules/{module_id}/assignments`
5//!
6//! Key points:
7//! - Assignments are created in the `setup` state by default.
8//! - Only lecturers or admins assigned to the module can create assignments.
9//! - Dates must be in ISO 8601 format (RFC 3339).
10//! - `assignment_type` must be either `"assignment"` or `"practical"`.
11//!
12//! Responses include standard `200 OK`, `400 Bad Request` for validation errors, and `500 Internal Server Error` for database issues.
13
14use axum::{
15    extract::{State, Path},
16    http::StatusCode,
17    response::IntoResponse,
18    Json,
19};
20use chrono::{DateTime, Utc};
21use sea_orm::{DbErr};
22use util::state::AppState;
23use crate::response::ApiResponse;
24use db::{
25    models::{
26        assignment::{
27            AssignmentType,
28            Model as AssignmentModel,
29        }
30    },
31};
32use crate::routes::modules::assignments::common::{AssignmentRequest, AssignmentResponse};
33
34/// POST /api/modules/{module_id}/assignments
35///
36/// Create a new assignment in a module.  
37/// The assignment is always created in the `setup` state by default.  
38/// Only accessible by lecturers or admins assigned to the module.
39///
40/// ### Path Parameters
41/// - `module_id` (`i64`): The ID of the module to create the assignment in.
42///
43/// ### Request Body (JSON)
44/// - `name` (`string`, required): The name of the assignment.
45/// - `description` (`string`, optional): A description of the assignment.
46/// - `assignment_type` (`string`, required): The type of assignment. Must be either `"assignment"` or `"practical"`.
47/// - `available_from` (`string`, required): The date/time from which the assignment is available (ISO 8601 format).
48/// - `due_date` (`string`, required): The due date/time for the assignment (ISO 8601 format).
49///
50/// ### Responses
51///
52/// - `200 OK`
53/// ```json
54/// {
55///   "success": true,
56///   "message": "Assignment created successfully",
57///   "data": {
58///     "id": 123,
59///     "module_id": 456,
60///     "name": "Assignment 1",
61///     "description": "This is a sample assignment",
62///     "assignment_type": "Assignment",
63///     "available_from": "2024-01-01T00:00:00Z",
64///     "due_date": "2024-01-31T23:59:59Z",
65///     "created_at": "2024-01-01T00:00:00Z",
66///     "updated_at": "2024-01-01T00:00:00Z"
67///   }
68/// }
69/// ```
70///
71/// - `400 Bad Request`
72/// ```json
73/// {
74///   "success": false,
75///   "message": "Invalid available_from datetime" // or "Invalid due_date datetime" or "assignment_type must be 'assignment' or 'practical'"
76/// }
77/// ```
78///
79/// - `500 Internal Server Error`
80/// ```json
81/// {
82///   "success": false,
83///   "message": "Assignment could not be inserted" // or "Database error"
84/// }
85/// ```
86pub async fn create_assignment(
87    State(app_state): State<AppState>,
88    Path(module_id): Path<i64>,
89    Json(req): Json<AssignmentRequest>,
90) -> impl IntoResponse {
91    let db = app_state.db();
92
93    let available_from = match DateTime::parse_from_rfc3339(&req.available_from)
94        .map(|dt| dt.with_timezone(&Utc)) {
95        Ok(date) => date,
96        Err(_) => {
97            return (
98                StatusCode::BAD_REQUEST,
99                Json(ApiResponse::<AssignmentResponse>::error(
100                    "Invalid available_from datetime",
101                )),
102            );
103        }
104    };
105
106    let due_date = match DateTime::parse_from_rfc3339(&req.due_date)
107        .map(|dt| dt.with_timezone(&Utc)) {
108        Ok(date) => date,
109        Err(_) => {
110            return (
111                StatusCode::BAD_REQUEST,
112                Json(ApiResponse::<AssignmentResponse>::error(
113                    "Invalid due_date datetime",
114                )),
115            );
116        }
117    };
118
119    let assignment_type = match req.assignment_type.parse::<AssignmentType>() {
120        Ok(t) => t,
121        Err(_) => {
122            return (
123                StatusCode::BAD_REQUEST,
124                Json(ApiResponse::<AssignmentResponse>::error(
125                    "assignment_type must be 'assignment' or 'practical'",
126                )),
127            );
128        }
129    };
130
131    match AssignmentModel::create(
132        db,
133        module_id,
134        &req.name,
135        req.description.as_deref(),
136        assignment_type,
137        available_from,
138        due_date,
139    )
140    .await
141    {
142        Ok(model) => {
143            let response = AssignmentResponse::from(model);
144            (
145                StatusCode::CREATED,
146                Json(ApiResponse::success(
147                    response,
148                    "Assignment created successfully",
149                )),
150            )
151        }
152        Err(DbErr::Custom(msg)) => (
153            StatusCode::BAD_REQUEST,
154            Json(ApiResponse::<AssignmentResponse>::error(&msg)),
155        ),
156        Err(DbErr::RecordNotInserted) => (
157            StatusCode::INTERNAL_SERVER_ERROR,
158            Json(ApiResponse::<AssignmentResponse>::error(
159                "Assignment could not be inserted",
160            )),
161        ),
162        Err(e) => {
163            eprintln!("DB error: {:?}", e);
164            (
165                StatusCode::INTERNAL_SERVER_ERROR,
166                Json(ApiResponse::<AssignmentResponse>::error("Database error")),
167            )
168        }
169    }
170}