1use crate::{domain::authorization::authorize, prelude::*};
4use headless_lms_models::ModelError;
5use headless_lms_models::library::TimeGranularity;
6use headless_lms_models::library::course_stats::StudentsByCountryTotalsResult;
7use headless_lms_utils::prelude::{UtilError, UtilErrorType};
8use models::library::course_stats::{AverageMetric, CohortActivity, CountResult};
9use std::collections::HashMap;
10use std::time::Duration;
11use uuid::Uuid;
12const CACHE_DURATION: Duration = Duration::from_secs(3600);
13
14async fn cached_stats_query<F, Fut, T>(
16 cache: &Cache,
17 endpoint: &str,
18 course_id: Uuid,
19 extra_params: Option<&str>,
20 duration: Duration,
21 f: F,
22) -> Result<T, ControllerError>
23where
24 F: FnOnce() -> Fut,
25 Fut: std::future::Future<Output = Result<T, ModelError>>,
26 T: serde::Serialize + serde::de::DeserializeOwned,
27{
28 let cache_key = match extra_params {
29 Some(params) => format!("stats:{}:{}:{}", endpoint, course_id, params),
30 None => format!("stats:{}:{}", endpoint, course_id),
31 };
32
33 let wrapped_f = || async {
34 f().await.map_err(|err| {
35 UtilError::new(UtilErrorType::Other, "Failed to get data", Some(err.into()))
36 })
37 };
38
39 cache
40 .get_or_set(cache_key, duration, wrapped_f)
41 .await
42 .map_err(|_| {
43 ControllerError::new(
44 ControllerErrorType::InternalServerError,
45 "Failed to get data",
46 None,
47 )
48 })
49}
50
51#[instrument(skip(pool))]
53async fn get_total_users_started_course(
54 pool: web::Data<PgPool>,
55 user: AuthUser,
56 course_id: web::Path<Uuid>,
57 cache: web::Data<Cache>,
58) -> ControllerResult<web::Json<CountResult>> {
59 let mut conn = pool.acquire().await?;
60 let token = authorize(
61 &mut conn,
62 Act::ViewStats,
63 Some(user.id),
64 Res::Course(*course_id),
65 )
66 .await?;
67
68 let res = cached_stats_query(
69 &cache,
70 "total-users-started-course",
71 *course_id,
72 None,
73 CACHE_DURATION,
74 || async {
75 models::library::course_stats::get_total_users_started_course(&mut conn, *course_id)
76 .await
77 },
78 )
79 .await?;
80
81 token.authorized_ok(web::Json(res))
82}
83
84#[instrument(skip(pool))]
86async fn get_total_users_completed_course(
87 pool: web::Data<PgPool>,
88 user: AuthUser,
89 course_id: web::Path<Uuid>,
90 cache: web::Data<Cache>,
91) -> ControllerResult<web::Json<CountResult>> {
92 let mut conn = pool.acquire().await?;
93 let token = authorize(
94 &mut conn,
95 Act::ViewStats,
96 Some(user.id),
97 Res::Course(*course_id),
98 )
99 .await?;
100
101 let res = cached_stats_query(
102 &cache,
103 "total-users-completed",
104 *course_id,
105 None,
106 CACHE_DURATION,
107 || async {
108 models::library::course_stats::get_total_users_completed_course(&mut conn, *course_id)
109 .await
110 },
111 )
112 .await?;
113
114 token.authorized_ok(web::Json(res))
115}
116
117#[instrument(skip(pool))]
119async fn get_total_users_returned_at_least_one_exercise(
120 pool: web::Data<PgPool>,
121 user: AuthUser,
122 course_id: web::Path<Uuid>,
123 cache: web::Data<Cache>,
124) -> ControllerResult<web::Json<CountResult>> {
125 let mut conn = pool.acquire().await?;
126 let token = authorize(
127 &mut conn,
128 Act::ViewStats,
129 Some(user.id),
130 Res::Course(*course_id),
131 )
132 .await?;
133
134 let res = cached_stats_query(
135 &cache,
136 "total-users-returned-exercises",
137 *course_id,
138 None,
139 CACHE_DURATION,
140 || async {
141 models::library::course_stats::get_total_users_returned_at_least_one_exercise(
142 &mut conn, *course_id,
143 )
144 .await
145 },
146 )
147 .await?;
148
149 token.authorized_ok(web::Json(res))
150}
151
152#[instrument(skip(pool))]
158async fn get_avg_time_to_first_submission_history(
159 pool: web::Data<PgPool>,
160 user: AuthUser,
161 path: web::Path<(Uuid, TimeGranularity, u16)>,
162 cache: web::Data<Cache>,
163) -> ControllerResult<web::Json<Vec<AverageMetric>>> {
164 let (course_id, granularity, time_window) = path.into_inner();
165 let mut conn = pool.acquire().await?;
166 let token = authorize(
167 &mut conn,
168 Act::ViewStats,
169 Some(user.id),
170 Res::Course(course_id),
171 )
172 .await?;
173
174 let cache_key = format!(
175 "avg-time-to-first-submission-{}-{}",
176 granularity, time_window
177 );
178 let res = cached_stats_query(
179 &cache,
180 &cache_key,
181 course_id,
182 None,
183 CACHE_DURATION,
184 || async {
185 models::library::course_stats::avg_time_to_first_submission_history(
186 &mut conn,
187 course_id,
188 granularity,
189 time_window,
190 )
191 .await
192 },
193 )
194 .await?;
195
196 token.authorized_ok(web::Json(res))
197}
198
199#[instrument(skip(pool))]
201async fn get_cohort_activity_history(
202 pool: web::Data<PgPool>,
203 user: AuthUser,
204 path: web::Path<(Uuid, TimeGranularity, u16, u16)>,
205 cache: web::Data<Cache>,
206) -> ControllerResult<web::Json<Vec<CohortActivity>>> {
207 let (course_id, granularity, history_window, tracking_window) = path.into_inner();
208 let mut conn = pool.acquire().await?;
209 let token = authorize(
210 &mut conn,
211 Act::ViewStats,
212 Some(user.id),
213 Res::Course(course_id),
214 )
215 .await?;
216
217 let res = cached_stats_query(
218 &cache,
219 &format!(
220 "cohort-activity-{}-{}-{}",
221 granularity, history_window, tracking_window
222 ),
223 course_id,
224 None,
225 CACHE_DURATION,
226 || async {
227 models::library::course_stats::get_cohort_activity_history(
228 &mut conn,
229 course_id,
230 granularity,
231 history_window,
232 tracking_window,
233 )
234 .await
235 },
236 )
237 .await?;
238
239 token.authorized_ok(web::Json(res))
240}
241
242#[instrument(skip(pool))]
244async fn get_total_users_started_all_language_versions(
245 pool: web::Data<PgPool>,
246 user: AuthUser,
247 course_id: web::Path<Uuid>,
248 cache: web::Data<Cache>,
249) -> ControllerResult<web::Json<CountResult>> {
250 let mut conn = pool.acquire().await?;
251 let token = authorize(
252 &mut conn,
253 Act::ViewStats,
254 Some(user.id),
255 Res::Course(*course_id),
256 )
257 .await?;
258
259 let course = models::courses::get_course(&mut conn, *course_id).await?;
261 let language_group_id = course.course_language_group_id;
262
263 let res = cached_stats_query(
264 &cache,
265 "all-language-versions-total-users-started",
266 language_group_id,
267 None,
268 CACHE_DURATION,
269 || async {
270 models::library::course_stats::get_total_users_started_all_language_versions_of_a_course(
271 &mut conn,
272 language_group_id,
273 )
274 .await
275 },
276 )
277 .await?;
278
279 token.authorized_ok(web::Json(res))
280}
281
282#[instrument(skip(pool))]
288async fn get_unique_users_starting_history_all_language_versions(
289 pool: web::Data<PgPool>,
290 user: AuthUser,
291 path: web::Path<(Uuid, TimeGranularity, u16)>,
292 cache: web::Data<Cache>,
293) -> ControllerResult<web::Json<Vec<CountResult>>> {
294 let (course_id, granularity, time_window) = path.into_inner();
295 let mut conn = pool.acquire().await?;
296 let token = authorize(
297 &mut conn,
298 Act::ViewStats,
299 Some(user.id),
300 Res::Course(course_id),
301 )
302 .await?;
303
304 let course = models::courses::get_course(&mut conn, course_id).await?;
306 let language_group_id = course.course_language_group_id;
307
308 let cache_key = format!(
309 "all-language-versions-users-starting-{}-{}",
310 granularity, time_window
311 );
312 let res = cached_stats_query(
313 &cache,
314 &cache_key,
315 language_group_id,
316 None,
317 CACHE_DURATION,
318 || async {
319 models::library::course_stats::unique_users_starting_history_all_language_versions(
320 &mut conn,
321 language_group_id,
322 granularity,
323 time_window,
324 )
325 .await
326 },
327 )
328 .await?;
329
330 token.authorized_ok(web::Json(res))
331}
332
333#[instrument(skip(pool))]
339async fn get_course_completions_history(
340 pool: web::Data<PgPool>,
341 user: AuthUser,
342 path: web::Path<(Uuid, TimeGranularity, u16)>,
343 cache: web::Data<Cache>,
344) -> ControllerResult<web::Json<Vec<CountResult>>> {
345 let (course_id, granularity, time_window) = path.into_inner();
346
347 let mut conn = pool.acquire().await?;
348 let token = authorize(
349 &mut conn,
350 Act::ViewStats,
351 Some(user.id),
352 Res::Course(course_id),
353 )
354 .await?;
355
356 let cache_key = format!("completions-{}-{}", granularity, time_window);
357 let res = cached_stats_query(
358 &cache,
359 &cache_key,
360 course_id,
361 None,
362 CACHE_DURATION,
363 || async {
364 models::library::course_stats::course_completions_history(
365 &mut conn,
366 course_id,
367 granularity,
368 time_window,
369 )
370 .await
371 },
372 )
373 .await?;
374
375 token.authorized_ok(web::Json(res))
376}
377
378#[instrument(skip(pool))]
384async fn get_users_returning_exercises_history(
385 pool: web::Data<PgPool>,
386 user: AuthUser,
387 path: web::Path<(Uuid, TimeGranularity, u16)>,
388 cache: web::Data<Cache>,
389) -> ControllerResult<web::Json<Vec<CountResult>>> {
390 let (course_id, granularity, time_window) = path.into_inner();
391 let mut conn = pool.acquire().await?;
392 let token = authorize(
393 &mut conn,
394 Act::ViewStats,
395 Some(user.id),
396 Res::Course(course_id),
397 )
398 .await?;
399
400 let cache_key = format!("users-returning-{}-{}", granularity, time_window);
401 let res = cached_stats_query(
402 &cache,
403 &cache_key,
404 course_id,
405 None,
406 CACHE_DURATION,
407 || async {
408 models::library::course_stats::users_returning_exercises_history(
409 &mut conn,
410 course_id,
411 granularity,
412 time_window,
413 )
414 .await
415 },
416 )
417 .await?;
418
419 token.authorized_ok(web::Json(res))
420}
421
422#[instrument(skip(pool))]
428async fn get_first_exercise_submissions_history(
429 pool: web::Data<PgPool>,
430 user: AuthUser,
431 path: web::Path<(Uuid, TimeGranularity, u16)>,
432 cache: web::Data<Cache>,
433) -> ControllerResult<web::Json<Vec<CountResult>>> {
434 let (course_id, granularity, time_window) = path.into_inner();
435 let mut conn = pool.acquire().await?;
436 let token = authorize(
437 &mut conn,
438 Act::ViewStats,
439 Some(user.id),
440 Res::Course(course_id),
441 )
442 .await?;
443
444 let cache_key = format!("first-submissions-{}-{}", granularity, time_window);
445 let res = cached_stats_query(
446 &cache,
447 &cache_key,
448 course_id,
449 None,
450 CACHE_DURATION,
451 || async {
452 models::library::course_stats::first_exercise_submissions_history(
453 &mut conn,
454 course_id,
455 granularity,
456 time_window,
457 )
458 .await
459 },
460 )
461 .await?;
462
463 token.authorized_ok(web::Json(res))
464}
465
466#[instrument(skip(pool))]
472async fn get_unique_users_starting_history(
473 pool: web::Data<PgPool>,
474 user: AuthUser,
475 path: web::Path<(Uuid, TimeGranularity, u16)>,
476 cache: web::Data<Cache>,
477) -> ControllerResult<web::Json<Vec<CountResult>>> {
478 let (course_id, granularity, time_window) = path.into_inner();
479 let mut conn = pool.acquire().await?;
480 let token = authorize(
481 &mut conn,
482 Act::ViewStats,
483 Some(user.id),
484 Res::Course(course_id),
485 )
486 .await?;
487
488 let cache_key = format!("users-starting-{}-{}", granularity, time_window);
489 let res = cached_stats_query(
490 &cache,
491 &cache_key,
492 course_id,
493 None,
494 CACHE_DURATION,
495 || async {
496 models::library::course_stats::unique_users_starting_history(
497 &mut conn,
498 course_id,
499 granularity,
500 time_window,
501 )
502 .await
503 },
504 )
505 .await?;
506
507 token.authorized_ok(web::Json(res))
508}
509
510#[instrument(skip(pool))]
512async fn get_total_users_started_course_by_instance(
513 pool: web::Data<PgPool>,
514 user: AuthUser,
515 course_id: web::Path<Uuid>,
516 cache: web::Data<Cache>,
517) -> ControllerResult<web::Json<HashMap<Uuid, CountResult>>> {
518 let mut conn = pool.acquire().await?;
519 let token = authorize(
520 &mut conn,
521 Act::ViewStats,
522 Some(user.id),
523 Res::Course(*course_id),
524 )
525 .await?;
526
527 let res = cached_stats_query(
528 &cache,
529 "total-users-started-course-by-instance",
530 *course_id,
531 None,
532 CACHE_DURATION,
533 || async {
534 models::library::course_stats::get_total_users_started_course_by_instance(
535 &mut conn, *course_id,
536 )
537 .await
538 },
539 )
540 .await?;
541
542 token.authorized_ok(web::Json(res))
543}
544
545#[instrument(skip(pool))]
547async fn get_total_users_completed_course_by_instance(
548 pool: web::Data<PgPool>,
549 user: AuthUser,
550 course_id: web::Path<Uuid>,
551 cache: web::Data<Cache>,
552) -> ControllerResult<web::Json<HashMap<Uuid, CountResult>>> {
553 let mut conn = pool.acquire().await?;
554 let token = authorize(
555 &mut conn,
556 Act::ViewStats,
557 Some(user.id),
558 Res::Course(*course_id),
559 )
560 .await?;
561
562 let res = cached_stats_query(
563 &cache,
564 "total-users-completed-by-instance",
565 *course_id,
566 None,
567 CACHE_DURATION,
568 || async {
569 models::library::course_stats::get_total_users_completed_course_by_instance(
570 &mut conn, *course_id,
571 )
572 .await
573 },
574 )
575 .await?;
576
577 token.authorized_ok(web::Json(res))
578}
579
580#[instrument(skip(pool))]
582async fn get_total_users_returned_at_least_one_exercise_by_instance(
583 pool: web::Data<PgPool>,
584 user: AuthUser,
585 course_id: web::Path<Uuid>,
586 cache: web::Data<Cache>,
587) -> ControllerResult<web::Json<HashMap<Uuid, CountResult>>> {
588 let mut conn = pool.acquire().await?;
589 let token = authorize(
590 &mut conn,
591 Act::ViewStats,
592 Some(user.id),
593 Res::Course(*course_id),
594 )
595 .await?;
596
597 let res = cached_stats_query(
598 &cache,
599 "total-users-returned-exercises-by-instance",
600 *course_id,
601 None,
602 CACHE_DURATION,
603 || async {
604 models::library::course_stats::get_total_users_returned_at_least_one_exercise_by_instance(
605 &mut conn, *course_id,
606 )
607 .await
608 },
609 )
610 .await?;
611
612 token.authorized_ok(web::Json(res))
613}
614
615#[instrument(skip(pool))]
621async fn get_course_completions_history_by_instance(
622 pool: web::Data<PgPool>,
623 user: AuthUser,
624 path: web::Path<(Uuid, TimeGranularity, u16)>,
625 cache: web::Data<Cache>,
626) -> ControllerResult<web::Json<HashMap<Uuid, Vec<CountResult>>>> {
627 let (course_id, granularity, time_window) = path.into_inner();
628 let mut conn = pool.acquire().await?;
629 let token = authorize(
630 &mut conn,
631 Act::ViewStats,
632 Some(user.id),
633 Res::Course(course_id),
634 )
635 .await?;
636
637 let cache_key = format!("completions-by-instance-{}-{}", granularity, time_window);
638 let res = cached_stats_query(
639 &cache,
640 &cache_key,
641 course_id,
642 None,
643 CACHE_DURATION,
644 || async {
645 models::library::course_stats::course_completions_history_by_instance(
646 &mut conn,
647 course_id,
648 granularity,
649 time_window,
650 )
651 .await
652 },
653 )
654 .await?;
655
656 token.authorized_ok(web::Json(res))
657}
658
659#[instrument(skip(pool))]
665async fn get_unique_users_starting_history_by_instance(
666 pool: web::Data<PgPool>,
667 user: AuthUser,
668 path: web::Path<(Uuid, TimeGranularity, u16)>,
669 cache: web::Data<Cache>,
670) -> ControllerResult<web::Json<HashMap<Uuid, Vec<CountResult>>>> {
671 let (course_id, granularity, time_window) = path.into_inner();
672 let mut conn = pool.acquire().await?;
673 let token = authorize(
674 &mut conn,
675 Act::ViewStats,
676 Some(user.id),
677 Res::Course(course_id),
678 )
679 .await?;
680
681 let cache_key = format!("users-starting-by-instance-{}-{}", granularity, time_window);
682 let res = cached_stats_query(
683 &cache,
684 &cache_key,
685 course_id,
686 None,
687 CACHE_DURATION,
688 || async {
689 models::library::course_stats::unique_users_starting_history_by_instance(
690 &mut conn,
691 course_id,
692 granularity,
693 time_window,
694 )
695 .await
696 },
697 )
698 .await?;
699
700 token.authorized_ok(web::Json(res))
701}
702
703#[instrument(skip(pool))]
709async fn get_first_exercise_submissions_history_by_instance(
710 pool: web::Data<PgPool>,
711 user: AuthUser,
712 path: web::Path<(Uuid, TimeGranularity, u16)>,
713 cache: web::Data<Cache>,
714) -> ControllerResult<web::Json<HashMap<Uuid, Vec<CountResult>>>> {
715 let (course_id, granularity, time_window) = path.into_inner();
716 let mut conn = pool.acquire().await?;
717 let token = authorize(
718 &mut conn,
719 Act::ViewStats,
720 Some(user.id),
721 Res::Course(course_id),
722 )
723 .await?;
724
725 let cache_key = format!(
726 "first-submissions-by-instance-{}-{}",
727 granularity, time_window
728 );
729 let res = cached_stats_query(
730 &cache,
731 &cache_key,
732 course_id,
733 None,
734 CACHE_DURATION,
735 || async {
736 models::library::course_stats::first_exercise_submissions_history_by_instance(
737 &mut conn,
738 course_id,
739 granularity,
740 time_window,
741 )
742 .await
743 },
744 )
745 .await?;
746
747 token.authorized_ok(web::Json(res))
748}
749
750#[instrument(skip(pool))]
756async fn get_users_returning_exercises_history_by_instance(
757 pool: web::Data<PgPool>,
758 user: AuthUser,
759 path: web::Path<(Uuid, TimeGranularity, u16)>,
760 cache: web::Data<Cache>,
761) -> ControllerResult<web::Json<HashMap<Uuid, Vec<CountResult>>>> {
762 let (course_id, granularity, time_window) = path.into_inner();
763 let mut conn = pool.acquire().await?;
764 let token = authorize(
765 &mut conn,
766 Act::ViewStats,
767 Some(user.id),
768 Res::Course(course_id),
769 )
770 .await?;
771
772 let cache_key = format!(
773 "users-returning-by-instance-{}-{}",
774 granularity, time_window
775 );
776 let res = cached_stats_query(
777 &cache,
778 &cache_key,
779 course_id,
780 None,
781 CACHE_DURATION,
782 || async {
783 models::library::course_stats::users_returning_exercises_history_by_instance(
784 &mut conn,
785 course_id,
786 granularity,
787 time_window,
788 )
789 .await
790 },
791 )
792 .await?;
793
794 token.authorized_ok(web::Json(res))
795}
796
797#[instrument(skip(pool))]
803async fn get_student_enrollments_by_country(
804 pool: web::Data<PgPool>,
805 user: AuthUser,
806 path: web::Path<(Uuid, TimeGranularity, u16, String)>,
807 cache: web::Data<Cache>,
808) -> ControllerResult<web::Json<Vec<CountResult>>> {
809 let (course_id, granularity, time_window, country) = path.into_inner();
810
811 let mut conn = pool.acquire().await?;
812 let token = authorize(
813 &mut conn,
814 Act::ViewStats,
815 Some(user.id),
816 Res::Course(course_id),
817 )
818 .await?;
819
820 let cache_key = format!(
821 "student-enrollments-by-country-{}-{}-{}",
822 granularity, time_window, country
823 );
824
825 let res = cached_stats_query(
826 &cache,
827 &cache_key,
828 course_id,
829 None,
830 CACHE_DURATION,
831 || async {
832 models::library::course_stats::student_enrollments_by_country(
833 &mut conn,
834 course_id,
835 granularity,
836 time_window,
837 country,
838 )
839 .await
840 },
841 )
842 .await?;
843
844 token.authorized_ok(web::Json(res))
845}
846
847#[instrument(skip(pool))]
853async fn get_student_completions_by_country(
854 pool: web::Data<PgPool>,
855 user: AuthUser,
856 path: web::Path<(Uuid, TimeGranularity, u16, String)>,
857 cache: web::Data<Cache>,
858) -> ControllerResult<web::Json<Vec<CountResult>>> {
859 let (course_id, granularity, time_window, country) = path.into_inner();
860
861 let mut conn = pool.acquire().await?;
862 let token = authorize(
863 &mut conn,
864 Act::ViewStats,
865 Some(user.id),
866 Res::Course(course_id),
867 )
868 .await?;
869
870 let cache_key = format!(
871 "student-completions-by-country-{}-{}-{}",
872 granularity, time_window, country
873 );
874
875 let res = cached_stats_query(
876 &cache,
877 &cache_key,
878 course_id,
879 None,
880 CACHE_DURATION,
881 || async {
882 models::library::course_stats::student_completions_by_country(
883 &mut conn,
884 course_id,
885 granularity,
886 time_window,
887 country,
888 )
889 .await
890 },
891 )
892 .await?;
893
894 token.authorized_ok(web::Json(res))
895}
896
897#[instrument(skip(pool))]
901async fn get_students_by_country_totals(
902 pool: web::Data<PgPool>,
903 user: AuthUser,
904 path: web::Path<Uuid>,
905 cache: web::Data<Cache>,
906) -> ControllerResult<web::Json<Vec<StudentsByCountryTotalsResult>>> {
907 let mut conn = pool.acquire().await?;
908 let course_id = path.into_inner();
909
910 let token = authorize(
911 &mut conn,
912 Act::ViewStats,
913 Some(user.id),
914 Res::Course(course_id),
915 )
916 .await?;
917
918 let cache_key = format!("students-by-country-totals-{}", course_id);
919
920 let res = cached_stats_query(
921 &cache,
922 &cache_key,
923 course_id,
924 None,
925 CACHE_DURATION,
926 || async {
927 models::library::course_stats::students_by_country_totals(&mut conn, course_id).await
928 },
929 )
930 .await?;
931
932 token.authorized_ok(web::Json(res))
933}
934
935#[instrument(skip(pool))]
942async fn get_first_exercise_submissions_by_module(
943 pool: web::Data<PgPool>,
944 user: AuthUser,
945 path: web::Path<(Uuid, TimeGranularity, u16)>,
946 cache: web::Data<Cache>,
947) -> ControllerResult<web::Json<HashMap<Uuid, Vec<CountResult>>>> {
948 let (course_id, granularity, time_window) = path.into_inner();
949
950 let mut conn = pool.acquire().await?;
951 let token = authorize(
952 &mut conn,
953 Act::ViewStats,
954 Some(user.id),
955 Res::Course(course_id),
956 )
957 .await?;
958
959 let cache_key = format!(
960 "first-submissions-by-module-{}-{}",
961 granularity, time_window
962 );
963
964 let res = cached_stats_query(
965 &cache,
966 &cache_key,
967 course_id,
968 None,
969 CACHE_DURATION,
970 || async {
971 models::library::course_stats::first_exercise_submissions_by_module(
972 &mut conn,
973 course_id,
974 granularity,
975 time_window,
976 )
977 .await
978 },
979 )
980 .await?;
981
982 token.authorized_ok(web::Json(res))
983}
984
985#[instrument(skip(pool))]
992async fn get_course_completions_history_by_custom_time_period(
993 pool: web::Data<PgPool>,
994 user: AuthUser,
995 path: web::Path<(Uuid, String, String)>,
996 cache: web::Data<Cache>,
997) -> ControllerResult<web::Json<Vec<CountResult>>> {
998 let (course_id, start_date, end_date) = path.into_inner();
999
1000 let mut conn = pool.acquire().await?;
1001 let token = authorize(
1002 &mut conn,
1003 Act::ViewStats,
1004 Some(user.id),
1005 Res::Course(course_id),
1006 )
1007 .await?;
1008
1009 let cache_key = format!("completions-custom-{}-{}", start_date, end_date);
1010
1011 let res = cached_stats_query(
1012 &cache,
1013 &cache_key,
1014 course_id,
1015 None,
1016 CACHE_DURATION,
1017 || async {
1018 models::library::course_stats::course_completions_history_by_custom_time_period(
1019 &mut conn,
1020 course_id,
1021 &start_date,
1022 &end_date,
1023 )
1024 .await
1025 },
1026 )
1027 .await?;
1028
1029 token.authorized_ok(web::Json(res))
1030}
1031
1032#[instrument(skip(pool))]
1036async fn get_unique_users_starting_history_custom_time_period(
1037 pool: web::Data<PgPool>,
1038 user: AuthUser,
1039 path: web::Path<(Uuid, String, String)>,
1040 cache: web::Data<Cache>,
1041) -> ControllerResult<web::Json<Vec<CountResult>>> {
1042 let (course_id, start_date, end_date) = path.into_inner();
1043 let mut conn = pool.acquire().await?;
1044 let token = authorize(
1045 &mut conn,
1046 Act::ViewStats,
1047 Some(user.id),
1048 Res::Course(course_id),
1049 )
1050 .await?;
1051
1052 let cache_key = format!("users-starting-custom-{}-{}", start_date, end_date);
1053 let res = cached_stats_query(
1054 &cache,
1055 &cache_key,
1056 course_id,
1057 None,
1058 CACHE_DURATION,
1059 || async {
1060 models::library::course_stats::unique_users_starting_history_by_custom_time_period(
1061 &mut conn,
1062 course_id,
1063 &start_date,
1064 &end_date,
1065 )
1066 .await
1067 },
1068 )
1069 .await?;
1070
1071 token.authorized_ok(web::Json(res))
1072}
1073
1074#[instrument(skip(pool))]
1076async fn get_total_users_started_course_custom_time_period(
1077 pool: web::Data<PgPool>,
1078 user: AuthUser,
1079 path: web::Path<(Uuid, String, String)>,
1080 cache: web::Data<Cache>,
1081) -> ControllerResult<web::Json<CountResult>> {
1082 let (course_id, start_date, end_date) = path.into_inner();
1083 let mut conn = pool.acquire().await?;
1084 let token = authorize(
1085 &mut conn,
1086 Act::ViewStats,
1087 Some(user.id),
1088 Res::Course(course_id),
1089 )
1090 .await?;
1091
1092 let cache_key = format!("total-users-started-custom-{}-{}", start_date, end_date);
1093 let res = cached_stats_query(
1094 &cache,
1095 &cache_key,
1096 course_id,
1097 None,
1098 CACHE_DURATION,
1099 || async {
1100 models::library::course_stats::get_total_users_started_course_custom_time_period(
1101 &mut conn,
1102 course_id,
1103 &start_date,
1104 &end_date,
1105 )
1106 .await
1107 },
1108 )
1109 .await?;
1110
1111 token.authorized_ok(web::Json(res))
1112}
1113
1114#[instrument(skip(pool))]
1116async fn get_total_users_completed_course_custom_time_period(
1117 pool: web::Data<PgPool>,
1118 user: AuthUser,
1119 path: web::Path<(Uuid, String, String)>,
1120 cache: web::Data<Cache>,
1121) -> ControllerResult<web::Json<CountResult>> {
1122 let (course_id, start_date, end_date) = path.into_inner();
1123 let mut conn = pool.acquire().await?;
1124 let token = authorize(
1125 &mut conn,
1126 Act::ViewStats,
1127 Some(user.id),
1128 Res::Course(course_id),
1129 )
1130 .await?;
1131
1132 let cache_key = format!("total-users-completed-custom-{}-{}", start_date, end_date);
1133 let res = cached_stats_query(
1134 &cache,
1135 &cache_key,
1136 course_id,
1137 None,
1138 CACHE_DURATION,
1139 || async {
1140 models::library::course_stats::get_total_users_completed_course_custom_time_period(
1141 &mut conn,
1142 course_id,
1143 &start_date,
1144 &end_date,
1145 )
1146 .await
1147 },
1148 )
1149 .await?;
1150
1151 token.authorized_ok(web::Json(res))
1152}
1153
1154#[instrument(skip(pool))]
1156async fn get_total_users_returned_exercises_custom_time_period(
1157 pool: web::Data<PgPool>,
1158 user: AuthUser,
1159 path: web::Path<(Uuid, String, String)>,
1160 cache: web::Data<Cache>,
1161) -> ControllerResult<web::Json<CountResult>> {
1162 let (course_id, start_date, end_date) = path.into_inner();
1163 let mut conn = pool.acquire().await?;
1164 let token = authorize(
1165 &mut conn,
1166 Act::ViewStats,
1167 Some(user.id),
1168 Res::Course(course_id),
1169 )
1170 .await?;
1171
1172 let cache_key = format!("total-users-returned-custom-{}-{}", start_date, end_date);
1173 let res = cached_stats_query(
1174 &cache,
1175 &cache_key,
1176 course_id,
1177 None,
1178 CACHE_DURATION,
1179 || async {
1180 models::library::course_stats::get_total_users_returned_exercises_custom_time_period(
1181 &mut conn,
1182 course_id,
1183 &start_date,
1184 &end_date,
1185 )
1186 .await
1187 },
1188 )
1189 .await?;
1190
1191 token.authorized_ok(web::Json(res))
1192}
1193
1194pub fn _add_routes(cfg: &mut web::ServiceConfig) {
1195 cfg.route(
1196 "/total-users-started-course",
1197 web::get().to(get_total_users_started_course),
1198 )
1199 .route(
1200 "/total-users-completed",
1201 web::get().to(get_total_users_completed_course),
1202 )
1203 .route(
1204 "/total-users-returned-exercises",
1205 web::get().to(get_total_users_returned_at_least_one_exercise),
1206 )
1207 .route(
1208 "/by-instance/total-users-started-course",
1209 web::get().to(get_total_users_started_course_by_instance),
1210 )
1211 .route(
1212 "/by-instance/total-users-completed",
1213 web::get().to(get_total_users_completed_course_by_instance),
1214 )
1215 .route(
1216 "/by-instance/total-users-returned-exercises",
1217 web::get().to(get_total_users_returned_at_least_one_exercise_by_instance),
1218 )
1219 .route(
1220 "/first-submissions-history/{granularity}/{time_window}",
1221 web::get().to(get_first_exercise_submissions_history),
1222 )
1223 .route(
1224 "/by-instance/first-submissions-history/{granularity}/{time_window}",
1225 web::get().to(get_first_exercise_submissions_history_by_instance),
1226 )
1227 .route(
1228 "/users-returning-exercises-history/{granularity}/{time_window}",
1229 web::get().to(get_users_returning_exercises_history),
1230 )
1231 .route(
1232 "/by-instance/users-returning-exercises-history/{granularity}/{time_window}",
1233 web::get().to(get_users_returning_exercises_history_by_instance),
1234 )
1235 .route(
1236 "/completions-history/{granularity}/{time_window}",
1237 web::get().to(get_course_completions_history),
1238 )
1239 .route(
1240 "/by-instance/completions-history/{granularity}/{time_window}",
1241 web::get().to(get_course_completions_history_by_instance),
1242 )
1243 .route(
1244 "/users-starting-history/{granularity}/{time_window}",
1245 web::get().to(get_unique_users_starting_history),
1246 )
1247 .route(
1248 "/by-instance/users-starting-history/{granularity}/{time_window}",
1249 web::get().to(get_unique_users_starting_history_by_instance),
1250 )
1251 .route(
1252 "/avg-time-to-first-submission/{granularity}/{time_window}",
1253 web::get().to(get_avg_time_to_first_submission_history),
1254 )
1255 .route(
1256 "/cohort-activity/{granularity}/{history_window}/{tracking_window}",
1257 web::get().to(get_cohort_activity_history),
1258 )
1259 .route(
1260 "/all-language-versions/total-users-started",
1261 web::get().to(get_total_users_started_all_language_versions),
1262 )
1263 .route(
1264 "/all-language-versions/users-starting-history/{granularity}/{time_window}",
1265 web::get().to(get_unique_users_starting_history_all_language_versions),
1266 )
1267 .route(
1268 "/completions-history/custom-time-period/{start_date}/{end_date}",
1269 web::get().to(get_course_completions_history_by_custom_time_period),
1270 )
1271 .route(
1272 "/student-enrollments-by-country/{granularity}/{time_window}/{country}",
1273 web::get().to(get_student_enrollments_by_country),
1274 )
1275 .route(
1276 "/student-completions-by-country/{granularity}/{time_window}/{country}",
1277 web::get().to(get_student_completions_by_country),
1278 )
1279 .route(
1280 "/students-by-country-totals",
1281 web::get().to(get_students_by_country_totals),
1282 )
1283 .route(
1284 "/by-module/first-submissions/{granularity}/{time_window}",
1285 web::get().to(get_first_exercise_submissions_by_module),
1286 )
1287 .route(
1288 "/users-starting-history/custom-time-period/{start_date}/{end_date}",
1289 web::get().to(get_unique_users_starting_history_custom_time_period),
1290 )
1291 .route(
1292 "/total-users-started-course/custom-time-period/{start_date}/{end_date}",
1293 web::get().to(get_total_users_started_course_custom_time_period),
1294 )
1295 .route(
1296 "/total-users-completed/custom-time-period/{start_date}/{end_date}",
1297 web::get().to(get_total_users_completed_course_custom_time_period),
1298 )
1299 .route(
1300 "/total-users-returned-exercises/custom-time-period/{start_date}/{end_date}",
1301 web::get().to(get_total_users_returned_exercises_custom_time_period),
1302 );
1303}