headless_lms_models/credit_registrations/
student_view.rs1use super::registration::is_waiting_for_enrolment;
4use super::state::{CreditRegistrationErrorCode, CreditRegistrationState};
5use crate::library::credit_registration::PendingPreconditions;
6use crate::library::credit_registration::enrolment_check_schedule::EnrolmentCheckSource;
7use crate::prelude::*;
8
9#[derive(Debug, Clone)]
12pub struct StudentCreditRegistration {
13 pub id: Uuid,
14 pub course_id: Uuid,
15 pub course_name: String,
16 pub course_slug: String,
17 pub course_module_id: Uuid,
18 pub course_module_name: Option<String>,
19 pub uh_course_code: Option<String>,
20 pub ects_credits: Option<f32>,
21 pub course_module_completion_id: Uuid,
22 pub created_at: DateTime<Utc>,
23 pub completion_date: DateTime<Utc>,
24 pub state: CreditRegistrationState,
25 pub error_code: Option<CreditRegistrationErrorCode>,
26 pub next_attempt_at: DateTime<Utc>,
27 pub registered_at: Option<DateTime<Utc>>,
28 pub sisu_attainment_id: Option<String>,
29 pub student_number: Option<DbSecret>,
32 pub credits: Option<f32>,
33 pub grade_id: Option<String>,
34 pub grade_scale_id: Option<String>,
37 pub attempt_number: i32,
38 pub superseded_by_id: Option<Uuid>,
39 pub superseded_at: Option<DateTime<Utc>>,
40 pub enrolment_checked_at: Option<DateTime<Utc>>,
41 pub enrolment_check_requested_at: Option<DateTime<Utc>>,
44 pub no_usable_enrolment_since: Option<DateTime<Utc>>,
45 pub enrolment_check_anchor_at: Option<DateTime<Utc>>,
46 pub enrolment_check_source: EnrolmentCheckSource,
47 pub enrolment_resolved: bool,
50 pub enrolment_realisation_name: Option<String>,
52 pub submitted_at: Option<DateTime<Utc>>,
53 pub has_submitted_attainment: bool,
55 pub partially_registered_at: Option<DateTime<Utc>>,
58 pub enrolment_link: Option<String>,
60 pub completion_eligible: bool,
61 pub has_verified_student_number: bool,
62 pub course_code_allowed: bool,
63}
64
65impl StudentCreditRegistration {
66 pub fn preconditions(&self) -> PendingPreconditions {
68 PendingPreconditions {
69 completion_eligible: self.completion_eligible,
70 has_verified_student_number: self.has_verified_student_number,
71 course_code_allowed: self.course_code_allowed,
72 }
73 }
74
75 pub fn has_usable_enrolment(&self) -> bool {
82 self.enrolment_resolved && self.state != CreditRegistrationState::NoUsableEnrolment
83 }
84
85 pub fn is_waiting_for_enrolment(&self) -> bool {
87 is_waiting_for_enrolment(
88 self.state,
89 self.enrolment_check_anchor_at,
90 self.no_usable_enrolment_since,
91 )
92 }
93
94 pub fn is_prompted_check_unanswered(&self) -> bool {
98 self.state == CreditRegistrationState::NoUsableEnrolment
99 && self.enrolment_check_source.is_prompted()
100 }
101}
102
103#[derive(Debug, Default, Clone, Copy)]
105pub struct StudentRegistrationFilter {
106 pub course_module_id: Option<Uuid>,
107 pub course_id: Option<Uuid>,
108 pub enrolment_banner_due: bool,
111}
112
113pub async fn get_student_facing_by_user_id(
116 conn: &mut PgConnection,
117 user_id: Uuid,
118 filter: StudentRegistrationFilter,
119) -> ModelResult<Vec<StudentCreditRegistration>> {
120 let res = sqlx::query_as!(
121 StudentCreditRegistration,
122 r#"
123SELECT cr.id,
124 cr.course_id,
125 c.name AS course_name,
126 c.slug AS course_slug,
127 cr.course_module_id,
128 cm.name AS course_module_name,
129 cm.uh_course_code,
130 cm.ects_credits,
131 cr.course_module_completion_id,
132 cr.created_at,
133 cmc.completion_date,
134 cr.state,
135 cr.error_code AS "error_code?",
136 cr.next_attempt_at,
137 cr.registered_at,
138 cr.sisu_attainment_id,
139 cr.student_number,
140 cr.credits,
141 cr.grade_id,
142 cr.grade_scale_id,
143 cr.attempt_number,
144 cr.superseded_by_id,
145 cr.superseded_at,
146 cr.enrolment_checked_at,
147 cr.enrolment_check_requested_at,
148 cr.no_usable_enrolment_since,
149 cr.enrolment_check_anchor_at,
150 cr.enrolment_check_source,
151 cr.selected_enrolment_id IS NOT NULL AS "enrolment_resolved!",
152 COALESCE(
153 cr.selected_enrolment_realisation_name->>'fi',
154 cr.selected_enrolment_realisation_name->>'en',
155 cr.selected_enrolment_realisation_name->>'sv'
156 ) AS "enrolment_realisation_name?",
157 cr.submitted_at,
158 cr.submitted_attainment_id IS NOT NULL AS "has_submitted_attainment!",
159 cr.partially_registered_at,
160 NULLIF(TRIM(cm.completion_registration_link_override), '') AS "enrolment_link?",
161 p.completion_eligible AS "completion_eligible!",
162 p.has_verified_student_number AS "has_verified_student_number!",
163 p.course_code_allowed AS "course_code_allowed!"
164FROM credit_registrations cr
165 JOIN courses c ON c.id = cr.course_id AND c.deleted_at IS NULL
166 JOIN course_modules cm ON cm.id = cr.course_module_id AND cm.deleted_at IS NULL
167 JOIN course_module_completions cmc ON cmc.id = cr.course_module_completion_id AND cmc.deleted_at IS NULL
168 JOIN credit_registration_preconditions p ON p.credit_registration_id = cr.id
169WHERE cr.user_id = $1
170 AND cr.deleted_at IS NULL
171 AND ($2::uuid IS NULL OR cr.course_module_id = $2)
172 AND ($3::uuid IS NULL OR cr.course_id = $3)
173 AND (
174 NOT $4::boolean
175 OR (
176 cr.state = 'no_usable_enrolment'
177 AND cr.enrolment_banner_dismissed_at IS NULL
178 )
179 )
180ORDER BY cmc.completion_date DESC,
181 cr.attempt_number DESC
182 "#,
183 user_id,
184 filter.course_module_id,
185 filter.course_id,
186 filter.enrolment_banner_due,
187 )
188 .fetch_all(conn)
189 .await?;
190 Ok(res)
191}
192
193pub async fn dismiss_enrolment_banner(
195 conn: &mut PgConnection,
196 id: Uuid,
197 user_id: Uuid,
198) -> ModelResult<()> {
199 sqlx::query!(
200 r#"
201UPDATE credit_registrations
202SET enrolment_banner_dismissed_at = now()
203WHERE id = $1
204 AND user_id = $2
205 AND deleted_at IS NULL
206 "#,
207 id,
208 user_id,
209 )
210 .execute(conn)
211 .await?;
212 Ok(())
213}