api/routes/modules/announcements/put.rs
1//! Edit announcement handler.
2//!
3//! Provides an endpoint to update an existing announcement for a specific module.
4//!
5//! **Permissions:** Only authorized users (lecturer/assistant) can edit announcements.
6
7use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, Json};
8use db::models::announcements::Model as AnnouncementModel;
9use crate::{response::ApiResponse, routes::modules::announcements::common::AnnouncementRequest};
10use util::state::AppState;
11
12/// PUT /api/modules/{module_id}/announcements/{announcement_id}
13///
14/// Updates a single announcement under the given module.
15///
16/// # AuthZ / AuthN
17/// - Requires a valid `Bearer` token (JWT).
18/// - Caller must be **lecturer** or **assistant_lecturer** on the module
19/// (enforced by `require_lecturer_or_assistant_lecturer` on this route).
20///
21/// # Path Parameters
22/// - `module_id` — ID of the parent module (used for nesting & auth).
23/// - `announcement_id` — ID of the announcement to update.
24///
25/// # Request Body
26/// JSON matching `AnnouncementRequest`:
27/// ```json
28/// {
29/// "title": "New title (or empty string to keep existing)",
30/// "body": "Updated body (or empty string to keep existing)",
31/// "pinned": true
32/// }
33/// ```
34///
35/// **Partial update semantics:**
36/// - `title`: if empty string `""`, the existing title is kept.
37/// - `body`: if empty string `""`, the existing body is kept.
38/// - `pinned`: always updated to the provided boolean.
39///
40/// # Example cURL
41/// ```bash
42/// curl -X PUT "https://your.api/api/modules/101/announcements/1234" \
43/// -H "Authorization: Bearer <JWT>" \
44/// -H "Content-Type: application/json" \
45/// -d '{"title":"Exam venue update","body":"**Hall A** instead of Hall B.","pinned":false}'
46/// ```
47///
48/// # Responses
49/// - `200 OK` — Returns the updated announcement.
50/// - `401 UNAUTHORIZED` — Missing/invalid token.
51/// - `403 FORBIDDEN` — Authenticated but not lecturer/assistant on this module.
52/// - `422 UNPROCESSABLE ENTITY` — Malformed/invalid JSON for `AnnouncementRequest`.
53/// - `500 INTERNAL SERVER ERROR` — Database error.
54///
55/// ## 200 OK — Example
56/// ```json
57/// {
58/// "success": true,
59/// "data": {
60/// "id": 1234,
61/// "module_id": 101,
62/// "user_id": 5,
63/// "title": "Exam venue update",
64/// "body": "**Hall A** instead of Hall B.",
65/// "pinned": false,
66/// "created_at": "2025-08-16T12:00:00Z",
67/// "updated_at": "2025-08-16T12:30:00Z"
68/// },
69/// "message": "Announcement updated successfully"
70/// }
71/// ```
72///
73/// ## 422 Unprocessable Entity — Example
74/// ```json
75/// {
76/// "success": false,
77/// "message": "Unprocessable Entity"
78/// }
79/// ```
80///
81/// ## 500 Internal Server Error — Example
82/// ```json
83/// {
84/// "success": false,
85/// "message": "Failed to update announcement"
86/// }
87/// ```
88pub async fn edit_announcement(
89 State(app_state): State<AppState>,
90 Path((_, announcement_id)): Path<(i64, i64)>,
91 Json(req): Json<AnnouncementRequest>,
92) -> impl IntoResponse {
93 let db = app_state.db();
94
95 match AnnouncementModel::update(db, announcement_id, &req.title, &req.body, req.pinned).await {
96 Ok(updated_announcement) => (
97 StatusCode::OK,
98 Json(ApiResponse::success(
99 updated_announcement,
100 "Announcement updated successfully",
101 )),
102 ),
103 Err(_) => (
104 StatusCode::INTERNAL_SERVER_ERROR,
105 Json(ApiResponse::error("Failed to update announcement")),
106 ),
107 }
108}