1use super::registration::{CreditRegistration, is_waiting_for_enrolment};
4use super::state::{CreditRegistrationErrorCode, CreditRegistrationState, ResubmissionFacts};
5use super::teacher_view::search_pattern_of;
6use crate::library::credit_registration::{CreditRegistrationPendingReason, PendingPreconditions};
7use crate::prelude::*;
8use crate::verified_student_numbers::StudentNumberVerificationMethod;
9
10#[derive(Debug, Clone)]
16pub struct AdminCreditRegistration {
17 pub id: Uuid,
18 pub created_at: DateTime<Utc>,
19 pub user_id: Uuid,
20 pub first_name: Option<String>,
21 pub last_name: Option<String>,
22 pub email: Option<String>,
24 pub course_id: Uuid,
25 pub course_name: String,
26 pub course_module_id: Uuid,
27 pub course_module_name: Option<String>,
28 pub course_instance_id: Uuid,
29 pub course_module_completion_id: Uuid,
30 pub completion_date: DateTime<Utc>,
31 pub state: CreditRegistrationState,
32 pub state_entered_at: DateTime<Utc>,
33 pub error_code: Option<CreditRegistrationErrorCode>,
34 pub needs_admin_attention: bool,
35 pub next_attempt_at: DateTime<Utc>,
36 pub last_attempt_at: Option<DateTime<Utc>>,
37 pub submitted_at: Option<DateTime<Utc>>,
38 pub registered_at: Option<DateTime<Utc>>,
39 pub terminal_at: Option<DateTime<Utc>>,
40 pub student_number: Option<DbSecret>,
42 pub sisu_person_id: Option<DbSecret>,
43 pub uh_course_code: Option<String>,
44 pub selected_enrolment_id: Option<String>,
45 pub grade_scale_id: Option<String>,
46 pub grade_id: Option<String>,
47 pub credits: Option<f32>,
48 pub submitted_attainment_id: Option<String>,
49 pub sisu_attainment_id: Option<String>,
50 pub submit_retry_count: i32,
51 pub verify_attempt_count: i32,
52 pub attempt_number: i32,
53 pub superseded_by_id: Option<Uuid>,
54 pub verified_student_number: Option<DbSecret>,
56 pub verified_student_number_at: Option<DateTime<Utc>>,
57 pub verified_student_number_via: Option<StudentNumberVerificationMethod>,
58 pub resubmit_not_before: Option<DateTime<Utc>>,
59 pub partially_registered_at: Option<DateTime<Utc>>,
60 pub not_registered_reimport_count: i32,
61 pub no_usable_enrolment_since: Option<DateTime<Utc>>,
62 pub enrolment_checked_at: Option<DateTime<Utc>>,
63 pub enrolment_check_anchor_at: Option<DateTime<Utc>>,
64 pub enrolment_check_due_at: Option<DateTime<Utc>>,
65 pub enrolment_checks_stopped_at: Option<DateTime<Utc>>,
66 pub completion_eligible: bool,
67 pub has_verified_student_number: bool,
68 pub course_code_allowed: bool,
69 pub total_count: i64,
72}
73
74impl AdminCreditRegistration {
75 pub fn resubmission_facts(&self) -> ResubmissionFacts {
77 ResubmissionFacts {
78 state: self.state,
79 is_superseded: self.superseded_by_id.is_some(),
80 error_code: self.error_code,
81 resubmit_not_before: self.resubmit_not_before,
82 submitted_at: self.submitted_at,
83 }
84 }
85
86 pub fn is_waiting_for_enrolment(&self) -> bool {
88 is_waiting_for_enrolment(
89 self.state,
90 self.enrolment_check_anchor_at,
91 self.no_usable_enrolment_since,
92 )
93 }
94
95 pub fn pending_reason(&self) -> Option<CreditRegistrationPendingReason> {
98 (self.state == CreditRegistrationState::Pending)
99 .then(|| {
100 PendingPreconditions {
101 completion_eligible: self.completion_eligible,
102 has_verified_student_number: self.has_verified_student_number,
103 course_code_allowed: self.course_code_allowed,
104 }
105 .reason()
106 })
107 .flatten()
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
113pub enum AdminCreditRegistrationSort {
114 #[default]
115 LastActivity,
116 Created,
117 TimeInState,
118 Attempts,
119}
120
121impl AdminCreditRegistrationSort {
122 fn as_str(self) -> &'static str {
124 match self {
125 Self::LastActivity => "last_activity",
126 Self::Created => "created",
127 Self::TimeInState => "time_in_state",
128 Self::Attempts => "attempts",
129 }
130 }
131}
132
133#[derive(Debug, Clone, Default)]
135pub struct AdminCreditRegistrationFilters<'a> {
136 pub states: Option<&'a [CreditRegistrationState]>,
137 pub error_codes: Option<&'a [CreditRegistrationErrorCode]>,
138 pub course_id: Option<Uuid>,
139 pub course_module_id: Option<Uuid>,
140 pub user_id: Option<Uuid>,
141 pub student_number: Option<&'a str>,
142 pub needs_admin_attention: bool,
143 pub submitted_after: Option<DateTime<Utc>>,
144 pub submitted_before: Option<DateTime<Utc>>,
145 pub search: Option<&'a str>,
148 pub search_id: Option<Uuid>,
152 pub id: Option<Uuid>,
154 pub course_module_completion_id: Option<Uuid>,
156 pub credit_registration_ids: Option<&'a [Uuid]>,
158 pub include_superseded: bool,
160}
161
162async fn admin_facing_page(
165 conn: &mut PgConnection,
166 filters: &AdminCreditRegistrationFilters<'_>,
167 sort: AdminCreditRegistrationSort,
168 limit: i64,
169 offset: i64,
170) -> ModelResult<Vec<AdminCreditRegistration>> {
171 let search_pattern = filters.search.map(search_pattern_of);
172 let res = sqlx::query_as!(
173 AdminCreditRegistration,
174 r#"
175SELECT cr.id,
176 cr.created_at,
177 cr.user_id,
178 ud.first_name AS "first_name?",
179 ud.last_name AS "last_name?",
180 ud.email AS "email?",
181 cr.course_id,
182 c.name AS course_name,
183 cr.course_module_id,
184 cm.name AS course_module_name,
185 cr.course_instance_id,
186 cr.course_module_completion_id,
187 cmc.completion_date,
188 cr.state,
189 cr.state_entered_at,
190 cr.error_code AS "error_code?",
191 cr.needs_admin_attention,
192 cr.next_attempt_at,
193 cr.last_attempt_at,
194 cr.submitted_at,
195 cr.registered_at,
196 cr.terminal_at,
197 cr.student_number,
198 cr.sisu_person_id,
199 cr.uh_course_code,
200 cr.selected_enrolment_id,
201 cr.grade_scale_id,
202 cr.grade_id,
203 cr.credits,
204 cr.submitted_attainment_id,
205 cr.sisu_attainment_id,
206 cr.submit_retry_count,
207 cr.verify_attempt_count,
208 cr.attempt_number,
209 cr.superseded_by_id,
210 vsn.student_number AS "verified_student_number?",
211 vsn.verified_at AS "verified_student_number_at?",
212 vsn.verified_via AS "verified_student_number_via?",
213 cr.resubmit_not_before,
214 cr.partially_registered_at,
215 cr.not_registered_reimport_count,
216 cr.no_usable_enrolment_since,
217 cr.enrolment_checked_at,
218 cr.enrolment_check_anchor_at,
219 cr.enrolment_check_due_at,
220 cr.enrolment_checks_stopped_at,
221 p.completion_eligible AS "completion_eligible!",
222 p.has_verified_student_number AS "has_verified_student_number!",
223 p.course_code_allowed AS "course_code_allowed!",
224 COUNT(*) OVER () AS "total_count!"
225FROM credit_registrations cr
226 JOIN courses c ON c.id = cr.course_id
227 JOIN course_modules cm ON cm.id = cr.course_module_id
228 JOIN course_module_completions cmc ON cmc.id = cr.course_module_completion_id
229 JOIN credit_registration_preconditions p ON p.credit_registration_id = cr.id
230 LEFT JOIN user_details ud ON ud.user_id = cr.user_id
231 LEFT JOIN verified_student_numbers vsn ON vsn.user_id = cr.user_id
232 AND vsn.deleted_at IS NULL
233WHERE cr.deleted_at IS NULL
234 AND ($1::bool OR cr.superseded_by_id IS NULL)
235 AND (
236 $2::credit_registration_state [] IS NULL
237 OR cr.state = ANY($2)
238 )
239 AND (
240 $3::credit_registration_error_code [] IS NULL
241 OR cr.error_code = ANY($3)
242 )
243 AND ($4::uuid IS NULL OR cr.course_id = $4)
244 AND ($5::uuid IS NULL OR cr.course_module_id = $5)
245 AND ($6::uuid IS NULL OR cr.user_id = $6)
246 AND (
247 $7::text IS NULL
248 OR cr.student_number = $7
249 OR vsn.student_number = $7
250 )
251 AND (NOT $8::bool OR cr.needs_admin_attention)
252 AND ($9::timestamptz IS NULL OR cr.submitted_at >= $9)
253 AND ($10::timestamptz IS NULL OR cr.submitted_at <= $10)
254 AND (
255 $11::text IS NULL
256 OR ud.name_search_helper LIKE '%' || $11 || '%' ESCAPE '\'
257 OR ud.email_search_helper LIKE '%' || $11 || '%' ESCAPE '\'
258 OR LOWER(cr.student_number) LIKE '%' || $11 || '%' ESCAPE '\'
259 OR LOWER(vsn.student_number) LIKE '%' || $11 || '%' ESCAPE '\'
260 OR LOWER(cr.submitted_attainment_id) LIKE '%' || $11 || '%' ESCAPE '\'
261 OR LOWER(cr.sisu_attainment_id) LIKE '%' || $11 || '%' ESCAPE '\'
262 OR LOWER(cr.error_message) LIKE '%' || $11 || '%' ESCAPE '\'
263 )
264 AND (
265 $12::uuid IS NULL
266 OR cr.id = $12
267 OR cr.user_id = $12
268 OR cr.course_module_completion_id = $12
269 )
270 AND (
271 $13::uuid [] IS NULL
272 OR cr.id = ANY($13)
273 )
274 AND ($17::uuid IS NULL OR cr.id = $17)
275 AND (
276 $18::uuid IS NULL
277 OR cr.course_module_completion_id = $18
278 )
279ORDER BY CASE
280 WHEN $14::text = 'attempts' THEN cr.submit_retry_count + cr.verify_attempt_count
281 END DESC NULLS LAST,
282 CASE $14::text
283 WHEN 'created' THEN cr.created_at
284 WHEN 'time_in_state' THEN cr.state_entered_at
285 ELSE COALESCE(cr.last_attempt_at, cr.state_entered_at)
286 END DESC,
287 cr.id
288LIMIT $15 OFFSET $16
289 "#,
290 filters.include_superseded,
291 filters.states as Option<&[CreditRegistrationState]>,
292 filters.error_codes as Option<&[CreditRegistrationErrorCode]>,
293 filters.course_id,
294 filters.course_module_id,
295 filters.user_id,
296 filters.student_number,
297 filters.needs_admin_attention,
298 filters.submitted_after,
299 filters.submitted_before,
300 search_pattern.as_deref(),
301 filters.search_id,
302 filters.credit_registration_ids as Option<&[Uuid]>,
303 sort.as_str(),
304 limit,
305 offset,
306 filters.id,
307 filters.course_module_completion_id,
308 )
309 .fetch_all(conn)
310 .await?;
311 Ok(res)
312}
313
314pub async fn get_admin_facing(
316 conn: &mut PgConnection,
317 filters: &AdminCreditRegistrationFilters<'_>,
318 sort: AdminCreditRegistrationSort,
319 limit: i64,
320 offset: i64,
321) -> ModelResult<Vec<AdminCreditRegistration>> {
322 admin_facing_page(conn, filters, sort, limit, offset).await
323}
324
325pub async fn get_live_by_states(
329 conn: &mut PgConnection,
330 states: &[CreditRegistrationState],
331 limit_per_state: i64,
332) -> ModelResult<Vec<CreditRegistration>> {
333 let res = sqlx::query_as!(
334 CreditRegistration,
335 r#"
336SELECT cr.*
337FROM credit_registrations cr
338 JOIN (
339 SELECT id,
340 ROW_NUMBER() OVER (
341 PARTITION BY state
342 ORDER BY state_entered_at DESC
343 ) AS rn
344 FROM credit_registrations
345 WHERE state = ANY($1)
346 AND superseded_by_id IS NULL
347 AND deleted_at IS NULL
348 ) ranked ON ranked.id = cr.id
349WHERE ranked.rn <= $2
350ORDER BY cr.state_entered_at DESC
351 "#,
352 states as &[CreditRegistrationState],
353 limit_per_state,
354 )
355 .fetch_all(conn)
356 .await?;
357 Ok(res)
358}