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}