headless_lms_models/library/
students_view.rs1use crate::chapters::{
3 self, ChapterAvailability, CourseUserInfo, DatabaseChapter, UserChapterProgress,
4};
5use crate::prelude::*;
6use crate::user_details::UserDetail;
7use crate::user_exercise_states::UserExerciseState;
8use chrono::{DateTime, Utc};
9
10#[derive(Clone, PartialEq, Deserialize, Serialize)]
11#[cfg_attr(feature = "ts_rs", derive(TS))]
12pub struct ProgressOverview {
13 pub user_details: Vec<UserDetail>,
14 pub chapters: Vec<DatabaseChapter>,
15 pub user_exercise_states: Vec<UserExerciseState>,
16 pub chapter_availability: Vec<ChapterAvailability>,
17 pub user_chapter_progress: Vec<UserChapterProgress>,
18}
19
20pub async fn get_progress(
21 conn: &mut PgConnection,
22 course_id: Uuid,
23) -> ModelResult<ProgressOverview> {
24 let user_details = crate::user_details::get_users_by_course_id(conn, course_id).await?;
25 let chapters = crate::chapters::course_chapters(conn, course_id).await?;
26 let user_exercise_states =
27 crate::user_exercise_states::get_all_for_course(conn, course_id).await?;
28 let chapter_availability = chapters::fetch_chapter_availability(conn, course_id).await?;
29 let user_chapter_progress = chapters::fetch_user_chapter_progress(conn, course_id).await?;
30
31 Ok(ProgressOverview {
32 user_details,
33 chapters,
34 user_exercise_states,
35 chapter_availability,
36 user_chapter_progress,
37 })
38}
39
40pub async fn get_course_users(
41 conn: &mut PgConnection,
42 course_id: Uuid,
43) -> ModelResult<Vec<CourseUserInfo>> {
44 let rows = chapters::fetch_course_users(conn, course_id).await?;
45 Ok(rows)
46}
47
48#[derive(Clone, PartialEq, Deserialize, Serialize, sqlx::FromRow)]
49#[cfg_attr(feature = "ts_rs", derive(TS))]
50pub struct CompletionGridRow {
51 pub student: String,
52 pub module: Option<String>, pub grade: String, pub status: String, }
56
57pub async fn get_completions_grid_by_course_id(
60 conn: &mut PgConnection,
61 course_id: Uuid,
62) -> ModelResult<Vec<CompletionGridRow>> {
63 let rows_raw = sqlx::query!(
64 r#"
65WITH modules AS (
66 SELECT id AS module_id, name AS module_name, order_number
67 FROM course_modules
68 WHERE course_id = $1
69 AND deleted_at IS NULL
70),
71enrolled AS (
72 SELECT DISTINCT user_id
73 FROM course_instance_enrollments
74 WHERE course_id = $1
75 AND deleted_at IS NULL
76),
77latest_cmc AS (
78 SELECT DISTINCT ON (cmc.user_id, cmc.course_module_id)
79 cmc.id,
80 cmc.user_id,
81 cmc.course_module_id,
82 cmc.grade,
83 cmc.passed,
84 cmc.completion_date
85 FROM course_module_completions cmc
86 WHERE cmc.course_id = $1
87 AND cmc.deleted_at IS NULL
88 ORDER BY cmc.user_id, cmc.course_module_id, cmc.completion_date DESC
89),
90cmcr AS (
91 SELECT course_module_completion_id
92 FROM course_module_completion_registered_to_study_registries
93 WHERE course_id = $1
94 AND deleted_at IS NULL
95)
96SELECT
97 ud.first_name AS "first_name?",
98 ud.last_name AS "last_name?",
99 m.module_name AS "module_name?",
100 r.grade AS "grade?",
101 r.passed AS "passed?",
102 (r.id IS NOT NULL AND r.id IN (SELECT course_module_completion_id FROM cmcr)) AS "is_registered?"
103FROM modules m
104CROSS JOIN enrolled e
105JOIN users u ON u.id = e.user_id
106LEFT JOIN user_details ud ON ud.user_id = u.id
107LEFT JOIN latest_cmc r
108 ON r.user_id = e.user_id
109 AND r.course_module_id = m.module_id
110"#,
111 course_id
112 )
113 .fetch_all(conn)
114 .await?;
115
116 let rows = rows_raw
117 .into_iter()
118 .map(|r| {
119 let first = r.first_name.unwrap_or_default().trim().to_string();
120 let last = r.last_name.unwrap_or_default().trim().to_string();
121
122 let student = if first.is_empty() && last.is_empty() {
123 "(Missing name)".to_string()
124 } else {
125 format!("{first} {last}").trim().to_string()
126 };
127
128 let grade = match (r.grade, r.passed) {
129 (Some(g), _) => g.to_string(),
130 (None, Some(true)) => "Passed".to_string(),
131 (None, Some(false)) => "Failed".to_string(),
132 (None, None) => "-".to_string(),
133 };
134
135 let status = if r.is_registered.unwrap_or(false) {
136 "Registered".to_string()
137 } else {
138 "-".to_string()
139 };
140
141 CompletionGridRow {
142 student,
143 module: r.module_name,
144 grade,
145 status,
146 }
147 })
148 .collect();
149
150 Ok(rows)
151}
152
153#[derive(Clone, PartialEq, Deserialize, Serialize, sqlx::FromRow)]
154#[cfg_attr(feature = "ts_rs", derive(TS))]
155pub struct CertificateGridRow {
156 pub student: String,
157 pub certificate: String,
158 pub date_issued: Option<DateTime<Utc>>,
159 pub verification_id: Option<String>,
160 pub certificate_id: Option<Uuid>,
161 pub name_on_certificate: Option<String>,
162}
163
164pub async fn get_certificates_grid_by_course_id(
166 conn: &mut PgConnection,
167 course_id: Uuid,
168) -> ModelResult<Vec<CertificateGridRow>> {
169 let rows = sqlx::query_as!(
170 CertificateGridRow,
171 r#"
172WITH enrolled AS (
173 SELECT DISTINCT user_id
174 FROM course_instance_enrollments
175 WHERE course_id = $1
176 AND deleted_at IS NULL
177),
178user_certs AS (
179 -- one latest certificate per user for this course
180 SELECT DISTINCT ON (gc.user_id)
181 gc.user_id,
182 gc.id,
183 gc.created_at AS latest_issued_at,
184 gc.verification_id,
185 gc.name_on_certificate
186 FROM generated_certificates gc
187 JOIN certificate_configuration_to_requirements cctr
188 ON gc.certificate_configuration_id = cctr.certificate_configuration_id
189 AND cctr.deleted_at IS NULL
190 JOIN course_modules cm
191 ON cm.id = cctr.course_module_id
192 AND cm.deleted_at IS NULL
193 WHERE cm.course_id = $1
194 AND gc.deleted_at IS NULL
195 ORDER BY gc.user_id, gc.created_at DESC
196)
197SELECT
198 /* non-null */
199 CASE
200 WHEN ud.first_name IS NULL OR TRIM(ud.first_name) = ''
201 OR ud.last_name IS NULL OR TRIM(ud.last_name) = ''
202 THEN '(Missing name)'
203 ELSE TRIM(ud.first_name) || ' ' || TRIM(ud.last_name)
204 END AS "student!",
205
206 /* non-null */
207 CASE
208 WHEN uc.user_id IS NOT NULL THEN 'Course Certificate'
209 ELSE 'No Certificate'
210 END AS "certificate!",
211
212 /* nullable */
213 uc.latest_issued_at AS "date_issued?",
214 uc.verification_id AS "verification_id?",
215 uc.id AS "certificate_id?",
216 uc.name_on_certificate AS "name_on_certificate?"
217
218FROM enrolled e
219JOIN users u ON u.id = e.user_id
220LEFT JOIN user_details ud ON ud.user_id = u.id
221LEFT JOIN user_certs uc ON uc.user_id = e.user_id
222ORDER BY
223 COALESCE(NULLIF(LOWER(TRIM(ud.last_name)), ''), 'zzzzzz'),
224 COALESCE(NULLIF(LOWER(TRIM(ud.first_name)), ''), 'zzzzzz')
225 "#,
226 course_id
227 )
228 .fetch_all(conn)
229 .await?;
230
231 Ok(rows)
232}