1use std::collections::HashMap;
2
3use crate::{
4 chapters,
5 chapters::DatabaseChapter,
6 exercises,
7 prelude::*,
8 user_details::UserDetail,
9 users::{self, User},
10};
11
12#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
13#[cfg_attr(feature = "ts_rs", derive(TS))]
14pub struct CourseInstance {
15 pub id: Uuid,
16 pub created_at: DateTime<Utc>,
17 pub updated_at: DateTime<Utc>,
18 pub deleted_at: Option<DateTime<Utc>>,
19 pub course_id: Uuid,
20 pub starts_at: Option<DateTime<Utc>>,
21 pub ends_at: Option<DateTime<Utc>>,
22 pub name: Option<String>,
23 pub description: Option<String>,
24 pub teacher_in_charge_name: String,
25 pub teacher_in_charge_email: String,
26 pub support_email: Option<String>,
27}
28
29impl CourseInstance {
30 pub fn is_open(&self) -> bool {
31 self.starts_at.map(|sa| sa < Utc::now()).unwrap_or_default()
32 }
33}
34
35#[derive(Debug, Deserialize)]
36#[cfg_attr(feature = "ts_rs", derive(TS))]
37pub struct CourseInstanceForm {
38 pub name: Option<String>,
39 pub description: Option<String>,
40 pub teacher_in_charge_name: String,
41 pub teacher_in_charge_email: String,
42 pub support_email: Option<String>,
43 pub opening_time: Option<DateTime<Utc>>,
44 pub closing_time: Option<DateTime<Utc>>,
45}
46
47#[derive(Debug, Clone, Copy)]
48pub struct NewCourseInstance<'a> {
49 pub course_id: Uuid,
50 pub name: Option<&'a str>,
51 pub description: Option<&'a str>,
52 pub teacher_in_charge_name: &'a str,
53 pub teacher_in_charge_email: &'a str,
54 pub support_email: Option<&'a str>,
55 pub opening_time: Option<DateTime<Utc>>,
56 pub closing_time: Option<DateTime<Utc>>,
57}
58
59pub async fn insert(
60 conn: &mut PgConnection,
61 pkey_policy: PKeyPolicy<Uuid>,
62 new_course_instance: NewCourseInstance<'_>,
63) -> ModelResult<CourseInstance> {
64 let course_instance = sqlx::query_as!(
65 CourseInstance,
66 r#"
67INSERT INTO course_instances (
68 id,
69 course_id,
70 name,
71 description,
72 teacher_in_charge_name,
73 teacher_in_charge_email,
74 support_email
75 )
76VALUES ($1, $2, $3, $4, $5, $6, $7)
77RETURNING id,
78 created_at,
79 updated_at,
80 deleted_at,
81 course_id,
82 starts_at,
83 ends_at,
84 name,
85 description,
86 teacher_in_charge_name,
87 teacher_in_charge_email,
88 support_email
89"#,
90 pkey_policy.into_uuid(),
91 new_course_instance.course_id,
92 new_course_instance.name,
93 new_course_instance.description,
94 new_course_instance.teacher_in_charge_name,
95 new_course_instance.teacher_in_charge_email,
96 new_course_instance.support_email,
97 )
98 .fetch_one(conn)
99 .await?;
100 Ok(course_instance)
101}
102
103pub async fn get_course_instance(
104 conn: &mut PgConnection,
105 course_instance_id: Uuid,
106) -> ModelResult<CourseInstance> {
107 let course_instance = sqlx::query_as!(
108 CourseInstance,
109 r#"
110SELECT id,
111 created_at,
112 updated_at,
113 deleted_at,
114 course_id,
115 starts_at,
116 ends_at,
117 name,
118 description,
119 teacher_in_charge_name,
120 teacher_in_charge_email,
121 support_email
122FROM course_instances
123WHERE id = $1
124 AND deleted_at IS NULL;
125 "#,
126 course_instance_id,
127 )
128 .fetch_one(conn)
129 .await?;
130 Ok(course_instance)
131}
132
133pub async fn get_course_instance_with_info(
134 conn: &mut PgConnection,
135 course_instance_id: Uuid,
136) -> ModelResult<CourseInstanceWithCourseInfo> {
137 let course_instance = sqlx::query_as!(
138 CourseInstanceWithCourseInfo,
139 r#"
140SELECT
141 c.id AS course_id,
142 c.slug AS course_slug,
143 c.name AS course_name,
144 c.description AS course_description,
145 ci.id AS course_instance_id,
146 ci.name AS course_instance_name,
147 ci.description AS course_instance_description,
148 o.name AS organization_name
149FROM course_instances AS ci
150 LEFT JOIN courses AS c ON ci.course_id = c.id
151 LEFT JOIN organizations AS o ON o.id = c.organization_id
152WHERE ci.id = $1
153 AND ci.deleted_at IS NULL
154 AND c.deleted_at IS NULL
155 AND o.deleted_at IS NULL
156 "#,
157 course_instance_id,
158 )
159 .fetch_one(conn)
160 .await?;
161 Ok(course_instance)
162}
163
164pub async fn get_default_by_course_id(
165 conn: &mut PgConnection,
166 course_id: Uuid,
167) -> ModelResult<CourseInstance> {
168 let res = sqlx::query_as!(
169 CourseInstance,
170 "
171SELECT *
172FROM course_instances
173WHERE course_id = $1
174 AND name IS NULL
175 AND deleted_at IS NULL
176 ",
177 course_id
178 )
179 .fetch_one(conn)
180 .await?;
181 Ok(res)
182}
183
184pub async fn get_organization_id(
185 conn: &mut PgConnection,
186 course_instance_id: Uuid,
187) -> ModelResult<Uuid> {
188 let res = sqlx::query!(
189 "
190SELECT courses.organization_id
191FROM course_instances
192 JOIN courses ON courses.id = course_instances.course_id
193WHERE course_instances.id = $1
194",
195 course_instance_id
196 )
197 .fetch_one(conn)
198 .await?;
199 Ok(res.organization_id)
200}
201
202pub async fn current_course_instance_of_user(
203 conn: &mut PgConnection,
204 user_id: Uuid,
205 course_id: Uuid,
206) -> ModelResult<Option<CourseInstance>> {
207 let course_instance_enrollment = sqlx::query_as!(
208 CourseInstance,
209 r#"
210SELECT i.id,
211 i.created_at,
212 i.updated_at,
213 i.deleted_at,
214 i.course_id,
215 i.starts_at,
216 i.ends_at,
217 i.name,
218 i.description,
219 i.teacher_in_charge_name,
220 i.teacher_in_charge_email,
221 i.support_email
222FROM user_course_settings ucs
223 JOIN course_instances i ON (ucs.current_course_instance_id = i.id)
224WHERE ucs.user_id = $1
225 AND ucs.current_course_id = $2
226 AND ucs.deleted_at IS NULL;
227 "#,
228 user_id,
229 course_id,
230 )
231 .fetch_optional(conn)
232 .await?;
233 Ok(course_instance_enrollment)
234}
235
236pub async fn course_instance_by_users_latest_enrollment(
237 conn: &mut PgConnection,
238 user_id: Uuid,
239 course_id: Uuid,
240) -> ModelResult<Option<CourseInstance>> {
241 let course_instance = sqlx::query_as!(
242 CourseInstance,
243 r#"
244SELECT i.id,
245 i.created_at,
246 i.updated_at,
247 i.deleted_at,
248 i.course_id,
249 i.starts_at,
250 i.ends_at,
251 i.name,
252 i.description,
253 i.teacher_in_charge_name,
254 i.teacher_in_charge_email,
255 i.support_email
256FROM course_instances i
257 JOIN course_instance_enrollments ie ON (i.id = ie.course_id)
258WHERE i.course_id = $1
259 AND i.deleted_at IS NULL
260 AND ie.user_id = $2
261 AND ie.deleted_at IS NULL
262ORDER BY ie.created_at DESC;
263 "#,
264 course_id,
265 user_id,
266 )
267 .fetch_optional(conn)
268 .await?;
269 Ok(course_instance)
270}
271
272pub async fn get_all_course_instances(conn: &mut PgConnection) -> ModelResult<Vec<CourseInstance>> {
273 let course_instances = sqlx::query_as!(
274 CourseInstance,
275 r#"
276SELECT id,
277 created_at,
278 updated_at,
279 deleted_at,
280 course_id,
281 starts_at,
282 ends_at,
283 name,
284 description,
285 teacher_in_charge_name,
286 teacher_in_charge_email,
287 support_email
288FROM course_instances
289WHERE deleted_at IS NULL
290"#
291 )
292 .fetch_all(conn)
293 .await?;
294 Ok(course_instances)
295}
296
297pub async fn get_course_instances_for_course(
298 conn: &mut PgConnection,
299 course_id: Uuid,
300) -> ModelResult<Vec<CourseInstance>> {
301 let course_instances = sqlx::query_as!(
302 CourseInstance,
303 r#"
304SELECT id,
305 created_at,
306 updated_at,
307 deleted_at,
308 course_id,
309 starts_at,
310 ends_at,
311 name,
312 description,
313 teacher_in_charge_name,
314 teacher_in_charge_email,
315 support_email
316FROM course_instances
317WHERE course_id = $1
318 AND deleted_at IS NULL;
319 "#,
320 course_id,
321 )
322 .fetch_all(conn)
323 .await?;
324 Ok(course_instances)
325}
326
327pub async fn get_course_instance_ids_with_course_id(
328 conn: &mut PgConnection,
329 course_id: Uuid,
330) -> ModelResult<Vec<Uuid>> {
331 let res = sqlx::query!(
332 r#"
333SELECT id
334FROM course_instances
335WHERE course_id = $1
336 AND deleted_at IS NULL;
337 "#,
338 course_id,
339 )
340 .map(|r| r.id)
341 .fetch_all(conn)
342 .await?;
343 Ok(res)
344}
345
346#[derive(Debug, Serialize)]
347#[cfg_attr(feature = "ts_rs", derive(TS))]
348pub struct ChapterScore {
349 #[serde(flatten)]
350 pub chapter: DatabaseChapter,
351 pub score_given: f32,
352 pub score_total: i32,
353}
354
355#[derive(Debug, Default, Serialize)]
356#[cfg_attr(feature = "ts_rs", derive(TS))]
357pub struct PointMap(pub HashMap<Uuid, f32>);
358
359#[derive(Debug, Serialize)]
360#[cfg_attr(feature = "ts_rs", derive(TS))]
361pub struct Points {
362 pub chapter_points: Vec<ChapterScore>,
363 pub users: Vec<UserDetail>,
364 pub user_chapter_points: HashMap<Uuid, PointMap>,
366}
367
368pub async fn get_points(
369 conn: &mut PgConnection,
370 instance_id: Uuid,
371 _pagination: Pagination, ) -> ModelResult<Points> {
373 let mut chapter_point_totals = HashMap::<Uuid, i32>::new();
374 let mut exercise_to_chapter_id = HashMap::new();
375 let course_instance = crate::course_instances::get_course_instance(conn, instance_id).await?;
376 let exercises =
377 exercises::get_exercises_by_course_id(&mut *conn, course_instance.course_id).await?;
378 for exercise in exercises {
379 if let Some(chapter_id) = exercise.chapter_id {
380 let total = chapter_point_totals.entry(chapter_id).or_default();
382 *total += exercise.score_maximum;
383 exercise_to_chapter_id.insert(exercise.id, chapter_id);
384 }
385 }
386
387 let users: HashMap<Uuid, User> =
388 users::get_users_by_course_instance_enrollment(conn, instance_id)
389 .await?
390 .into_iter()
391 .map(|u| (u.id, u))
392 .collect();
393 let mut chapter_points_given = HashMap::<Uuid, f32>::new();
394 let states = sqlx::query!(
395 "
396SELECT user_id,
397 exercise_id,
398 score_given
399FROM user_exercise_states
400WHERE course_id = $1
401 AND deleted_at IS NULL
402ORDER BY user_id ASC
403",
404 course_instance.course_id,
405 )
406 .fetch_all(&mut *conn)
407 .await?;
408 let mut user_chapter_points = HashMap::<Uuid, PointMap>::new();
409 for state in states {
410 let user = match users.get(&state.user_id) {
411 Some(user) => user,
412 None => {
413 tracing::warn!(
414 "user {} has an exercise state but no enrollment",
415 state.user_id
416 );
417 continue;
418 }
419 };
420 if let Some(chapter_id) = exercise_to_chapter_id.get(&state.exercise_id).copied() {
421 let chapter_points = user_chapter_points.entry(user.id).or_default();
422 let user_given = chapter_points.0.entry(chapter_id).or_default();
423 let chapter_given = chapter_points_given.entry(chapter_id).or_default();
424 let score_given = state.score_given.unwrap_or_default();
425 *user_given += score_given;
426 *chapter_given += score_given;
427 }
428 }
429
430 let chapters = chapters::course_instance_chapters(&mut *conn, instance_id).await?;
431 let mut chapter_points: Vec<ChapterScore> = chapters
432 .into_iter()
433 .map(|c| ChapterScore {
434 score_given: chapter_points_given.get(&c.id).copied().unwrap_or_default(),
435 score_total: chapter_point_totals.get(&c.id).copied().unwrap_or_default(),
436 chapter: c,
437 })
438 .collect();
439 chapter_points.sort_by_key(|c| c.chapter.chapter_number);
440
441 let list_of_users = users.into_values().collect::<Vec<_>>();
442 let user_id_to_details =
443 crate::user_details::get_users_details_by_user_id_map(&mut *conn, &list_of_users).await?;
444
445 Ok(Points {
446 chapter_points,
447 users: list_of_users
448 .into_iter()
449 .filter_map(|user| user_id_to_details.get(&user.id).cloned())
450 .collect::<Vec<_>>(),
451 user_chapter_points,
452 })
453}
454
455pub async fn edit(
456 conn: &mut PgConnection,
457 instance_id: Uuid,
458 update: CourseInstanceForm,
459) -> ModelResult<()> {
460 sqlx::query!(
461 "
462UPDATE course_instances
463SET name = $1,
464 description = $2,
465 teacher_in_charge_name = $3,
466 teacher_in_charge_email = $4,
467 support_email = $5,
468 starts_at = $6,
469 ends_at = $7
470WHERE id = $8
471",
472 update.name,
473 update.description,
474 update.teacher_in_charge_name,
475 update.teacher_in_charge_email,
476 update.support_email,
477 update.opening_time,
478 update.closing_time,
479 instance_id
480 )
481 .execute(conn)
482 .await?;
483 Ok(())
484}
485
486pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
487 sqlx::query!(
488 "
489UPDATE course_instances
490SET deleted_at = now()
491WHERE id = $1
492AND deleted_at IS NULL
493",
494 id
495 )
496 .execute(conn)
497 .await?;
498 Ok(())
499}
500
501pub async fn get_course_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<Uuid> {
502 let res = sqlx::query!(
503 "
504SELECT course_id
505FROM course_instances
506WHERE id = $1
507",
508 id
509 )
510 .fetch_one(conn)
511 .await?;
512 Ok(res.course_id)
513}
514
515pub async fn is_open(conn: &mut PgConnection, id: Uuid) -> ModelResult<bool> {
516 let res = sqlx::query!(
517 "
518SELECT starts_at,
519 ends_at
520FROM course_instances
521WHERE id = $1
522",
523 id
524 )
525 .fetch_one(conn)
526 .await?;
527 let has_started = match res.starts_at {
528 Some(starts_at) => starts_at <= Utc::now(),
529 None => true,
530 };
531 let has_ended = match res.ends_at {
532 Some(ends_at) => ends_at <= Utc::now(),
533 None => false,
534 };
535 let is_open = has_started && !has_ended;
536 Ok(is_open)
537}
538
539pub async fn get_by_ids(
540 conn: &mut PgConnection,
541 course_instance_ids: &[Uuid],
542) -> ModelResult<Vec<CourseInstance>> {
543 let course_instances = sqlx::query_as!(
544 CourseInstance,
545 r#"
546SELECT *
547FROM course_instances
548WHERE id IN (SELECT * FROM UNNEST($1::uuid[]))
549 "#,
550 course_instance_ids
551 )
552 .fetch_all(conn)
553 .await?;
554 Ok(course_instances)
555}
556
557pub struct CourseInstanceWithCourseInfo {
558 pub course_id: Uuid,
559 pub course_slug: String,
560 pub course_name: String,
561 pub course_description: Option<String>,
562 pub course_instance_id: Uuid,
563 pub course_instance_name: Option<String>,
564 pub course_instance_description: Option<String>,
565 pub organization_name: String,
566}
567
568pub async fn get_enrolled_course_instances_for_user(
569 conn: &mut PgConnection,
570 user_id: Uuid,
571) -> ModelResult<Vec<CourseInstanceWithCourseInfo>> {
572 let course_instances = sqlx::query_as!(
573 CourseInstanceWithCourseInfo,
574 r#"
575SELECT
576 c.id AS course_id,
577 c.slug AS course_slug,
578 c.name AS course_name,
579 c.description AS course_description,
580 ci.id AS course_instance_id,
581 ci.name AS course_instance_name,
582 ci.description AS course_instance_description,
583 o.name AS organization_name
584FROM course_instances AS ci
585 JOIN course_instance_enrollments AS cie ON ci.id = cie.course_instance_id
586 LEFT JOIN courses AS c ON ci.course_id = c.id
587 LEFT JOIN organizations AS o ON o.id = c.organization_id
588WHERE cie.user_id = $1
589 AND ci.deleted_at IS NULL
590 AND cie.deleted_at IS NULL
591 AND c.deleted_at IS NULL
592 AND o.deleted_at IS NULL
593"#,
594 user_id
595 )
596 .fetch_all(conn)
597 .await?;
598 Ok(course_instances)
599}
600
601pub async fn get_enrolled_course_instances_for_user_with_exercise_type(
602 conn: &mut PgConnection,
603 user_id: Uuid,
604 exercise_type: &str,
605) -> ModelResult<Vec<CourseInstanceWithCourseInfo>> {
606 let course_instances = sqlx::query_as!(
607 CourseInstanceWithCourseInfo,
608 r#"
609SELECT DISTINCT ON (ci.id)
610 c.id AS course_id,
611 c.slug AS course_slug,
612 c.name AS course_name,
613 c.description AS course_description,
614 ci.id AS course_instance_id,
615 ci.name AS course_instance_name,
616 ci.description AS course_instance_description,
617 o.name AS organization_name
618FROM course_instances AS ci
619 JOIN course_instance_enrollments AS cie ON ci.id = cie.course_instance_id
620 LEFT JOIN courses AS c ON ci.course_id = c.id
621 LEFT JOIN exercises AS e ON e.course_id = c.id
622 LEFT JOIN exercise_slides AS es ON es.exercise_id = e.id
623 LEFT JOIN exercise_tasks AS et ON et.exercise_slide_id = es.id
624 LEFT JOIN organizations AS o ON o.id = c.organization_id
625WHERE cie.user_id = $1
626 AND et.exercise_type = $2
627 AND ci.deleted_at IS NULL
628 AND cie.deleted_at IS NULL
629 AND c.deleted_at IS NULL
630 AND e.deleted_at IS NULL
631 AND es.deleted_at IS NULL
632 AND et.deleted_at IS NULL
633"#,
634 user_id,
635 exercise_type,
636 )
637 .fetch_all(conn)
638 .await?;
639 Ok(course_instances)
640}
641
642pub async fn reset_progress_on_course_instance_for_user(
644 conn: &mut PgConnection,
645 user_id: Uuid,
646 course_id: Uuid,
647) -> ModelResult<()> {
648 let mut tx = conn.begin().await?;
649 sqlx::query!(
650 "
651UPDATE exercise_slide_submissions
652SET deleted_at = now()
653WHERE user_id = $1
654 AND course_id = $2
655 AND deleted_at IS NULL
656 ",
657 user_id,
658 course_id
659 )
660 .execute(&mut *tx)
661 .await?;
662 sqlx::query!(
663 "
664UPDATE exercise_task_submissions
665SET deleted_at = now()
666WHERE exercise_slide_submission_id IN (
667 SELECT id
668 FROM exercise_slide_submissions
669 WHERE user_id = $1
670 AND course_id = $2
671 )
672 AND deleted_at IS NULL
673",
674 user_id,
675 course_id
676 )
677 .execute(&mut *tx)
678 .await?;
679 sqlx::query!(
680 "
681UPDATE peer_review_queue_entries
682SET deleted_at = now()
683WHERE user_id = $1
684 AND course_id = $2
685 AND deleted_at IS NULL
686",
687 user_id,
688 course_id
689 )
690 .execute(&mut *tx)
691 .await?;
692 sqlx::query!(
693 "
694UPDATE peer_or_self_review_submissions
695SET deleted_at = now()
696WHERE user_id = $1
697 AND course_id = $2
698 AND deleted_at IS NULL
699",
700 user_id,
701 course_id
702 )
703 .execute(&mut *tx)
704 .await?;
705 sqlx::query!(
706 "
707UPDATE peer_or_self_review_question_submissions
708SET deleted_at = now()
709WHERE peer_or_self_review_submission_id IN (
710 SELECT id
711 FROM peer_or_self_review_submissions
712 WHERE user_id = $1
713 AND course_id = $2
714 )
715 AND deleted_at IS NULL
716",
717 user_id,
718 course_id
719 )
720 .execute(&mut *tx)
721 .await?;
722 sqlx::query!(
723 "
724UPDATE exercise_task_gradings
725SET deleted_at = now()
726WHERE exercise_task_submission_id IN (
727 SELECT id
728 FROM exercise_task_submissions
729 WHERE exercise_slide_submission_id IN (
730 SELECT id
731 FROM exercise_slide_submissions
732 WHERE user_id = $1
733 AND course_id = $2
734 )
735 )
736 AND deleted_at IS NULL
737",
738 user_id,
739 course_id
740 )
741 .execute(&mut *tx)
742 .await?;
743
744 sqlx::query!(
745 "
746UPDATE user_exercise_states
747SET deleted_at = now()
748WHERE user_id = $1
749 AND course_id = $2
750 AND deleted_at IS NULL
751",
752 user_id,
753 course_id
754 )
755 .execute(&mut *tx)
756 .await?;
757 sqlx::query!(
758 "
759UPDATE user_exercise_task_states
760SET deleted_at = now()
761WHERE user_exercise_slide_state_id IN (
762 SELECT id
763 FROM user_exercise_slide_states
764 WHERE user_exercise_state_id IN (
765 SELECT id
766 FROM user_exercise_states
767 WHERE user_id = $1
768 AND course_id = $2
769 )
770 )
771 AND deleted_at IS NULL
772",
773 user_id,
774 course_id
775 )
776 .execute(&mut *tx)
777 .await?;
778 sqlx::query!(
779 "
780UPDATE user_exercise_slide_states
781SET deleted_at = now()
782WHERE user_exercise_state_id IN (
783 SELECT id
784 FROM user_exercise_states
785 WHERE user_id = $1
786 AND course_id = $2
787 )
788 AND deleted_at IS NULL
789",
790 user_id,
791 course_id
792 )
793 .execute(&mut *tx)
794 .await?;
795 sqlx::query!(
796 "
797UPDATE teacher_grading_decisions
798SET deleted_at = now()
799WHERE user_exercise_state_id IN (
800 SELECT id
801 FROM user_exercise_states
802 WHERE user_id = $1
803 AND course_id = $2
804 )
805 AND deleted_at IS NULL
806",
807 user_id,
808 course_id
809 )
810 .execute(&mut *tx)
811 .await?;
812 sqlx::query!(
813 "
814UPDATE course_module_completions
815SET deleted_at = now()
816WHERE user_id = $1
817AND course_id = $2
818AND deleted_at IS NULL
819",
820 user_id,
821 course_id
822 )
823 .execute(&mut *tx)
824 .await?;
825 sqlx::query!(
826 "
827UPDATE generated_certificates
828SET deleted_at = NOW()
829WHERE user_id = $1
830 AND certificate_configuration_id IN (
831 SELECT certificate_configuration_id
832 FROM certificate_configuration_to_requirements
833 WHERE course_module_id IN (
834 SELECT id
835 FROM course_modules
836 WHERE course_id = $2
837 )
838 AND deleted_at IS NULL
839 )
840 AND deleted_at IS NULL
841",
842 user_id,
843 course_id
844 )
845 .execute(&mut *tx)
846 .await?;
847
848 tx.commit().await?;
849 Ok(())
850}
851
852pub async fn get_course_average_duration(
853 conn: &mut PgConnection,
854 course_id: Uuid,
855) -> ModelResult<Option<i64>> {
856 let res = sqlx::query!(
857 "
858SELECT AVG(
859 EXTRACT(
860 EPOCH
861 FROM cmc.completion_date - ce.created_at
862 )
863 )::int8 AS average_duration_seconds
864FROM course_instance_enrollments ce
865 JOIN course_module_completions cmc ON (
866 cmc.course_id = ce.course_id
867 AND cmc.user_id = ce.user_id
868 )
869WHERE ce.course_id = $1
870 AND ce.deleted_at IS NULL
871 AND cmc.deleted_at IS NULL;
872 ",
873 course_id
874 )
875 .fetch_optional(conn)
876 .await?;
877
878 Ok(res.map(|r| r.average_duration_seconds).unwrap_or_default())
879}
880
881pub async fn get_student_duration(
882 conn: &mut PgConnection,
883 user_id: Uuid,
884 course_id: Uuid,
885) -> ModelResult<Option<i64>> {
886 let res = sqlx::query!(
887 "
888SELECT COALESCE(
889 EXTRACT(
890 EPOCH
891 FROM cmc.completion_date - ce.created_at
892 )::int8,
893 0
894 ) AS student_duration_seconds
895FROM course_instance_enrollments ce
896 JOIN course_module_completions cmc ON (
897 cmc.course_id = ce.course_id
898 AND cmc.user_id = ce.user_id
899 )
900WHERE ce.course_id = $1
901 AND ce.user_id = $2
902 AND ce.deleted_at IS NULL
903 AND cmc.deleted_at IS NULL;
904 ",
905 course_id,
906 user_id
907 )
908 .fetch_optional(conn)
909 .await?;
910
911 Ok(res.map(|r| r.student_duration_seconds).unwrap_or_default())
912}
913
914#[cfg(test)]
915mod test {
916 use super::*;
917 use crate::{
918 course_instance_enrollments::NewCourseInstanceEnrollment, exercise_tasks::NewExerciseTask,
919 test_helper::*,
920 };
921
922 #[tokio::test]
923 async fn allows_only_one_instance_per_course_without_name() {
924 insert_data!(:tx, :user, :org, course: course_id);
925
926 let mut tx1 = tx.begin().await;
927 let mut instance = NewCourseInstance {
929 course_id,
930 name: None,
931 description: None,
932 teacher_in_charge_name: "teacher",
933 teacher_in_charge_email: "teacher@example.com",
934 support_email: None,
935 opening_time: None,
936 closing_time: None,
937 };
938 insert(tx1.as_mut(), PKeyPolicy::Generate, instance)
939 .await
940 .unwrap_err();
941 tx1.rollback().await;
942
943 let mut tx2 = tx.begin().await;
944 instance.name = Some("name");
946 insert(tx2.as_mut(), PKeyPolicy::Generate, instance)
947 .await
948 .unwrap();
949 }
950
951 #[tokio::test]
952 async fn gets_enrolled_course_instances_for_user_with_exercise_type() {
953 insert_data!(:tx, user:user_id, :org, course:course_id, :instance, course_module:_course_module_id, chapter:chapter_id, page:page_id, :exercise, slide:exercise_slide_id);
954
955 crate::course_instance_enrollments::insert_enrollment_and_set_as_current(
957 tx.as_mut(),
958 NewCourseInstanceEnrollment {
959 course_id,
960 user_id,
961 course_instance_id: instance.id,
962 },
963 )
964 .await
965 .unwrap();
966 let course_instances =
967 get_enrolled_course_instances_for_user_with_exercise_type(tx.as_mut(), user_id, "tmc")
968 .await
969 .unwrap();
970 assert!(
971 course_instances.is_empty(),
972 "user should not be enrolled on any course with tmc exercises"
973 );
974
975 crate::exercise_tasks::insert(
977 tx.as_mut(),
978 PKeyPolicy::Generate,
979 NewExerciseTask {
980 assignment: Vec::new(),
981 exercise_slide_id,
982 exercise_type: "tmc".to_string(),
983 model_solution_spec: None,
984 private_spec: None,
985 public_spec: None,
986 order_number: 1,
987 },
988 )
989 .await
990 .unwrap();
991 let course_instances =
992 get_enrolled_course_instances_for_user_with_exercise_type(tx.as_mut(), user_id, "tmc")
993 .await
994 .unwrap();
995 assert_eq!(
996 course_instances.len(),
997 1,
998 "user should be enrolled on one course with tmc exercises"
999 );
1000 tx.rollback().await;
1001 }
1002
1003 #[tokio::test]
1004 async fn gets_course_average_duration_with_empty_database() {
1005 insert_data!(:tx, :user, :org, :course);
1006 let duration = get_course_average_duration(tx.as_mut(), course)
1007 .await
1008 .unwrap();
1009 assert!(duration.is_none())
1010 }
1011}