Skip to main content

headless_lms_models/credit_registrations/
competing_credits.rs

1//! The other credits of a student's module that a new attempt is weighed against before it is sent.
2
3use super::state::CreditRegistrationState;
4use crate::library::credit_registration::grade_mapping::{GradeSource, MappedGrade, map_grade};
5use crate::prelude::*;
6
7/// Another live attempt, of any completion of the same student and module, that the study registry
8/// already holds.
9#[derive(Debug, Clone)]
10pub struct LiveSuccessForModule {
11    pub id: Uuid,
12    pub credit: RecordedCredit,
13}
14
15/// A credit for the student and module that our own records say the study registry holds.
16#[derive(Debug, Clone)]
17pub struct RecordedCredit {
18    /// `None` for a credit the pull path registered, which freezes nothing.
19    pub grade_scale_id: Option<String>,
20    pub grade_id: Option<String>,
21    pub completion_passed: bool,
22    pub completion_grade: Option<i32>,
23}
24
25impl RecordedCredit {
26    /// The grade the registry holds: the frozen one, or for a credit with nothing frozen (settled
27    /// before a freeze, or registered by the pull path), its completion's, which the registry held
28    /// at least as well.
29    pub fn held_grade(&self) -> Option<MappedGrade> {
30        MappedGrade::from_columns(self.grade_scale_id.as_deref(), self.grade_id.as_deref()).or_else(
31            || {
32                map_grade(GradeSource {
33                    passed: self.completion_passed,
34                    grade: self.completion_grade,
35                    enrolment_grade_scale_id: None,
36                })
37                .ok()
38            },
39        )
40    }
41}
42
43/// Every credit for the row's student and module that our records say the registry holds: another
44/// of our live attempts in a success state, or a registrar's pull-path registration.
45///
46/// For an answer that lists no existing attainments. Not [`lock_live_successes_for_same_module`],
47/// which locks, and leaves out the pull path because nothing there can be marked for replacement.
48pub async fn get_recorded_credits_for_same_module(
49    conn: &mut PgConnection,
50    id: Uuid,
51) -> ModelResult<Vec<RecordedCredit>> {
52    let res = sqlx::query_as!(
53        RecordedCredit,
54        r#"
55SELECT other.grade_scale_id::text AS "grade_scale_id?",
56  other.grade_id::text AS "grade_id?",
57  cmc.passed AS "completion_passed!",
58  cmc.grade AS "completion_grade?"
59FROM credit_registrations cr
60  JOIN credit_registrations other ON other.user_id = cr.user_id
61  AND other.course_module_id = cr.course_module_id
62  AND other.id <> cr.id
63  JOIN course_module_completions cmc ON cmc.id = other.course_module_completion_id
64WHERE cr.id = $1
65  AND other.deleted_at IS NULL
66  AND other.superseded_by_id IS NULL
67  AND other.state = ANY($2::credit_registration_state [])
68UNION ALL
69SELECT NULL::text,
70  NULL::text,
71  cmc.passed,
72  cmc.grade
73FROM credit_registrations cr
74  JOIN course_module_completion_registered_to_study_registries r ON r.user_id = cr.user_id
75  AND r.course_module_id = cr.course_module_id
76  JOIN course_module_completions cmc ON cmc.id = r.course_module_completion_id
77WHERE cr.id = $1
78  -- A null registrar is our own mirror of a row the first half already reads.
79  AND r.study_registry_registrar_id IS NOT NULL
80  AND r.deleted_at IS NULL
81        "#,
82        id,
83        &CreditRegistrationState::SUCCESS_STATES as &[CreditRegistrationState],
84    )
85    .fetch_all(conn)
86    .await?;
87    Ok(res)
88}
89
90/// Readies a row that resolve-enrolments settles as `duplicate` without sending it, in the caller's
91/// transaction and before the transition.
92///
93/// Drops the row's claim on `uq_credit_registrations_person_module`, left from an earlier freeze,
94/// since the slot belongs to the credit the registry holds, and the frozen credits, which nothing
95/// registered. Keeps `weighed_grade`, the grade we would have sent, as the grade a later regrade of
96/// the completion has to beat.
97pub async fn prepare_unsent_duplicate(
98    conn: &mut PgConnection,
99    id: Uuid,
100    weighed_grade: Option<&MappedGrade>,
101) -> ModelResult<()> {
102    sqlx::query!(
103        r#"
104UPDATE credit_registrations
105SET sisu_person_id = NULL,
106  grade_scale_id = $2,
107  grade_id = $3,
108  credits = NULL
109WHERE id = $1
110  AND deleted_at IS NULL
111        "#,
112        id,
113        weighed_grade.map(|grade| grade.grade_scale_id.as_str()),
114        weighed_grade.map(|grade| grade.grade_id.as_str()),
115    )
116    .execute(conn)
117    .await?;
118    Ok(())
119}
120
121/// The student's other live attempts for the row's module that the registry already holds, locked
122/// until the caller's transaction ends.
123///
124/// What another attempt has to beat to be sent, and what it replaces when it does, through
125/// [`mark_pending_superseded`](super::mark_pending_superseded). Includes rows already waiting on an
126/// earlier replacement, which a better one takes over.
127pub async fn lock_live_successes_for_same_module(
128    conn: &mut PgConnection,
129    id: Uuid,
130) -> ModelResult<Vec<LiveSuccessForModule>> {
131    let rows = sqlx::query!(
132        r#"
133SELECT other.id,
134  other.grade_scale_id,
135  other.grade_id,
136  cmc.passed AS completion_passed,
137  cmc.grade AS completion_grade
138FROM credit_registrations cr
139  JOIN credit_registrations other ON other.user_id = cr.user_id
140  AND other.course_module_id = cr.course_module_id
141  AND other.id <> cr.id
142  JOIN course_module_completions cmc ON cmc.id = other.course_module_completion_id
143WHERE cr.id = $1
144  AND other.deleted_at IS NULL
145  AND other.superseded_by_id IS NULL
146  AND other.state = ANY($2::credit_registration_state [])
147ORDER BY other.id FOR
148UPDATE OF other
149        "#,
150        id,
151        &CreditRegistrationState::SUCCESS_STATES as &[CreditRegistrationState],
152    )
153    .fetch_all(conn)
154    .await?;
155    Ok(rows
156        .into_iter()
157        .map(|row| LiveSuccessForModule {
158            id: row.id,
159            credit: RecordedCredit {
160                grade_scale_id: row.grade_scale_id,
161                grade_id: row.grade_id,
162                completion_passed: row.completion_passed,
163                completion_grade: row.completion_grade,
164            },
165        })
166        .collect())
167}