headless_lms_server/controllers/main_frontend/
global_stats.rs

1//! Controllers for requests starting with `/api/v0/main-frontend/global-stats`.
2
3use crate::{domain::authorization::authorize, prelude::*};
4
5use models::library::TimeGranularity;
6use models::library::global_stats::{
7    CourseCompletionStats, DomainCompletionStats, GlobalCourseModuleStatEntry, GlobalStatEntry,
8};
9use std::collections::HashMap;
10use utoipa::OpenApi;
11
12#[derive(OpenApi)]
13#[openapi(
14    paths(
15        get_number_of_people_completed_a_course,
16        get_number_of_people_registered_completion_to_study_registry,
17        get_number_of_people_done_at_least_one_exercise,
18        get_number_of_people_started_course,
19        get_course_module_stats_by_completions_registered_to_study_registry,
20        get_completion_stats_by_email_domain,
21        get_course_completion_stats_for_email_domain
22    ),
23    components(schemas(TimeGranularity))
24)]
25pub(crate) struct MainFrontendGlobalStatsApiDoc;
26
27/**
28GET `/api/v0/main-frontend/global-stats/number-of-people-completed-a-course`
29
30Query parameters:
31- granularity: String - Either "year" or "month" (defaults to "year")
32*/
33#[utoipa::path(
34    get,
35    path = "/number-of-people-completed-a-course",
36    operation_id = "getNumberOfPeopleCompletedACourse",
37    tag = "global-stats",
38    params(
39        ("granularity" = Option<TimeGranularity>, Query, description = "Time granularity")
40    ),
41    responses(
42        (status = 200, description = "Global completion stats", body = [GlobalStatEntry])
43    )
44)]
45#[instrument(skip(pool))]
46async fn get_number_of_people_completed_a_course(
47    pool: web::Data<PgPool>,
48    user: AuthUser,
49    query: web::Query<HashMap<String, String>>,
50) -> ControllerResult<web::Json<Vec<GlobalStatEntry>>> {
51    let mut conn = pool.acquire().await?;
52    let token = authorize(
53        &mut conn,
54        Act::ViewStats,
55        Some(user.id),
56        Res::GlobalPermissions,
57    )
58    .await?;
59
60    let granularity = query
61        .get("granularity")
62        .map(|s| s.parse().unwrap_or(TimeGranularity::Year))
63        .unwrap_or(TimeGranularity::Year);
64
65    let res = models::library::global_stats::get_number_of_people_completed_a_course(
66        &mut conn,
67        granularity,
68    )
69    .await?;
70
71    token.authorized_ok(web::Json(res))
72}
73
74/**
75GET `/api/v0/main-frontend/global-stats/number-of-people-registered-completion-to-study-registry`
76
77Query parameters:
78- granularity: String - Either "year" or "month" (defaults to "year")
79*/
80#[utoipa::path(
81    get,
82    path = "/number-of-people-registered-completion-to-study-registry",
83    operation_id = "getNumberOfPeopleRegisteredCompletionToStudyRegistry",
84    tag = "global-stats",
85    params(
86        ("granularity" = Option<TimeGranularity>, Query, description = "Time granularity")
87    ),
88    responses(
89        (status = 200, description = "Study registry completion stats", body = [GlobalStatEntry])
90    )
91)]
92#[instrument(skip(pool))]
93async fn get_number_of_people_registered_completion_to_study_registry(
94    pool: web::Data<PgPool>,
95    user: AuthUser,
96    query: web::Query<HashMap<String, String>>,
97) -> ControllerResult<web::Json<Vec<GlobalStatEntry>>> {
98    let mut conn = pool.acquire().await?;
99    let token = authorize(
100        &mut conn,
101        Act::ViewStats,
102        Some(user.id),
103        Res::GlobalPermissions,
104    )
105    .await?;
106
107    let granularity = query
108        .get("granularity")
109        .map(|s| s.parse().unwrap_or(TimeGranularity::Year))
110        .unwrap_or(TimeGranularity::Year);
111
112    let res = models::library::global_stats::get_number_of_people_registered_completion_to_study_registry(&mut conn, granularity).await?;
113
114    token.authorized_ok(web::Json(res))
115}
116
117/**
118GET `/api/v0/main-frontend/global-stats/number-of-people-done-at-least-one-exercise`
119
120Query parameters:
121- granularity: String - Either "year" or "month" (defaults to "year")
122*/
123#[utoipa::path(
124    get,
125    path = "/number-of-people-done-at-least-one-exercise",
126    operation_id = "getNumberOfPeopleDoneAtLeastOneExercise",
127    tag = "global-stats",
128    params(
129        ("granularity" = Option<TimeGranularity>, Query, description = "Time granularity")
130    ),
131    responses(
132        (status = 200, description = "Exercise participation stats", body = [GlobalStatEntry])
133    )
134)]
135#[instrument(skip(pool))]
136async fn get_number_of_people_done_at_least_one_exercise(
137    pool: web::Data<PgPool>,
138    user: AuthUser,
139    query: web::Query<HashMap<String, String>>,
140) -> ControllerResult<web::Json<Vec<GlobalStatEntry>>> {
141    let mut conn = pool.acquire().await?;
142    let token = authorize(
143        &mut conn,
144        Act::ViewStats,
145        Some(user.id),
146        Res::GlobalPermissions,
147    )
148    .await?;
149
150    let granularity = query
151        .get("granularity")
152        .map(|s| s.parse().unwrap_or(TimeGranularity::Year))
153        .unwrap_or(TimeGranularity::Year);
154
155    let res = models::library::global_stats::get_number_of_people_done_at_least_one_exercise(
156        &mut conn,
157        granularity,
158    )
159    .await?;
160
161    token.authorized_ok(web::Json(res))
162}
163
164/**
165GET `/api/v0/main-frontend/global-stats/number-of-people-started-course`
166
167Query parameters:
168- granularity: String - Either "year" or "month" (defaults to "year")
169*/
170#[utoipa::path(
171    get,
172    path = "/number-of-people-started-course",
173    operation_id = "getNumberOfPeopleStartedCourse",
174    tag = "global-stats",
175    params(
176        ("granularity" = Option<TimeGranularity>, Query, description = "Time granularity")
177    ),
178    responses(
179        (status = 200, description = "Course start stats", body = [GlobalStatEntry])
180    )
181)]
182#[instrument(skip(pool))]
183async fn get_number_of_people_started_course(
184    pool: web::Data<PgPool>,
185    user: AuthUser,
186    query: web::Query<HashMap<String, String>>,
187) -> ControllerResult<web::Json<Vec<GlobalStatEntry>>> {
188    let mut conn = pool.acquire().await?;
189    let token = authorize(
190        &mut conn,
191        Act::ViewStats,
192        Some(user.id),
193        Res::GlobalPermissions,
194    )
195    .await?;
196
197    let granularity = query
198        .get("granularity")
199        .map(|s| s.parse().unwrap_or(TimeGranularity::Year))
200        .unwrap_or(TimeGranularity::Year);
201
202    let res =
203        models::library::global_stats::get_number_of_people_started_course(&mut conn, granularity)
204            .await?;
205
206    token.authorized_ok(web::Json(res))
207}
208
209/**
210 * GET `/api/v0/main-frontend/global-stats/course-module-stats-by-completions-registered-to-study-registry`
211 *
212 * Query parameters:
213 * - granularity: String - Either "year" or "month" (defaults to "year")
214 */
215#[utoipa::path(
216    get,
217    path = "/course-module-stats-by-completions-registered-to-study-registry",
218    operation_id = "getCourseModuleStatsByCompletionsRegisteredToStudyRegistry",
219    tag = "global-stats",
220    params(
221        ("granularity" = Option<TimeGranularity>, Query, description = "Time granularity")
222    ),
223    responses(
224        (
225            status = 200,
226            description = "Course module completion stats",
227            body = [GlobalCourseModuleStatEntry]
228        )
229    )
230)]
231#[instrument(skip(pool))]
232async fn get_course_module_stats_by_completions_registered_to_study_registry(
233    pool: web::Data<PgPool>,
234    user: AuthUser,
235    query: web::Query<HashMap<String, String>>,
236) -> ControllerResult<web::Json<Vec<GlobalCourseModuleStatEntry>>> {
237    let mut conn = pool.acquire().await?;
238    let token = authorize(
239        &mut conn,
240        Act::ViewStats,
241        Some(user.id),
242        Res::GlobalPermissions,
243    )
244    .await?;
245
246    let granularity = query
247        .get("granularity")
248        .map(|s| s.parse().unwrap_or(TimeGranularity::Year))
249        .unwrap_or(TimeGranularity::Year);
250
251    let res = models::library::global_stats::get_course_module_stats_by_completions_registered_to_study_registry(&mut conn, granularity).await?;
252
253    token.authorized_ok(web::Json(res))
254}
255
256/**
257 * GET `/api/v0/main-frontend/global-stats/completion-stats-by-email-domain`
258 *
259 * Query parameters:
260 * - year: Optional<i32> - Filter results to specific year (e.g. ?year=2023)
261 */
262#[utoipa::path(
263    get,
264    path = "/completion-stats-by-email-domain",
265    operation_id = "getCompletionStatsByEmailDomain",
266    tag = "global-stats",
267    params(
268        ("year" = Option<i32>, Query, description = "Optional year")
269    ),
270    responses(
271        (
272            status = 200,
273            description = "Completion stats by email domain",
274            body = [DomainCompletionStats]
275        )
276    )
277)]
278#[instrument(skip(pool))]
279async fn get_completion_stats_by_email_domain(
280    pool: web::Data<PgPool>,
281    user: AuthUser,
282    query: web::Query<HashMap<String, String>>,
283) -> ControllerResult<web::Json<Vec<DomainCompletionStats>>> {
284    let mut conn = pool.acquire().await?;
285    let token = authorize(
286        &mut conn,
287        Act::ViewStats,
288        Some(user.id),
289        Res::GlobalPermissions,
290    )
291    .await?;
292
293    let year = query.get("year").and_then(|y| y.parse::<i32>().ok());
294
295    let res = models::library::global_stats::get_completion_stats_by_email_domain(&mut conn, year)
296        .await?;
297
298    token.authorized_ok(web::Json(res))
299}
300
301/**
302 * GET `/api/v0/main-frontend/global-stats/course-completion-stats-for-email-domain`
303 *
304 * Query parameters:
305 * - email_domain: String - The email domain to get stats for (required)
306 * - year: Optional<i32> - Filter results to specific year (e.g. ?year=2023)
307 */
308#[utoipa::path(
309    get,
310    path = "/course-completion-stats-for-email-domain",
311    operation_id = "getCourseCompletionStatsForEmailDomain",
312    tag = "global-stats",
313    params(
314        ("email_domain" = String, Query, description = "Email domain"),
315        ("year" = Option<i32>, Query, description = "Optional year")
316    ),
317    responses(
318        (
319            status = 200,
320            description = "Course completion stats for email domain",
321            body = [CourseCompletionStats]
322        )
323    )
324)]
325#[instrument(skip(pool))]
326async fn get_course_completion_stats_for_email_domain(
327    pool: web::Data<PgPool>,
328    user: AuthUser,
329    query: web::Query<HashMap<String, String>>,
330) -> ControllerResult<web::Json<Vec<CourseCompletionStats>>> {
331    let mut conn = pool.acquire().await?;
332    let token = authorize(
333        &mut conn,
334        Act::ViewStats,
335        Some(user.id),
336        Res::GlobalPermissions,
337    )
338    .await?;
339
340    let email_domain = query
341        .get("email_domain")
342        .ok_or_else(|| {
343            ControllerError::new(
344                ControllerErrorType::BadRequest,
345                "email_domain is required".to_string(),
346                None,
347            )
348        })?
349        .to_string();
350
351    let year = query.get("year").and_then(|y| y.parse::<i32>().ok());
352
353    let res = models::library::global_stats::get_course_completion_stats_for_email_domain(
354        &mut conn,
355        email_domain,
356        year,
357    )
358    .await?;
359
360    token.authorized_ok(web::Json(res))
361}
362
363/**
364Add a route for each controller in this module.
365
366The name starts with an underline in order to appear before other functions in the module documentation.
367
368We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
369*/
370pub fn _add_routes(cfg: &mut ServiceConfig) {
371    cfg.route(
372        "/number-of-people-completed-a-course",
373        web::get().to(get_number_of_people_completed_a_course),
374    )
375    .route(
376        "/number-of-people-registered-completion-to-study-registry",
377        web::get().to(get_number_of_people_registered_completion_to_study_registry),
378    )
379    .route(
380        "/number-of-people-done-at-least-one-exercise",
381        web::get().to(get_number_of_people_done_at_least_one_exercise),
382    )
383    .route(
384        "/number-of-people-started-course",
385        web::get().to(get_number_of_people_started_course),
386    )
387    .route(
388        "/course-module-stats-by-completions-registered-to-study-registry",
389        web::get().to(get_course_module_stats_by_completions_registered_to_study_registry),
390    )
391    .route(
392        "/completion-stats-by-email-domain",
393        web::get().to(get_completion_stats_by_email_domain),
394    )
395    .route(
396        "/course-completion-stats-for-email-domain",
397        web::get().to(get_course_completion_stats_for_email_domain),
398    );
399}