Skip to main content

headless_lms_server/domain/csv_export/
submissions.rs

1use 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
52/// Writes the exam submissions as csv into the writer
53pub 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
96/// The `data_json` and `data_files` columns of one answer, in that order.
97///
98/// `data_json` holds what it always has: the answer itself for a JSON answer, the plugin's metadata
99/// for a file one. The files go in their own column so a file answer does not export as nothing.
100fn 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
138/// Writes the course submissions as csv into the writer
139pub 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}