1use super::TimeGranularity;
16use crate::credit_registrations::CreditRegistrationState;
17use crate::prelude::*;
18use utoipa::ToSchema;
19
20#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
21
22pub struct GlobalStatEntry {
23 pub course_name: String,
24 pub course_id: Uuid,
25 pub organization_id: Uuid,
26 pub organization_name: String,
27 pub year: i32,
28 pub month: Option<i32>, pub value: i64,
30}
31
32#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
33
34pub struct GlobalCourseModuleStatEntry {
35 pub course_name: String,
36 pub course_id: Uuid,
37 pub course_module_id: Uuid,
38 pub course_module_name: Option<String>,
39 pub organization_id: Uuid,
40 pub organization_name: String,
41 pub year: String,
42 pub value: i64,
43 pub course_module_ects_credits: Option<f32>,
44}
45
46#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
47
48pub struct DomainCompletionStats {
49 pub email_domain: String,
50 pub total_completions: i64,
51 pub unique_users: i64,
52 pub registered_completion_percentage: Option<f64>,
53 pub registered_completions: i64,
54 pub not_registered_completions: i64,
55 pub users_with_some_registered_completions: i64,
56 pub users_with_some_unregistered_completions: i64,
57 pub registered_ects_credits: f32,
58 pub not_registered_ects_credits: f32,
59}
60
61#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
62
63pub struct CourseCompletionStats {
64 pub course_id: Uuid,
65 pub course_name: String,
66 pub total_completions: i64,
67 pub unique_users: i64,
68 pub registered_completion_percentage: Option<f64>,
69 pub registered_completions: i64,
70 pub not_registered_completions: i64,
71 pub users_with_some_registered_completions: i64,
72 pub users_with_some_unregistered_completions: i64,
73 pub registered_ects_credits: f32,
74 pub not_registered_ects_credits: f32,
75}
76
77pub async fn get_number_of_people_completed_a_course(
78 conn: &mut PgConnection,
79 granularity: TimeGranularity,
80) -> ModelResult<Vec<GlobalStatEntry>> {
81 let res = sqlx::query_as!(
82 GlobalStatEntry,
83 r#"
84SELECT c.name AS course_name,
85 EXTRACT('year' FROM completion_date)::int as "year!",
86 CASE WHEN $1 = 'Month' THEN EXTRACT('month' FROM completion_date)::int ELSE NULL END as "month",
87 COUNT(DISTINCT user_id) as "value!",
88 c.id as "course_id!",
89 o.id as "organization_id",
90 o.name as "organization_name"
91FROM course_module_completions cmc
92JOIN courses c ON cmc.course_id = c.id
93JOIN organizations o ON c.organization_id = o.id
94WHERE cmc.deleted_at IS NULL
95 AND c.is_draft = FALSE
96 AND c.deleted_at IS NULL
97 AND c.is_test_mode = FALSE
98GROUP BY c.name, c.id, o.id, o.name, "year!", "month"
99ORDER BY c.id, "year!", "month"
100"#,
101 granularity.to_string()
102 )
103 .fetch_all(conn)
104 .await?;
105 Ok(res)
106}
107
108pub async fn get_number_of_people_registered_completion_to_study_registry(
109 conn: &mut PgConnection,
110 granularity: TimeGranularity,
111) -> ModelResult<Vec<GlobalStatEntry>> {
112 let res = sqlx::query_as!(
113 GlobalStatEntry,
114 r#"
115WITH registered_completions AS (
116 SELECT course_module_completion_id,
117 user_id,
118 course_id
119 FROM course_module_completion_registered_to_study_registries
120 WHERE deleted_at IS NULL
121 UNION
122 SELECT course_module_completion_id,
123 user_id,
124 course_id
125 FROM credit_registrations
126 WHERE deleted_at IS NULL
127 AND state = ANY($2::credit_registration_state [])
128)
129SELECT c.name AS course_name,
130 EXTRACT('year' FROM cms.completion_date)::int as "year!",
131 CASE WHEN $1 = 'Month' THEN EXTRACT('month' FROM cms.completion_date)::int ELSE NULL END as "month",
132 COUNT(DISTINCT rc.user_id) as "value!",
133 c.id as "course_id!",
134 o.id as "organization_id",
135 o.name as "organization_name"
136FROM registered_completions rc
137JOIN course_module_completions cms ON rc.course_module_completion_id = cms.id
138JOIN courses c ON rc.course_id = c.id
139JOIN organizations o ON c.organization_id = o.id
140WHERE c.is_draft = FALSE
141 AND c.deleted_at IS NULL
142 AND c.is_test_mode = FALSE
143GROUP BY c.name, c.id, o.id, o.name, "year!", "month"
144ORDER BY c.id, "year!", "month"
145"#,
146 granularity.to_string(),
147 &CreditRegistrationState::SUCCESS_STATES as &[CreditRegistrationState],
148 )
149 .fetch_all(conn)
150 .await?;
151 Ok(res)
152}
153
154pub async fn get_number_of_people_done_at_least_one_exercise(
155 conn: &mut PgConnection,
156 granularity: TimeGranularity,
157) -> ModelResult<Vec<GlobalStatEntry>> {
158 dbg!(&granularity);
159 let res = sqlx::query_as!(
160 GlobalStatEntry,
161 r#"
162SELECT c.name AS course_name,
163 EXTRACT('year' FROM ess.created_at)::int as "year!",
164 CASE WHEN $1 = 'Month' THEN EXTRACT('month' FROM ess.created_at)::int ELSE NULL END as "month",
165 COUNT(DISTINCT ess.user_id) as "value!",
166 c.id as "course_id!",
167 o.id as "organization_id",
168 o.name as "organization_name"
169FROM exercise_slide_submissions ess
170JOIN courses c ON ess.course_id = c.id
171JOIN organizations o ON c.organization_id = o.id
172WHERE ess.deleted_at IS NULL
173 AND c.is_draft = FALSE
174 AND c.deleted_at IS NULL
175 AND c.is_test_mode = FALSE
176GROUP BY c.name, c.id, o.id, o.name, "year!", "month"
177ORDER BY c.id, "year!", "month"
178"#,
179 granularity.to_string()
180 )
181 .fetch_all(conn)
182 .await?;
183 dbg!(&res);
184 Ok(res)
185}
186
187pub async fn get_number_of_people_started_course(
188 conn: &mut PgConnection,
189 granularity: TimeGranularity,
190) -> ModelResult<Vec<GlobalStatEntry>> {
191 let res = sqlx::query_as!(
192 GlobalStatEntry,
193 r#"
194SELECT c.name AS course_name,
195 EXTRACT('year' FROM cie.created_at)::int as "year!",
196 CASE WHEN $1 = 'Month' THEN EXTRACT('month' FROM cie.created_at)::int ELSE NULL END as "month",
197 COUNT(DISTINCT cie.user_id) as "value!",
198 c.id as "course_id!",
199 o.id as "organization_id",
200 o.name as "organization_name"
201FROM course_instance_enrollments cie
202JOIN courses c ON cie.course_id = c.id
203JOIN organizations o ON c.organization_id = o.id
204WHERE cie.deleted_at IS NULL
205 AND c.is_draft = FALSE
206 AND c.deleted_at IS NULL
207 AND c.is_test_mode = FALSE
208GROUP BY c.name, c.id, o.id, o.name, "year!", "month"
209ORDER BY c.id, "year!", "month"
210"#,
211 granularity.to_string()
212 )
213 .fetch_all(conn)
214 .await?;
215 Ok(res)
216}
217
218pub async fn get_course_module_stats_by_completions_registered_to_study_registry(
219 conn: &mut PgConnection,
220 granularity: TimeGranularity,
221) -> ModelResult<Vec<GlobalCourseModuleStatEntry>> {
222 let res = sqlx::query_as!(
223 GlobalCourseModuleStatEntry,
224 r#"
225SELECT c.name as course_name,
226 q.year as "year!",
227 q.value as "value!",
228 q.course_module_id as "course_module_id!",
229 c.id as "course_id",
230 cm.name as "course_module_name",
231 cm.ects_credits as "course_module_ects_credits",
232 o.id as "organization_id",
233 o.name as "organization_name"
234FROM (
235 WITH registered_completions AS (
236 SELECT course_module_completion_id,
237 user_id,
238 course_module_id
239 FROM course_module_completion_registered_to_study_registries
240 WHERE deleted_at IS NULL
241 UNION
242 SELECT course_module_completion_id,
243 user_id,
244 course_module_id
245 FROM credit_registrations
246 WHERE deleted_at IS NULL
247 AND state = ANY($2::credit_registration_state [])
248 )
249 SELECT rc.course_module_id,
250 CASE WHEN $1 = 'Month' THEN
251 EXTRACT('year' FROM cms.completion_date)::VARCHAR || '-' || LPAD(EXTRACT('month' FROM cms.completion_date)::VARCHAR, 2, '0')
252 ELSE
253 EXTRACT('year' FROM cms.completion_date)::VARCHAR
254 END as year,
255 COUNT(DISTINCT rc.user_id) as value
256 FROM registered_completions rc
257 JOIN course_module_completions cms ON rc.course_module_completion_id = cms.id
258 GROUP BY rc.course_module_id,
259 year
260 ORDER BY rc.course_module_id,
261 year
262 ) q
263 JOIN course_modules cm ON q.course_module_id = cm.id
264 JOIN courses c ON cm.course_id = c.id
265 JOIN organizations o ON c.organization_id = o.id
266WHERE c.is_draft = FALSE
267 AND c.deleted_at IS NULL
268 AND c.is_test_mode = FALSE
269"#,
270 granularity.to_string(),
271 &CreditRegistrationState::SUCCESS_STATES as &[CreditRegistrationState],
272 )
273 .fetch_all(conn)
274 .await?;
275 Ok(res)
276}
277
278pub async fn get_completion_stats_by_email_domain(
300 conn: &mut PgConnection,
301 year: Option<i32>,
302) -> ModelResult<Vec<DomainCompletionStats>> {
303 let res = sqlx::query_as!(
304 DomainCompletionStats,
305 r#"
306WITH unique_registrations AS (
307SELECT course_module_completion_id
308FROM course_module_completion_registered_to_study_registries
309WHERE deleted_at IS NULL
310UNION
311SELECT course_module_completion_id
312FROM credit_registrations
313WHERE deleted_at IS NULL
314 AND state = ANY($2::credit_registration_state [])
315),
316deduped_completions AS (
317SELECT *
318FROM (
319 SELECT cmc.*,
320 CASE
321 WHEN ur.course_module_completion_id IS NOT NULL THEN 1
322 ELSE 0
323 END AS is_registered,
324 ROW_NUMBER() OVER (
325 PARTITION BY cmc.user_id,
326 cmc.course_module_id
327 ORDER BY CASE
328 WHEN ur.course_module_completion_id IS NOT NULL THEN 1
329 ELSE 0
330 END DESC,
331 cmc.created_at DESC
332 ) AS rn
333 FROM course_module_completions cmc
334 LEFT JOIN unique_registrations ur ON cmc.id = ur.course_module_completion_id
335 WHERE cmc.deleted_at IS NULL
336 AND (
337 $1::int IS NULL
338 OR EXTRACT(
339 YEAR
340 FROM cmc.completion_date
341 ) = $1
342 )
343 ) sub
344WHERE rn = 1
345)
346SELECT u.email_domain AS "email_domain!",
347COUNT(DISTINCT d.id) AS "total_completions!",
348COUNT(DISTINCT d.user_id) AS "unique_users!",
349ROUND(
350 (
351 SUM(
352 CASE
353 WHEN ur.course_module_completion_id IS NOT NULL THEN 1
354 ELSE 0
355 END
356 ) * 100.0
357 ) / NULLIF(COUNT(DISTINCT d.id), 0),
358 2
359)::float8 AS "registered_completion_percentage",
360SUM(
361 CASE
362 WHEN ur.course_module_completion_id IS NOT NULL THEN 1
363 ELSE 0
364 END
365) AS "registered_completions!",
366SUM(
367 CASE
368 WHEN ur.course_module_completion_id IS NULL THEN 1
369 ELSE 0
370 END
371) AS "not_registered_completions!",
372COUNT(
373 DISTINCT CASE
374 WHEN ur.course_module_completion_id IS NOT NULL THEN d.user_id
375 END
376) AS "users_with_some_registered_completions!",
377COUNT(
378 DISTINCT CASE
379 WHEN ur.course_module_completion_id IS NULL THEN d.user_id
380 END
381) AS "users_with_some_unregistered_completions!",
382COALESCE(
383 SUM(
384 CASE
385 WHEN ur.course_module_completion_id IS NOT NULL THEN cm.ects_credits
386 ELSE 0
387 END
388 ),
389 0
390) AS "registered_ects_credits!",
391COALESCE(
392 SUM(
393 CASE
394 WHEN ur.course_module_completion_id IS NULL THEN cm.ects_credits
395 ELSE 0
396 END
397 ),
398 0
399) AS "not_registered_ects_credits!"
400FROM deduped_completions d
401JOIN users u ON d.user_id = u.id
402AND u.deleted_at IS NULL
403LEFT JOIN unique_registrations ur ON d.id = ur.course_module_completion_id
404JOIN courses c ON d.course_id = c.id
405AND c.deleted_at IS NULL
406JOIN course_modules cm ON d.course_module_id = cm.id
407AND cm.deleted_at IS NULL
408WHERE d.prerequisite_modules_completed = TRUE
409AND c.is_draft = FALSE
410AND c.is_test_mode = FALSE
411AND cm.enable_registering_completion_to_uh_open_university = TRUE
412AND cm.ects_credits IS NOT NULL
413AND cm.ects_credits > 0
414GROUP BY u.email_domain
415ORDER BY "total_completions!" DESC,
416email_domain
417 "#,
418 year,
419 &CreditRegistrationState::SUCCESS_STATES as &[CreditRegistrationState],
420 )
421 .fetch_all(conn)
422 .await?;
423 Ok(res)
424}
425
426pub async fn get_course_completion_stats_for_email_domain(
436 conn: &mut PgConnection,
437 email_domain: String,
438 year: Option<i32>,
439) -> ModelResult<Vec<CourseCompletionStats>> {
440 let res = sqlx::query_as!(
441 CourseCompletionStats,
442 r#"
443WITH unique_registrations AS (
444 SELECT course_module_completion_id
445 FROM course_module_completion_registered_to_study_registries
446 WHERE deleted_at IS NULL
447 UNION
448 SELECT course_module_completion_id
449 FROM credit_registrations
450 WHERE deleted_at IS NULL
451 AND state = ANY($3::credit_registration_state [])
452),
453deduped_completions AS (
454 SELECT *
455 FROM (
456 SELECT cmc.*,
457 CASE
458 WHEN ur.course_module_completion_id IS NOT NULL THEN 1
459 ELSE 0
460 END AS is_registered,
461 ROW_NUMBER() OVER (
462 PARTITION BY cmc.user_id,
463 cmc.course_module_id
464 ORDER BY CASE
465 WHEN ur.course_module_completion_id IS NOT NULL THEN 1
466 ELSE 0
467 END DESC,
468 cmc.created_at DESC
469 ) AS rn
470 FROM course_module_completions cmc
471 LEFT JOIN unique_registrations ur ON cmc.id = ur.course_module_completion_id
472 WHERE cmc.deleted_at IS NULL
473 AND (
474 $2::int IS NULL
475 OR EXTRACT(
476 YEAR
477 FROM cmc.completion_date
478 ) = $2
479 )
480 ) sub
481 WHERE rn = 1
482)
483SELECT c.id AS "course_id!",
484 c.name AS "course_name!",
485 COUNT(DISTINCT d.id) AS "total_completions!",
486 COUNT(DISTINCT d.user_id) AS "unique_users!",
487 ROUND(
488 (
489 SUM(
490 CASE
491 WHEN ur.course_module_completion_id IS NOT NULL THEN 1
492 ELSE 0
493 END
494 ) * 100.0
495 ) / NULLIF(COUNT(DISTINCT d.id), 0),
496 2
497 )::float8 AS "registered_completion_percentage",
498 SUM(
499 CASE
500 WHEN ur.course_module_completion_id IS NOT NULL THEN 1
501 ELSE 0
502 END
503 ) AS "registered_completions!",
504 SUM(
505 CASE
506 WHEN ur.course_module_completion_id IS NULL THEN 1
507 ELSE 0
508 END
509 ) AS "not_registered_completions!",
510 COUNT(
511 DISTINCT CASE
512 WHEN ur.course_module_completion_id IS NOT NULL THEN d.user_id
513 END
514 ) AS "users_with_some_registered_completions!",
515 COUNT(
516 DISTINCT CASE
517 WHEN ur.course_module_completion_id IS NULL THEN d.user_id
518 END
519 ) AS "users_with_some_unregistered_completions!",
520 COALESCE(
521 SUM(
522 CASE
523 WHEN ur.course_module_completion_id IS NOT NULL THEN cm.ects_credits
524 ELSE 0
525 END
526 ),
527 0
528 ) AS "registered_ects_credits!",
529 COALESCE(
530 SUM(
531 CASE
532 WHEN ur.course_module_completion_id IS NULL THEN cm.ects_credits
533 ELSE 0
534 END
535 ),
536 0
537 ) AS "not_registered_ects_credits!"
538FROM deduped_completions d
539 JOIN users u ON d.user_id = u.id
540 AND u.deleted_at IS NULL
541 LEFT JOIN unique_registrations ur ON d.id = ur.course_module_completion_id
542 JOIN courses c ON d.course_id = c.id
543 AND c.deleted_at IS NULL
544 JOIN course_modules cm ON d.course_module_id = cm.id
545 AND cm.deleted_at IS NULL
546WHERE d.prerequisite_modules_completed = TRUE
547 AND c.is_draft = FALSE
548 AND c.is_test_mode = FALSE
549 AND cm.enable_registering_completion_to_uh_open_university = TRUE
550 AND cm.ects_credits IS NOT NULL
551 AND cm.ects_credits > 0
552 AND u.email_domain = $1
553GROUP BY c.id,
554 c.name
555ORDER BY "total_completions!" DESC,
556 c.id
557 "#,
558 email_domain,
559 year,
560 &CreditRegistrationState::SUCCESS_STATES as &[CreditRegistrationState],
561 )
562 .fetch_all(conn)
563 .await?;
564 Ok(res)
565}