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