headless_lms_server/domain/csv_export/
credit_registrations_export.rs1use anyhow::Result;
2use async_trait::async_trait;
3use bytes::Bytes;
4use headless_lms_models::credit_registrations::TeacherCreditRegistrationFilters;
5use serde::Serialize;
6use sqlx::PgConnection;
7use std::io::Write;
8use tokio::sync::mpsc::UnboundedSender;
9use uuid::Uuid;
10
11use crate::controllers::main_frontend::course_credit_registrations::build_teacher_registrations;
12use crate::domain::csv_export::CsvWriter;
13use crate::prelude::*;
14
15use super::{
16 super::authorization::{AuthorizationToken, AuthorizedResponse},
17 CSVExportAdapter, CsvExportDataLoader,
18};
19
20const PAGE_SIZE: i64 = 500;
22
23pub struct CreditRegistrationsExportOperation {
24 pub course_id: Uuid,
25}
26
27#[async_trait]
28impl CsvExportDataLoader for CreditRegistrationsExportOperation {
29 async fn load_data(
30 &self,
31 sender: UnboundedSender<Result<AuthorizedResponse<Bytes>, ControllerError>>,
32 conn: &mut PgConnection,
33 token: AuthorizationToken,
34 ) -> anyhow::Result<CSVExportAdapter> {
35 export_credit_registrations(
36 &mut *conn,
37 self.course_id,
38 CSVExportAdapter {
39 sender,
40 authorization_token: token,
41 },
42 )
43 .await
44 }
45}
46
47pub async fn export_credit_registrations<W>(
53 conn: &mut PgConnection,
54 course_id: Uuid,
55 writer: W,
56) -> Result<W>
57where
58 W: Write + Send + 'static,
59{
60 let headers = IntoIterator::into_iter([
61 "user_id".to_string(),
62 "first_name".to_string(),
63 "last_name".to_string(),
64 "email".to_string(),
65 "course_module".to_string(),
66 "completion_date".to_string(),
67 "state".to_string(),
68 "student_facing_status".to_string(),
69 "error_code".to_string(),
70 "needs_admin_attention".to_string(),
71 "attempt_number".to_string(),
72 "superseded".to_string(),
73 "student_number".to_string(),
74 "student_number_verified_at".to_string(),
75 "student_number_verified_via".to_string(),
76 "enrolment_realisation".to_string(),
77 "grade_id".to_string(),
78 "credits".to_string(),
79 "registered_at".to_string(),
80 "sisu_attainment_id".to_string(),
81 "linking_email_status".to_string(),
82 "linking_email_sent_at".to_string(),
83 "linking_email_recipient".to_string(),
84 ]);
85 let writer = CsvWriter::new_with_initialized_headers(writer, headers).await?;
86
87 let mut offset = 0;
88 loop {
89 let rows = headless_lms_models::credit_registrations::get_teacher_facing_by_course_id(
90 conn,
91 course_id,
92 &TeacherCreditRegistrationFilters::default(),
93 PAGE_SIZE,
94 offset,
95 )
96 .await?;
97 let page_len = rows.len() as i64;
98 for row in build_teacher_registrations(conn, course_id, rows).await? {
101 writer.write_record(vec![
102 row.user_id.to_string(),
103 row.first_name.unwrap_or_default(),
104 row.last_name.unwrap_or_default(),
105 row.email.unwrap_or_default(),
106 row.course_module_name.unwrap_or_default(),
107 row.completion_date.to_rfc3339(),
108 wire_value(&row.state),
109 wire_value(&row.student_facing_status),
110 wire_value(&row.error_code),
111 row.needs_admin_attention.to_string(),
112 row.attempt_number.to_string(),
113 row.superseded.to_string(),
114 row.student_number.unwrap_or_default(),
115 optional_time(row.student_number_verified_at),
116 wire_value(&row.student_number_verified_via),
117 row.enrolment_realisation_name.unwrap_or_default(),
118 row.grade_id.unwrap_or_default(),
119 row.credits.map(|c| c.to_string()).unwrap_or_default(),
120 optional_time(row.registered_at),
121 row.sisu_attainment_id.unwrap_or_default(),
122 row.linking_email
123 .as_ref()
124 .map(|mail| wire_value(&mail.email_send_status))
125 .unwrap_or_default(),
126 row.linking_email
127 .as_ref()
128 .and_then(|mail| mail.sent_at)
129 .map(|sent_at| sent_at.to_rfc3339())
130 .unwrap_or_default(),
131 row.linking_email
132 .map(|mail| mail.emailed_to_masked)
133 .unwrap_or_default(),
134 ]);
135 }
136 if page_len < PAGE_SIZE {
137 break;
138 }
139 offset += PAGE_SIZE;
140 }
141
142 let writer = writer.finish().await?;
143 Ok(writer)
144}
145
146fn wire_value(value: &impl Serialize) -> String {
149 serde_json::to_value(value)
150 .ok()
151 .and_then(|value| value.as_str().map(str::to_string))
152 .unwrap_or_default()
153}
154
155fn optional_time(value: Option<DateTime<Utc>>) -> String {
156 value.map(|time| time.to_rfc3339()).unwrap_or_default()
157}