Skip to main content

headless_lms_models/library/credit_registration/
pending_reason.rs

1//! Why a `pending` ledger row is still pending.
2//!
3//! The ledger records only that a row is waiting, never what for: the answer changes the moment the
4//! student links a number, and a stored copy would be a cache to keep true. Every
5//! surface that names the blocker derives it here, from the same facts
6//! `preconditions::pending_moves` decides the row's next state from.
7
8use utoipa::ToSchema;
9
10use crate::prelude::*;
11
12/// What a `pending` row is waiting for.
13#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Hash, ToSchema)]
14#[serde(rename_all = "snake_case")]
15pub enum CreditRegistrationPendingReason {
16    /// The completion is not registrable yet: a prerequisite module, or a suspected-cheating review.
17    Completion,
18    StudentNumber,
19}
20
21/// The preconditions a submission waits on, as they stand for one ledger row.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct PendingPreconditions {
24    pub completion_eligible: bool,
25    pub has_verified_student_number: bool,
26}
27
28impl PendingPreconditions {
29    /// Nothing outstanding, which is what a row about to leave `pending` looks like.
30    pub const ALL_MET: Self = Self {
31        completion_eligible: true,
32        has_verified_student_number: true,
33    };
34
35    /// The first unmet precondition, or `None` once all of them are met and the next precondition
36    /// tick moves the row on.
37    ///
38    /// The order is the recompute's own: it is what decides which single thing a student is asked
39    /// for, and asking for a student number before the completion is even registrable would be
40    /// asking early.
41    pub fn reason(self) -> Option<CreditRegistrationPendingReason> {
42        if !self.completion_eligible {
43            Some(CreditRegistrationPendingReason::Completion)
44        } else if !self.has_verified_student_number {
45            Some(CreditRegistrationPendingReason::StudentNumber)
46        } else {
47            None
48        }
49    }
50}
51
52/// Live `pending` rows per blocker, for the surfaces that used to read the three states off the
53/// ledger.
54#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Default, ToSchema)]
55pub struct PendingReasonCounts {
56    pub completion_count: i64,
57    pub student_number_count: i64,
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use CreditRegistrationPendingReason as Reason;
64
65    /// A student is asked for one thing at a time, in the order the pipeline needs them.
66    #[test]
67    fn the_first_unmet_precondition_is_the_one_reported() {
68        assert_eq!(PendingPreconditions::ALL_MET.reason(), None);
69        assert_eq!(
70            PendingPreconditions {
71                completion_eligible: false,
72                has_verified_student_number: false,
73            }
74            .reason(),
75            Some(Reason::Completion)
76        );
77        assert_eq!(
78            PendingPreconditions {
79                has_verified_student_number: false,
80                ..PendingPreconditions::ALL_MET
81            }
82            .reason(),
83            Some(Reason::StudentNumber)
84        );
85    }
86}