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