headless_lms_server/domain/csv_export/
course_instance_export.rs1use anyhow::Result;
2use bytes::Bytes;
3use headless_lms_models::course_instances;
4
5use async_trait::async_trait;
6use itertools::Itertools;
7use models::library::progressing;
8
9use crate::domain::csv_export::CsvWriter;
10
11use sqlx::PgConnection;
12use std::io::Write;
13use tokio::sync::mpsc::UnboundedSender;
14
15use uuid::Uuid;
16
17use crate::prelude::*;
18
19use super::{
20    super::authorization::{AuthorizationToken, AuthorizedResponse},
21    CSVExportAdapter, CsvExportDataLoader, course_module_completion_info_to_grade_string,
22};
23
24pub struct CompletionsExportOperation {
25    pub course_instance_id: Uuid,
26}
27
28#[async_trait]
29impl CsvExportDataLoader for CompletionsExportOperation {
30    async fn load_data(
31        &self,
32        sender: UnboundedSender<Result<AuthorizedResponse<Bytes>, ControllerError>>,
33        conn: &mut PgConnection,
34        token: AuthorizationToken,
35    ) -> anyhow::Result<CSVExportAdapter> {
36        export_completions(
37            &mut *conn,
38            self.course_instance_id,
39            CSVExportAdapter {
40                sender,
41                authorization_token: token,
42            },
43        )
44        .await
45    }
46}
47
48pub async fn export_completions<W>(
50    conn: &mut PgConnection,
51    course_instance_id: Uuid,
52    writer: W,
53) -> Result<W>
54where
55    W: Write + Send + 'static,
56{
57    let course_instance = course_instances::get_course_instance(conn, course_instance_id).await?;
59    let summary =
60        progressing::get_course_instance_completion_summary(conn, &course_instance).await?;
61
62    let mut modules = summary.course_modules;
64    modules.sort_by_key(|m| m.order_number);
65
66    let mut headers = vec![
68        "user_id".to_string(),
69        "first_name".to_string(),
70        "last_name".to_string(),
71        "email".to_string(),
72    ];
73    for module in &modules {
74        let module_name = module.name.as_deref().unwrap_or("default_module");
75        headers.push(format!("{module_name}_grade"));
76        headers.push(format!("{module_name}_registered"));
77        headers.push(format!("{module_name}_completion_date"));
78    }
79
80    let writer = CsvWriter::new_with_initialized_headers(writer, headers).await?;
82    for user in summary.users_with_course_module_completions {
83        let mut has_completed_some_module = false;
84
85        let mut csv_row = vec![
86            user.user_id.to_string(),
87            user.first_name.unwrap_or_default(),
88            user.last_name.unwrap_or_default(),
89            user.email,
90        ];
91        for module in &modules {
92            let user_completion = user
93                .completed_modules
94                .iter()
95                .sorted_by(|a, b| b.created_at.cmp(&a.created_at))
97                .find(|cm| cm.course_module_id == module.id);
98            if user_completion.is_some() {
99                has_completed_some_module = true;
100            }
101            let grade = course_module_completion_info_to_grade_string(user_completion);
102            csv_row.push(grade);
103            let registered = user_completion
104                .map(|cm| cm.registered.to_string())
105                .unwrap_or_default();
106            csv_row.push(registered);
107            csv_row.push(
108                user_completion
109                    .map(|uc| uc.completion_date.to_rfc3339())
110                    .unwrap_or_default(),
111            )
112        }
113        if has_completed_some_module {
116            writer.write_record(csv_row);
117        }
118    }
119    let writer = writer.finish().await?;
120    Ok(writer)
121}
122
123pub struct CourseInstancesExportOperation {
124    pub course_id: Uuid,
125}
126
127#[async_trait]
128impl CsvExportDataLoader for CourseInstancesExportOperation {
129    async fn load_data(
130        &self,
131        sender: UnboundedSender<Result<AuthorizedResponse<Bytes>, ControllerError>>,
132        conn: &mut PgConnection,
133        token: AuthorizationToken,
134    ) -> anyhow::Result<CSVExportAdapter> {
135        export_course_instances(
136            &mut *conn,
137            self.course_id,
138            CSVExportAdapter {
139                sender,
140                authorization_token: token,
141            },
142        )
143        .await
144    }
145}
146
147pub async fn export_course_instances<W>(
149    conn: &mut PgConnection,
150    course_id: Uuid,
151    writer: W,
152) -> Result<W>
153where
154    W: Write + Send + 'static,
155{
156    let course_instances =
157        course_instances::get_course_instances_for_course(conn, course_id).await?;
158
159    let headers = IntoIterator::into_iter([
160        "id".to_string(),
161        "created_at".to_string(),
162        "updated_at".to_string(),
163        "name".to_string(),
164    ]);
165    let writer = CsvWriter::new_with_initialized_headers(writer, headers).await?;
166
167    for next in course_instances.into_iter() {
168        let csv_row = vec![
169            next.id.to_string(),
170            next.created_at.to_rfc3339(),
171            next.updated_at.to_rfc3339(),
172            next.name.unwrap_or_default(),
173        ];
174        writer.write_record(csv_row);
175    }
176    let writer = writer.finish().await?;
177    Ok(writer)
178}