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