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
492",
493 id
494 )
495 .execute(conn)
496 .await?;
497 Ok(())
498}
499
500pub async fn get_course_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<Uuid> {
501 let res = sqlx::query!(
502 "
503SELECT course_id
504FROM course_instances
505WHERE id = $1
506",
507 id
508 )
509 .fetch_one(conn)
510 .await?;
511 Ok(res.course_id)
512}
513
514pub async fn is_open(conn: &mut PgConnection, id: Uuid) -> ModelResult<bool> {
515 let res = sqlx::query!(
516 "
517SELECT starts_at,
518 ends_at
519FROM course_instances
520WHERE id = $1
521",
522 id
523 )
524 .fetch_one(conn)
525 .await?;
526 let has_started = match res.starts_at {
527 Some(starts_at) => starts_at <= Utc::now(),
528 None => true,
529 };
530 let has_ended = match res.ends_at {
531 Some(ends_at) => ends_at <= Utc::now(),
532 None => false,
533 };
534 let is_open = has_started && !has_ended;
535 Ok(is_open)
536}
537
538pub async fn get_by_ids(
539 conn: &mut PgConnection,
540 course_instance_ids: &[Uuid],
541) -> ModelResult<Vec<CourseInstance>> {
542 let course_instances = sqlx::query_as!(
543 CourseInstance,
544 r#"
545SELECT *
546FROM course_instances
547WHERE id IN (SELECT * FROM UNNEST($1::uuid[]))
548 "#,
549 course_instance_ids
550 )
551 .fetch_all(conn)
552 .await?;
553 Ok(course_instances)
554}
555
556pub struct CourseInstanceWithCourseInfo {
557 pub course_id: Uuid,
558 pub course_slug: String,
559 pub course_name: String,
560 pub course_description: Option<String>,
561 pub course_instance_id: Uuid,
562 pub course_instance_name: Option<String>,
563 pub course_instance_description: Option<String>,
564 pub organization_name: String,
565}
566
567pub async fn get_enrolled_course_instances_for_user(
568 conn: &mut PgConnection,
569 user_id: Uuid,
570) -> ModelResult<Vec<CourseInstanceWithCourseInfo>> {
571 let course_instances = sqlx::query_as!(
572 CourseInstanceWithCourseInfo,
573 r#"
574SELECT
575 c.id AS course_id,
576 c.slug AS course_slug,
577 c.name AS course_name,
578 c.description AS course_description,
579 ci.id AS course_instance_id,
580 ci.name AS course_instance_name,
581 ci.description AS course_instance_description,
582 o.name AS organization_name
583FROM course_instances AS ci
584 JOIN course_instance_enrollments AS cie ON ci.id = cie.course_instance_id
585 LEFT JOIN courses AS c ON ci.course_id = c.id
586 LEFT JOIN organizations AS o ON o.id = c.organization_id
587WHERE cie.user_id = $1
588 AND ci.deleted_at IS NULL
589 AND cie.deleted_at IS NULL
590 AND c.deleted_at IS NULL
591 AND o.deleted_at IS NULL
592"#,
593 user_id
594 )
595 .fetch_all(conn)
596 .await?;
597 Ok(course_instances)
598}
599
600pub async fn get_enrolled_course_instances_for_user_with_exercise_type(
601 conn: &mut PgConnection,
602 user_id: Uuid,
603 exercise_type: &str,
604) -> ModelResult<Vec<CourseInstanceWithCourseInfo>> {
605 let course_instances = sqlx::query_as!(
606 CourseInstanceWithCourseInfo,
607 r#"
608SELECT DISTINCT ON (ci.id)
609 c.id AS course_id,
610 c.slug AS course_slug,
611 c.name AS course_name,
612 c.description AS course_description,
613 ci.id AS course_instance_id,
614 ci.name AS course_instance_name,
615 ci.description AS course_instance_description,
616 o.name AS organization_name
617FROM course_instances AS ci
618 JOIN course_instance_enrollments AS cie ON ci.id = cie.course_instance_id
619 LEFT JOIN courses AS c ON ci.course_id = c.id
620 LEFT JOIN exercises AS e ON e.course_id = c.id
621 LEFT JOIN exercise_slides AS es ON es.exercise_id = e.id
622 LEFT JOIN exercise_tasks AS et ON et.exercise_slide_id = es.id
623 LEFT JOIN organizations AS o ON o.id = c.organization_id
624WHERE cie.user_id = $1
625 AND et.exercise_type = $2
626 AND ci.deleted_at IS NULL
627 AND cie.deleted_at IS NULL
628 AND c.deleted_at IS NULL
629 AND e.deleted_at IS NULL
630 AND es.deleted_at IS NULL
631 AND et.deleted_at IS NULL
632"#,
633 user_id,
634 exercise_type,
635 )
636 .fetch_all(conn)
637 .await?;
638 Ok(course_instances)
639}
640
641pub async fn reset_progress_on_course_instance_for_user(
643 conn: &mut PgConnection,
644 user_id: Uuid,
645 course_id: Uuid,
646) -> ModelResult<()> {
647 let mut tx = conn.begin().await?;
648 sqlx::query!(
649 "
650UPDATE exercise_slide_submissions
651SET deleted_at = now()
652WHERE user_id = $1
653 AND course_id = $2
654 AND deleted_at IS NULL
655 ",
656 user_id,
657 course_id
658 )
659 .execute(&mut *tx)
660 .await?;
661 sqlx::query!(
662 "
663UPDATE exercise_task_submissions
664SET deleted_at = now()
665WHERE exercise_slide_submission_id IN (
666 SELECT id
667 FROM exercise_slide_submissions
668 WHERE user_id = $1
669 AND course_id = $2
670 )
671 AND deleted_at IS NULL
672",
673 user_id,
674 course_id
675 )
676 .execute(&mut *tx)
677 .await?;
678 sqlx::query!(
679 "
680UPDATE peer_review_queue_entries
681SET deleted_at = now()
682WHERE user_id = $1
683 AND course_id = $2
684 AND deleted_at IS NULL
685",
686 user_id,
687 course_id
688 )
689 .execute(&mut *tx)
690 .await?;
691 sqlx::query!(
692 "
693UPDATE peer_or_self_review_submissions
694SET deleted_at = now()
695WHERE user_id = $1
696 AND course_id = $2
697 AND deleted_at IS NULL
698",
699 user_id,
700 course_id
701 )
702 .execute(&mut *tx)
703 .await?;
704 sqlx::query!(
705 "
706UPDATE peer_or_self_review_question_submissions
707SET deleted_at = now()
708WHERE peer_or_self_review_submission_id IN (
709 SELECT id
710 FROM peer_or_self_review_submissions
711 WHERE user_id = $1
712 AND course_id = $2
713 )
714 AND deleted_at IS NULL
715",
716 user_id,
717 course_id
718 )
719 .execute(&mut *tx)
720 .await?;
721 sqlx::query!(
722 "
723UPDATE exercise_task_gradings
724SET deleted_at = now()
725WHERE exercise_task_submission_id IN (
726 SELECT id
727 FROM exercise_task_submissions
728 WHERE exercise_slide_submission_id IN (
729 SELECT id
730 FROM exercise_slide_submissions
731 WHERE user_id = $1
732 AND course_id = $2
733 )
734 )
735 AND deleted_at IS NULL
736",
737 user_id,
738 course_id
739 )
740 .execute(&mut *tx)
741 .await?;
742
743 sqlx::query!(
744 "
745UPDATE user_exercise_states
746SET deleted_at = now()
747WHERE user_id = $1
748 AND course_id = $2
749 AND deleted_at IS NULL
750",
751 user_id,
752 course_id
753 )
754 .execute(&mut *tx)
755 .await?;
756 sqlx::query!(
757 "
758UPDATE user_exercise_task_states
759SET deleted_at = now()
760WHERE user_exercise_slide_state_id IN (
761 SELECT id
762 FROM user_exercise_slide_states
763 WHERE user_exercise_state_id IN (
764 SELECT id
765 FROM user_exercise_states
766 WHERE user_id = $1
767 AND course_id = $2
768 )
769 )
770 AND deleted_at IS NULL
771",
772 user_id,
773 course_id
774 )
775 .execute(&mut *tx)
776 .await?;
777 sqlx::query!(
778 "
779UPDATE user_exercise_slide_states
780SET deleted_at = now()
781WHERE user_exercise_state_id IN (
782 SELECT id
783 FROM user_exercise_states
784 WHERE user_id = $1
785 AND course_id = $2
786 )
787 AND deleted_at IS NULL
788",
789 user_id,
790 course_id
791 )
792 .execute(&mut *tx)
793 .await?;
794 sqlx::query!(
795 "
796UPDATE teacher_grading_decisions
797SET deleted_at = now()
798WHERE user_exercise_state_id IN (
799 SELECT id
800 FROM user_exercise_states
801 WHERE user_id = $1
802 AND course_id = $2
803 )
804 AND deleted_at IS NULL
805",
806 user_id,
807 course_id
808 )
809 .execute(&mut *tx)
810 .await?;
811 sqlx::query!(
812 "
813UPDATE course_module_completions
814SET deleted_at = now()
815WHERE user_id = $1
816AND course_id = $2
817AND deleted_at IS NULL
818",
819 user_id,
820 course_id
821 )
822 .execute(&mut *tx)
823 .await?;
824 sqlx::query!(
825 "
826UPDATE generated_certificates
827SET deleted_at = NOW()
828WHERE user_id = $1
829 AND certificate_configuration_id IN (
830 SELECT certificate_configuration_id
831 FROM certificate_configuration_to_requirements
832 WHERE course_module_id IN (
833 SELECT id
834 FROM course_modules
835 WHERE course_id = $2
836 )
837 AND deleted_at IS NULL
838 )
839 AND deleted_at IS NULL
840",
841 user_id,
842 course_id
843 )
844 .execute(&mut *tx)
845 .await?;
846
847 tx.commit().await?;
848 Ok(())
849}
850
851pub async fn get_course_average_duration(
852 conn: &mut PgConnection,
853 course_id: Uuid,
854) -> ModelResult<Option<i64>> {
855 let res = sqlx::query!(
856 "
857SELECT AVG(
858 EXTRACT(
859 EPOCH
860 FROM cmc.completion_date - ce.created_at
861 )
862 )::int8 AS average_duration_seconds
863FROM course_instance_enrollments ce
864 JOIN course_module_completions cmc ON (
865 cmc.course_id = ce.course_id
866 AND cmc.user_id = ce.user_id
867 )
868WHERE ce.course_id = $1
869 AND ce.deleted_at IS NULL
870 AND cmc.deleted_at IS NULL;
871 ",
872 course_id
873 )
874 .fetch_optional(conn)
875 .await?;
876
877 Ok(res.map(|r| r.average_duration_seconds).unwrap_or_default())
878}
879
880pub async fn get_student_duration(
881 conn: &mut PgConnection,
882 user_id: Uuid,
883 course_id: Uuid,
884) -> ModelResult<Option<i64>> {
885 let res = sqlx::query!(
886 "
887SELECT COALESCE(
888 EXTRACT(
889 EPOCH
890 FROM cmc.completion_date - ce.created_at
891 )::int8,
892 0
893 ) AS student_duration_seconds
894FROM course_instance_enrollments ce
895 JOIN course_module_completions cmc ON (
896 cmc.course_id = ce.course_id
897 AND cmc.user_id = ce.user_id
898 )
899WHERE ce.course_id = $1
900 AND ce.user_id = $2
901 AND ce.deleted_at IS NULL
902 AND cmc.deleted_at IS NULL;
903 ",
904 course_id,
905 user_id
906 )
907 .fetch_optional(conn)
908 .await?;
909
910 Ok(res.map(|r| r.student_duration_seconds).unwrap_or_default())
911}
912
913#[cfg(test)]
914mod test {
915 use super::*;
916 use crate::{
917 course_instance_enrollments::NewCourseInstanceEnrollment, exercise_tasks::NewExerciseTask,
918 test_helper::*,
919 };
920
921 #[tokio::test]
922 async fn allows_only_one_instance_per_course_without_name() {
923 insert_data!(:tx, :user, :org, course: course_id);
924
925 let mut tx1 = tx.begin().await;
926 let mut instance = NewCourseInstance {
928 course_id,
929 name: None,
930 description: None,
931 teacher_in_charge_name: "teacher",
932 teacher_in_charge_email: "teacher@example.com",
933 support_email: None,
934 opening_time: None,
935 closing_time: None,
936 };
937 insert(tx1.as_mut(), PKeyPolicy::Generate, instance)
938 .await
939 .unwrap_err();
940 tx1.rollback().await;
941
942 let mut tx2 = tx.begin().await;
943 instance.name = Some("name");
945 insert(tx2.as_mut(), PKeyPolicy::Generate, instance)
946 .await
947 .unwrap();
948 }
949
950 #[tokio::test]
951 async fn gets_enrolled_course_instances_for_user_with_exercise_type() {
952 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);
953
954 crate::course_instance_enrollments::insert_enrollment_and_set_as_current(
956 tx.as_mut(),
957 NewCourseInstanceEnrollment {
958 course_id,
959 user_id,
960 course_instance_id: instance.id,
961 },
962 )
963 .await
964 .unwrap();
965 let course_instances =
966 get_enrolled_course_instances_for_user_with_exercise_type(tx.as_mut(), user_id, "tmc")
967 .await
968 .unwrap();
969 assert!(
970 course_instances.is_empty(),
971 "user should not be enrolled on any course with tmc exercises"
972 );
973
974 crate::exercise_tasks::insert(
976 tx.as_mut(),
977 PKeyPolicy::Generate,
978 NewExerciseTask {
979 assignment: Vec::new(),
980 exercise_slide_id,
981 exercise_type: "tmc".to_string(),
982 model_solution_spec: None,
983 private_spec: None,
984 public_spec: None,
985 order_number: 1,
986 },
987 )
988 .await
989 .unwrap();
990 let course_instances =
991 get_enrolled_course_instances_for_user_with_exercise_type(tx.as_mut(), user_id, "tmc")
992 .await
993 .unwrap();
994 assert_eq!(
995 course_instances.len(),
996 1,
997 "user should be enrolled on one course with tmc exercises"
998 );
999 tx.rollback().await;
1000 }
1001
1002 #[tokio::test]
1003 async fn gets_course_average_duration_with_empty_database() {
1004 insert_data!(:tx, :user, :org, :course);
1005 let duration = get_course_average_duration(tx.as_mut(), course)
1006 .await
1007 .unwrap();
1008 assert!(duration.is_none())
1009 }
1010}