headless_lms_server/domain/csv_export/
submissions.rs1use anyhow::Result;
2use bytes::Bytes;
3
4use futures::TryStreamExt;
5use headless_lms_models::exercise_task_submissions::{self, AnswerData, AnswerFields};
6
7use async_trait::async_trait;
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,
22};
23
24pub struct ExamSubmissionExportOperation {
25 pub exam_id: Uuid,
26 pub file_store: web::Data<dyn FileStore>,
27 pub app_conf: web::Data<ApplicationConfiguration>,
28}
29
30#[async_trait]
31impl CsvExportDataLoader for ExamSubmissionExportOperation {
32 async fn load_data(
33 &self,
34 sender: UnboundedSender<Result<AuthorizedResponse<Bytes>, ControllerError>>,
35 conn: &mut PgConnection,
36 token: AuthorizationToken,
37 ) -> anyhow::Result<CSVExportAdapter> {
38 export_exam_submissions(
39 &mut *conn,
40 self.exam_id,
41 CSVExportAdapter {
42 sender,
43 authorization_token: token,
44 },
45 self.file_store.as_ref(),
46 self.app_conf.as_ref(),
47 )
48 .await
49 }
50}
51
52pub async fn export_exam_submissions<W>(
54 conn: &mut PgConnection,
55 exam_id: Uuid,
56 writer: W,
57 file_store: &dyn FileStore,
58 app_conf: &ApplicationConfiguration,
59) -> Result<W>
60where
61 W: Write + Send + 'static,
62{
63 let headers = IntoIterator::into_iter([
64 "id".to_string(),
65 "user_id".to_string(),
66 "created_at".to_string(),
67 "exercise_id".to_string(),
68 "exercise_task_id".to_string(),
69 "score_given".to_string(),
70 "data_json".to_string(),
71 "data_files".to_string(),
72 ]);
73
74 let mut stream =
75 exercise_task_submissions::stream_exam_submissions(conn, exam_id, file_store, app_conf);
76
77 let writer = CsvWriter::new_with_initialized_headers(writer, headers).await?;
78 while let Some(next) = stream.try_next().await? {
79 let (data_json, data_files) = answer_columns(next.answer)?;
80 let csv_row = vec![
81 next.id.to_string(),
82 next.user_id.to_string(),
83 next.created_at.to_rfc3339(),
84 next.exercise_id.to_string(),
85 next.exercise_task_id.to_string(),
86 next.score_given.unwrap_or(0.0).to_string(),
87 data_json,
88 data_files,
89 ];
90 writer.write_record(csv_row);
91 }
92 let writer = writer.finish().await?;
93 Ok(writer)
94}
95
96fn answer_columns(answer: Option<AnswerData>) -> Result<(String, String)> {
101 let fields = AnswerFields::from(answer);
102 let data_json = match fields.data_json {
103 Some(data) => serde_json::to_string(&data)?,
104 None => String::new(),
105 };
106 let data_files = serde_json::to_string(&fields.data_files.unwrap_or_default())?;
107 Ok((data_json, data_files))
108}
109
110pub struct CourseSubmissionExportOperation {
111 pub course_id: Uuid,
112 pub file_store: web::Data<dyn FileStore>,
113 pub app_conf: web::Data<ApplicationConfiguration>,
114}
115
116#[async_trait]
117impl CsvExportDataLoader for CourseSubmissionExportOperation {
118 async fn load_data(
119 &self,
120 sender: UnboundedSender<Result<AuthorizedResponse<Bytes>, ControllerError>>,
121 conn: &mut PgConnection,
122 token: AuthorizationToken,
123 ) -> anyhow::Result<CSVExportAdapter> {
124 export_course_exercise_task_submissions(
125 &mut *conn,
126 self.course_id,
127 CSVExportAdapter {
128 sender,
129 authorization_token: token,
130 },
131 self.file_store.as_ref(),
132 self.app_conf.as_ref(),
133 )
134 .await
135 }
136}
137
138pub async fn export_course_exercise_task_submissions<W>(
140 conn: &mut PgConnection,
141 course_id: Uuid,
142 writer: W,
143 file_store: &dyn FileStore,
144 app_conf: &ApplicationConfiguration,
145) -> Result<W>
146where
147 W: Write + Send + 'static,
148{
149 let headers = IntoIterator::into_iter([
150 "exercise_slide_submission_id".to_string(),
151 "exercise_task_submission_id".to_string(),
152 "user_id".to_string(),
153 "created_at".to_string(),
154 "course_id".to_string(),
155 "exercise_id".to_string(),
156 "exercise_task_id".to_string(),
157 "score_given".to_string(),
158 "data_json".to_string(),
159 "data_files".to_string(),
160 ]);
161
162 let mut stream =
163 exercise_task_submissions::stream_course_submissions(conn, course_id, file_store, app_conf);
164
165 let writer = CsvWriter::new_with_initialized_headers(writer, headers).await?;
166 while let Some(next) = stream.try_next().await? {
167 let (data_json, data_files) = answer_columns(next.answer)?;
168 let csv_row = vec![
169 next.exercise_slide_submission_id.to_string(),
170 next.id.to_string(),
171 next.user_id.to_string(),
172 next.created_at.to_rfc3339(),
173 next.course_id
174 .map(|o| o.to_string())
175 .unwrap_or_else(|| "".to_string()),
176 next.exercise_id.to_string(),
177 next.exercise_task_id.to_string(),
178 next.score_given.unwrap_or(0.0).to_string(),
179 data_json,
180 data_files,
181 ];
182 writer.write_record(csv_row);
183 }
184 let writer = writer.finish().await?;
185 Ok(writer)
186}