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 is_partially_registered: bool,
55 pub enrolment_link: Option<String>,
57 pub completion_eligible: bool,
58 pub has_verified_student_number: bool,
59 pub course_code_allowed: bool,
60}
61
62impl StudentCreditRegistration {
63 pub fn preconditions(&self) -> PendingPreconditions {
65 PendingPreconditions {
66 completion_eligible: self.completion_eligible,
67 has_verified_student_number: self.has_verified_student_number,
68 course_code_allowed: self.course_code_allowed,
69 }
70 }
71
72 pub fn has_usable_enrolment(&self) -> bool {
79 self.enrolment_resolved && self.state != CreditRegistrationState::NoUsableEnrolment
80 }
81
82 pub fn is_waiting_for_enrolment(&self) -> bool {
84 is_waiting_for_enrolment(
85 self.state,
86 self.enrolment_check_anchor_at,
87 self.no_usable_enrolment_since,
88 )
89 }
90
91 pub fn is_requested_check_unanswered(&self) -> bool {
94 self.state == CreditRegistrationState::NoUsableEnrolment
95 && self.enrolment_check_source.is_request()
96 }
97}
98
99#[derive(Debug, Default, Clone, Copy)]
101pub struct StudentRegistrationFilter {
102 pub course_module_id: Option<Uuid>,
103 pub course_id: Option<Uuid>,
104 pub enrolment_banner_due: bool,
107}
108
109pub async fn get_student_facing_by_user_id(
112 conn: &mut PgConnection,
113 user_id: Uuid,
114 filter: StudentRegistrationFilter,
115) -> ModelResult<Vec<StudentCreditRegistration>> {
116 let res = sqlx::query_as!(
117 StudentCreditRegistration,
118 r#"
119SELECT cr.id,
120 cr.course_id,
121 c.name AS course_name,
122 c.slug AS course_slug,
123 cr.course_module_id,
124 cm.name AS course_module_name,
125 cm.uh_course_code,
126 cm.ects_credits,
127 cr.course_module_completion_id,
128 cr.created_at,
129 cmc.completion_date,
130 cr.state,
131 cr.error_code AS "error_code?",
132 cr.next_attempt_at,
133 cr.registered_at,
134 cr.sisu_attainment_id,
135 cr.student_number,
136 cr.credits,
137 cr.grade_id,
138 cr.grade_scale_id,
139 cr.attempt_number,
140 cr.superseded_by_id,
141 cr.superseded_at,
142 cr.enrolment_checked_at,
143 cr.enrolment_check_requested_at,
144 cr.no_usable_enrolment_since,
145 cr.enrolment_check_anchor_at,
146 cr.enrolment_check_source,
147 cr.selected_enrolment_id IS NOT NULL AS "enrolment_resolved!",
148 COALESCE(
149 cr.selected_enrolment_realisation_name->>'fi',
150 cr.selected_enrolment_realisation_name->>'en',
151 cr.selected_enrolment_realisation_name->>'sv'
152 ) AS "enrolment_realisation_name?",
153 cr.submitted_at,
154 cr.partially_registered_at IS NOT NULL AS "is_partially_registered!",
155 NULLIF(TRIM(cm.completion_registration_link_override), '') AS "enrolment_link?",
156 p.completion_eligible AS "completion_eligible!",
157 p.has_verified_student_number AS "has_verified_student_number!",
158 p.course_code_allowed AS "course_code_allowed!"
159FROM credit_registrations cr
160 JOIN courses c ON c.id = cr.course_id AND c.deleted_at IS NULL
161 JOIN course_modules cm ON cm.id = cr.course_module_id AND cm.deleted_at IS NULL
162 JOIN course_module_completions cmc ON cmc.id = cr.course_module_completion_id AND cmc.deleted_at IS NULL
163 JOIN credit_registration_preconditions p ON p.credit_registration_id = cr.id
164WHERE cr.user_id = $1
165 AND cr.deleted_at IS NULL
166 AND ($2::uuid IS NULL OR cr.course_module_id = $2)
167 AND ($3::uuid IS NULL OR cr.course_id = $3)
168 AND (
169 NOT $4::boolean
170 OR (
171 cr.state = 'no_usable_enrolment'
172 AND cr.enrolment_banner_dismissed_at IS NULL
173 )
174 )
175ORDER BY cmc.completion_date DESC,
176 cr.attempt_number DESC
177 "#,
178 user_id,
179 filter.course_module_id,
180 filter.course_id,
181 filter.enrolment_banner_due,
182 )
183 .fetch_all(conn)
184 .await?;
185 Ok(res)
186}
187
188pub async fn dismiss_enrolment_banner(
190 conn: &mut PgConnection,
191 id: Uuid,
192 user_id: Uuid,
193) -> ModelResult<()> {
194 sqlx::query!(
195 r#"
196UPDATE credit_registrations
197SET enrolment_banner_dismissed_at = now()
198WHERE id = $1
199 AND user_id = $2
200 AND deleted_at IS NULL
201 "#,
202 id,
203 user_id,
204 )
205 .execute(conn)
206 .await?;
207 Ok(())
208}