Skip to main content

headless_lms_server/controllers/main_frontend/credit_registration_admin/
audit.rs

1//! The Audit tab: every hand action on the pipeline, whoever took it.
2//!
3//! Two actor kinds share this log. Teachers may retry and resend on their own course, so filtering
4//! by `actor_role` is what makes "which teachers are retrying, and on what" answerable at all — an
5//! admin acting while looking at a course is otherwise indistinguishable from the course's teacher.
6
7use 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, one_or_many};
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    /// `None` for a phase target, which is named by `target_phase`, and for a bulk action over a
25    /// selection, whose ids are in `details`.
26    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    /// `global_admin` or `course_teacher`.
33    pub actor_role: String,
34    /// The course whose edit permission authorised a teacher action.
35    pub actor_course_id: Option<Uuid>,
36    pub course_name: Option<String>,
37    pub reason: Option<String>,
38    pub before_state: Option<CreditRegistrationState>,
39    pub after_state: Option<CreditRegistrationState>,
40    pub details: Option<serde_json::Value>,
41    pub affected_row_count: Option<i32>,
42}
43
44#[derive(Debug, Deserialize)]
45pub struct ListAdminActionsQuery {
46    page: Option<u32>,
47    limit: Option<u32>,
48    #[serde(default, deserialize_with = "one_or_many")]
49    action: Option<Vec<CreditRegistrationAdminAction>>,
50    actor_user_id: Option<Uuid>,
51    actor_role: Option<String>,
52    target_kind: Option<CreditRegistrationAdminActionTarget>,
53    target_id: Option<Uuid>,
54    target_phase: Option<String>,
55    course_id: Option<Uuid>,
56    from: Option<DateTime<Utc>>,
57    to: Option<DateTime<Utc>>,
58}
59
60/**
61GET `/api/v0/main-frontend/credit-registration-admin/audit` - A page of the global action log,
62newest first.
63
64Covers admin and course-teacher actors alike, and every target kind: a registration, a course
65module, a course, a phase, a student-number link or its token.
66*/
67#[instrument(skip(pool))]
68#[utoipa::path(
69    get,
70    path = "/audit",
71    operation_id = "listCreditRegistrationAdminActions",
72    tag = "credit-registration-admin",
73    params(
74        ("page" = Option<u32>, Query, description = "Page number, from 1"),
75        ("limit" = Option<u32>, Query, description = "Rows per page"),
76        ("action" = Option<Vec<CreditRegistrationAdminAction>>, Query, description = "Action kinds; repeat the parameter for several"),
77        ("actor_user_id" = Option<Uuid>, Query, description = "Who acted"),
78        ("actor_role" = Option<String>, Query, description = "global_admin or course_teacher"),
79        ("target_kind" = Option<CreditRegistrationAdminActionTarget>, Query, description = "What was acted on"),
80        ("target_id" = Option<Uuid>, Query, description = "One target row"),
81        ("target_phase" = Option<String>, Query, description = "One pipeline phase"),
82        ("course_id" = Option<Uuid>, Query, description = "Actions on this course, and actions its teachers took"),
83        ("from" = Option<DateTime<Utc>>, Query, description = "Taken at or after"),
84        ("to" = Option<DateTime<Utc>>, Query, description = "Taken at or before")
85    ),
86    responses(
87        (status = 200, description = "A page of the action log", body = Page<CreditRegistrationAdminActionRow>)
88    )
89)]
90pub async fn list_credit_registration_admin_actions(
91    user: AuthUser,
92    pool: web::Data<PgPool>,
93    query: web::Query<ListAdminActionsQuery>,
94) -> ControllerResult<web::Json<Page<CreditRegistrationAdminActionRow>>> {
95    let mut conn = pool.acquire().await?;
96    let token = authorize_credit_registration_admin(&mut conn, user.id).await?;
97
98    let pagination = parse_pagination(query.page, query.limit, 50)?;
99    let actor_role = non_empty(query.actor_role.as_deref());
100    let target_phase = non_empty(query.target_phase.as_deref());
101    let filters = CreditRegistrationAdminActionFilters {
102        actions: query.action.as_deref(),
103        actor_user_id: query.actor_user_id,
104        actor_role,
105        target_kind: query.target_kind,
106        target_id: query.target_id,
107        target_phase,
108        course_id: query.course_id,
109        from: query.from,
110        to: query.to,
111    };
112    let rows = credit_registration_admin_actions::get_page(
113        &mut conn,
114        &filters,
115        pagination.limit(),
116        pagination.offset(),
117    )
118    .await?;
119    let total_count = rows.first().map_or(0, |row| row.total_count);
120
121    token.authorized_ok(web::Json(Page::new(
122        pagination,
123        rows.into_iter().map(to_action_row).collect(),
124        total_count,
125    )))
126}
127
128fn to_action_row(row: CreditRegistrationAdminActionListRow) -> CreditRegistrationAdminActionRow {
129    CreditRegistrationAdminActionRow {
130        actor_first_name: row.actor_first_name,
131        actor_last_name: row.actor_last_name,
132        actor_email: row.actor_email,
133        course_name: row.course_name,
134        id: row.action.id,
135        created_at: row.action.created_at,
136        action: row.action.action,
137        target_kind: row.action.target_kind,
138        target_id: row.action.target_id,
139        target_phase: row.action.target_phase,
140        actor_user_id: row.action.actor_user_id,
141        actor_role: row.action.actor_role,
142        actor_course_id: row.action.actor_course_id,
143        reason: row.action.reason,
144        before_state: row.action.before_state,
145        after_state: row.action.after_state,
146        details: row.action.details,
147        affected_row_count: row.action.affected_row_count,
148    }
149}
150
151pub fn _add_routes(cfg: &mut ServiceConfig) {
152    cfg.route(
153        "/audit",
154        web::get().to(list_credit_registration_admin_actions),
155    );
156}