Skip to main content

headless_lms_server/controllers/main_frontend/course_credit_registrations/
actions.rs

1//! What has been done by hand to this course's credit registrations, so two teachers do not both
2//! click retry.
3
4use headless_lms_models::credit_registration_admin_actions::{
5    CreditRegistrationAdminAction, CreditRegistrationAdminActionFilters,
6    CreditRegistrationAdminActionTarget,
7};
8use headless_lms_models::credit_registrations::CreditRegistrationState;
9use std::collections::HashMap;
10use utoipa::ToSchema;
11
12use crate::prelude::*;
13
14/// Enough history to see what colleagues have been doing today without becoming an audit trail of
15/// its own; the global audit view is the admin dashboard's.
16const MAX_ACTIONS: i64 = 100;
17
18/// One audited manual action on this course, named rather than keyed: a teacher reads this to find
19/// out whether a colleague has already acted.
20#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
21pub struct CourseCreditRegistrationAction {
22    pub id: Uuid,
23    pub created_at: DateTime<Utc>,
24    pub action: CreditRegistrationAdminAction,
25    pub target_kind: CreditRegistrationAdminActionTarget,
26    pub target_id: Option<Uuid>,
27    pub actor_user_id: Uuid,
28    /// `global_admin` or `course_teacher`; support acting on the course looks different from a
29    /// colleague acting on it.
30    pub actor_role: String,
31    pub actor_first_name: Option<String>,
32    pub actor_last_name: Option<String>,
33    pub reason: Option<String>,
34    pub before_state: Option<CreditRegistrationState>,
35    pub after_state: Option<CreditRegistrationState>,
36    pub affected_row_count: Option<i32>,
37}
38
39/**
40GET `/api/v0/main-frontend/course-credit-registrations/courses/{course_id}/actions` - The manual
41actions taken on this course's credit registrations, newest first.
42*/
43#[instrument(skip(pool))]
44#[utoipa::path(
45    get,
46    path = "/courses/{course_id}/actions",
47    operation_id = "getCourseCreditRegistrationActions",
48    tag = "course-credit-registrations",
49    params(("course_id" = Uuid, Path, description = "Course id")),
50    responses(
51        (status = 200, description = "The course's manual actions", body = Vec<CourseCreditRegistrationAction>)
52    )
53)]
54pub async fn get_course_credit_registration_actions(
55    user: AuthUser,
56    pool: web::Data<PgPool>,
57    course_id: web::Path<Uuid>,
58) -> ControllerResult<web::Json<Vec<CourseCreditRegistrationAction>>> {
59    let mut conn = pool.acquire().await?;
60    let token =
61        super::authorize_credit_registration_teacher(&mut conn, user.id, *course_id).await?;
62
63    let records = models::credit_registration_admin_actions::get_page(
64        &mut conn,
65        &CreditRegistrationAdminActionFilters {
66            course_id: Some(*course_id),
67            ..Default::default()
68        },
69        MAX_ACTIONS,
70        0,
71    )
72    .await?
73    .into_iter()
74    .map(|row| row.action)
75    .collect::<Vec<_>>();
76    let actor_ids: Vec<Uuid> = records.iter().map(|record| record.actor_user_id).collect();
77    let actors: HashMap<Uuid, (Option<String>, Option<String>)> =
78        models::user_details::get_user_details_by_user_ids(&mut conn, &actor_ids)
79            .await?
80            .into_iter()
81            .map(|details| (details.user_id, (details.first_name, details.last_name)))
82            .collect();
83
84    let res = records
85        .into_iter()
86        .map(|record| {
87            let (actor_first_name, actor_last_name) = actors
88                .get(&record.actor_user_id)
89                .cloned()
90                .unwrap_or_default();
91            CourseCreditRegistrationAction {
92                actor_first_name,
93                actor_last_name,
94                id: record.id,
95                created_at: record.created_at,
96                action: record.action,
97                target_kind: record.target_kind,
98                target_id: record.target_id,
99                actor_user_id: record.actor_user_id,
100                actor_role: record.actor_role,
101                reason: record.reason,
102                before_state: record.before_state,
103                after_state: record.after_state,
104                affected_row_count: record.affected_row_count,
105            }
106        })
107        .collect();
108
109    token.authorized_ok(web::Json(res))
110}
111
112pub fn _add_routes(cfg: &mut ServiceConfig) {
113    cfg.route(
114        "/courses/{course_id}/actions",
115        web::get().to(get_course_credit_registration_actions),
116    );
117}