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}