1use headless_lms_models::credit_registration_admin_actions::{
8 self, CreditRegistrationAdminAction, CreditRegistrationAdminActionFilters,
9 CreditRegistrationAdminActionListRow, CreditRegistrationAdminActionTarget,
10};
11use headless_lms_models::credit_registrations::CreditRegistrationState;
12use utoipa::ToSchema;
13
14use crate::prelude::*;
15
16use super::authorize_credit_registration_admin;
17
18#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
19pub struct CreditRegistrationAdminActionRow {
20 pub id: Uuid,
21 pub created_at: DateTime<Utc>,
22 pub action: CreditRegistrationAdminAction,
23 pub target_kind: CreditRegistrationAdminActionTarget,
24 pub target_id: Option<Uuid>,
27 pub target_phase: Option<String>,
28 pub actor_user_id: Uuid,
29 pub actor_first_name: Option<String>,
30 pub actor_last_name: Option<String>,
31 pub actor_email: Option<String>,
32 pub actor_role: String,
34 pub actor_course_id: Option<Uuid>,
36 pub course_name: Option<String>,
39 pub target_user_id: Option<Uuid>,
42 pub target_first_name: Option<String>,
43 pub target_last_name: Option<String>,
44 pub target_email: Option<String>,
46 pub reason: Option<String>,
47 pub before_state: Option<CreditRegistrationState>,
48 pub after_state: Option<CreditRegistrationState>,
49 pub details: Option<serde_json::Value>,
50 pub affected_row_count: Option<i32>,
51}
52
53#[derive(Debug, Deserialize)]
54pub struct ListAdminActionsQuery {
55 page: Option<u32>,
56 limit: Option<u32>,
57 action: Option<Vec<CreditRegistrationAdminAction>>,
58 actor_user_id: Option<Uuid>,
59 actor_role: Option<String>,
60 target_kind: Option<CreditRegistrationAdminActionTarget>,
61 target_id: Option<Uuid>,
62 target_phase: Option<String>,
63 user_id: Option<Uuid>,
64 course_id: Option<Uuid>,
65 from: Option<DateTime<Utc>>,
66 to: Option<DateTime<Utc>>,
67}
68
69#[instrument(skip(pool))]
77#[utoipa::path(
78 get,
79 path = "/audit",
80 operation_id = "listCreditRegistrationAdminActions",
81 tag = "credit-registration-admin",
82 params(
83 ("page" = Option<u32>, Query, description = "Page number, from 1"),
84 ("limit" = Option<u32>, Query, description = "Rows per page"),
85 ("action" = Option<Vec<CreditRegistrationAdminAction>>, Query, description = "Action kinds; repeat the parameter for several"),
86 ("actor_user_id" = Option<Uuid>, Query, description = "Who acted"),
87 ("actor_role" = Option<String>, Query, description = "global_admin or course_teacher"),
88 ("target_kind" = Option<CreditRegistrationAdminActionTarget>, Query, description = "What was acted on"),
89 ("target_id" = Option<Uuid>, Query, description = "One target row"),
90 ("target_phase" = Option<String>, Query, description = "One pipeline phase"),
91 ("user_id" = Option<Uuid>, Query, description = "Every action about this student: their registrations, student number links, tokens, and actions naming them or their student number"),
92 ("course_id" = Option<Uuid>, Query, description = "Actions on this course, and actions its teachers took"),
93 ("from" = Option<DateTime<Utc>>, Query, description = "Taken at or after"),
94 ("to" = Option<DateTime<Utc>>, Query, description = "Taken at or before")
95 ),
96 responses(
97 (status = 200, description = "A page of the action log", body = Page<CreditRegistrationAdminActionRow>)
98 )
99)]
100pub async fn list_credit_registration_admin_actions(
101 user: AuthUser,
102 pool: web::Data<PgPool>,
103 query: MultiQuery<ListAdminActionsQuery>,
104) -> ControllerResult<web::Json<Page<CreditRegistrationAdminActionRow>>> {
105 let mut conn = pool.acquire().await?;
106 let token = authorize_credit_registration_admin(&mut conn, user.id).await?;
107
108 let pagination = parse_pagination(query.page, query.limit, 50)?;
109 let actor_role = non_empty(query.actor_role.as_deref());
110 let target_phase = non_empty(query.target_phase.as_deref());
111 let filters = CreditRegistrationAdminActionFilters {
112 actions: query.action.as_deref(),
113 actor_user_id: query.actor_user_id,
114 actor_role,
115 target_kind: query.target_kind,
116 target_id: query.target_id,
117 target_phase,
118 user_id: query.user_id,
119 course_id: query.course_id,
120 from: query.from,
121 to: query.to,
122 };
123 let rows = credit_registration_admin_actions::get_page(
124 &mut conn,
125 &filters,
126 pagination.limit(),
127 pagination.offset(),
128 )
129 .await?;
130 let total_count = rows.first().map_or(0, |row| row.total_count);
131
132 token.authorized_ok(web::Json(Page::new(
133 pagination,
134 rows.into_iter().map(to_action_row).collect(),
135 total_count,
136 )))
137}
138
139fn to_action_row(row: CreditRegistrationAdminActionListRow) -> CreditRegistrationAdminActionRow {
140 CreditRegistrationAdminActionRow {
141 actor_first_name: row.actor_first_name,
142 actor_last_name: row.actor_last_name,
143 actor_email: row.actor_email,
144 course_name: row.course_name,
145 target_user_id: row.target_user_id,
146 target_first_name: row.target_first_name,
147 target_last_name: row.target_last_name,
148 target_email: row.target_email,
149 id: row.action.id,
150 created_at: row.action.created_at,
151 action: row.action.action,
152 target_kind: row.action.target_kind,
153 target_id: row.action.target_id,
154 target_phase: row.action.target_phase,
155 actor_user_id: row.action.actor_user_id,
156 actor_role: row.action.actor_role,
157 actor_course_id: row.action.actor_course_id,
158 reason: row.action.reason,
159 before_state: row.action.before_state,
160 after_state: row.action.after_state,
161 details: row.action.details,
162 affected_row_count: row.action.affected_row_count,
163 }
164}
165
166const EARLIER_REASON_LIMIT: i64 = 20;
168
169#[derive(Debug, Deserialize)]
171pub struct EarlierReasonsQuery {
172 action: CreditRegistrationAdminAction,
173}
174
175#[instrument(skip(pool))]
182#[utoipa::path(
183 get,
184 path = "/audit/earlier-reasons",
185 operation_id = "getCreditRegistrationEarlierReasons",
186 tag = "credit-registration-admin",
187 params(("action" = CreditRegistrationAdminAction, Query, description = "The action the reasons were given for")),
188 responses(
189 (status = 200, description = "Earlier reasons, most recently used first", body = Vec<String>)
190 )
191)]
192pub async fn get_credit_registration_earlier_reasons(
193 user: AuthUser,
194 pool: web::Data<PgPool>,
195 query: web::Query<EarlierReasonsQuery>,
196) -> ControllerResult<web::Json<Vec<String>>> {
197 let mut conn = pool.acquire().await?;
198 let token = authorize_credit_registration_admin(&mut conn, user.id).await?;
199 let reasons = credit_registration_admin_actions::get_earlier_reasons(
200 &mut conn,
201 query.action,
202 EARLIER_REASON_LIMIT,
203 )
204 .await?;
205 token.authorized_ok(web::Json(reasons))
206}
207
208pub fn _add_routes(cfg: &mut ServiceConfig) {
209 cfg.route(
210 "/audit",
211 web::get().to(list_credit_registration_admin_actions),
212 )
213 .route(
214 "/audit/earlier-reasons",
215 web::get().to(get_credit_registration_earlier_reasons),
216 );
217}